@stage-labs/metro 0.1.0-beta.201 → 0.1.0-beta.202

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 (163) hide show
  1. package/dist/claude.js +4 -15
  2. package/dist/cli.js +5 -3
  3. package/dist/hold.js +1 -2
  4. package/dist/runtime-install.js +4 -5
  5. package/dist/runtime.js +1 -4
  6. package/dist/serve.js +4 -5
  7. package/dist/service.js +82 -21
  8. package/dist/update.js +1 -1
  9. package/package.json +2 -2
  10. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
  11. package/runtime/node_modules/@metro-labs/core/src/events.ts +4 -2
  12. package/runtime/node_modules/@metro-labs/core/src/lines.ts +12 -25
  13. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +45 -22
  14. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +0 -1
  15. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +8 -80
  16. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +3 -5
  17. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +4 -9
  18. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +9 -39
  19. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +1 -1
  20. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +9 -28
  21. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +0 -1
  22. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +3 -3
  23. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +8 -116
  24. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +11 -35
  25. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +7 -9
  26. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +1 -1
  27. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +1 -1
  28. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +1 -1
  29. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +1 -1
  30. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -3
  31. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +2 -2
  32. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +3 -29
  33. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +5 -8
  34. package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +1 -1
  35. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +5 -8
  36. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +3 -6
  37. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +25 -3
  38. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +2 -3
  39. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -2
  40. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +1 -1
  41. package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +6 -56
  42. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +3 -3
  43. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -1
  44. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +7 -6
  45. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +1 -1
  46. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +7 -7
  47. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -4
  48. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +1 -1
  49. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +2 -4
  50. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +19 -22
  51. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +1 -2
  52. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +6 -8
  53. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +3 -6
  54. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +1 -1
  55. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +2 -3
  56. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +28 -1
  57. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  58. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +4 -4
  59. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +1 -1
  60. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +1 -1
  61. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +1 -1
  62. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +3 -2
  63. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +1 -1
  64. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +2 -2
  65. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +1 -1
  66. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +3 -3
  67. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +1 -1
  68. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +3 -3
  69. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +6 -6
  70. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +7 -7
  71. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +1 -1
  72. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -2
  73. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +1 -1
  74. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +10 -31
  75. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +1 -1
  76. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +1 -1
  77. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +2 -2
  78. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
  79. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +1 -2
  80. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +2 -2
  81. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  82. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +3 -3
  83. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +2 -2
  84. package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +2 -2
  85. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +2 -2
  86. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -2
  88. package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +1 -1
  89. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -4
  90. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +3 -8
  91. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +2 -2
  92. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +2 -2
  93. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +2 -2
  94. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +6 -7
  95. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +1 -1
  96. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +2 -2
  97. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +2 -2
  98. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +1 -1
  99. package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +5 -44
  100. package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +4 -4
  101. package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +6 -7
  102. package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +12 -13
  103. package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +5 -22
  104. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +1 -19
  105. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +7 -35
  106. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +5 -10
  107. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +1 -1
  108. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +1 -2
  109. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +1 -1
  110. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +11 -12
  111. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +2 -5
  112. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +0 -1
  113. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +1 -15
  114. package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +0 -6
  115. package/runtime/node_modules/@metro-labs/outlook/src/format.ts +0 -1
  116. package/runtime/node_modules/@metro-labs/outlook/src/state.ts +1 -1
  117. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +4 -22
  118. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +2 -2
  119. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +1 -3
  120. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +1 -1
  121. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +1 -2
  122. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +1 -1
  123. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +4 -37
  124. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +7 -23
  125. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +1 -4
  126. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +4 -9
  127. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +3 -3
  128. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -11
  129. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +2 -12
  130. package/runtime/node_modules/@metro-labs/threema/src/files.ts +4 -15
  131. package/runtime/node_modules/@metro-labs/threema/src/format.ts +2 -7
  132. package/runtime/node_modules/@metro-labs/whatsapp/package.json +1 -2
  133. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +5 -26
  134. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +5 -6
  135. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -3
  136. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +1 -1
  137. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -12
  138. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +1 -1
  139. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +14 -5
  140. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +1 -1
  141. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +31 -78
  142. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -4
  143. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +1 -6
  144. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +1 -1
  145. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +1 -1
  146. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +1 -1
  147. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +2 -2
  148. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
  149. package/runtime/runtime.json +1 -1
  150. package/runtime/node_modules/@metro-labs/core/src/identity.ts +0 -7
  151. package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +0 -2
  152. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +0 -12
  153. package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +0 -13
  154. package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +0 -1
  155. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +0 -29
  156. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +0 -43
  157. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +0 -165
  158. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -1
  159. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +0 -22
  160. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -1
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -1
  162. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
  163. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -1
