@stage-labs/metro 0.1.0-beta.152 → 0.1.0-beta.153

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.152",
3
+ "version": "0.1.0-beta.153",
4
4
  "description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro",
3
- "version": "0.1.0-beta.152",
3
+ "version": "0.1.0-beta.153",
4
4
  "description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
5
5
  "author": {
6
6
  "name": "Bonustrack",
@@ -23,7 +23,8 @@
23
23
  "./stations/station-runtime": "./src/stations/station-runtime.ts",
24
24
  "./stations/account-store": "./src/stations/account-store.ts",
25
25
  "./stations/attachments": "./src/stations/attachments.ts",
26
- "./stations/messaging-normalize": "./src/stations/messaging-normalize.ts"
26
+ "./stations/messaging-normalize": "./src/stations/messaging-normalize.ts",
27
+ "./stations/profile": "./src/stations/profile.ts"
27
28
  },
28
29
  "scripts": {
29
30
  "build": "tsc",
@@ -0,0 +1,58 @@
1
+ import { TrainError } from '../train-error.js';
2
+
3
+ export const PROFILE_FIELDS = ['name', 'bio', 'avatar'] as const;
4
+ export type ProfileField = (typeof PROFILE_FIELDS)[number];
5
+
6
+ export const NAME_MAX = 64;
7
+ export const BIO_MAX = 512;
8
+
9
+ export interface ProfileAvatar {
10
+ path: string;
11
+ mime: string;
12
+ name: string;
13
+ }
14
+
15
+ export interface ProfileChange {
16
+ name?: string;
17
+ bio?: string;
18
+ avatar?: ProfileAvatar;
19
+ }
20
+
21
+ export interface ProfileApplied {
22
+ account: string;
23
+ applied: ProfileField[];
24
+ }
25
+
26
+ const text = (value: unknown, field: string, max: number): string | undefined => {
27
+ if (value === undefined) return undefined;
28
+ if (typeof value !== 'string') throw new TrainError('bad_request', `${field} must be a string`);
29
+ const trimmed = value.trim();
30
+ if (trimmed === '') throw new TrainError('bad_request', `${field} cannot be empty`);
31
+ if (trimmed.length > max) throw new TrainError('bad_request', `${field} is over ${String(max)} characters`);
32
+ return trimmed;
33
+ };
34
+
35
+ function avatarOf(value: unknown): ProfileAvatar | undefined {
36
+ if (value === undefined) return undefined;
37
+ const raw = (value ?? {}) as Record<string, unknown>;
38
+ if (typeof raw.path !== 'string' || raw.path === '') throw new TrainError('bad_request', 'avatar needs a local file');
39
+ return { path: raw.path, mime: typeof raw.mime === 'string' ? raw.mime : 'application/octet-stream', name: typeof raw.name === 'string' ? raw.name : 'avatar' };
40
+ }
41
+
42
+ export function parseProfileChange(args: Record<string, unknown>): ProfileChange {
43
+ const change: ProfileChange = {};
44
+ const name = text(args.name, 'name', NAME_MAX);
45
+ const bio = text(args.bio, 'bio', BIO_MAX);
46
+ const avatar = avatarOf(args.avatar);
47
+ if (name !== undefined) change.name = name;
48
+ if (bio !== undefined) change.bio = bio;
49
+ if (avatar !== undefined) change.avatar = avatar;
50
+ if (Object.keys(change).length === 0) throw new TrainError('bad_request', 'set_profile needs at least one of name, bio, avatar');
51
+ return change;
52
+ }
53
+
54
+ export const fieldsOf = (change: ProfileChange): ProfileField[] => PROFILE_FIELDS.filter((f) => change[f] !== undefined);
55
+
56
+ export const assertImage = (avatar: ProfileAvatar): void => {
57
+ if (!avatar.mime.startsWith('image/')) throw new TrainError('bad_request', `avatar must be an image, not ${avatar.mime}`);
58
+ };
@@ -1,3 +1,5 @@
1
+ import type { ProfileField } from './profile.js';
2
+
1
3
  export type Verb =
2
4
  | 'send'
3
5
  | 'reply'
@@ -98,6 +100,7 @@ export interface Station {
98
100
  messageVerbs: ReadonlySet<Verb>;
99
101
  groupOps?: ReadonlySet<GroupOp>;
100
102
  resolvesSenders?: boolean;
103
+ profileFields?: ReadonlySet<ProfileField>;
101
104
  attachmentMode: AttachmentMode;
102
105
  sendAttachments?(
103
106
  line: string,
@@ -0,0 +1,92 @@
1
+ import type { CanonicalAttachment, Station, ToolResult } from '@metro-labs/core/stations/types';
2
+ import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
3
+ import { str } from '@metro-labs/core/str';
4
+ import { STATIONS, stationByName } from '../stations/registry.js';
5
+ import { cleanupAttachments, resolveAttachments, type ResolvedAttachment } from '../stations/attach-resolve.js';
6
+ import { errResult, makeCtx, okJson, toErr } from './ctx.js';
7
+ import { allowedAgents, currentIdentity } from './request-identity.js';
8
+
9
+ const SOURCE = { type: 'string' };
10
+
11
+ export const SET_PROFILE_TOOL = {
12
+ name: 'set_profile',
13
+ description:
14
+ 'Change how one of your accounts presents itself on its network: the display name, the bio ' +
15
+ '(about line) and the avatar. Args: station (required), account? (which account of that station; ' +
16
+ 'needed only when you have several), name?, bio?, avatar? (an image given like an attachment: ' +
17
+ 'exactly one of upload, url, path, data, plus mime and name). At least one of name, bio, avatar. ' +
18
+ 'Each station takes what its network allows (`list_accounts` reports the fields under ' +
19
+ '`profiles`); a field the station cannot set is refused by name, never dropped. Discord changes ' +
20
+ 'the bot username and app description (Discord allows two username changes an hour); Telegram ' +
21
+ 'bots set their name, description and photo; a Telegram user account sets its first name, bio ' +
22
+ '(70 chars) and photo; WhatsApp sets the profile name, about and picture. Visible to everyone ' +
23
+ 'who talks to the account, so do it when asked, not on your own. Returns {account, applied}.',
24
+ inputSchema: {
25
+ type: 'object',
26
+ properties: {
27
+ station: { type: 'string', description: 'discord-bot | telegram-bot | telegram | whatsapp' },
28
+ account: { type: 'string', description: 'The account id on that station, from list_accounts.' },
29
+ name: { type: 'string', description: 'The display name.' },
30
+ bio: { type: 'string', description: 'The bio or about line.' },
31
+ avatar: {
32
+ type: 'object',
33
+ description: 'The new picture, as an attachment source.',
34
+ properties: { upload: SOURCE, url: SOURCE, path: SOURCE, data: SOURCE, mime: SOURCE, name: SOURCE },
35
+ },
36
+ },
37
+ required: ['station'],
38
+ },
39
+ };
40
+
41
+ export const profileCapabilities = (): Record<string, ProfileField[]> => {
42
+ const out: Record<string, ProfileField[]> = {};
43
+ for (const s of STATIONS) if (s.profileFields !== undefined) out[s.name] = [...s.profileFields];
44
+ return out;
45
+ };
46
+
47
+ const asked = (a: Record<string, unknown>): ProfileField[] => PROFILE_FIELDS.filter((f) => a[f] !== undefined);
48
+
49
+ type Target = { station: Station; fields: ProfileField[] } | { refused: string };
50
+
51
+ function targetOf(a: Record<string, unknown>): Target {
52
+ const name = str(a.station);
53
+ if (!name) return { refused: 'set_profile requires `station`' };
54
+ const station = stationByName(name);
55
+ if (!station) return { refused: `no station named ${name}` };
56
+ const fields = station.profileFields;
57
+ if (fields === undefined) return { refused: `${name} accounts have no profile metro can set` };
58
+ const wanted = asked(a);
59
+ if (wanted.length === 0) return { refused: 'set_profile needs at least one of name, bio, avatar' };
60
+ const refused = wanted.filter((f) => !fields.has(f));
61
+ if (refused.length > 0) return { refused: `${name} cannot set ${refused.join(', ')}; it takes ${[...fields].join(', ')}` };
62
+ return { station, fields: wanted };
63
+ }
64
+
65
+ function trainArgs(a: Record<string, unknown>, picture: ResolvedAttachment | undefined): Record<string, unknown> {
66
+ const args: Record<string, unknown> = {};
67
+ if (typeof a.account === 'string') args.account = a.account;
68
+ if (typeof a.name === 'string') args.name = a.name;
69
+ if (typeof a.bio === 'string') args.bio = a.bio;
70
+ if (picture !== undefined) args.avatar = { path: picture.path, mime: picture.mime, name: picture.name };
71
+ return args;
72
+ }
73
+
74
+ export async function dispatchSetProfile(a: Record<string, unknown>): Promise<ToolResult> {
75
+ const target = targetOf(a);
76
+ if ('refused' in target) return errResult(target.refused);
77
+ const avatar = a.avatar === undefined ? [] : [a.avatar as CanonicalAttachment];
78
+ let resolved: ResolvedAttachment[];
79
+ try {
80
+ resolved = await resolveAttachments(avatar, { allowed: allowedAgents(currentIdentity()) });
81
+ } catch (e) {
82
+ return toErr('set_profile', e);
83
+ }
84
+ try {
85
+ const { result } = await makeCtx(target.station.name).call('set_profile', trainArgs(a, resolved[0]));
86
+ return okJson(result);
87
+ } catch (e) {
88
+ return toErr('set_profile', e);
89
+ } finally {
90
+ await cleanupAttachments(resolved);
91
+ }
92
+ }
@@ -21,6 +21,7 @@ import {
21
21
  dispatchRemoveMembers,
22
22
  } from './group-tools.js';
23
23
  import { dispatchCreateUpload } from './upload-tool.js';
24
+ import { dispatchSetProfile, profileCapabilities, SET_PROFILE_TOOL } from './profile-tool.js';
24
25
  import { callTargetDenied, lineTargetDenied } from '../agents/scope.js';
25
26
  import { SOURCE_KEYS } from '../stations/attach-resolve.js';
26
27
  import { decodedLengthOf } from '../stations/attach-inline.js';
@@ -49,6 +50,7 @@ const CORE_DISPATCH: Record<
49
50
  remove_members: dispatchRemoveMembers,
50
51
  export_invite: dispatchInviteLink,
51
52
  create_upload: dispatchCreateUpload,
53
+ set_profile: dispatchSetProfile,
52
54
  };
53
55
 
54
56
  const toolList = (): { tools: unknown[] } => ({
@@ -62,6 +64,7 @@ const toolList = (): { tools: unknown[] } => ({
62
64
  })),
63
65
  ),
64
66
  LIST_ACCOUNTS_TOOL,
67
+ SET_PROFILE_TOOL,
65
68
  ],
66
69
  });
