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

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 +364 -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 +31 -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 +145 -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
@@ -1,137 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { STATE_DIR } from '../paths.js';
4
- import { errMsg, log } from '../log.js';
5
- const API_BASE = 'https://api.telegram.org';
6
- function token() {
7
- const t = process.env.TELEGRAM_BOT_TOKEN;
8
- if (!t)
9
- throw new Error('TELEGRAM_BOT_TOKEN is not set');
10
- return t;
11
- }
12
- export async function tg(method, body, timeoutMs = 30_000) {
13
- const res = await fetch(`${API_BASE}/bot${token()}/${method}`, {
14
- method: 'POST',
15
- headers: { 'Content-Type': 'application/json' },
16
- body: JSON.stringify(body),
17
- signal: AbortSignal.timeout(timeoutMs),
18
- });
19
- const json = (await res.json());
20
- if (!json.ok)
21
- throw new Error(`telegram ${method}: ${json.description ?? 'unknown error'}`);
22
- return json.result;
23
- }
24
- export function buildSendBody(chatId, text, opts) {
25
- const body = { chat_id: chatId, text };
26
- if (opts.parseMode)
27
- body.parse_mode = opts.parseMode;
28
- if (opts.disableLinkPreview)
29
- body.link_preview_options = { is_disabled: true };
30
- if (opts.buttons?.length)
31
- body.reply_markup = { inline_keyboard: opts.buttons };
32
- return body;
33
- }
34
- export async function getMe() {
35
- return tg('getMe', {});
36
- }
37
- const CACHE_MAX = 200;
38
- const cacheFile = join(STATE_DIR, 'telegram-attachments.json');
39
- function readCache() {
40
- try {
41
- return existsSync(cacheFile) ? JSON.parse(readFileSync(cacheFile, 'utf8')) : {};
42
- }
43
- catch (err) {
44
- log.warn({ err: errMsg(err) }, 'telegram attachment cache read failed');
45
- return {};
46
- }
47
- }
48
- function cacheAttachment(chatId, messageId, att) {
49
- try {
50
- const data = readCache();
51
- const key = `${chatId}:${messageId}`;
52
- data[key] = [...(data[key] ?? []), att];
53
- const keys = Object.keys(data);
54
- for (const stale of keys.slice(0, Math.max(0, keys.length - CACHE_MAX)))
55
- delete data[stale];
56
- writeFileSync(cacheFile, JSON.stringify(data, null, 2));
57
- }
58
- catch (err) {
59
- log.warn({ err: errMsg(err) }, 'telegram attachment cache write failed');
60
- }
61
- }
62
- export function getCachedAttachments(chatId, messageId) {
63
- return readCache()[`${chatId}:${messageId}`] ?? [];
64
- }
65
- export async function downloadAttachment(fileId, mime) {
66
- const file = await tg('getFile', { file_id: fileId });
67
- const res = await fetch(`${API_BASE}/file/bot${token()}/${file.file_path}`, {
68
- signal: AbortSignal.timeout(30_000),
69
- });
70
- if (!res.ok)
71
- throw new Error(`download failed: ${res.status}`);
72
- const buf = Buffer.from(await res.arrayBuffer());
73
- // Trust the cached mime — it's the authoritative one from the message
74
- // metadata (`image/jpeg` for photos, the document's mime_type for files).
75
- // The Telegram CDN often returns `application/octet-stream` as Content-Type,
76
- // which would otherwise wipe out our extension classification downstream.
77
- return { data: buf.toString('base64'), mime };
78
- }
79
- async function messageToText(m, chatId) {
80
- if (m.text)
81
- return m.text;
82
- const caption = m.caption ?? '';
83
- if (m.photo?.length) {
84
- const photo = m.photo[m.photo.length - 1];
85
- cacheAttachment(chatId, m.message_id, { file_id: photo.file_id, mime: 'image/jpeg' });
86
- return [caption, '[image]'].filter(Boolean).join(' ');
87
- }
88
- if (m.document?.mime_type?.startsWith('image/')) {
89
- cacheAttachment(chatId, m.message_id, { file_id: m.document.file_id, mime: m.document.mime_type });
90
- return [caption, '[image]'].filter(Boolean).join(' ');
91
- }
92
- if (m.voice)
93
- return [caption, '[voice]'].filter(Boolean).join(' ');
94
- if (m.audio)
95
- return [caption, '[audio]'].filter(Boolean).join(' ');
96
- return caption || null;
97
- }
98
- let onInboundHandler = () => { };
99
- export function onInbound(handler) {
100
- onInboundHandler = handler;
101
- }
102
- export async function startPolling() {
103
- // A registered webhook short-circuits getUpdates — clear it defensively.
104
- await tg('deleteWebhook', { drop_pending_updates: false }).catch(() => { });
105
- let offset = 0;
106
- const initial = await tg('getUpdates', { timeout: 0 });
107
- if (initial.length)
108
- offset = initial[initial.length - 1].update_id + 1;
109
- while (true) {
110
- try {
111
- const updates = await tg('getUpdates', { offset, timeout: 50 }, 60_000);
112
- for (const u of updates) {
113
- offset = u.update_id + 1;
114
- void dispatchUpdate(u);
115
- }
116
- }
117
- catch (err) {
118
- log.error({ err: errMsg(err) }, 'telegram poll error');
119
- await new Promise(r => setTimeout(r, 1000));
120
- }
121
- }
122
- }
123
- async function dispatchUpdate(u) {
124
- const m = u.message;
125
- if (!m?.chat?.id || typeof m.message_id !== 'number')
126
- return;
127
- const base = { chat_id: m.chat.id, message_id: m.message_id };
128
- try {
129
- const text = await messageToText(m, m.chat.id);
130
- if (text === null)
131
- return;
132
- onInboundHandler({ ...base, text });
133
- }
134
- catch (err) {
135
- onInboundHandler({ ...base, text: `[message processing failed: ${errMsg(err)}]` });
136
- }
137
- }
@@ -1,21 +0,0 @@
1
- // `<platform>:<chat>[/<message_id>]` — the wire format of every metro
2
- // inbound `to` field, and the only address shape any subcommand accepts.
3
- export function parseAddress(to, requireMessage) {
4
- const colon = to.indexOf(':');
5
- if (colon === -1) {
6
- throw new Error(`invalid --to (expected '<platform>:<chat>[/<message_id>]'): ${to}`);
7
- }
8
- const platform = to.slice(0, colon);
9
- if (platform !== 'telegram' && platform !== 'discord') {
10
- throw new Error(`unknown platform '${platform}' in --to (expected 'telegram' or 'discord')`);
11
- }
12
- const rest = to.slice(colon + 1);
13
- const slash = rest.indexOf('/');
14
- const chat = slash === -1 ? rest : rest.slice(0, slash);
15
- const messageId = slash === -1 ? undefined : rest.slice(slash + 1);
16
- if (!chat)
17
- throw new Error(`empty chat/channel id in --to: ${to}`);
18
- if (requireMessage && !messageId)
19
- throw new Error(`--to must include /<message_id>: ${to}`);
20
- return { platform, chat, messageId };
21
- }
@@ -1,274 +0,0 @@
1
- // JSON-RPC over WebSocket client for the Codex app-server. By default
2
- // connects over the Unix domain socket exposed by `codex remote-control`
3
- // (see paths.DEFAULT_CODEX_SOCKET). On every metro inbound it calls
4
- // `turn/start`, which lands the JSON line in the agent's history as a
5
- // user message and wakes the agent — the Codex equivalent of Claude
6
- // Code's `Monitor`.
7
- //
8
- // Wire format: standard JSON-RPC 2.0 over WebSocket. Methods we use:
9
- // - initialize → handshake, declares clientInfo.
10
- // - thread/list → discover existing threads on connect.
11
- // - thread/started → notification; track new threads as they appear.
12
- // - turn/started → notification; mark a turn in flight.
13
- // - turn/completed → notification; drain queued inbounds.
14
- // - turn/start → push a user message and wake the agent.
15
- //
16
- // If no thread exists yet, inbounds queue in memory; they fire as soon
17
- // as a thread is started or learned. If the connection drops, reconnect
18
- // with linear backoff and replay the queue. Connection failures don't
19
- // break metro — stdout emit always runs first, so Claude Code Monitor
20
- // users keep working regardless.
21
- import { createConnection } from 'node:net';
22
- import { WebSocket } from 'ws';
23
- import { errMsg, log } from '../log.js';
24
- const RECONNECT_DELAY_MS = 2_000;
25
- const MAX_QUEUE = 100;
26
- // Backstop: if `turn/completed` never arrives (the daemon doesn't broadcast
27
- // it to all clients, or we miss it for any reason), unstick after this long.
28
- // Generous enough that any normal turn finishes well within it.
29
- const TURN_TIMEOUT_MS = 120_000;
30
- /**
31
- * Accept any of these forms for METRO_CODEX_RC:
32
- * ws://host:port/ TCP WebSocket.
33
- * wss://host/ TCP WebSocket over TLS.
34
- * unix:///abs/path UDS WebSocket (the default for `codex remote-control`).
35
- * /abs/path shorthand for unix:///abs/path.
36
- */
37
- export function parseCodexUrl(input) {
38
- if (input.startsWith('ws://') || input.startsWith('wss://')) {
39
- return { kind: 'tcp', url: input };
40
- }
41
- if (input.startsWith('unix://')) {
42
- return { kind: 'unix', path: input.replace(/^unix:\/+/, '/') };
43
- }
44
- if (input.startsWith('/')) {
45
- return { kind: 'unix', path: input };
46
- }
47
- throw new Error(`unsupported METRO_CODEX_RC: ${input} (expected ws://…, wss://…, unix://…, or absolute path)`);
48
- }
49
- function openSocket(endpoint) {
50
- if (endpoint.kind === 'tcp')
51
- return new WebSocket(endpoint.url);
52
- // UDS WebSocket: ws library upgrades any duplex stream, so we feed it a
53
- // unix-socket Net connection via `createConnection`. The `ws://localhost/`
54
- // URL is a placeholder — only the framing matters at this point; the
55
- // bytes flow over the UDS we just opened.
56
- return new WebSocket('ws://localhost/', {
57
- createConnection: () => createConnection({ path: endpoint.path }),
58
- });
59
- }
60
- export class CodexRC {
61
- clientVersion;
62
- ws = null;
63
- nextId = 1;
64
- pending = new Map();
65
- threadId = null;
66
- queue = [];
67
- connected = false;
68
- connecting = false;
69
- turnInFlight = false;
70
- turnTimeout = null;
71
- closed = false;
72
- endpoint;
73
- displayUrl;
74
- constructor(url, clientVersion) {
75
- this.clientVersion = clientVersion;
76
- this.endpoint = parseCodexUrl(url);
77
- this.displayUrl = url;
78
- }
79
- start() {
80
- void this.connect();
81
- }
82
- stop() {
83
- this.closed = true;
84
- this.ws?.close();
85
- this.ws = null;
86
- }
87
- /**
88
- * Push an inbound JSON line to the Codex agent. If not yet connected or
89
- * no thread is active, queues until ready (FIFO, capped).
90
- */
91
- push(line) {
92
- if (this.queue.length >= MAX_QUEUE) {
93
- log.warn({ url: this.displayUrl }, 'codex-rc queue full, dropping oldest inbound');
94
- this.queue.shift();
95
- }
96
- this.queue.push(line);
97
- void this.drainQueue();
98
- }
99
- async connect() {
100
- if (this.closed || this.connected || this.connecting)
101
- return;
102
- this.connecting = true;
103
- try {
104
- const ws = openSocket(this.endpoint);
105
- this.ws = ws;
106
- // Register the persistent error handler BEFORE awaiting open — without
107
- // it, an error fired during the upgrade (UDS missing, daemon down,
108
- // bad URL) is "unhandled" and crashes the process under Bun. The
109
- // once('error') below covers the connect-time rejection; this on()
110
- // covers later errors and the rare double-emit.
111
- ws.on('error', err => log.warn({ err: errMsg(err) }, 'codex-rc websocket error'));
112
- await new Promise((resolve, reject) => {
113
- ws.once('open', resolve);
114
- ws.once('error', err => reject(err));
115
- });
116
- ws.on('message', data => this.onMessage(data));
117
- ws.on('close', () => this.onClose());
118
- await this.call('initialize', {
119
- clientInfo: { name: 'metro', version: this.clientVersion, title: null },
120
- });
121
- try {
122
- const list = await this.call('thread/list', {});
123
- const active = (list.data ?? []).find(t => t.status !== 'archived');
124
- if (active)
125
- this.threadId = active.id;
126
- }
127
- catch (err) {
128
- log.warn({ err: errMsg(err) }, 'codex-rc thread/list failed (non-fatal)');
129
- }
130
- this.connected = true;
131
- log.info({ url: this.displayUrl, thread: this.threadId ?? '(none yet)' }, 'codex-rc connected');
132
- void this.drainQueue();
133
- }
134
- catch (err) {
135
- log.warn({ err: errMsg(err), url: this.displayUrl }, 'codex-rc connect failed; retrying');
136
- this.scheduleReconnect();
137
- }
138
- finally {
139
- this.connecting = false;
140
- }
141
- }
142
- onMessage(raw) {
143
- let msg;
144
- try {
145
- msg = JSON.parse(raw.toString());
146
- }
147
- catch (err) {
148
- log.warn({ err: errMsg(err) }, 'codex-rc malformed message');
149
- return;
150
- }
151
- log.debug({ id: msg.id, method: msg.method, hasError: !!msg.error }, 'codex-rc ← message');
152
- if (msg.id !== undefined && this.pending.has(msg.id)) {
153
- const p = this.pending.get(msg.id);
154
- this.pending.delete(msg.id);
155
- if (msg.error)
156
- p.reject(new Error(msg.error.message ?? 'rpc error'));
157
- else
158
- p.resolve(msg.result);
159
- return;
160
- }
161
- if (msg.method === 'thread/started') {
162
- const params = msg.params;
163
- if (params?.thread?.id) {
164
- this.threadId = params.thread.id;
165
- log.info({ thread: this.threadId }, 'codex-rc thread started');
166
- void this.drainQueue();
167
- }
168
- }
169
- else if (msg.method === 'thread/status/changed') {
170
- // Codex 0.130 doesn't emit turn/started or turn/completed to a non-
171
- // owner connection (the TUI gets them; metro doesn't). Instead the
172
- // daemon broadcasts thread/status/changed transitions: status is
173
- // either a string (`"idle"` / `"notLoaded"` / `"systemError"`) or an
174
- // object (`{"active": {...}}`) per the v2 protocol enum. Treat
175
- // anything other than `active` as ready-for-next-turn.
176
- const params = msg.params;
177
- if (params?.threadId === this.threadId) {
178
- const isActive = typeof params.status === 'object' && params.status !== null && 'active' in params.status;
179
- log.debug({ thread: this.threadId, isActive, status: params.status }, 'codex-rc thread status changed');
180
- if (isActive) {
181
- this.turnInFlight = true;
182
- }
183
- else {
184
- this.clearTurnTimeout();
185
- this.turnInFlight = false;
186
- void this.drainQueue();
187
- }
188
- }
189
- }
190
- else if (msg.method === 'turn/completed') {
191
- // Backstop in case some codex version does emit turn lifecycle
192
- // notifications on the metro connection.
193
- log.debug({ thread: this.threadId, queue: this.queue.length }, 'codex-rc turn/completed; draining');
194
- this.clearTurnTimeout();
195
- this.turnInFlight = false;
196
- void this.drainQueue();
197
- }
198
- else if (msg.method === 'turn/started') {
199
- log.debug({ thread: this.threadId }, 'codex-rc turn/started');
200
- this.turnInFlight = true;
201
- }
202
- else if (msg.method === 'thread/closed' || msg.method === 'thread/archived') {
203
- log.warn({ method: msg.method, params: msg.params }, 'codex-rc thread closed/archived; clearing thread reference');
204
- this.threadId = null;
205
- }
206
- }
207
- onClose() {
208
- if (this.closed)
209
- return;
210
- this.connected = false;
211
- this.ws = null;
212
- for (const p of this.pending.values())
213
- p.reject(new Error('websocket closed'));
214
- this.pending.clear();
215
- this.scheduleReconnect();
216
- }
217
- scheduleReconnect() {
218
- if (this.closed)
219
- return;
220
- this.connected = false;
221
- setTimeout(() => void this.connect(), RECONNECT_DELAY_MS);
222
- }
223
- async drainQueue() {
224
- if (!this.connected || !this.threadId || this.turnInFlight)
225
- return;
226
- if (this.queue.length === 0)
227
- return;
228
- const line = this.queue[0];
229
- this.turnInFlight = true;
230
- this.armTurnTimeout();
231
- log.debug({ thread: this.threadId, queue: this.queue.length }, 'codex-rc → turn/start');
232
- try {
233
- await this.call('turn/start', {
234
- threadId: this.threadId,
235
- input: [{ type: 'text', text: line, textElements: [] }],
236
- });
237
- this.queue.shift();
238
- }
239
- catch (err) {
240
- log.warn({ err: errMsg(err) }, 'codex-rc turn/start failed; will retry');
241
- this.clearTurnTimeout();
242
- this.turnInFlight = false;
243
- setTimeout(() => void this.drainQueue(), 1_000);
244
- }
245
- }
246
- // If `turn/completed` never arrives (the daemon doesn't broadcast it to
247
- // metro's connection, or we miss it for any reason), unstick after the
248
- // backstop so subsequent inbounds don't queue forever.
249
- armTurnTimeout() {
250
- this.clearTurnTimeout();
251
- this.turnTimeout = setTimeout(() => {
252
- log.warn({ thread: this.threadId, queue: this.queue.length }, `codex-rc turn/completed not received within ${TURN_TIMEOUT_MS}ms; force-clearing single-flight gate`);
253
- this.turnInFlight = false;
254
- this.turnTimeout = null;
255
- void this.drainQueue();
256
- }, TURN_TIMEOUT_MS);
257
- }
258
- clearTurnTimeout() {
259
- if (this.turnTimeout) {
260
- clearTimeout(this.turnTimeout);
261
- this.turnTimeout = null;
262
- }
263
- }
264
- call(method, params) {
265
- if (!this.ws)
266
- return Promise.reject(new Error('not connected'));
267
- const id = this.nextId++;
268
- const ws = this.ws;
269
- return new Promise((resolve, reject) => {
270
- this.pending.set(id, { resolve: resolve, reject });
271
- ws.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
272
- });
273
- }
274
- }
@@ -1,31 +0,0 @@
1
- // Tiny .env reader/writer. Used by `metro setup` (read/write the global
2
- // config file) and by paths.ts (load env vars into process.env at startup).
3
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { dirname } from 'node:path';
5
- const LINE_RE = /^\s*([A-Za-z_]\w*)\s*=\s*(.*?)\s*$/;
6
- const QUOTED_RE = /^(['"])(.*)\1$/;
7
- export function readDotenv(path) {
8
- if (!existsSync(path))
9
- return {};
10
- const out = {};
11
- for (const line of readFileSync(path, 'utf8').split('\n')) {
12
- const m = line.match(LINE_RE);
13
- if (m)
14
- out[m[1]] = m[2].replace(QUOTED_RE, '$2');
15
- }
16
- return out;
17
- }
18
- export function writeDotenv(path, env) {
19
- mkdirSync(dirname(path), { recursive: true });
20
- writeFileSync(path, Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n') + '\n');
21
- chmodSync(path, 0o600);
22
- }
23
- // Load .env at `path` into process.env, but only for keys that aren't already
24
- // set — first definer wins, so callers control precedence by the order they
25
- // invoke this.
26
- export function loadDotenvIntoProcess(path) {
27
- for (const [k, v] of Object.entries(readDotenv(path))) {
28
- if (process.env[k] === undefined)
29
- process.env[k] = v;
30
- }
31
- }
package/dist/log.js DELETED
@@ -1,12 +0,0 @@
1
- // Pino → stderr. Stdout is reserved for command output (`metro`'s inbound
2
- // JSON lines, subcommand results, --json) — any stray write there breaks
3
- // parsing. Override level with METRO_LOG_LEVEL.
4
- import pino from 'pino';
5
- export const log = pino({ name: 'metro', level: process.env.METRO_LOG_LEVEL || 'info' }, pino.destination(2));
6
- export const errMsg = (err) => {
7
- if (err instanceof Error)
8
- return err.message;
9
- if (err && typeof err === 'object' && 'message' in err)
10
- return String(err.message);
11
- return String(err);
12
- };
package/dist/paths.js DELETED
@@ -1,45 +0,0 @@
1
- import { mkdirSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { loadDotenvIntoProcess } from './lib/dotenv.js';
5
- import { log } from './log.js';
6
- // Lockfile, typing-stop signals, attachment cache. Override with METRO_STATE_DIR.
7
- export const STATE_DIR = process.env.METRO_STATE_DIR ?? join(homedir(), '.cache', 'metro');
8
- mkdirSync(STATE_DIR, { recursive: true });
9
- // Where `metro setup` writes the global .env. Override with METRO_CONFIG_DIR
10
- // or the standard $XDG_CONFIG_HOME.
11
- const CONFIG_DIR = process.env.METRO_CONFIG_DIR ??
12
- join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'metro');
13
- export const CONFIG_ENV_FILE = join(CONFIG_DIR, '.env');
14
- // Default codex app-server WebSocket. The TUI's `--remote <ADDR>` flag
15
- // only accepts ws:// (no UDS), so this is the canonical URL we recommend
16
- // users align on across the daemon, the TUI, and metro:
17
- // codex app-server --listen ws://127.0.0.1:8421
18
- // codex --remote ws://127.0.0.1:8421
19
- // METRO_CODEX_RC=ws://127.0.0.1:8421 metro
20
- // Override via METRO_CODEX_RC if a different port/host is needed.
21
- export const DEFAULT_CODEX_RC_URL = 'ws://127.0.0.1:8421';
22
- export function skillDir(runtime, scope) {
23
- const base = scope === 'user' ? homedir() : process.cwd();
24
- const root = runtime === 'claude-code' ? '.claude' : '.agents';
25
- return join(base, root, 'skills', 'metro');
26
- }
27
- // Precedence: process.env (already set) > cwd/.env > <CONFIG_DIR>/.env.
28
- // loadDotenvIntoProcess only fills vars that aren't already populated, so the
29
- // first call that defines a key wins.
30
- export function loadMetroEnv() {
31
- loadDotenvIntoProcess(join(process.cwd(), '.env'));
32
- loadDotenvIntoProcess(CONFIG_ENV_FILE);
33
- }
34
- export function configuredPlatforms() {
35
- return {
36
- telegram: !!process.env.TELEGRAM_BOT_TOKEN,
37
- discord: !!process.env.DISCORD_BOT_TOKEN,
38
- };
39
- }
40
- export function requireConfiguredPlatform(p) {
41
- if (p.telegram || p.discord)
42
- return;
43
- log.fatal('no platforms configured — run `metro setup telegram <token>` or `metro setup discord <token>`');
44
- process.exit(2);
45
- }
package/dist/tail.js DELETED
@@ -1,161 +0,0 @@
1
- // Standalone inbound stream. Polls Telegram + connects to Discord, prints
2
- // one JSON line per inbound message on stdout. Designed to be launched by
3
- // an agent and observed via Bash+Monitor (Claude Code) or unified_exec
4
- // polling (Codex).
5
- //
6
- // On every inbound: fires a 👀 reaction and starts a typing indicator that
7
- // refreshes until the agent replies (signaled by `metro reply` touching
8
- // .typing-stop/<key>) or the 60s safety cap is hit.
9
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
10
- import { join } from 'node:path';
11
- import pkg from '../package.json' with { type: 'json' };
12
- import * as discord from './channels/discord.js';
13
- import * as telegram from './channels/telegram.js';
14
- import { tg } from './channels/telegram.js';
15
- import { CodexRC } from './lib/codex-rc.js';
16
- import { configuredPlatforms, loadMetroEnv, STATE_DIR, requireConfiguredPlatform } from './paths.js';
17
- import { errMsg, log } from './log.js';
18
- loadMetroEnv();
19
- const platforms = configuredPlatforms();
20
- requireConfiguredPlatform(platforms);
21
- // Telegram allows only one getUpdates poller per bot token. If another
22
- // `metro` instance is already running, exit cleanly instead of fighting
23
- // (409 spam). Stale lockfiles (PID dead) are reclaimed.
24
- const LOCK_FILE = join(STATE_DIR, '.tail-lock');
25
- function processIsAlive(pid) {
26
- try {
27
- process.kill(pid, 0);
28
- return true;
29
- }
30
- catch {
31
- return false;
32
- }
33
- }
34
- if (existsSync(LOCK_FILE)) {
35
- const pid = Number(readFileSync(LOCK_FILE, 'utf8').trim());
36
- if (Number.isInteger(pid) && pid > 0 && processIsAlive(pid)) {
37
- log.info({ pid }, 'another `metro` instance is already polling; exiting');
38
- process.exit(0);
39
- }
40
- try {
41
- unlinkSync(LOCK_FILE);
42
- }
43
- catch { }
44
- }
45
- writeFileSync(LOCK_FILE, String(process.pid));
46
- function releaseLock() {
47
- try {
48
- if (existsSync(LOCK_FILE) && readFileSync(LOCK_FILE, 'utf8').trim() === String(process.pid)) {
49
- unlinkSync(LOCK_FILE);
50
- }
51
- }
52
- catch { }
53
- }
54
- process.on('exit', releaseLock);
55
- const TYPING_DIR = join(STATE_DIR, '.typing-stop');
56
- const TYPING_REFRESH_MS = 4_000;
57
- const TYPING_MAX_MS = 60_000;
58
- mkdirSync(TYPING_DIR, { recursive: true });
59
- // Codex push channel. Set METRO_CODEX_RC to the codex app-server URL
60
- // (typically `ws://127.0.0.1:8421` matching `codex app-server --listen
61
- // ws://127.0.0.1:8421`) to inject each inbound into the agent's history
62
- // via JSON-RPC `turn/start`. Codex's TUI `--remote` flag only accepts
63
- // ws://, so the daemon, the TUI, and metro must all share the same URL.
64
- // Unset → metro behaves exactly as before; stdout emit always runs first
65
- // so Claude Code Monitor users are unaffected.
66
- const codexRC = process.env.METRO_CODEX_RC ? new CodexRC(process.env.METRO_CODEX_RC, pkg.version) : null;
67
- codexRC?.start();
68
- const emit = (line) => {
69
- const json = JSON.stringify(line);
70
- process.stdout.write(`${json}\n`);
71
- codexRC?.push(json);
72
- };
73
- const typingActive = new Map();
74
- const typingKey = (platform, chat) => `${platform}_${chat}`;
75
- function fireTyping(platform, chat) {
76
- if (platform === 'telegram') {
77
- void tg('sendChatAction', { chat_id: chat, action: 'typing' }).catch(err => log.warn({ err: errMsg(err) }, 'telegram typing failed'));
78
- }
79
- else {
80
- void discord.sendTyping(chat).catch(err => log.warn({ err: errMsg(err) }, 'discord typing failed'));
81
- }
82
- }
83
- function startTyping(platform, chat) {
84
- const k = typingKey(platform, chat);
85
- typingActive.set(k, { platform, chat, started: Date.now() });
86
- // Clear any stale stop signal so the new typing actually fires.
87
- const stopFile = join(TYPING_DIR, k);
88
- if (existsSync(stopFile)) {
89
- try {
90
- unlinkSync(stopFile);
91
- }
92
- catch { }
93
- }
94
- fireTyping(platform, chat);
95
- }
96
- setInterval(() => {
97
- const now = Date.now();
98
- for (const [k, e] of typingActive) {
99
- const stopFile = join(TYPING_DIR, k);
100
- if (existsSync(stopFile)) {
101
- try {
102
- unlinkSync(stopFile);
103
- }
104
- catch { }
105
- typingActive.delete(k);
106
- continue;
107
- }
108
- if (now - e.started > TYPING_MAX_MS) {
109
- typingActive.delete(k);
110
- continue;
111
- }
112
- fireTyping(e.platform, e.chat);
113
- }
114
- }, TYPING_REFRESH_MS);
115
- if (platforms.telegram) {
116
- const me = await telegram.getMe();
117
- log.info({ bot: `@${me.username}` }, 'telegram ready');
118
- telegram.onInbound(m => {
119
- void tg('setMessageReaction', {
120
- chat_id: m.chat_id,
121
- message_id: m.message_id,
122
- reaction: [{ type: 'emoji', emoji: '👀' }],
123
- }).catch(err => log.warn({ err: errMsg(err) }, 'telegram auto-react failed'));
124
- const chat = String(m.chat_id);
125
- startTyping('telegram', chat);
126
- emit({ platform: 'telegram', to: `telegram:${chat}/${m.message_id}`, text: m.text });
127
- });
128
- void telegram.startPolling();
129
- }
130
- if (platforms.discord) {
131
- await discord.startGateway();
132
- const me = await discord.getMe();
133
- log.info({ bot: me.username }, 'discord ready');
134
- discord.onInbound(m => {
135
- void discord
136
- .setReaction(m.channel_id, m.message_id, '👀')
137
- .catch(err => log.warn({ err: errMsg(err) }, 'discord auto-react failed'));
138
- startTyping('discord', m.channel_id);
139
- emit({ platform: 'discord', to: `discord:${m.channel_id}/${m.message_id}`, text: m.text });
140
- });
141
- }
142
- // `process.on('exit', releaseLock)` above runs whenever process.exit is
143
- // called. We also `await discord.shutdownGateway()` here so the bot flips
144
- // offline immediately on SIGTERM / SIGINT instead of waiting ~45s for the
145
- // gateway's missed-heartbeat timeout. SIGKILL bypasses this (nothing we can
146
- // do); the lockfile auto-reclaims on the next start either way.
147
- let shuttingDown = false;
148
- async function shutdown() {
149
- if (shuttingDown)
150
- return;
151
- shuttingDown = true;
152
- codexRC?.stop();
153
- if (platforms.discord) {
154
- await discord.shutdownGateway().catch(err => log.warn({ err: errMsg(err) }, 'discord shutdown failed'));
155
- }
156
- process.exit(0);
157
- }
158
- process.stdin.on('end', shutdown);
159
- process.stdin.on('close', shutdown);
160
- process.on('SIGINT', shutdown);
161
- process.on('SIGTERM', shutdown);