@stage-labs/metro 0.1.0-beta.324 → 0.1.0-beta.326

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 (176) hide show
  1. package/dist/agent.js +1 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/lock.js +15 -3
  4. package/dist/root-release-io.js +1 -2
  5. package/dist/root-release.js +2 -3
  6. package/dist/route.js +1 -1
  7. package/dist/service.js +4 -4
  8. package/dist/version.js +1 -1
  9. package/package.json +1 -1
  10. package/runtime/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
  11. package/runtime/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
  12. package/runtime/node_modules/@metro-labs/core/src/lines.ts +2 -8
  13. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +0 -6
  14. package/runtime/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
  15. package/runtime/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
  16. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
  17. package/runtime/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
  18. package/runtime/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
  19. package/runtime/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
  20. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
  21. package/runtime/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
  22. package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
  23. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
  24. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +4 -4
  25. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +2 -6
  26. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +6 -6
  27. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +0 -24
  28. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -3
  29. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -4
  30. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +4 -9
  31. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -6
  32. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +3 -3
  33. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +2 -2
  34. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +1 -1
  35. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +1 -1
  36. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +3 -2
  37. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  38. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +3 -7
  39. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +2 -2
  40. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +10 -19
  41. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +1 -5
  42. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +3 -4
  43. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +1 -2
  44. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +2 -4
  45. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  46. package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +2 -5
  47. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +1 -5
  48. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +2 -4
  49. package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/announced.ts +1 -1
  50. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +0 -1
  51. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +2 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/tailscale-guard.ts +26 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +1 -3
  54. package/runtime/node_modules/@metro-labs/daemon/src/routes/gmail-attach.ts +3 -3
  55. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +6 -21
  56. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -5
  57. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +6 -6
  58. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +0 -1
  59. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +12 -61
  60. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +13 -25
  61. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +5 -5
  62. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +2 -7
  63. package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +2 -6
  64. package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +12 -12
  65. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +5 -23
  66. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +1 -7
  67. package/runtime/node_modules/@metro-labs/discord-bot/src/channels.ts +2 -2
  68. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +10 -35
  69. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +5 -11
  70. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +6 -34
  71. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +11 -14
  72. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +1 -3
  73. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +2 -3
  74. package/runtime/node_modules/@metro-labs/gmail/src/accounts.ts +3 -3
  75. package/runtime/node_modules/@metro-labs/gmail/src/auth.ts +8 -8
  76. package/runtime/node_modules/@metro-labs/gmail/src/managed.ts +3 -2
  77. package/runtime/node_modules/@metro-labs/http/src/agent-signature.ts +1 -1
  78. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +3 -4
  79. package/runtime/node_modules/@metro-labs/http/src/connector-discovery.ts +0 -113
  80. package/runtime/node_modules/@metro-labs/http/src/connector-input.ts +1 -1
  81. package/runtime/node_modules/@metro-labs/http/src/connector-mcp.ts +12 -13
  82. package/runtime/node_modules/@metro-labs/http/src/connector-oauth.ts +1 -1
  83. package/runtime/node_modules/@metro-labs/http/src/connector-policy.ts +1 -6
  84. package/runtime/node_modules/@metro-labs/http/src/connector-public-url.ts +4 -2
  85. package/runtime/node_modules/@metro-labs/http/src/connector-tools.ts +1 -11
  86. package/runtime/node_modules/@metro-labs/http/src/connector-url.ts +0 -6
  87. package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +1 -1
  88. package/runtime/node_modules/@metro-labs/http/src/mcp-server-tools.ts +0 -2
  89. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +1 -1
  90. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +2 -2
  91. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +6 -7
  92. package/runtime/node_modules/@metro-labs/inference/package.json +1 -0
  93. package/runtime/node_modules/@metro-labs/inference/src/attempts.ts +3 -3
  94. package/runtime/node_modules/@metro-labs/inference/src/bedrock.ts +1 -1
  95. package/runtime/node_modules/@metro-labs/inference/src/codex-translate.ts +2 -2
  96. package/runtime/node_modules/@metro-labs/inference/src/delivered.ts +1 -1
  97. package/runtime/node_modules/@metro-labs/inference/src/effort.ts +0 -6
  98. package/runtime/node_modules/@metro-labs/inference/src/fallback.ts +7 -5
  99. package/runtime/node_modules/@metro-labs/inference/src/forward.ts +5 -5
  100. package/runtime/node_modules/@metro-labs/inference/src/measure.ts +2 -3
  101. package/runtime/node_modules/@metro-labs/inference/src/named.ts +3 -3
  102. package/runtime/node_modules/@metro-labs/inference/src/probes.ts +98 -0
  103. package/runtime/node_modules/@metro-labs/inference/src/usage.ts +5 -5
  104. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +6 -8
  105. package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +2 -1
  106. package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +3 -5
  107. package/runtime/node_modules/@metro-labs/outlook/src/me.ts +6 -6
  108. package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +10 -13
  109. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +7 -26
  110. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +1 -1
  111. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +3 -6
  112. package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +3 -2
  113. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +6 -20
  114. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +1 -7
  115. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +0 -2
  116. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +1 -16
  117. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +1 -8
  118. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +2 -1
  119. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +1 -8
  120. package/runtime/node_modules/@metro-labs/threema/src/chats.ts +4 -3
  121. package/runtime/node_modules/@metro-labs/threema/src/format.ts +15 -17
  122. package/runtime/node_modules/@metro-labs/threema/src/groups.ts +2 -4
  123. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +2 -1
  124. package/runtime/node_modules/@metro-labs/threema/src/messages.ts +2 -1
  125. package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +7 -7
  126. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +1 -3
  127. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +0 -2
  128. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +1 -1
  129. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +1 -1
  130. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +7 -0
  131. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +4 -10
  132. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -5
  133. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +14 -26
  134. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +1 -7
  135. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +17 -58
  136. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +1 -6
  137. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +10 -31
  138. package/runtime/node_modules/@metro-labs/xmtp/src/frames.ts +12 -14
  139. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +2 -3
  140. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +3 -9
  141. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +8 -9
  142. package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +4 -2
  143. package/runtime/node_modules/@metro-labs/xmtp/src/network.ts +2 -3
  144. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +2 -1
  145. package/runtime/node_modules/@metro-labs/xmtp/src/read-attachments.ts +2 -2
  146. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +2 -1
  147. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +23 -46
  148. package/runtime/node_modules/@metro-labs/xmtp/src/wallet.ts +5 -4
  149. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +12 -15
  150. package/runtime/runtime.json +1 -1
  151. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
  152. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
  153. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/lines.ts +2 -8
  154. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/protocol.ts +0 -6
  155. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
  156. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
  157. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
  158. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
  159. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
  160. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
  161. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
  162. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
  163. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
  164. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
  165. package/runtime/sdk-runner/node_modules/@metro-labs/core/src/train-error.ts +4 -4
  166. package/runtime/sdk-runner/runner.json +1 -1
  167. package/runtime/sdk-runner/src/approvals.ts +2 -2
  168. package/runtime/sdk-runner/src/automation-command.ts +2 -2
  169. package/runtime/sdk-runner/src/calls.ts +0 -6
  170. package/runtime/sdk-runner/src/errors.ts +1 -1
  171. package/runtime/sdk-runner/src/link.ts +1 -1
  172. package/runtime/sdk-runner/src/runner.ts +2 -2
  173. package/runtime/sdk-runner/src/session-watch.ts +5 -34
  174. package/runtime/sdk-runner/src/task-state.ts +1 -1
  175. package/runtime/sdk-runner/src/tool-proxy.ts +1 -1
  176. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +0 -40