67
70
 
@@ -82,6 +85,7 @@ async function handleListAccounts(
82
85
  return okJson({
83
86
  accounts: await gatherAccounts(allowedAgents(identity)),
84
87
  capabilities: accountStationCapabilities(),
88
+ profiles: profileCapabilities(),
85
89
  });
86
90
  } catch (e) {
87
91
  return errResult(`metro list_accounts failed: ${String(e)}`);
@@ -92,7 +96,7 @@ function stationForTool(
92
96
  name: string,
93
97
  args: Record<string, unknown>,
94
98
  ): string | undefined {
95
- if (name === 'create_group') return str(args.station) || undefined;
99
+ if (name === 'create_group' || name === 'set_profile') return str(args.station) || undefined;
96
100
  return STATION_TOOLS.get(name)?.station.name;
97
101
  }
98
102
 
@@ -27,6 +27,7 @@ import {
27
27
  } from '@metro-labs/core/stations/station-runtime';
28
28
  import { joinVoice, leaveVoice, voiceDebug, voiceTranscribe } from './voice.js';
29
29
  import { speak } from './voice-speak.js';
30
+ import { setProfile } from './profile.js';
30
31
  import { discordMembers } from './members.js';
31
32
  import {
32
33
  groupAddHandler,
@@ -375,6 +376,7 @@ const HANDLERS: Record<string, StationHandler> = {
375
376
  channel,
376
377
  listMembers,
377
378
  set_presence: presence,
379
+ set_profile: setProfile,
378
380
  joinVoice,
379
381
  leaveVoice,
380
382
  speak,
@@ -0,0 +1,48 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Client } from 'discord.js';
3
+ import { TrainError } from '@metro-labs/core/train-error';
4
+ import {
5
+ assertImage,
6
+ fieldsOf,
7
+ parseProfileChange,
8
+ type ProfileApplied,
9
+ type ProfileChange,
10
+ } from '@metro-labs/core/stations/profile';
11
+ import { accountFor, accounts } from './accounts.js';
12
+ import { respond } from './wire.js';
13
+
14
+ export async function applyProfile(
15
+ client: Client,
16
+ change: ProfileChange,
17
+ ): Promise<void> {
18
+ const user = client.user;
19
+ if (!user) throw new TrainError('discord_not_ready', 'the gateway is not ready yet');
20
+ if (change.avatar !== undefined) assertImage(change.avatar);
21
+ const avatar = change.avatar === undefined ? undefined : await readFile(change.avatar.path);
22
+ if (change.name !== undefined || avatar !== undefined)
23
+ await user.edit({
24
+ ...(change.name === undefined ? {} : { username: change.name }),
25
+ ...(avatar === undefined ? {} : { avatar }),
26
+ });
27
+ if (change.bio !== undefined) {
28
+ const app = client.application;
29
+ if (!app) throw new TrainError('discord_not_ready', 'the application is not loaded yet');
30
+ await app.edit({ description: change.bio });
31
+ }
32
+ }
33
+
34
+ export async function setProfile(
35
+ id: string,
36
+ args: Record<string, unknown>,
37
+ ): Promise<void> {
38
+ const accountId = accountFor({ account: args.account as string | undefined });
39
+ const acct = accounts.get(accountId);
40
+ if (!acct) {
41
+ respond(id, { error: `unknown account '${accountId}'` });
42
+ return;
43
+ }
44
+ const change = parseProfileChange(args);
45
+ await applyProfile(acct.client, change);
46
+ const result: ProfileApplied = { account: accountId, applied: fieldsOf(change) };
47
+ respond(id, { result });
48
+ }
@@ -1,4 +1,5 @@
1
1
  import type { GroupOp, Station, Verb } from '@metro-labs/core/stations/types';
2
+ import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
2
3
 
3
4
  export const discordBotStation: Station = {
4
5
  name: 'discord-bot',
@@ -15,5 +16,6 @@ export const discordBotStation: Station = {
15
16
  ]),
16
17
  groupOps: new Set<GroupOp>(['create_group', 'add_members', 'remove_members']),
17
18
  attachmentMode: 'canonical',
19
+ profileFields: new Set<ProfileField>(PROFILE_FIELDS),
18
20
  tools: [],
19
21
  };
@@ -14,6 +14,7 @@ import {
14
14
  type CanonicalAttachment,
15
15
  } from './media-actions.js';
16
16
  import { clampLimit, shapeHistory } from './history.js';
17
+ import { makeSetProfile } from './profile.js';
17
18
  import { fetchMembers, isRestricted, restrictedMemberList } from './members.js';
18
19
  import {
19
20
  groupAddMembers,
@@ -248,6 +249,7 @@ export function makeHandleCall(
248
249
  groupAddMembers: makeGroupAdd(clientFor),
249
250
  groupRemoveMembers: makeGroupRemove(clientFor),
250
251
  groupInviteLink: makeGroupInvite(clientFor),
252
+ set_profile: makeSetProfile(clientFor),
251
253
  },
252
254
  normalize: normalizeTelegramUser,
253
255
  });
