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

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 (171) 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/marketplace/plugin/orchestrator.md +1 -1
  12. package/runtime/node_modules/@metro-labs/core/src/events.ts +4 -2
  13. package/runtime/node_modules/@metro-labs/core/src/lines.ts +12 -25
  14. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +45 -22
  15. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +0 -1
  16. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +8 -80
  17. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +3 -5
  18. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +4 -9
  19. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +3 -1
  20. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +9 -39
  21. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +1 -1
  22. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +9 -28
  23. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +0 -1
  24. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +3 -3
  25. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +8 -116
  26. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +11 -35
  27. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +7 -9
  28. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +1 -1
  29. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +1 -1
  30. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +1 -1
  31. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +1 -1
  32. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -3
  33. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +2 -2
  34. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +3 -29
  35. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +5 -8
  36. package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +1 -1
  37. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +5 -8
  38. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +3 -6
  39. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +25 -3
  40. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +2 -3
  41. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -2
  42. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +1 -1
  43. package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +6 -56
  44. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +3 -3
  45. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -1
  46. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +7 -6
  47. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +1 -1
  48. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +8 -7
  49. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -4
  50. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +1 -1
  51. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +2 -4
  52. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +19 -22
  53. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +1 -2
  54. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +6 -8
  55. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +3 -6
  56. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +1 -1
  57. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +2 -3
  58. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +28 -1
  59. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
  60. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +4 -4
  61. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +1 -1
  62. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +1 -1
  63. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +1 -1
  64. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +3 -2
  65. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +1 -1
  66. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +2 -2
  67. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +1 -1
  68. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +3 -3
  69. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +1 -1
  70. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +3 -3
  71. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +6 -6
  72. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +7 -7
  73. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +1 -1
  74. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -2
  75. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +1 -1
  76. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +10 -31
  77. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +1 -1
  78. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +1 -1
  79. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +2 -2
  80. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
  81. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +1 -2
  82. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +2 -2
  83. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +19 -1
  84. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
  85. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +3 -3
  86. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +2 -2
  87. package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +2 -2
  88. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +14 -0
  89. package/runtime/node_modules/@metro-labs/daemon/src/mcp/typing.ts +46 -0
  90. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +2 -2
  91. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
  92. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -2
  93. package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +1 -1
  94. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -4
  95. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +3 -8
  96. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +2 -2
  97. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +2 -2
  98. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +2 -2
  99. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +6 -7
  100. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +1 -1
  101. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +2 -2
  102. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +2 -2
  103. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +1 -1
  104. package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +5 -44
  105. package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +4 -4
  106. package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +6 -7
  107. package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +12 -13
  108. package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +5 -22
  109. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +1 -19
  110. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +7 -35
  111. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +5 -10
  112. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +1 -1
  113. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +1 -2
  114. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +1 -1
  115. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +11 -12
  116. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +2 -5
  117. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +0 -1
  118. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +1 -15
  119. package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +0 -6
  120. package/runtime/node_modules/@metro-labs/outlook/src/format.ts +0 -1
  121. package/runtime/node_modules/@metro-labs/outlook/src/state.ts +1 -1
  122. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +4 -22
  123. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +2 -2
  124. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +1 -3
  125. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +1 -1
  126. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +1 -2
  127. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +1 -1
  128. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +4 -37
  129. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +19 -23
  130. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +1 -4
  131. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +4 -9
  132. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +3 -3
  133. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +2 -0
  134. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -11
  135. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +2 -12
  136. package/runtime/node_modules/@metro-labs/threema/src/files.ts +4 -15
  137. package/runtime/node_modules/@metro-labs/threema/src/format.ts +2 -7
  138. package/runtime/node_modules/@metro-labs/whatsapp/package.json +1 -2
  139. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +5 -26
  140. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +14 -6
  141. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +5 -0
  142. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -3
  143. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +1 -1
  144. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +2 -0
  145. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -12
  146. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +1 -1
  147. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +14 -5
  148. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +1 -1
  149. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +31 -78
  150. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -4
  151. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +1 -6
  152. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +1 -1
  153. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +1 -1
  154. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +1 -1
  155. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +2 -2
  156. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
  157. package/runtime/runtime.json +1 -1
  158. package/runtime/node_modules/@metro-labs/core/src/identity.ts +0 -7
  159. package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +0 -2
  160. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +0 -12
  161. package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +0 -13
  162. package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +0 -1
  163. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +0 -29
  164. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +0 -43
  165. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +0 -165
  166. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -1
  167. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +0 -22
  168. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -1
  169. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -1
  170. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
  171. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -1