@@ -8,7 +8,7 @@ import {
8
8
  } from '@metro-labs/core/stations/station-runtime';
9
9
  import { accountFor, accounts, targetOf } from './accounts.js';
10
10
  import type { UserClient } from './client.js';
11
- import { normalizeTelegramUser } from './normalize.js';
11
+ import { messagingAliases } from '@metro-labs/core/stations/messaging-normalize';
12
12
  import {
13
13
  sendAttachments,
14
14
  type CanonicalAttachment,
@@ -251,6 +251,6 @@ export function makeHandleCall(
251
251
  set_profile: makeSetProfile(clientFor),
252
252
  profile: makeReadProfile(clientFor),
253
253
  },
254
- normalize: normalizeTelegramUser,
254
+ normalize: messagingAliases(),
255
255
  });
256
256
  }
@@ -1,6 +1,6 @@
1
1
  import type { Chat, Message, Peer, User } from '@mtcute/bun';
2
2
  import { lineOf } from './accounts.js';
3
- import { mintId } from './wire.js';
3
+ import { mintId } from '@metro-labs/core/stations/station-runtime';
4
4
  import { isDownloadable, pendingDescriptorOf } from './attachments.js';
5
5
 
6
6
  const isUser = (peer: Peer): peer is User => peer.type === 'user';