@@ -0,0 +1,36 @@
1
+ import type { TelegramClient } from '@mtcute/bun';
2
+ import {
3
+ assertImage,
4
+ fieldsOf,
5
+ parseProfileChange,
6
+ type ProfileApplied,
7
+ type ProfileChange,
8
+ } from '@metro-labs/core/stations/profile';
9
+ import { respond, type StationHandler } from '@metro-labs/core/stations/station-runtime';
10
+ import { str } from '@metro-labs/core/str';
11
+ import { accountFor } from './accounts.js';
12
+ import type { UserClient } from './client.js';
13
+
14
+ const BIO_MAX = 70;
15
+
16
+ export async function applyProfile(tg: TelegramClient, change: ProfileChange): Promise<void> {
17
+ if (change.name !== undefined || change.bio !== undefined)
18
+ await tg.updateProfile({
19
+ ...(change.name === undefined ? {} : { firstName: change.name }),
20
+ ...(change.bio === undefined ? {} : { bio: change.bio.slice(0, BIO_MAX) }),
21
+ });
22
+ if (change.avatar !== undefined) {
23
+ assertImage(change.avatar);
24
+ await tg.setMyProfilePhoto({ type: 'photo', media: `file:${change.avatar.path}` });
25
+ }
26
+ }
27
+
28
+ export function makeSetProfile(clientFor: (accountId: string) => UserClient): StationHandler {
29
+ return async (id, args) => {
30
+ const accountId = accountFor({ account: str(args.account) });
31
+ const change = parseProfileChange(args);
32
+ await applyProfile(clientFor(accountId).tg, change);
33
+ const result: ProfileApplied = { account: accountId, applied: fieldsOf(change) };
34
+ respond(id, { result });
35
+ };
36
+ }
@@ -1,4 +1,5 @@
1
1
  import type { GroupOp, Station, Verb } from '@metro-labs/core/stations/types';