@@ -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(
@@ -128,6 +127,14 @@ function makeReact(clientFor: ClientFor): StationHandler {
128
127
  };
129
128
  }
130
129
 
130
+ function makeTyping(clientFor: ClientFor): StationHandler {
131
+ return async (id, args) => {
132
+ const { accountId, client, jid } = resolve(args, clientFor);
133
+ await guard(() => client.setTyping(jid, args.on !== false));
134
+ respond(id, { result: { ok: true, account: accountId } });
135
+ };
136
+ }
137
+
131
138
  function makeEdit(clientFor: ClientFor): StationHandler {
132
139
  return async (id, args) => {
133
140
  const { accountId, client, jid } = resolve(args, clientFor);
@@ -202,12 +209,13 @@ export function makeHandleCall(
202
209
  accounts: makeAccounts(clientFor),
203
210
  send: makeSend(clientFor),
204
211
  react: makeReact(clientFor),
212
+ typing: makeTyping(clientFor),
205
213
  edit: makeEdit(clientFor),
206
214
  delete: makeDelete(clientFor),
207
215
  resolve_sender: makeResolveSender(clientFor),
208
216
  set_profile: makeSetProfile(clientFor),
209
217
  profile: makeProfile(clientFor),
210
218
  },
211
- normalize: normalizeWhatsApp,
219
+ normalize: messagingAliases(),
212
220
  });
213
221
  }
@@ -50,6 +50,7 @@ export interface WAClient {
50
50
  sendText(jid: string, text: string, quotedId?: string): Promise<string>;
51
51
  sendMedia(jid: string, media: WAMedia, quotedId?: string): Promise<string>;
52
52
  sendReaction(jid: string, messageId: string, emoji: string): Promise<void>;
53
+ setTyping(jid: string, on: boolean): Promise<void>;
53
54
  editMessage(jid: string, messageId: string, text: string): Promise<void>;
54
55
  deleteMessage(jid: string, messageId: string): Promise<void>;
55
56
  reuploadMedia(m: WAMessage): Promise<WAMessage>;
@@ -339,6 +340,10 @@ export function createClient(account: WhatsAppAccount): WAClient {
339
340
  quotedId ? quotedOpts(st, jid, quotedId) : undefined,
340
341
  );
341
342
  },