@@ -72,7 +72,6 @@ export function envelope(
72
72
  ? [pendingDescriptorOf(media)]
73
73
  : undefined;
74
74
  return {
75
- kind: 'inbound',
76
75
  id: mintId(),
77
76
  ts: m.date.toISOString(),
78
77
  station: 'telegram',
@@ -108,7 +107,6 @@ export interface ReactionInput {
108
107
 
109
108
  export function reactionEnvelope(r: ReactionInput): Record<string, unknown> {
110
109
  return {
111
- kind: 'react',
112
110
  id: mintId(),
113
111
  ts: r.date.toISOString(),
114
112
  station: 'telegram',
@@ -1,6 +1,6 @@
1
1
  import type { Message } from '@mtcute/bun';
2
2
  import { errMsg, log } from '@metro-labs/core/log';
3
- import { emit } from './wire.js';
3
+ import { emit } from '@metro-labs/core/stations/station-runtime';
4
4
  import { reportAttachment } from '@metro-labs/core/stations/train-events';
5
5
  import { envelope, isOwnEcho } from './format.js';
6
6
  import { downloadMedia, isDownloadable } from './attachments.js';
@@ -6,12 +6,11 @@ import type { UserClient } from './client.js';
6
6
  export interface CanonicalAttachment {
7
7
  kind?: string;
8
8
  path?: string;
9
- url?: string;
10
9
  mime?: string;
11
10
  name?: string;
12
11
  }
13
12
 
14
- const srcOf = (att: CanonicalAttachment): string => att.path ?? att.url ?? '';
13
+ const srcOf = (att: CanonicalAttachment): string => att.path ?? '';
15
14
 
16
15
  function isImage(att: CanonicalAttachment): boolean {
17
16
  if (att.kind === 'image') return true;
@@ -1,7 +1,7 @@
1
1
  import type { tl } from '@mtcute/bun';
2
2
  import { getBarePeerId, getMarkedPeerId } from '@mtcute/bun';
3
3
  import { errMsg } from '@metro-labs/core/log';
4
- import { emit } from './wire.js';
4
+ import { emit } from '@metro-labs/core/stations/station-runtime';
5
5
  import { reactionEnvelope } from './format.js';
6
6
  import type { UserClient } from './client.js';
7
7
 
@@ -1,15 +1,9 @@
1
- import { homedir } from 'node:os';
2
- import { join } from 'node:path';
3
1
  import {
4
2
  makeAccountStore,
5
3
  resolveAccountId,
6
4
  } from '@metro-labs/core/stations/account-store';
7
5
  import { Line } from '@metro-labs/core/lines';
8
6
 
9
- const ACCOUNTS_FILE =
10
- process.env.TELEGRAM_BOT_ACCOUNTS_FILE ??
11
- join(homedir(), '.metro', 'telegram-bot-accounts.json');
12
-
13
7
  export interface AccountConfig {
14
8
  id: string;
15
9
  token: string;
@@ -17,20 +11,15 @@ export interface AccountConfig {
17
11
 
18
12
  export const { loadAccounts } = makeAccountStore<AccountConfig>({
19
13
  prefix: 'telegram-bot',
20
- file: ACCOUNTS_FILE,
21
14
  validate(raw, die) {
22
- const seenId = new Set<string>();
23
15
  const seenTok = new Set<string>();
24
16
  for (const a of raw) {
25
- if (!a.id) die('account missing id');
26
17
  if (!a.token || typeof a.token !== 'string')
27
18
  die(`account '${a.id}' missing token`);
28
- if (seenId.has(a.id)) die(`duplicate account id '${a.id}'`);
29
19
  if (seenTok.has(a.token))
30
20
  die(
31
21
  `account '${a.id}' reuses a token used by another account (409 on getUpdates)`,
32
22
  );
33
- seenId.add(a.id);
34
23
  seenTok.add(a.token);
35
24
  }
36
25
  },
@@ -49,37 +38,15 @@ export async function tg<T>(
49
38
  accountId: string,
50
39
  method: string,
51
40
  body: unknown,
52
- timeoutMs = 30_000,
53
- ): Promise<T> {
54
- const acct = accounts.get(accountId);
55
- if (!acct) throw new Error(`unknown account '${accountId}'`);
56
- const res = await fetch(`${acct.api}/${method}`, {
57
- method: 'POST',
58
- headers: { 'Content-Type': 'application/json' },
59
- body: JSON.stringify(body),
60
- signal: AbortSignal.timeout(timeoutMs),
61
- });
62
- const json = (await res.json()) as {
63
- ok: boolean;
64
- description?: string;
65
- result?: T;
66
- };
67
- if (!json.ok)
68
- throw new Error(`telegram-bot ${method}: ${json.description ?? 'unknown'}`);
69
- return json.result as T;
70
- }
71
-
72
- export async function tgForm<T>(
73
- accountId: string,
74
- method: string,
75
- form: FormData,
76
- timeoutMs = 60_000,
41
+ timeoutMs = body instanceof FormData ? 60_000 : 30_000,
77
42
  ): Promise<T> {
78
43
  const acct = accounts.get(accountId);
79
44
  if (!acct) throw new Error(`unknown account '${accountId}'`);
45
+ const form = body instanceof FormData;
80
46
  const res = await fetch(`${acct.api}/${method}`, {
81
47
  method: 'POST',
82
- body: form,
48
+ ...(form ? {} : { headers: { 'Content-Type': 'application/json' } }),
49
+ body: form ? body : JSON.stringify(body),
83
50
  signal: AbortSignal.timeout(timeoutMs),
84
51
  });
85
52
  const json = (await res.json()) as {
@@ -1,17 +1,12 @@
1
1
  import { accounts, tg, targetOf } from './accounts.js';
2
- import { respond } from './wire.js';
3
2
  import { errMsg } from '@metro-labs/core/log';
4
- import { normalizeTelegram } from '@metro-labs/core/stations/messaging-normalize';
3
+ import { messagingAliases } from '@metro-labs/core/stations/messaging-normalize';
5
4
  import {
6
5
  adminMemberList,
7
6
  inaccessibleMemberList,
8
7
  type TgChatMember,
9
8
  } from './members.js';
10
- import {
11
- makeStation,
12
- type CallMsg,
13
- type StationHandler,
14
- } from '@metro-labs/core/stations/station-runtime';
9
+ import { makeStation, respond, type CallMsg, type StationHandler } from '@metro-labs/core/stations/station-runtime';
15
10
  import { mediaKindOf } from './attachments.js';
16
11
  import {
17
12
  emitOutbound,
@@ -65,13 +60,10 @@ interface SendArgs {
65
60
  line: string;
66
61
  text: string;
67
62
  replyTo?: string;
68
- parseMode?: string;
69
- buttons?: { text: string; url: string }[][];
70
63
  account?: string;
71
64
  attachments?: {
72
65
  kind?: string;
73
66
  path?: string;
74
- url?: string;
75
67
  mime?: string;
76
68
  name?: string;
77
69
  }[];
@@ -80,11 +72,8 @@ interface SendArgs {
80
72
 
81
73
  type WireAttachment = NonNullable<SendArgs['attachments']>[number];
82
74
 
83
- const attachmentSrc = (att: WireAttachment): string | undefined =>
84
- att.path ?? att.url;
85
-
86
75
  const attachmentKind = (att: WireAttachment): string =>
87
- mediaKindOf(att.kind, att.mime, attachmentSrc(att) ?? att.name);
76
+ mediaKindOf(att.kind, att.mime, att.path ?? att.name);
88
77
 
89
78
  async function sendOneAttachment(
90
79
  a: SendArgs,
@@ -96,10 +85,9 @@ async function sendOneAttachment(
96
85
  if (!mf) throw new Error(`telegram-bot cannot send a '${kind}' attachment`);
97
86
  const sent = await sendMedia(mf.method, mf.field, {
98
87
  line: a.line,
99
- path: attachmentSrc(att),
88
+ path: att.path,
100
89
  caption,
101
90
  replyTo: a.replyTo,
102
- parseMode: a.parseMode,
103
91
  account: a.account,
104
92
  name: att.name,
105
93
  });
@@ -144,13 +132,11 @@ async function send(id: string, args: Record<string, unknown>): Promise<void> {
144
132
  await sendAttachments(id, a);
145
133
  return;
146
134
  }
147
- const { line, text, replyTo, parseMode, buttons, account } = a;
135
+ const { line, text, replyTo, account } = a;
148
136
  const { accountId, chatId, topicId } = targetOf(line, account);
149
137
  const body: Record<string, unknown> = { chat_id: chatId, text };
150
138
  if (topicId !== undefined) body.message_thread_id = topicId;
151
139
  if (replyTo) body.reply_parameters = { message_id: Number(replyTo) };
152
- if (parseMode) body.parse_mode = parseMode;
153
- if (buttons) body.reply_markup = { inline_keyboard: buttons };
154
140
  const sent = await tg<{ message_id: number }>(accountId, 'sendMessage', body);
155
141
  finishSend(id, accountId, line, String(sent.message_id), text, replyTo);
156
142
  }
@@ -172,11 +158,10 @@ async function react(id: string, args: Record<string, unknown>): Promise<void> {
172
158
  }
173
159
 
174
160
  async function edit(id: string, args: Record<string, unknown>): Promise<void> {
175
- const { line, messageId, text, parseMode, account } = args as {
161
+ const { line, messageId, text, account } = args as {
176
162
  line: string;
177
163
  messageId: string;
178
164
  text: string;
179
- parseMode?: string;
180
165
  account?: string;
181
166
  };
182
167
  const { accountId, chatId } = targetOf(line, account);
@@ -184,7 +169,6 @@ async function edit(id: string, args: Record<string, unknown>): Promise<void> {
184
169
  chat_id: chatId,
185
170
  message_id: Number(messageId),
186
171
  text,
187
- parse_mode: parseMode,
188
172
  });
189
173
  respond(id, { result: { ok: true, account: accountId } });
190
174
  }
@@ -242,5 +226,5 @@ const HANDLERS: Record<string, StationHandler> = {
242
226
 
243
227
  export const handleCall = makeStation({
244
228
  handlers: HANDLERS,
245
- normalize: normalizeTelegram,
229
+ normalize: messagingAliases(),
246
230
  });
@@ -1,6 +1,6 @@
1
1
  import { accounts, lineOf } from './accounts.js';
2
2
  import { reportAttachment } from '@metro-labs/core/stations/train-events';
3
- import { mintId } from './wire.js';
3
+ import { mintId } from '@metro-labs/core/stations/station-runtime';
4
4
  import { mediaRefOf, saveTelegramMedia } from './attachments.js';
5
5
  import type { TgMsg, TgReaction, TgReactionCount, TgUser } from './types.js';
6
6
 
@@ -80,7 +80,6 @@ export const fullName = (u: TgUser | undefined): string | undefined => {
80
80
  export function envelope(accountId: string, m: TgMsg): Record<string, unknown> {
81
81
  const { line } = lineForMsg(accountId, m);
82
82
  return {
83
- kind: 'inbound',
84
83
  id: mintId(),
85
84
  ts: new Date(m.date * 1000).toISOString(),
86
85
  station: 'telegram-bot',
@@ -111,7 +110,6 @@ export function reactionEnvelope(
111
110
  const added = newEmojis.filter((e) => !oldEmojis.includes(e));
112
111
  if (!added.length) return null;
113
112
  return {
114
- kind: 'react',
115
113
  id: mintId(),
116
114
  ts: new Date(r.date * 1000).toISOString(),
117
115
  station: 'telegram-bot',
@@ -141,7 +139,6 @@ export function reactionCountEnvelope(
141
139
  const emoji = top.type.emoji ?? '';
142
140
  if (!emoji) return null;
143
141
  return {
144
- kind: 'react',
145
142
  id: mintId(),
146
143
  ts: new Date(rc.date * 1000).toISOString(),
147
144
  station: 'telegram-bot',
@@ -1,6 +1,6 @@
1
- import { tgForm, targetOf } from './accounts.js';
1
+ import { tg, targetOf } from './accounts.js';
2
2
  import { selfUri } from '@metro-labs/core/stations/train-events';
3
- import { emit, mintId, respond } from './wire.js';
3
+ import { emit, mintId, respond } from '@metro-labs/core/stations/station-runtime';
4
4
  import { appendFile } from '@metro-labs/core/stations/attachments';
5
5
 
6
6
  export function emitOutbound(
@@ -11,18 +11,16 @@ export function emitOutbound(
11
11
  replyTo?: string,
12
12
  ): void {
13
13
  emit({
14
- kind: 'outbound',
15
14
  id: mintId(),
16
15
  ts: new Date().toISOString(),
17
16
  station: 'telegram-bot',
18
17
  line,
19
- from: selfUri('telegram-bot', accountId),
18
+ from: selfUri(),
20
19
  to: line,
21
20
  message_id: messageId,
22
21
  text,
23
22
  reply_to: replyTo,
24
23
  ...(replyTo ? { event: { type: 'reply', replyTo } } : {}),
25
- account: accountId,
26
24
  payload: { account: accountId },
27
25
  });
28
26
  }
@@ -47,7 +45,6 @@ interface MediaArgs {
47
45
  path: string;
48
46
  caption?: string;
49
47
  replyTo?: string;
50
- parseMode?: string;
51
48
  account?: string;
52
49
  name?: string;
53
50
  }
@@ -62,7 +59,6 @@ export async function sendMedia(
62
59
  path,
63
60
  caption,
64
61
  replyTo,
65
- parseMode,
66
62
  account,
67
63
  name: fileName,
68
64
  } = args as unknown as MediaArgs;
@@ -71,7 +67,6 @@ export async function sendMedia(
71
67
  form.append('chat_id', String(chatId));
72
68
  if (topicId !== undefined) form.append('message_thread_id', String(topicId));
73
69
  if (caption) form.append('caption', caption);
74
- if (parseMode) form.append('parse_mode', parseMode);
75
70
  if (replyTo)
76
71
  form.append(
77
72
  'reply_parameters',
@@ -79,7 +74,7 @@ export async function sendMedia(
79
74
  );
80
75
  const name = fileName ?? path.split('/').pop() ?? fieldName;
81
76
  await appendFile(form, fieldName, path, name);
82
- const r = await tgForm<{ message_id: number }>(accountId, method, form);
77
+ const r = await tg<{ message_id: number }>(accountId, method, form);
83
78
  return { accountId, message_id: r.message_id };
84
79
  }
85
80
 
@@ -9,15 +9,15 @@ import {
9
9
  } from '@metro-labs/core/stations/profile';
10
10
  import { makeProfileCache, nonEmpty, type SenderProfile } from '@metro-labs/core/stations/sender-profile';
11
11
  import { TrainError } from '@metro-labs/core/train-error';
12
- import { accountFor, tg, tgForm } from './accounts.js';
13
- import { respond } from './wire.js';
12
+ import { accountFor, tg } from './accounts.js';
13
+ import { respond } from '@metro-labs/core/stations/station-runtime';
14
14
 
15
15
  async function setPhoto(accountId: string, avatar: ProfileAvatar): Promise<void> {
16
16
  assertImage(avatar);
17
17
  const form = new FormData();
18
18
  form.append('photo', JSON.stringify({ type: 'static', photo: 'attach://avatar' }));
19
19
  await appendFile(form, 'avatar', avatar.path, avatar.name);
20
- await tgForm(accountId, 'setMyProfilePhoto', form);
20
+ await tg(accountId, 'setMyProfilePhoto', form);
21
21
  }
22
22
 
23
23
  export async function applyProfile(accountId: string, change: ProfileChange): Promise<void> {
@@ -1,5 +1,3 @@
1
- import { homedir } from 'node:os';
2
- import { join } from 'node:path';
3
1
  import {
4
2
  makeAccountStore,
5
3
  resolveAccountId,
@@ -12,10 +10,6 @@ import { isGatewayId, normalizeThreemaId, parsePrivateKey } from './ids.js';
12
10
  import { GroupStore, parseGroupKey } from './groups.js';
13
11
  import type { GroupRef } from './messages.js';
14
12
 
15
- const ACCOUNTS_FILE =
16
- process.env.THREEMA_ACCOUNTS_FILE ??
17
- join(homedir(), '.metro', 'threema-accounts.json');
18
-
19
13
  export interface AccountConfig {
20
14
  id: string;
21
15
  gatewayId: string;
@@ -26,7 +20,6 @@ export interface AccountConfig {
26
20
  }
27
21
 
28
22
  function checkAccount(a: AccountConfig, die: Die): void {
29
- if (!a.id) die('account missing id');
30
23
  if (typeof a.gatewayId !== 'string' || !isGatewayId(a.gatewayId))
31
24
  die(`account '${a.id}' has no Gateway ID`);
32
25
  if (typeof a.secret !== 'string' || a.secret === '')
@@ -37,16 +30,12 @@ function checkAccount(a: AccountConfig, die: Die): void {
37
30
 
38
31
  export const { loadAccounts } = makeAccountStore<AccountConfig>({
39
32
  prefix: 'threema',
40
- file: ACCOUNTS_FILE,
41
33
  validate(raw, die) {
42
- const seenId = new Set<string>();
43
34
  const seenGateway = new Set<string>();
44
35
  for (const a of raw) {
45
36
  checkAccount(a, die);
46
- if (seenId.has(a.id)) die(`duplicate account id '${a.id}'`);
47
37
  if (seenGateway.has(a.gatewayId))
48
38
  die(`account '${a.id}' reuses the Gateway ID of another account`);
49
- seenId.add(a.id);
50
39
  seenGateway.add(a.gatewayId);
51
40
  }
52
41
  },
@@ -4,7 +4,7 @@ import {
4
4
  respond,
5
5
  type CallMsg,
6
6
  } from '@metro-labs/core/stations/station-runtime';
7
- import type { Normalized } from '@metro-labs/core/stations/messaging-normalize';
7
+ import { messagingAliases } from '@metro-labs/core/stations/messaging-normalize';
8
8
  import { accountFor, accounts, publicKeyFor, targetOf, type Account } from './accounts.js';
9
9
  import { hexToBytes, macMatches, open } from './crypto.js';
10
10
  import { groupLineOf, type InboundMeta } from './format.js';
@@ -118,17 +118,7 @@ async function callback(id: string, args: Args): Promise<void> {
118
118
  respond(id, { result: { ok: true, kind: deliver(acct, meta, decode(plain)) } });
119
119
  }
120
120
 
121
- export function normalizeThreema(action: string, env: Args): Normalized {
122
- if (action === 'reply')
123
- return {
124
- action: 'send',
125
- args: { line: env.line, text: env.text, replyTo: env.replyTo, account: env.account },
126
- };
127
- if (action === 'unreact') return { action: 'react', args: { ...env, action: 'removed' } };
128
- return { action, args: env };
129
- }
130
-
131
121
  export const handleCall = makeStation({
132
122
  handlers: { accounts: listAccounts, send, react, callback, listMembers },
133
- normalize: normalizeThreema,
123
+ normalize: messagingAliases({ action: 'removed' }),
134
124
  });
@@ -1,6 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { emit } from '@metro-labs/core/stations/station-runtime';
3
- import { attachmentFailedEvent, attachmentSavedEvent, emitInbound } from '@metro-labs/core/stations/train-events';
2
+ import { emitInbound, reportAttachment, type SavedFile } from '@metro-labs/core/stations/train-events';
4
3
  import { assertAttachmentSize, isImageMime, kindOf, saveBufferToCache } from '@metro-labs/core/stations/attachments';
5
4
  import { TrainError } from '@metro-labs/core/train-error';
6
5
  import type { Account } from './accounts.js';
@@ -30,12 +29,11 @@ export const encodeFileFor = (group: GroupRef | null, file: FileData): Uint8Arra
30
29
 
31
30
  export const fileLabel = (file: OutgoingFile): string => kindOf(file.mime, file.path);
32
31
 
33
- async function fetchFile(acct: Account, m: InboundMeta, file: FileData): Promise<{ path: string; bytes: number }> {
32
+ async function fetchFile(acct: Account, m: InboundMeta, file: FileData): Promise<SavedFile> {
34
33
  const sealed = await downloadBlob(acct.cfg, file.blobId);
35
34
  const plain = openBlob(sealed, hexToBytes(file.key, 'blob key'));
36
35
  if (plain === null) throw new TrainError('threema_bad_blob', 'the file did not decrypt with the key in the message', { retryable: false });
37
- const saved = await saveBufferToCache(plain, m.messageId, 0, { mime: file.mime, name: file.name });
38
- return { path: saved.path, bytes: saved.bytes };
36
+ return saveBufferToCache(plain, m.messageId, 0, { mime: file.mime, name: file.name });
39
37
  }
40
38
 
41
39
  export function deliverFile(acct: Account, m: InboundMeta, file: FileData, room: Room, sentByUs: (id: string) => boolean): void {
@@ -44,15 +42,6 @@ export function deliverFile(acct: Account, m: InboundMeta, file: FileData, room:
44
42
  const forId = String(env.id);
45
43
  const payload = { ...(env.payload as Record<string, unknown>), attachments: [{ kind: kindOf(file.mime, file.name), name: file.name, mime: file.mime, ...(file.size === null ? {} : { size: file.size }) }] };
46
44
  emitInbound(acct.cfg.id, { ...env, payload });
47
- const at = { station: 'threema', account: acct.cfg.id, line, forId, index: 0 };
48
45
  const kind = kindOf(file.mime, file.name);
49
- fetchFile(acct, m, file)
50
- .then(({ path, bytes }) => {
51
- emit(attachmentSavedEvent({ ...at, saved: { path, mime: file.mime, name: file.name }, extra: { kind, size: bytes } }));
52
- })
53
- .catch((err: unknown) => {
54
- const reason = err instanceof Error ? err.message : String(err);
55
- process.stderr.write(`threema[${acct.cfg.id}]: could not fetch ${file.name} from ${m.from}: ${reason}\n`);
56
- emit(attachmentFailedEvent({ ...at, reason, extra: { kind } }));
57
- });
46
+ reportAttachment(fetchFile(acct, m, file), { station: 'threema', account: acct.cfg.id, line, forId, index: 0 }, { saved: { kind }, failed: { kind } });
58
47
  }
@@ -53,7 +53,6 @@ function base(
53
53
  ): Record<string, unknown> {
54
54
  const group = room.group;
55
55
  return {
56
- kind: 'inbound',
57
56
  id: mintId(),
58
57
  ts: tsOf(m.date),
59
58
  station: 'threema',
@@ -127,18 +126,16 @@ export function emitOutbound(
127
126
  replyTo?: string,
128
127
  ): void {
129
128
  emit({
130
- kind: 'outbound',
131
129
  id: mintId(),
132
130
  ts: new Date().toISOString(),
133
131
  station: 'threema',
134
132
  line,
135
- from: selfUri('threema', accountId),
133
+ from: selfUri(),
136
134
  to: line,
137
135
  message_id: messageId,
138
136
  text,
139
137
  reply_to: replyTo,
140
138
  ...(replyTo ? { event: { type: 'reply', replyTo } } : {}),
141
- account: accountId,
142
139
  payload: { account: accountId },
143
140
  });
144
141
  }
@@ -152,18 +149,16 @@ export function emitOutboundReaction(
152
149
  removed: boolean,
153
150
  ): void {
154
151
  emit({
155
- kind: 'outbound',
156
152
  id: mintId(),
157
153
  ts: new Date().toISOString(),
158
154
  station: 'threema',
159
155
  line,
160
- from: selfUri('threema', accountId),
156
+ from: selfUri(),
161
157
  to: line,
162
158
  message_id: messageId,
163
159
  text: `[react ${emoji}${removed ? ' (removed)' : ''}]`,
164
160
  emoji,
165
161
  event: { type: 'react', emoji, targetId, ...(removed ? { removed: true } : {}) },
166
- account: accountId,
167
162
  payload: { account: accountId },
168
163
  });
169
164
  }
@@ -6,8 +6,7 @@
6
6
  "exports": {
7
7
  ".": "./src/station.ts",
8
8
  "./train": "./src/index.ts",
9
- "./login": "./src/login.ts",
10
- "./tokens": "./src/token-store.ts"
9
+ "./login": "./src/login.ts"
11
10
  },
12
11
  "scripts": {
13
12
  "build": "tsc",
@@ -1,5 +1,3 @@
1
- import { homedir } from 'node:os';
2
- import { join } from 'node:path';
3
1
  import {
4
2
  makeAccountStore,
5
3
  resolveAccountId,
@@ -7,30 +5,17 @@ import {
7
5
  } from '@metro-labs/core/stations/account-store';
8
6
  import type { WhatsAppAccount } from './types.js';
9
7
 
10
- const ACCOUNTS_FILE =
11
- process.env.WHATSAPP_ACCOUNTS_FILE ??
12
- join(homedir(), '.metro', 'whatsapp-accounts.json');
13
-
14
8
  const isJid = (s: string): boolean => s.includes('@');
15
9
 
16
- function validateAccount(
17
- a: WhatsAppAccount,
18
- seen: Set<string>,
19
- die: Die,
20
- ): void {
21
- if (!a.id) die('account missing id');
10
+ function validateAccount(a: WhatsAppAccount, die: Die): void {
22
11
  if (!a.phone || typeof a.phone !== 'string')
23
12
  die(`account '${a.id}' missing phone`);
24
- if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
25
- seen.add(a.id);
26
13
  }
27
14
 
28
15
  export const { loadAccounts } = makeAccountStore<WhatsAppAccount>({
29
16
  prefix: 'whatsapp',
30
- file: ACCOUNTS_FILE,
31
17
  validate(raw, die) {
32
- const seen = new Set<string>();
33
- for (const a of raw) validateAccount(a, seen, die);
18
+ for (const a of raw) validateAccount(a, die);
34
19
  },
35
20
  });
36
21
 
@@ -49,18 +34,12 @@ interface Target {
49
34
  jid: string;
50
35
  }
51
36
 
52
- function splitScoped(path: string[]): { accountId: string; jid?: string } {
53
- const first = path[0];
54
- if (path.length >= 2 && first !== undefined && !isJid(first))
55
- return { accountId: first, jid: path[1] };
56
- return { accountId: 'default', jid: first };
57
- }
58
-
59
37
  export function targetOf(line: string): Target | undefined {
60
38
  const prefix = 'metro://whatsapp/';
61
39
  if (!line.startsWith(prefix)) return undefined;
62
40
  const path = line.slice(prefix.length).split('/').filter(Boolean);
63
- const { accountId, jid } = splitScoped(path);
64
- if (jid === undefined || !isJid(jid)) return undefined;
41
+ const [accountId, jid] = path;
42
+ if (path.length !== 2 || accountId === undefined || jid === undefined) return undefined;
43
+ if (!isJid(jid)) return undefined;
65
44
  return { accountId, jid };
66
45
  }
@@ -7,8 +7,8 @@ import {
7
7
  type CallMsg,
8
8
  type StationHandler,
9
9
  } from '@metro-labs/core/stations/station-runtime';
10
+ import { messagingAliases } from '@metro-labs/core/stations/messaging-normalize';
10
11
  import { accountFor, accounts, targetOf } from './accounts.js';
11
- import { normalizeWhatsApp } from './normalize.js';
12
12
  import { phoneNumberOf, senderLookup } from './resolve.js';
13
13
  import { assertImage, fieldsOf, parseProfileChange, type ProfileApplied } from '@metro-labs/core/stations/profile';
14
14
  import type { WAClient } from './client.js';
@@ -50,8 +50,7 @@ async function guard<T>(run: () => Promise<T>): Promise<T> {
50
50
 
51
51
  interface WireAttachment {
52
52
  kind?: string;
53
- path?: string;
54
- url?: string;
53
+ path: string;
55
54
  mime?: string;
56
55
  name?: string;
57
56
  }
@@ -59,7 +58,7 @@ interface WireAttachment {
59
58
  function attachmentsOf(args: Args): WireAttachment[] {
60
59
  const raw = args.attachments;
61
60
  if (!Array.isArray(raw)) return [];
62
- return (raw as WireAttachment[]).filter((a) => Boolean(a.path ?? a.url));
61
+ return (raw as Partial<WireAttachment>[]).filter((a): a is WireAttachment => Boolean(a.path));
63
62
  }
64
63
 
65
64
  async function sendMediaSet(
@@ -74,7 +73,7 @@ async function sendMediaSet(
74
73
  for (let i = 0; i < atts.length; i++) {
75
74
  const att = atts[i];
76
75
  if (!att) continue;
77
- const path = att.path ?? att.url ?? '';
76
+ const path = att.path;
78
77
  const kind = att.kind ?? kindOf(att.mime ?? '', path);
79
78
  messageId = await guard(() =>
80
79
  client.sendMedia(
@@ -208,6 +207,6 @@ export function makeHandleCall(
208
207
  set_profile: makeSetProfile(clientFor),
209
208
  profile: makeProfile(clientFor),
210
209
  },
211
- normalize: normalizeWhatsApp,
210
+ normalize: messagingAliases(),
212
211
  });
213
212
  }
@@ -1,5 +1,5 @@
1
1
  import { lineOf } from './accounts.js';
2
- import { mintId } from './wire.js';
2
+ import { mintId } from '@metro-labs/core/stations/station-runtime';
3
3
  import type { WAMediaRef } from './media.js';
4
4
 
5
5
  export interface InboundMessage {
@@ -40,7 +40,6 @@ function attachmentView(ref: WAMediaRef): Record<string, unknown> {
40
40
 
41
41
  export function envelope(m: InboundMessage): Record<string, unknown> {
42
42
  return {
43
- kind: 'inbound',
44
43
  id: mintId(),
45
44
  ts: m.date.toISOString(),
46
45
  station: 'whatsapp',
@@ -63,7 +62,6 @@ export function envelope(m: InboundMessage): Record<string, unknown> {
63
62
 
64
63
  export function reactionEnvelope(r: ReactionInput): Record<string, unknown> {
65
64
  return {
66
- kind: 'react',
67
65
  id: mintId(),
68
66
  ts: r.date.toISOString(),
69
67
  station: 'whatsapp',