2
+ import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
2
3
 
3
4
  export const telegramStation: Station = {
4
5
  name: 'telegram',
@@ -20,5 +21,6 @@ export const telegramStation: Station = {
20
21
  'invite_link',
21
22
  ]),
22
23
  attachmentMode: 'canonical',
24
+ profileFields: new Set<ProfileField>(PROFILE_FIELDS),
23
25
  tools: [],
24
26
  };
@@ -23,6 +23,7 @@ import {
23
23
  sendLocation,
24
24
  sendMedia,
25
25
  } from './media-actions.js';
26
+ import { setProfile } from './profile.js';
26
27
 
27
28
 
28
29
 
@@ -271,6 +272,7 @@ const HANDLERS: Record<string, StationHandler> = {
271
272
  edit,
272
273
  delete: remove,
273
274
  listMembers,
275
+ set_profile: setProfile,
274
276
  send_photo: (id, args) =>
275
277
  media(id, 'sendPhoto', 'photo', ((args.caption as string) ?? '') + ' [image]', args),
276
278
  send_document: (id, args) =>
@@ -0,0 +1,33 @@
1
+ import { appendFile } from '@metro-labs/core/stations/attachments';
2
+ import {
3
+ assertImage,
4
+ fieldsOf,
5
+ parseProfileChange,
6
+ type ProfileApplied,
7
+ type ProfileAvatar,
8
+ type ProfileChange,
9
+ } from '@metro-labs/core/stations/profile';
10
+ import { accountFor, tg, tgForm } from './accounts.js';
11
+ import { respond } from './wire.js';
12
+
13
+ async function setPhoto(accountId: string, avatar: ProfileAvatar): Promise<void> {
14
+ assertImage(avatar);
15
+ const form = new FormData();
16
+ form.append('photo', JSON.stringify({ type: 'static', photo: 'attach://avatar' }));
17
+ await appendFile(form, 'avatar', avatar.path, avatar.name);
18
+ await tgForm(accountId, 'setMyProfilePhoto', form);
19
+ }
20
+
21
+ export async function applyProfile(accountId: string, change: ProfileChange): Promise<void> {
22
+ if (change.name !== undefined) await tg(accountId, 'setMyName', { name: change.name });
23
+ if (change.bio !== undefined) await tg(accountId, 'setMyDescription', { description: change.bio });
24
+ if (change.avatar !== undefined) await setPhoto(accountId, change.avatar);
25
+ }
26
+
27
+ export async function setProfile(id: string, args: Record<string, unknown>): Promise<void> {
28
+ const accountId = accountFor({ account: args.account as string | undefined });
29
+ const change = parseProfileChange(args);
30
+ await applyProfile(accountId, change);
31
+ const result: ProfileApplied = { account: accountId, applied: fieldsOf(change) };
32
+ respond(id, { result });
33
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Station, Verb } from '@metro-labs/core/stations/types';
2
+ import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
2
3
 
3
4
  export const telegramBotStation: Station = {
4
5
  name: 'telegram-bot',
@@ -13,5 +14,6 @@ export const telegramBotStation: Station = {
13
14
  'delete',
14
15
  ]),
15
16
  attachmentMode: 'canonical',
17
+ profileFields: new Set<ProfileField>(PROFILE_FIELDS),
16
18
  tools: [],
17
19
  };