343
+ async setTyping(jid, on) {
344
+ const sock = await ready(st);
345
+ await sock.sendPresenceUpdate(on ? 'composing' : 'paused', jid);
346
+ },
342
347
  async sendReaction(jid, messageId, emoji) {
343
348
  const target = targetKey(st.keys, jid, messageId, 'react to');
344
349
  await send(st, jid, { react: { text: emoji, key: target } });
@@ -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',
@@ -1,5 +1,5 @@
1
1
  import type { WAMessage } from 'baileys';
2
- import { emit } from './wire.js';
2
+ import { emit } from '@metro-labs/core/stations/station-runtime';
3
3
  import { reportAttachment } from '@metro-labs/core/stations/train-events';
4
4
  import { lineOf } from './accounts.js';
5
5
  import {
@@ -13,7 +13,9 @@ export const whatsappStation: Station = {
13
13
  'unreact',
14
14
  'edit',
15
15
  'delete',
16
+ 'typing',
16
17
  ]),
18
+ typingRefreshMs: 20_000,
17
19
  attachmentMode: 'canonical',
18
20
  profileFields: new Set<ProfileField>(PROFILE_FIELDS),
19
21
  readsProfiles: true,
@@ -15,10 +15,6 @@ import {
15
15
  } from '@metro-labs/core/stations/account-store';
16
16
  import { Line } from '@metro-labs/core/lines';
17
17
 
18
- const ACCOUNTS_FILE =
19
- process.env.XMTP_ACCOUNTS_FILE ??
20
- join(homedir(), '.metro', 'xmtp-accounts.json');
21
-
22
18
  export interface AccountConfig {
23
19
  id: string;
24
20
  privateKey?: string;
@@ -28,15 +24,8 @@ export interface AccountConfig {
28
24
 
29
25
  export const { loadAccounts } = makeAccountStore<AccountConfig>({
30
26
  prefix: 'xmtp',
31
- file: ACCOUNTS_FILE,
32
27
  validate(raw, die) {
33
- const seen = new Set<string>();
34
- for (const a of raw) {
35
- if (!a.id) die('account missing id');
36
- if (!a.privateKey) die(`account '${a.id}' needs a privateKey`);
37
- if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
38
- seen.add(a.id);
39
- }
28
+ for (const a of raw) if (!a.privateKey) die(`account '${a.id}' needs a privateKey`);
40
29
  },
41
30
  });
42
31
 
@@ -1,6 +1,6 @@
1
1
  import { errMsg } from '@metro-labs/core/log';
2
2
  import { accountForCall, convOf, lineOf, type Account } from './accounts.js';
3
- import { respond } from './wire.js';
3
+ import { respond } from '@metro-labs/core/stations/station-runtime';
4
4
  import type { GroupLike } from './labels.js';
5
5
 
6
6
  type Args = Record<string, unknown>;
@@ -1,6 +1,7 @@
1
1
  import { IdentifierKind } from '@xmtp/node-sdk';
2
2
  import { accountForCall, convOf, lineOf, parseLine } from './accounts.js';
3
- import { respond } from './wire.js';
3
+ import { respond } from '@metro-labs/core/stations/station-runtime';
4
+ import { resolveMsgId } from './wire.js';
4
5
  import { TrainError } from '@metro-labs/core/train-error';
5
6
  import {
6
7
  buildGroupInfo,
@@ -33,15 +34,23 @@ async function newDm(id: string, args: Args): Promise<void> {
33
34
  });
34
35
  }
35
36
 
36
- async function query(id: string, args: Args): Promise<void> {
37
- const { line, limit } = args as { line: string; limit?: number };
37
+ function upTo<T extends { id: string }>(all: T[], before: string | undefined): T[] {
38
+ if (!before) return all;
39
+ const target = resolveMsgId(before);
40
+ const at = all.findIndex((m) => m.id === target);
41
+ if (at < 0) throw new TrainError('NOT_FOUND', `message ${before} is not in this conversation`);
42
+ return all.slice(0, at);
43
+ }
44
+
45
+ async function read(id: string, args: Args): Promise<void> {
46
+ const { line, limit, before } = args as { line: string; limit?: number; before?: string };
38
47
  const { conv } = await convOf(line);
39
48
  if (!conv)
40
49
  throw new TrainError('NOT_FOUND', `conversation not found for ${line}`);
41
50
  const lim = Math.min(Math.max(1, limit ?? 20), 200);
42
51
  await conv.sync().catch(() => undefined);
43
52
  const all = await conv.messages();
44
- const slice = all.slice(-lim);
53
+ const slice = upTo(all, before).slice(-lim);
45
54
  const parsed = parseLine(line);
46
55
  if (!parsed)
47
56
  throw new TrainError('NOT_FOUND', `could not parse line ${line}`);
@@ -86,7 +95,7 @@ export const convHandlers: Record<string, Handler> = {
86
95
  newDm,
87
96
  updateChannelMeta,
88
97
  closeGroup,
89
- query,
98
+ read,
90
99
  groupInfo,
91
100
  listMembers,
92
101
  groupCreate: async (id, args) => {
@@ -1,5 +1,5 @@
1
1
  import { accountForCall, convOf, lineOf } from './accounts.js';
2
- import { respond } from './wire.js';
2
+ import { respond } from '@metro-labs/core/stations/station-runtime';
3
3
  import { warmGroupName } from './conv-helpers.js';
4
4
  import { mergeAppData, readAppData, type GroupLike } from './labels.js';
5
5
  import { TrainError } from '@metro-labs/core/train-error';
@@ -1,19 +1,19 @@
1
- import { ReactionAction, ReactionSchema } from '@xmtp/node-sdk';
1
+ import { ReactionAction, ReactionSchema, type Conversation } from '@xmtp/node-sdk';
2
2
  import type { Reply } from '@xmtp/node-bindings';
3
3
  import {
4
4
  AttachmentCodec,
5
5
  type Attachment,
6
6
  } from '@xmtp/content-type-remote-attachment';
7
7
  import { convOf } from './accounts.js';
8
- import { resolveMsgId, respond } from './wire.js';
8
+ import { resolveMsgId } from './wire.js';
9
9
  import { emitOutbound } from './emit.js';
10
10
  import { PollCodec, buildPollContent } from './codecs.js';
11
11
  import { convHandlers } from './actions-conv.js';
12
- import { normalizeXmtp } from '@metro-labs/core/stations/messaging-normalize';
12
+ import { messagingAliases } from '@metro-labs/core/stations/messaging-normalize';
13
13
  import { TrainError } from '@metro-labs/core/train-error';
14
14
  import { claimNameAction, nameAction, setProfile } from './profile.js';
15
15
  import { profileAction } from './sender.js';
16
- import { makeStation, type CallMsg } from '@metro-labs/core/stations/station-runtime';
16
+ import { makeStation, respond, type CallMsg } from '@metro-labs/core/stations/station-runtime';
17
17
 
18
18
  type Args = Record<string, unknown>;
19
19
 
@@ -32,68 +32,47 @@ async function send(id: string, args: Args): Promise<void> {
32
32
  }
33
33
 
34
34
  async function ask(id: string, args: Args): Promise<void> {
35
- const { line, pollId } = args as { line: string; pollId?: string };
35
+ const { line } = args as { line: string };
36
36
  const { acct, conv } = await convOf(line);
37
37
  if (!conv) throw noConv(line);
38
- const fallbackId = `poll_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
39
- const mintedId =
40
- pollId ??
41
- (typeof crypto?.randomUUID === 'function'
42
- ? crypto.randomUUID()
43
- : fallbackId);
38
+ const mintedId = crypto.randomUUID();
44
39
  const { poll, title } = buildPollContent(args, mintedId);
45
40
  const sentId = await conv.send(new PollCodec().encode(poll));
46
41
  emitOutbound(acct.cfg.id, line, sentId, `📊 Poll: ${title}`);
47
42
  respond(id, { result: { messageId: sentId, pollId: mintedId } });
48
43
  }
49
44
 
45
+ async function referenceInboxOf(conv: Conversation, xmtpMsgId: string): Promise<string> {
46
+ const recent = await conv.messages({ limit: 200, direction: 1 });
47
+ const inbox = recent.find((m) => m.id === xmtpMsgId)?.senderInboxId;
48
+ if (!inbox)
49
+ throw new TrainError('NOT_FOUND', `could not resolve referenceInboxId for ${xmtpMsgId}`);
50
+ return inbox;
51
+ }
52
+
50
53
  async function react(id: string, args: Args): Promise<void> {
51
- const {
52
- line,
53
- messageId,
54
- emoji,
55
- action: reactAction,
56
- schema: reactSchema,
57
- } = args as {
54
+ const { line, messageId, emoji, action: reactAction } = args as {
58
55
  line: string;
59
56
  messageId: string;
60
57
  emoji: string;
61
58
  action?: 'added' | 'removed';
62
- schema?: string;
63
- referenceInboxId?: string;
64
59
  };
65
60
  const { acct, conv } = await convOf(line);
66
61
  if (!conv) throw noConv(line);
67
62
  const xmtpMsgId = resolveMsgId(messageId);
68
- let refInbox = (args as { referenceInboxId?: string }).referenceInboxId;
69
- if (!refInbox) {
70
- const recent = await conv.messages({ limit: 200, direction: 1 });
71
- refInbox = recent.find((m) => m.id === xmtpMsgId)?.senderInboxId;
72
- if (!refInbox)
73
- throw new TrainError(
74
- 'NOT_FOUND',
75
- `could not resolve referenceInboxId for ${xmtpMsgId}`,
76
- );
77
- }
78
- const schemaEnum =
79
- reactSchema === 'custom'
80
- ? ReactionSchema.Custom
81
- : reactSchema === 'shortcode'
82
- ? ReactionSchema.Shortcode
83
- : ReactionSchema.Unicode;
63
+ const removed = reactAction === 'removed';
84
64
  const sentId = await conv.sendReaction({
85
65
  reference: xmtpMsgId,
86
- referenceInboxId: refInbox,
87
- action:
88
- reactAction === 'removed' ? ReactionAction.Removed : ReactionAction.Added,
66
+ referenceInboxId: await referenceInboxOf(conv, xmtpMsgId),
67
+ action: removed ? ReactionAction.Removed : ReactionAction.Added,
89
68
  content: emoji,
90
- schema: schemaEnum,
69
+ schema: ReactionSchema.Unicode,
91
70
  });
92
71
  emitOutbound(
93
72
  acct.cfg.id,
94
73
  line,
95
74
  sentId,
96
- `[react ${emoji}${reactAction === 'removed' ? ' (removed)' : ''}]`,
75
+ `[react ${emoji}${removed ? ' (removed)' : ''}]`,
97
76
  { type: 'react', emoji, targetId: xmtpMsgId },
98
77
  );
99
78
  respond(id, { result: { messageId: sentId } });
@@ -151,54 +130,28 @@ const IMG_MIME_BY_EXT: Record<string, string> = {
151
130
  webp: 'image/webp',
152
131
  };
153
132
 
154
- async function loadImageBytes(
155
- path: string | undefined,
156
- dataB64: string | undefined,
157
- ): Promise<Uint8Array> {
158
- if (path) {
159
- const { readFileSync } = await import('node:fs');
160
- return new Uint8Array(readFileSync(path));
161
- }
162
- if (dataB64) return new Uint8Array(Buffer.from(dataB64, 'base64'));
163
- throw badArgs('sendImage requires path or dataB64');
164
- }
165
-
166
- function imageMime(
167
- mimeType: string | undefined,
168
- filename: string | undefined,
169
- path: string | undefined,
170
- ): string {
171
- if (mimeType) return mimeType;
172
- const ext = (filename ?? path ?? '').toLowerCase().split('.').pop() ?? '';
133
+ function imageMime(path: string): string {
134
+ const ext = path.toLowerCase().split('.').pop() ?? '';
173
135
  return IMG_MIME_BY_EXT[ext] ?? 'image/png';
174
136
  }
175
137
 
176
- function imageFilename(
177
- filename: string | undefined,
178
- path: string | undefined,
179
- ): string {
180
- if (filename) return filename;
181
- const baseName = path ? path.split('/').pop() : undefined;
138
+ function imageFilename(path: string): string {
139
+ const baseName = path.split('/').pop();
182
140
  return baseName != null && baseName !== '' ? baseName : 'image.png';
183
141
  }
184
142
 
185
143
  async function sendImage(id: string, args: Args): Promise<void> {
186
- const { line, path, dataB64, filename, mimeType } = args as {
187
- line: string;
188
- path?: string;
189
- dataB64?: string;
190
- filename?: string;
191
- mimeType?: string;
192
- };
144
+ const { line, path } = args as { line: string; path?: string };
145
+ if (!path) throw badArgs('sendImage requires path');
193
146
  const { acct, conv } = await convOf(line);
194
147
  if (!conv) throw noConv(line);
195
- const bytes = await loadImageBytes(path, dataB64);
196
- const mime = imageMime(mimeType, filename, path);
197
- const fname = imageFilename(filename, path);
148
+ const { readFileSync } = await import('node:fs');
149
+ const mime = imageMime(path);
150
+ const fname = imageFilename(path);
198
151
  const attachment: Attachment = {
199
152
  filename: fname,
200
153
  mimeType: mime,
201
- data: bytes,
154
+ data: new Uint8Array(readFileSync(path)),
202
155
  };
203
156
  const sentId = await conv.send(new AttachmentCodec().encode(attachment));
204
157
  emitOutbound(acct.cfg.id, line, sentId, `[${mime.split('/')[0]}: ${fname}]`);
@@ -240,5 +193,5 @@ export type { CallMsg };
240
193
 
241
194
  export const handleCall = makeStation({
242
195
  handlers,
243
- normalize: normalizeXmtp,
196
+ normalize: messagingAliases({ action: 'removed' }),
244
197
  });
@@ -1,7 +1,8 @@
1
1
  import type { Conversation, DecodedMessage } from '@xmtp/node-sdk';
2
2
  import { lineOf } from './accounts.js';
3
3
  import { selfUri } from '@metro-labs/core/stations/train-events';
4
- import { emit, mintId, rememberSent, rememberUid } from './wire.js';
4
+ import { emit, mintId } from '@metro-labs/core/stations/station-runtime';
5
+ import { rememberSent, rememberUid } from './wire.js';
5
6
  import { typedEnvelope, type EnvelopeCtx } from './emit-payloads.js';
6
7
  import type { StructuredEvent } from '@metro-labs/core/events';
7
8
 
@@ -53,16 +54,14 @@ export function emitOutbound(
53
54
  rememberUid(uid, messageId);
54
55
  rememberSent(messageId);
55
56
  emit({
56
- kind: 'outbound',
57
57
  id: uid,
58
58
  ts: new Date().toISOString(),
59
59
  station: 'xmtp',
60
60
  line,
61
- from: selfUri('xmtp', accountId),
61
+ from: selfUri(),
62
62
  to: line,
63
63
  message_id: messageId,
64
64
  text,
65
- account: accountId,
66
65
  ...(event ? { event } : {}),
67
66
  payload: { account: accountId },
68
67
  });
@@ -68,12 +68,7 @@ async function handleStreamMessage(
68
68
  const env = envelope(id, msg, conv);
69
69
  Object.assign(env, senderFieldsNow(acct, msg.senderInboxId));
70
70
  const name = await groupNameFor(msg.conversationId, conv);
71
- if (name) {
72
- env.line_name = name;
73
- env.lineName = name;
74
- const p = (env.payload ?? {}) as Record<string, unknown>;
75
- env.payload = { ...p, lineName: name };
76
- }
71
+ if (name) env.line_name = name;
77
72
  emitInbound(id, env);
78
73
  }
79
74
 
@@ -13,7 +13,7 @@ import {
13
13
  import { accountForCall } from './accounts.js';
14
14
  import { claimName, nameOf, parseLabel } from './names.js';
15
15
  import { sendSponsored, type SmartAccount } from './smart.js';
16
- import { respond } from './wire.js';
16
+ import { respond } from '@metro-labs/core/stations/station-runtime';
17
17
 
18
18
  export const BASENAME_REGISTRY = '0xB94704422c2a1E396835A571837Aa5AE53285a95' as const;
19
19
  export const BASENAME_L2_RESOLVER = '0xC6d566A56A1aFf6508b41f6c90ff131615583BCD' as const;
@@ -2,7 +2,7 @@ import { createPublicClient, http, namehash, type Hex } from 'viem';
2
2
  import { base } from 'viem/chains';
3
3
  import { normalize } from 'viem/ens';
4
4
  import { makeProfileCache, type SenderProfile as Profile } from '@metro-labs/core/stations/sender-profile';
5
- import { respond } from './wire.js';
5
+ import { respond } from '@metro-labs/core/stations/station-runtime';
6
6
  import { TrainError } from '@metro-labs/core/train-error';
7
7
  import { accountForCall, accounts, type Account } from './accounts.js';
8
8
  import { resolveAddresses } from './conv-helpers.js';
@@ -65,7 +65,7 @@ export async function xmtpSendAttachments(
65
65
  ): Promise<string[]> {
66
66
  const sent: string[] = [];
67
67
  for (const a of atts) {
68
- const src = a.path ?? a.url ?? '';
68
+ const src = a.path ?? '';
69
69
  if (!src) continue;
70
70
  const mime = a.mime ?? guessMime(src);
71
71
  if (isImageMime(mime) || isImageExt(src)) {
@@ -1,6 +1,6 @@
1
1
  import { errMsg } from '@metro-labs/core/log';
2
2
  import { emitInbound, selfUri } 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
 
5
5
  const WHISPER_BIN = process.env.METRO_WHISPER_BIN ?? 'whisper-cli';
6
6
  const WHISPER_MODEL =
@@ -67,7 +67,7 @@ export async function transcribeAndEmit(
67
67
  ts: new Date().toISOString(),
68
68
  station: 'xmtp',
69
69
  line,
70
- from: selfUri('xmtp', accountId),
70
+ from: selfUri(),
71
71
  text: `🎙️ ${text}`,
72
72
  payload: {
73
73
  contentType: 'transcript',
@@ -1,5 +1,3 @@
1
- export { emit, respond, mintId } from '@metro-labs/core/stations/station-runtime';
2
-
3
1
  const UID_MAP_MAX = 5000;
4
2
  const uidToXmtp = new Map<string, string>();
5
3
 
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.201"
2
+ "version": "0.1.0-beta.203"
3
3
  }
@@ -1,7 +0,0 @@
1
- import { Line } from './lines.js';
2
-
3
- export const userSelf = (): Line =>
4
- (process.env.METRO_FROM ?? 'metro://user') as Line;
5
-
6
- export const daemonSelf = (): Line =>
7
- (process.env.METRO_FROM ?? process.env.METRO_SELF_URI ?? 'metro://user') as Line;
@@ -1,2 +0,0 @@
1
- export const NEEDS_APPROVAL = (where: string, tool: string): string =>
2
- `Needs the owner's approval for ${where} (${tool}). Make this exact call from a background worker so Claude Code asks the owner in chat, and wait for the answer. An approval given only in the terminal does not count.`;
@@ -1,12 +0,0 @@
1
- const accountStrippedLine = (line: string): string => {
2
- const parts = line.split('/');
3
- if (parts.length < 5) return line;
4
- return [parts[0], parts[1], parts[2], ...parts.slice(4)].join('/');
5
- };
6
-
7
- export const dedupeKey = (
8
- station: string,
9
- line: string,
10
- kind: string,
11
- messageId: string,
12
- ): string => `${station} ${accountStrippedLine(line)} ${kind} ${messageId}`;
@@ -1,13 +0,0 @@
1
- const shortId = (id: string): string => (id.length > 10 ? `${id.slice(0, 6)}…` : id);
2
-
3
- export function reactionEmoji(raw: unknown): string {
4
- if (typeof raw === 'string') return raw;
5
- const obj = raw as { name?: string; reaction?: string } | undefined;
6
- return obj?.name ?? obj?.reaction ?? '';
7
- }
8
-
9
- export function reactContent(emoji: string, target: string, removed: boolean): string {
10
- const verb = removed ? 'removed from' : 'reacted to';
11
- const label = removed ? emoji || 'reaction' : emoji || 'reacted';
12
- return `${label} ${verb} message ${shortId(target)}`.trim();
13
- }
@@ -1 +0,0 @@
1
- export const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
@@ -1,29 +0,0 @@
1
- import { authHeaders, type ConnectorAuth, type OAuthAuth } from './verify.js';
2
-
3
- export type RelayTarget =
4
- | { kind: 'ok'; url: string; headers: Record<string, string> }
5
- | { kind: 'missing' }
6
- | { kind: 'signin' };
7
-
8
- function staleUsable(auth: OAuthAuth, now = Date.now()): boolean {
9
- return auth.expiresAt === undefined || auth.expiresAt > now;
10
- }
11
-
12
- export function unrefreshedTarget(
13
- url: string,
14
- auth: OAuthAuth,
15
- force: boolean,
16
- now = Date.now(),
17
- ): RelayTarget {
18
- if (force) return { kind: 'signin' };
19
- return { kind: 'ok', url, headers: staleUsable(auth, now) ? authHeaders(auth) : {} };
20
- }
21
-
22
- export function fixedTarget(
23
- url: string,
24
- auth: Exclude<ConnectorAuth, OAuthAuth>,
25
- force: boolean,
26
- ): RelayTarget {
27
- if (force) return { kind: 'signin' };
28
- return { kind: 'ok', url, headers: authHeaders(auth) };
29
- }