@@ -22,12 +22,7 @@ import {
22
22
  } from '../agents/map.js';
23
23
  import { setKeyMap } from '../agents/keys.js';
24
24
  import { STATIONS, type StationName } from '@metro-labs/core/station-names';
25
-
26
- interface StationTarget {
27
- file: string;
28
- fileEnv: string;
29
- trainImport: string | null;
30
- }
25
+ import { stationByName } from './registry.js';
31
26
 
32
27
  export interface LoadedAccount {
33
28
  station: StationName;
@@ -48,59 +43,15 @@ export interface LoadedAgent {
48
43
 
49
44
  const METRO_DIR = join(homedir(), '.metro');
50
45
 
51
- const STATION_TARGETS: Record<StationName, StationTarget> = {
52
- xmtp: {
53
- file: 'xmtp-accounts.json',
54
- fileEnv: 'XMTP_ACCOUNTS_FILE',
55
- trainImport: '@metro-labs/xmtp/train',
56
- },
57
- 'telegram-bot': {
58
- file: 'telegram-bot-accounts.json',
59
- fileEnv: 'TELEGRAM_BOT_ACCOUNTS_FILE',
60
- trainImport: '@metro-labs/telegram-bot/train',
61
- },
62
- 'telegram': {
63
- file: 'telegram-accounts.json',
64
- fileEnv: 'TELEGRAM_ACCOUNTS_FILE',
65
- trainImport: '@metro-labs/telegram/train',
66
- },
67
- 'discord-bot': {
68
- file: 'discord-bot-accounts.json',
69
- fileEnv: 'DISCORD_BOT_ACCOUNTS_FILE',
70
- trainImport: '@metro-labs/discord-bot/train',
71
- },
72
- whatsapp: {
73
- file: 'whatsapp-accounts.json',
74
- fileEnv: 'WHATSAPP_ACCOUNTS_FILE',
75
- trainImport: '@metro-labs/whatsapp/train',
76
- },
77
- threema: {
78
- file: 'threema-accounts.json',
79
- fileEnv: 'THREEMA_ACCOUNTS_FILE',
80
- trainImport: '@metro-labs/threema/train',
81
- },
82
- outlook: {
83
- file: 'outlook-accounts.json',
84
- fileEnv: 'OUTLOOK_ACCOUNTS_FILE',
85
- trainImport: '@metro-labs/outlook/train',
86
- },
87
- gmail: {
88
- file: 'gmail-accounts.json',
89
- fileEnv: 'GMAIL_ACCOUNTS_FILE',
90
- trainImport: '@metro-labs/gmail/train',
91
- },
92
- webhook: {
93
- file: 'webhook-accounts.json',
94
- fileEnv: 'WEBHOOK_ACCOUNTS_FILE',
95
- trainImport: null,
96
- },
97
- };
46
+ const STATION_SET = new Set<string>(STATIONS);
98
47
 
99
48
  export function accountFilePath(station: StationName): string {
100
- const target = STATION_TARGETS[station];
101
- return process.env[target.fileEnv] ?? join(METRO_DIR, target.file);
49
+ return process.env[`${station.toUpperCase().replace(/-/g, '_')}_ACCOUNTS_FILE`] ?? join(METRO_DIR, `${station}-accounts.json`);
102
50
  }
103
51
 
52
+ const trainImport = (station: StationName): string | null =>
53
+ stationByName(station)?.hasTrain === false ? null : `@metro-labs/${station}/train`;
54
+
104
55
  export type StationSource = () => Promise<LoadedAgent[]>;
105
56
 
106
57
  export const MOVABLE_STATIONS = new Set<StationName>(
@@ -154,7 +105,7 @@ function writeStations(list: LoadedAgent[]): WrittenStations {
154
105
  for (const agent of list) {
155
106
  names[agent.id] = agent.name;
156
107
  for (const a of agent.accounts) {
157
- if (!(a.station in STATION_TARGETS)) {
108
+ if (!STATION_SET.has(a.station)) {
158
109
  log.error(
159
110
  { station: a.station, id: a.id },
160
111
  'unknown station — this metro is older or newer than the row; skipping it',
@@ -185,9 +136,9 @@ function writeStations(list: LoadedAgent[]): WrittenStations {
185
136
  writeSecure(path, content);
186
137
  changed.push(station);
187
138
  }
188
- const { trainImport } = STATION_TARGETS[station];
189
- if (trainImport !== null)
190
- writeIfChanged(trainStubPath(station), `import '${trainImport}';\n`);
139
+ const train = trainImport(station);
140
+ if (train !== null)
141
+ writeIfChanged(trainStubPath(station), `import '${train}';\n`);
191
142
  active.set(station, accts.length);
192
143
  }
193
144
  return { active, changed };
@@ -197,9 +148,9 @@ const BLANKED = '[]\n';
197
148
 
198
149
  function pruneStations(present: Map<StationName, number>): StationName[] {
199
150
  const removed: StationName[] = [];
200
- for (const station of Object.keys(STATION_TARGETS) as StationName[]) {
151
+ for (const station of STATIONS) {
201
152
  if (present.has(station)) continue;
202
- const hasTrain = STATION_TARGETS[station].trainImport !== null;
153
+ const hasTrain = trainImport(station) !== null;
203
154
  const stub = trainStubPath(station);
204
155
  const stubGone = !hasTrain || !existsSync(stub);
205
156
  const file = currentText(accountFilePath(station));
@@ -44,10 +44,6 @@ export class TrainSupervisor {
44
44
 
45
45
  constructor(private dir: string = trainsDir()) {}
46
46
 
47
- running(): string[] {
48
- return [...this.trains.keys()];
49
- }
50
-
51
47
  onTrainEvent(handler: (event: TrainEvent, train: string) => void): void {
52
48
  this.onEvent = handler;
53
49
  }
@@ -89,13 +85,7 @@ export class TrainSupervisor {
89
85
  clearTimeout(timer);
90
86
  this.reloadTimers.delete(name);
91
87
  }
92
- t.stopped = true;
93
- if (t.restartTimer) {
94
- clearTimeout(t.restartTimer);
95
- t.restartTimer = null;
96
- }
97
- failAllPending(t.pending, `train '${name}' stopped`);
98
- const wait = killGracefully(t.proc);
88
+ const wait = this.halt(t, `train '${name}' stopped`);
99
89
  if (wait) await wait;
100
90
  this.trains.delete(name);
101
91
  log.info({ name }, 'train: stopped, last account detached');
@@ -133,13 +123,7 @@ export class TrainSupervisor {
133
123
  this.reloadTimers.clear();
134
124
  const tasks: Promise<unknown>[] = [];
135
125
  for (const t of this.trains.values()) {
136
- t.stopped = true;
137
- if (t.restartTimer) {
138
- clearTimeout(t.restartTimer);
139
- t.restartTimer = null;
140
- }
141
- failAllPending(t.pending, 'train shutting down');
142
- const wait = killGracefully(t.proc);
126
+ const wait = this.halt(t, 'train shutting down');
143
127
  if (wait) tasks.push(wait);
144
128
  }
145
129
  await Promise.all(tasks);
@@ -148,13 +132,7 @@ export class TrainSupervisor {
148
132
  async restart(name: string): Promise<void> {
149
133
  const t = this.trains.get(name);
150
134
  if (!t) throw new Error(`no train named '${name}'`);
151
- if (t.restartTimer) {
152
- clearTimeout(t.restartTimer);
153
- t.restartTimer = null;
154
- }
155
- failAllPending(t.pending, `train '${name}' restarting`);
156
- t.stopped = true;
157
- const wait = killGracefully(t.proc);
135
+ const wait = this.halt(t, `train '${name}' restarting`);
158
136
  if (wait) await wait;
159
137
  t.stopped = false;
160
138
  t.failCount = 0;
@@ -162,6 +140,16 @@ export class TrainSupervisor {
162
140
  this.spawn(t);
163
141
  }
164
142
 
143
+ private halt(t: TrainState, reason: string): Promise<unknown> | null {
144
+ t.stopped = true;
145
+ if (t.restartTimer) {
146
+ clearTimeout(t.restartTimer);
147
+ t.restartTimer = null;
148
+ }
149
+ failAllPending(t.pending, reason);
150
+ return killGracefully(t.proc);
151
+ }
152
+
165
153
  async call(
166
154
  name: string,
167
155
  action: string,
@@ -1,4 +1,5 @@
1
1
  import { readJson } from '@metro-labs/core/secure-fs';
2
+ import { isRecord } from '@metro-labs/core/is-record';
2
3
  import { accountFilePath } from './materialize.js';
3
4
 
4
5
  export interface ThreemaCallback {
@@ -11,11 +12,10 @@ const str = (v: unknown): string | undefined =>
11
12
  typeof v === 'string' && v !== '' ? v : undefined;
12
13
 
13
14
  function toCallback(row: unknown): ThreemaCallback | null {
14
- if (typeof row !== 'object' || row === null) return null;
15
- const rec = row as Record<string, unknown>;
16
- const id = str(rec.id);
17
- const callbackId = str(rec.callbackId);
18
- const callbackToken = str(rec.callbackToken);
15
+ if (!isRecord(row)) return null;
16
+ const id = str(row.id);
17
+ const callbackId = str(row.callbackId);
18
+ const callbackToken = str(row.callbackToken);
19
19
  if (id === undefined || callbackId === undefined || callbackToken === undefined) return null;
20
20
  return { id, callbackId, callbackToken };
21
21
  }
@@ -1,8 +1,7 @@
1
1
  import type { Endpoint } from '@metro-labs/core/endpoints';
2
2
  import { timingSafeEqual } from 'node:crypto';
3
- import { homedir } from 'node:os';
4
- import { join } from 'node:path';
5
3
  import { readJson } from '@metro-labs/core/secure-fs';
4
+ import { accountFilePath } from './materialize.js';
6
5
 
7
6
  interface AccountRecord {
8
7
  id?: unknown;
@@ -12,10 +11,6 @@ interface AccountRecord {
12
11
  createdAt?: unknown;
13
12
  }
14
13
 
15
- const accountsFile = (): string =>
16
- process.env.WEBHOOK_ACCOUNTS_FILE ??
17
- join(homedir(), '.metro', 'webhook-accounts.json');
18
-
19
14
  const str = (v: unknown): string | undefined =>
20
15
  typeof v === 'string' && v !== '' ? v : undefined;
21
16
 
@@ -32,7 +27,7 @@ function toEndpoint(raw: AccountRecord): Endpoint | null {
32
27
  }
33
28
 
34
29
  export function listEndpoints(): Endpoint[] {
35
- const raw = readJson<AccountRecord[]>(accountsFile(), [], {
30
+ const raw = readJson<AccountRecord[]>(accountFilePath('webhook'), [], {
36
31
  warn: 'webhook-accounts.json: malformed, ignoring',
37
32
  });
38
33
  if (!Array.isArray(raw)) return [];
@@ -114,12 +114,8 @@ export class Call {
114
114
  else if (raw.kind === 'leave' && fromCaller) this.hangUp('the caller hung up');
115
115
  }
116
116
 
117
- hangUp(reason: string): void {
118
- this.end(reason);
119
- }
120
-
121
117
  leave(reason: string): Promise<void> {
122
- this.end(reason);
118
+ this.hangUp(reason);
123
119
  return this.left;
124
120
  }
125
121
 
@@ -149,7 +145,7 @@ export class Call {
149
145
  return this.deps.train('callSignal', { line: this.start.line, signal });
150
146
  }
151
147
 
152
- private end(reason: string): void {
148
+ hangUp(reason: string): void {
153
149
  if (this.over) return;
154
150
  this.over = true;
155
151
  if (this.timer !== null) clearTimeout(this.timer);
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { errMsg, log } from '@metro-labs/core/log';
3
3
  import { isRecord } from '@metro-labs/core/is-record';
4
+ import { stringOf } from '@metro-labs/http/api-http';
4
5
  import type { TrainEvent } from '@metro-labs/core/trains/protocol';
5
6
  import { agentIdForLine, allowlistForLine, lineReceives, mayApprove, senderPermitted } from '../agents/map.js';
6
7
  import { Call, trainCall, type CallStart } from './call.js';
@@ -22,10 +23,9 @@ const INVITES_MAX = 256;
22
23
  const makeCall: MakeCall = (start, cfg, onOver) => new Call(start, cfg, onOver);
23
24
 
24
25
  const record = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
25
- const str = (value: unknown): string => (typeof value === 'string' ? value : '');
26
26
 
27
- const contentTypeOf = (env: TrainEvent): string => str(record(env.payload).contentType);
28
- const sourceIdOf = (env: TrainEvent): string => str(env.message_id) || str(env.id) || randomUUID();
27
+ const contentTypeOf = (env: TrainEvent): string => stringOf(record(env.payload).contentType);
28
+ const sourceIdOf = (env: TrainEvent): string => stringOf(env.message_id) || stringOf(env.id) || randomUUID();
29
29
 
30
30
  export const isCallEvent = (env: TrainEvent): boolean => CALL_TYPES.has(contentTypeOf(env));
31
31
 
@@ -46,16 +46,16 @@ function decline(line: string, callId: string): void {
46
46
  }
47
47
 
48
48
  function ringing(env: TrainEvent): boolean {
49
- const sent = Date.parse(str(env.ts));
49
+ const sent = Date.parse(stringOf(env.ts));
50
50
  const age = Date.now() - sent;
51
51
  return Number.isFinite(age) && age >= -5_000 && age < RING_MS;
52
52
  }
53
53
 
54
54
  function invite(env: TrainEvent, body: Record<string, unknown>, create: MakeCall): void {
55
- const line = str(env.line);
56
- const from = str(env.from);
57
- const callId = str(body.callId);
58
- const callerPeer = str(body.from);
55
+ const line = stringOf(env.line);
56
+ const from = stringOf(env.from);
57
+ const callId = stringOf(body.callId);
58
+ const callerPeer = stringOf(body.from);
59
59
  const key = JSON.stringify([line, from, callId]);
60
60
  if (callId === '' || callerPeer === '' || invites.has(key)) return;
61
61
  if (!ringing(env)) {
@@ -85,16 +85,16 @@ function invite(env: TrainEvent, body: Record<string, unknown>, create: MakeCall
85
85
 
86
86
  function authenticated(env: TrainEvent): boolean {
87
87
  if (env.sender_verified === false || (env.station !== undefined && env.station !== 'xmtp')) return false;
88
- const line = str(env.line);
88
+ const line = stringOf(env.line);
89
89
  const match = /^metro:\/\/xmtp\/([^/]+)\/[^/]+$/.exec(line);
90
- return match !== null && str(env.from).startsWith(`metro://xmtp/${String(match[1])}/user/`) && str(env.from).split('/').length === 6;
90
+ return match !== null && stringOf(env.from).startsWith(`metro://xmtp/${String(match[1])}/user/`) && stringOf(env.from).split('/').length === 6;
91
91
  }
92
92
 
93
93
  export function onCallEvent(env: TrainEvent, train = 'xmtp', create: MakeCall = makeCall): void {
94
94
  if (train !== 'xmtp' || !authenticated(env)) return;
95
95
  const body = record(record(env.payload).call);
96
96
  if (contentTypeOf(env) === 'callInvite') invite(env, body, create);
97
- else if (contentTypeOf(env) === 'callSignal' && current !== null && sameCaller(current, str(env.line), str(env.from)))
97
+ else if (contentTypeOf(env) === 'callSignal' && current !== null && sameCaller(current, stringOf(env.line), stringOf(env.from)))
98
98
  current.signalled(body);
99
99
  }
100
100
 
@@ -117,7 +117,7 @@ function leftoversOf(result: unknown): Leftover[] {
117
117
  const rows = isRecord(result) && Array.isArray(result.calls) ? result.calls : [];
118
118
  return rows
119
119
  .map((row) => record(row))
120
- .map((row) => ({ line: str(row.line), callId: str(row.callId), peer: str(row.peer) }))
120
+ .map((row) => ({ line: stringOf(row.line), callId: stringOf(row.callId), peer: stringOf(row.peer) }))
121
121
  .filter((row) => row.line !== '' && row.callId !== '' && row.peer !== '');
122
122
  }
123
123
 
@@ -12,7 +12,7 @@ import {
12
12
  outgoingFiles,
13
13
  type OutgoingFile,
14
14
  } from './send-files.js';
15
- import { makeStation, respond, type CallMsg, type StationHandler } from '@metro-labs/core/stations/station-runtime';
15
+ import { makeStation, respond, type StationHandler } from '@metro-labs/core/stations/station-runtime';
16
16
  import { readProfile, setProfile } from './profile.js';
17
17
  import { discordMembers } from './members.js';
18
18
  import { listChannels } from './channels.js';
@@ -28,30 +28,14 @@ async function sendMessage(
28
28
  body: Record<string, unknown>,
29
29
  files: OutgoingFile[],
30
30
  ): Promise<{ id: string; delivered: string[] }> {
31
- if (files.length === 0) {
32
- const plain = await rest<{ id: string }>(
33
- accountId,
34
- 'POST',
35
- `/channels/${channel}/messages`,
36
- body,
37
- );
38
- return { id: plain.id, delivered: [] };
39
- }
31
+ const path = `/channels/${channel}/messages`;
32
+ if (files.length === 0) return { id: (await rest<{ id: string }>(accountId, 'POST', path, body)).id, delivered: [] };
40
33
  const form = new FormData();
41
34
  form.append('payload_json', JSON.stringify(body));
42
35
  const delivered = await appendFiles(form, files);
43
- const res = await rest<{ id: string }>(
44
- accountId,
45
- 'POST',
46
- `/channels/${channel}/messages`,
47
- form,
48
- true,
49
- );
50
- return { id: res.id, delivered };
36
+ return { id: (await rest<{ id: string }>(accountId, 'POST', path, form, true)).id, delivered };
51
37
  }
52
38
 
53
- export type { CallMsg };
54
-
55
39
  async function send(id: string, args: Record<string, unknown>): Promise<void> {
56
40
  const { line, text, replyTo, account } = args as {
57
41
  line: string;
@@ -198,9 +182,7 @@ async function listMembers(
198
182
  }
199
183
 
200
184
  const HANDLERS: Record<string, StationHandler> = {
201
- accounts: (id) => {
202
- listAccounts(id);
203
- },
185
+ accounts: listAccounts,
204
186
  groupCreate: groupCreateHandler,
205
187
  groupAddMembers: groupAddHandler,
206
188
  groupRemoveMembers: groupRemoveHandler,
@@ -1,10 +1,4 @@
1
- import {
2
- saveBufferToCache,
3
- assertContentLength,
4
- } from '@metro-labs/core/stations/attachments';
5
- import type { SavedAttachment } from '@metro-labs/core/stations/attachments';
6
-
7
- export type { SavedAttachment };
1
+ import { assertContentLength, saveBufferToCache, type SavedAttachment } from '@metro-labs/core/stations/attachments';
8
2
 
9
3
  export interface DiscordAttachmentRef {
10
4
  url: string;
@@ -4,6 +4,7 @@ import {
4
4
  type ChannelEntry,
5
5
  type ChannelSnapshot,
6
6
  } from '@metro-labs/core/stations/channel-directory';
7
+ import { isRecord } from '@metro-labs/core/is-record';
7
8
  import { respond } from '@metro-labs/core/stations/station-runtime';
8
9
  import { TrainError } from '@metro-labs/core/train-error';
9
10
  import { ChannelType, PermissionFlagsBits } from 'discord.js';
@@ -42,8 +43,7 @@ function visible(channel: DiscordChannelMetadata): boolean {
42
43
  }
43
44
 
44
45
  function inaccessible(error: unknown): boolean {
45
- return typeof error === 'object' && error !== null && 'status' in error &&
46
- (error.status === 403 || error.status === 404);
46
+ return isRecord(error) && (error.status === 403 || error.status === 404);
47
47
  }
48
48
 
49
49
  class Snapshot {
@@ -22,21 +22,18 @@ function membersOf(args: Record<string, unknown>): string[] {
22
22
  return raw.filter((v) => typeof v === 'string' && v.length > 0) as string[];
23
23
  }
24
24
 
25
- async function addThreadMembers(
25
+ async function threadMembers(
26
26
  rest: RestFn,
27
27
  accountId: string,
28
28
  threadId: string,
29
- members: string[],
29
+ args: Record<string, unknown>,
30
+ method: 'PUT' | 'DELETE',
30
31
  ): Promise<MemberOutcome[]> {
31
32
  const outcomes: MemberOutcome[] = [];
32
- for (const userId of members) {
33
+ for (const userId of membersOf(args)) {
33
34
  try {
34
- await rest(
35
- accountId,
36
- 'PUT',
37
- `/channels/${threadId}/thread-members/${userId}`,
38
- );
39
- outcomes.push({ id: userId, status: 'added' });
35
+ await rest(accountId, method, `/channels/${threadId}/thread-members/${userId}`);
36
+ outcomes.push({ id: userId, status: method === 'PUT' ? 'added' : 'removed' });
40
37
  } catch (e) {
41
38
  outcomes.push({ id: userId, status: 'failed', reason: errMsg(e) });
42
39
  }
@@ -60,12 +57,7 @@ export async function groupCreate(
60
57
  `/channels/${parentChannelId}/threads`,
61
58
  { name, type, auto_archive_duration: AUTO_ARCHIVE },
62
59
  );
63
- const members = await addThreadMembers(
64
- rest,
65
- accountId,
66
- thread.id,
67
- membersOf(args),
68
- );
60
+ const members = await threadMembers(rest, accountId, thread.id, args, 'PUT');
69
61
  return {
70
62
  capability: { supported: true },
71
63
  line: lineOf(accountId, thread.id),
@@ -82,12 +74,7 @@ export async function groupAddMembers(
82
74
  line: string,
83
75
  args: Record<string, unknown>,
84
76
  ): Promise<GroupResult> {
85
- const members = await addThreadMembers(
86
- rest,
87
- accountId,
88
- threadId,
89
- membersOf(args),
90
- );
77
+ const members = await threadMembers(rest, accountId, threadId, args, 'PUT');
91
78
  return { capability: { supported: true }, line, id: threadId, members };
92
79
  }
93
80
 
@@ -98,18 +85,6 @@ export async function groupRemoveMembers(
98
85
  line: string,
99
86
  args: Record<string, unknown>,
100
87
  ): Promise<GroupResult> {
101
- const outcomes: MemberOutcome[] = [];
102
- for (const userId of membersOf(args)) {
103
- try {
104
- await rest(
105
- accountId,
106
- 'DELETE',
107
- `/channels/${threadId}/thread-members/${userId}`,
108
- );
109
- outcomes.push({ id: userId, status: 'removed' });
110
- } catch (e) {
111
- outcomes.push({ id: userId, status: 'failed', reason: errMsg(e) });
112
- }
113
- }
114
- return { capability: { supported: true }, line, id: threadId, members: outcomes };
88
+ const members = await threadMembers(rest, accountId, threadId, args, 'DELETE');
89
+ return { capability: { supported: true }, line, id: threadId, members };
115
90
  }
@@ -70,7 +70,7 @@ async function bootAccount(cfg: AccountConfig): Promise<void> {
70
70
  });
71
71
 
72
72
  client.on(Events.MessageReactionAdd, (r, u) => {
73
- void (async () => {
73
+ (async () => {
74
74
  try {
75
75
  if (r.partial) await r.fetch();
76
76
  if (u.partial) await u.fetch();
@@ -85,18 +85,12 @@ async function bootAccount(cfg: AccountConfig): Promise<void> {
85
85
  });
86
86
  });
87
87
 
88
- client.on(Events.MessageUpdate, (_old, _new) => {
89
- void (async () => {
90
- try {
91
- onEdit(accountId, _new.partial ? await _new.fetch() : _new);
92
- } catch (err) {
93
- process.stderr.write(
94
- `discord-bot[${accountId}] message update fetch failed: ${errMsg(err)}\n`,
95
- );
96
- }
88
+ client.on(Events.MessageUpdate, (_old, updated) => {
89
+ (async () => {
90
+ onEdit(accountId, updated.partial ? await updated.fetch() : updated);
97
91
  })().catch((err: unknown) => {
98
92
  process.stderr.write(
99
- `discord-bot[${accountId}] message update handler failed: ${errMsg(err)}\n`,
93
+ `discord-bot[${accountId}] message update fetch failed: ${errMsg(err)}\n`,
100
94
  );
101
95
  });
102
96
  });
@@ -97,38 +97,6 @@ export function mapRecipients(users: DiscordUser[]): MetroMember[] {
97
97
  });
98
98
  }
99
99
 
100
- async function fetchRoles(
101
- accountId: string,
102
- guildId: string,
103
- ): Promise<Map<string, RoleInfo>> {
104
- try {
105
- const roles = await rest<DiscordRole[]>(
106
- accountId,
107
- 'GET',
108
- `/guilds/${guildId}/roles`,
109
- );
110
- return indexRoles(roles);
111
- } catch {
112
- return new Map();
113
- }
114
- }
115
-
116
- async function fetchOwnerId(
117
- accountId: string,
118
- guildId: string,
119
- ): Promise<string | undefined> {
120
- try {
121
- const g = await rest<{ owner_id?: string }>(
122
- accountId,
123
- 'GET',
124
- `/guilds/${guildId}`,
125
- );
126
- return g.owner_id;
127
- } catch {
128
- return undefined;
129
- }
130
- }
131
-
132
100
  interface Collected {
133
101
  raw: DiscordGuildMember[];
134
102
  exhausted: boolean;
@@ -168,8 +136,12 @@ export async function listGuildMembers(
168
136
  limit?: number,
169
137
  ): Promise<MemberList> {
170
138
  const cap = Math.min(Math.max(1, limit ?? PAGE), 10_000);
171
- const roles = await fetchRoles(accountId, guildId);
172
- const ownerId = await fetchOwnerId(accountId, guildId);
139
+ const roles = await rest<DiscordRole[]>(accountId, 'GET', `/guilds/${guildId}/roles`)
140
+ .then(indexRoles)
141
+ .catch(() => new Map<string, RoleInfo>());
142
+ const ownerId = await rest<{ owner_id?: string }>(accountId, 'GET', `/guilds/${guildId}`)
143
+ .then((g) => g.owner_id)
144
+ .catch(() => undefined);
173
145
  try {
174
146
  const { raw, exhausted } = await paginate(accountId, guildId, cap);
175
147
  const members = mapGuildMembers(raw, roles, ownerId);
@@ -9,7 +9,7 @@ import {
9
9
  type ProfileChange,
10
10
  } from '@metro-labs/core/stations/profile';
11
11
  import type { SenderProfile } from '@metro-labs/core/stations/sender-profile';
12
- import { accountFor, accounts } from './accounts.js';
12
+ import { accountFor, accounts, type Account } from './accounts.js';
13
13
  import { respond } from '@metro-labs/core/stations/station-runtime';
14
14
 
15
15
  export async function applyProfile(
@@ -32,29 +32,26 @@ export async function applyProfile(
32
32
  }
33
33
  }
34
34
 
35
+ function accountOf(args: Record<string, unknown>): Account {
36
+ const accountId = accountFor({ account: args.account as string | undefined });
37
+ const acct = accounts.get(accountId);
38
+ if (!acct) throw new Error(`unknown account '${accountId}'`);
39
+ return acct;
40
+ }
41
+
35
42
  export async function setProfile(
36
43
  id: string,
37
44
  args: Record<string, unknown>,
38
45
  ): Promise<void> {
39
- const accountId = accountFor({ account: args.account as string | undefined });
40
- const acct = accounts.get(accountId);
41
- if (!acct) {
42
- respond(id, { error: `unknown account '${accountId}'` });
43
- return;
44
- }
46
+ const acct = accountOf(args);
45
47
  const change = parseProfileChange(args);
46
48
  await applyProfile(acct.client, change);
47
- const result: ProfileApplied = { account: accountId, applied: fieldsOf(change) };
49
+ const result: ProfileApplied = { account: acct.cfg.id, applied: fieldsOf(change) };
48
50
  respond(id, { result });
49
51
  }
50
52
 
51
53
  export async function readProfile(id: string, args: Record<string, unknown>): Promise<void> {
52
- const accountId = accountFor({ account: args.account as string | undefined });
53
- const acct = accounts.get(accountId);
54
- if (!acct) {
55
- respond(id, { error: `unknown account '${accountId}'` });
56
- return;
57
- }
54
+ const acct = accountOf(args);
58
55
  const user = typeof args.user === 'string' ? args.user.trim() : '';
59
56
  if (!/^\d+$/.test(user)) throw new TrainError('discord_user_required', 'profile needs the Discord id of the person', { retryable: false });
60
57
  const found = await acct.client.users.fetch(user);
@@ -32,9 +32,7 @@ export async function appendFiles(
32
32
  files: OutgoingFile[],
33
33
  ): Promise<string[]> {
34
34
  const delivered: string[] = [];
35
- for (let i = 0; i < files.length; i++) {
36
- const file = files[i];
37
- if (!file) continue;
35
+ for (const [i, file] of files.entries()) {
38
36
  await appendFile(form, `files[${i}]`, file.path, file.name);
39
37
  delivered.push(file.kind);
40
38
  }
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@metro-labs/core/is-record';
1
2
  import { API } from './api-base.js';
2
3
 
3
4
  export class DiscordTokenError extends Error {}
@@ -25,9 +26,7 @@ function botFetch(token: string, path: string): Promise<Response> {
25
26
  async function readJson(res: Response): Promise<Record<string, unknown>> {
26
27
  try {
27
28
  const body: unknown = await res.json();
28
- return typeof body === 'object' && body !== null
29
- ? (body as Record<string, unknown>)
30
- : {};
29
+ return isRecord(body) ? body : {};
31
30
  } catch {
32
31
  throw new DiscordTokenError('Discord returned a response Metro could not read');
33
32
  }