@@ -10,6 +10,7 @@ import {
10
10
  import { accountFor, accounts, targetOf } from './accounts.js';
11
11
  import { normalizeWhatsApp } from './normalize.js';
12
12
  import { phoneNumberOf, senderLookup } from './resolve.js';
13
+ import { assertImage, fieldsOf, parseProfileChange, type ProfileApplied } from '@metro-labs/core/stations/profile';
13
14
  import type { WAClient } from './client.js';
14
15
 
15
16
  type Args = Record<string, unknown>;
@@ -173,6 +174,17 @@ function makeResolveSender(clientFor: ClientFor): StationHandler {
173
174
  };
174
175
  }
175
176
 
177
+ function makeSetProfile(clientFor: ClientFor): StationHandler {
178
+ return async (id, args) => {
179
+ const accountId = accountFor({ account: str(args.account) });
180
+ const change = parseProfileChange(args);
181
+ if (change.avatar !== undefined) assertImage(change.avatar);
182
+ await guard(() => clientFor(accountId).setProfile(change));
183
+ const result: ProfileApplied = { account: accountId, applied: fieldsOf(change) };
184
+ respond(id, { result });
185
+ };
186
+ }
187
+
176
188
  export function makeHandleCall(
177
189
  clientFor: ClientFor,
178
190
  ): (msg: CallMsg) => Promise<void> {
@@ -184,6 +196,7 @@ export function makeHandleCall(
184
196
  edit: makeEdit(clientFor),
185
197
  delete: makeDelete(clientFor),
186
198
  resolve_sender: makeResolveSender(clientFor),
199
+ set_profile: makeSetProfile(clientFor),
187
200
  },
188
201
  normalize: normalizeWhatsApp,
189
202
  });
