@stage-labs/metro 0.1.0-beta.206 → 0.1.0-beta.208

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.206",
3
+ "version": "0.1.0-beta.208",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro",
3
- "version": "0.1.0-beta.206",
3
+ "version": "0.1.0-beta.208",
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",
@@ -24,6 +24,11 @@ const LOOKUP_MS = 8_000;
24
24
 
25
25
  const filled = (value: unknown): string | undefined => (typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined);
26
26
 
27
+ const AVATAR_DATA_MAX = 100_000;
28
+
29
+ const shownAvatar = (avatar: string): boolean =>
30
+ avatar.startsWith('https://') || (avatar.startsWith('data:image/') && avatar.length <= AVATAR_DATA_MAX);
31
+
27
32
  function cardOf(id: string, profile: unknown, seenName: string | undefined): SenderCard {
28
33
  const p = isRecord(profile) ? profile : {};
29
34
  const display = filled(p.display_name);
@@ -34,7 +39,7 @@ function cardOf(id: string, profile: unknown, seenName: string | undefined): Sen
34
39
  id,
35
40
  ...(name === undefined ? {} : { name }),
36
41
  ...(handle === undefined || handle === name ? {} : { handle }),
37
- ...(avatar?.startsWith('https://') === true ? { avatar } : {}),
42
+ ...(avatar !== undefined && shownAvatar(avatar) ? { avatar } : {}),
38
43
  };
39
44
  }
40
45
 
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { readdirSync, rmSync, statSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
3
+ import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { ensureSecureDir, readJson, writeSecure } from '@metro-labs/core/secure-fs';
6
6
  import { grantAllowsPath, issueGrant } from './attach-grant.js';
@@ -16,7 +16,7 @@ const SUFFIXES = ['', '.part', '.meta', '.owner', '.grant'] as const;
16
16
  const SWEEP_MS = 60_000;
17
17
 
18
18
  export const uploadDir = (): string =>
19
- process.env.METRO_UPLOAD_DIR ?? join(tmpdir(), 'metro-uploads');
19
+ process.env.METRO_UPLOAD_DIR ?? join(homedir(), '.metro', 'uploads');
20
20
 
21
21
  export const newUploadId = (): string =>
22
22
  `up_${randomBytes(16).toString('base64url')}`;
@@ -9,7 +9,7 @@ import {
9
9
  } from '@metro-labs/core/stations/profile';
10
10
  import { makeProfileCache, nonEmpty, type SenderProfile } from '@metro-labs/core/stations/sender-profile';
11
11
  import { TrainError } from '@metro-labs/core/train-error';
12
- import { accountFor, tg } from './accounts.js';
12
+ import { accountFor, accounts, tg } from './accounts.js';
13
13
  import { respond } from '@metro-labs/core/stations/station-runtime';
14
14
 
15
15
  async function setPhoto(accountId: string, avatar: ProfileAvatar): Promise<void> {
@@ -39,6 +39,24 @@ interface TgChat {
39
39
  first_name?: unknown;
40
40
  last_name?: unknown;
41
41
  bio?: unknown;
42
+ photo?: { small_file_id?: unknown };
43
+ }
44
+
45
+ const PHOTO_MAX = 64 * 1024;
46
+
47
+ async function photoData(accountId: string, fileId: unknown): Promise<string | undefined> {
48
+ if (typeof fileId !== 'string' || fileId === '') return undefined;
49
+ try {
50
+ const file = await tg<{ file_path?: string }>(accountId, 'getFile', { file_id: fileId }, 10_000);
51
+ const acct = accounts.get(accountId);
52
+ if (acct === undefined || typeof file.file_path !== 'string') return undefined;
53
+ const res = await fetch(`${acct.fileApi}/${file.file_path}`, { signal: AbortSignal.timeout(10_000) });
54
+ if (!res.ok) return undefined;
55
+ const bytes = Buffer.from(await res.arrayBuffer());
56
+ return bytes.length === 0 || bytes.length > PHOTO_MAX ? undefined : `data:image/jpeg;base64,${bytes.toString('base64')}`;
57
+ } catch {
58
+ return undefined;
59
+ }
42
60
  }
43
61
 
44
62
  const senders = makeProfileCache<SenderProfile>(
@@ -47,11 +65,13 @@ const senders = makeProfileCache<SenderProfile>(
47
65
  const chat = await tg<TgChat>(accountId, 'getChat', { chat_id: Number(user) }, 10_000);
48
66
  const handle = nonEmpty(chat.username);
49
67
  const display = nonEmpty([chat.first_name, chat.last_name].filter((part) => typeof part === 'string').join(' '));
68
+ const avatar = await photoData(accountId, chat.photo?.small_file_id);
50
69
  return {
51
70
  id: user,
52
71
  ...(handle === undefined ? {} : { name: `@${handle}` }),
53
72
  ...(display === undefined ? {} : { display_name: display }),
54
73
  ...(nonEmpty(chat.bio) === undefined ? {} : { about: nonEmpty(chat.bio) }),
74
+ ...(avatar === undefined ? {} : { avatar }),
55
75
  };
56
76
  },
57
77
  { onError: (key, err) => process.stderr.write(`telegram-bot: could not read the profile of ${key}: ${err instanceof Error ? err.message : String(err)}\n`) },
@@ -17,7 +17,7 @@ import type { ProfileChange } from '@metro-labs/core/stations/profile';
17
17
  import { makeProfileCache, nonEmpty, type SenderProfile } from '@metro-labs/core/stations/sender-profile';
18
18
  import { toInbound, toReaction, type ReactionEvent, type SelfRef } from './parse.js';
19
19
  import { baileysLogger } from './logger.js';
20
- import { makeNameBook, nameFiles, phoneOf, type NameBook } from './names.js';
20
+ import { makeNameBook, nameFiles, noteContact, phoneOf, type NameBook } from './names.js';
21
21
  import { useAccountAuthState } from './auth-state.js';
22
22
  import { knownKey, makeKeyCache, targetKey, type KeyCache } from './keys.js';
23
23
  import { makeOutbox, type Outbox } from './outbox.js';
@@ -102,7 +102,14 @@ function bindInbound(st: State, sock: WASocket): void {
102
102
  }
103
103
  });
104
104
  sock.ev.on('contacts.upsert', (contacts) => {
105
- for (const c of contacts) st.names.note(c.id, c.name ?? c.notify);
105
+ for (const c of contacts) noteContact(st.names, c);
106
+ });
107
+ sock.ev.on('contacts.update', (contacts) => {
108
+ for (const c of contacts) noteContact(st.names, c);
109
+ });
110
+ sock.ev.on('messaging-history.set', ({ contacts, messages }) => {
111
+ for (const c of contacts) noteContact(st.names, c);
112
+ for (const m of messages) if (m.key.fromMe !== true) st.names.note(m.key.participant ?? m.key.remoteJid, m.pushName);
106
113
  });
107
114
  sock.ev.on('messages.reaction', (events: ReactionEvent[]) => {
108
115
  if (!st.handlers) return;
@@ -56,6 +56,19 @@ export function makeNameBook(file?: string): NameBook {
56
56
  };
57
57
  }
58
58
 
59
+ export interface ContactNames {
60
+ id?: string | null;
61
+ lid?: string | null;
62
+ phoneNumber?: string | null;
63
+ name?: string | null;
64
+ notify?: string | null;
65
+ }
66
+
67
+ export function noteContact(book: NameBook, c: ContactNames): void {
68
+ const name = c.name ?? c.notify;
69
+ for (const jid of [c.id, c.lid, c.phoneNumber]) book.note(jid, name);
70
+ }
71
+
59
72
  export const phoneOf = (jid: string | null | undefined): string | undefined => {
60
73
  const digits = /^(\d{6,})(?::\d+)?@s\.whatsapp\.net$/.exec(jid ?? '')?.[1];
61
74
  return digits === undefined ? undefined : `+${digits}`;
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.206"
2
+ "version": "0.1.0-beta.208"
3
3
  }