@@ -13,6 +13,7 @@ import { errMsg } from '@metro-labs/core/log';
13
13
  import type { WhatsAppAccount } from './types.js';
14
14
  import type { InboundMessage, ReactionInput } from './format.js';
15
15
  import type { SenderFound } from './resolve.js';
16
+ import type { ProfileChange } from '@metro-labs/core/stations/profile';
16
17
  import { toInbound, toReaction, type ReactionEvent, type SelfRef } from './parse.js';
17
18
  import { baileysLogger } from './logger.js';
18
19
  import { useAccountAuthState } from './auth-state.js';
@@ -51,6 +52,7 @@ export interface WAClient {
51
52
  deleteMessage(jid: string, messageId: string): Promise<void>;
52
53
  reuploadMedia(m: WAMessage): Promise<WAMessage>;
53
54
  lookupSender(number: string): Promise<SenderFound>;
55
+ setProfile(change: ProfileChange): Promise<void>;
54
56
  disconnect(): Promise<void>;
55
57
  }
56
58
 
@@ -335,6 +337,16 @@ export function createClient(account: WhatsAppAccount): WAClient {
335
337
  const jid = jidNormalizedUser(found.jid);
336
338
  return { exists: true, jid, lid: await lidFor(st, sock, jid) };
337
339
  },
340
+ async setProfile(change) {
341
+ const sock = await ready(st);
342
+ if (change.name !== undefined) await sock.updateProfileName(change.name);
343
+ if (change.bio !== undefined) await sock.updateProfileStatus(change.bio);
344
+ if (change.avatar !== undefined) {
345
+ const me = sock.user?.id;
346
+ if (me === undefined) throw new TrainError('whatsapp_call', 'own jid unknown');
347
+ await sock.updateProfilePicture(me, { url: change.avatar.path });
348
+ }
349
+ },
338
350
  async disconnect() {
339
351
  st.closed = true;
340
352
  try {
@@ -1,4 +1,5 @@
1
1
  import type { Station, Verb } from '@metro-labs/core/stations/types';
2
+ import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
2
3
 
3
4
  export const whatsappStation: Station = {
4
5
  name: 'whatsapp',
@@ -13,6 +14,7 @@ export const whatsappStation: Station = {
13
14
  'delete',
14
15
  ]),
15
16
  attachmentMode: 'canonical',
17
+ profileFields: new Set<ProfileField>(PROFILE_FIELDS),
16
18
  resolvesSenders: true,
17
19
  tools: [],
18
20
  };
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.152"
2
+ "version": "0.1.0-beta.153"
3
3
  }