@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.21
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/dist/api.js +103 -0
- package/dist/cli.js +134 -565
- package/dist/control.js +102 -0
- package/dist/prompt.js +10 -0
- package/dist/runtime.js +94 -0
- package/dist/store.js +70 -0
- package/dist/update.js +33 -0
- package/dist/version.js +97 -0
- package/package.json +34 -22
- package/runtime/node_modules/@metro-labs/discord/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/discord/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord/src/format.ts +210 -0
- package/runtime/node_modules/@metro-labs/discord/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +335 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +124 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +322 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +338 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +357 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +125 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +41 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +364 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
- package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/telegram-user/package.json +28 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +81 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
- package/runtime/runtime.json +3 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -78
- package/dist/channels/discord.js +0 -148
- package/dist/channels/telegram.js +0 -137
- package/dist/lib/address.js +0 -21
- package/dist/lib/codex-rc.js +0 -274
- package/dist/lib/dotenv.js +0 -31
- package/dist/log.js +0 -12
- package/dist/paths.js +0 -45
- package/dist/tail.js +0 -161
- package/skills/metro/SKILL.md +0 -122
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type makeWASocket from 'baileys';
|
|
2
|
+
|
|
3
|
+
type BaileysLogger = NonNullable<
|
|
4
|
+
NonNullable<Parameters<typeof makeWASocket>[0]>['logger']
|
|
5
|
+
>;
|
|
6
|
+
|
|
7
|
+
const MAX_DETAIL = 400;
|
|
8
|
+
|
|
9
|
+
const LEVELS = ['trace', 'debug', 'info', 'warn', 'error'] as const;
|
|
10
|
+
|
|
11
|
+
type Level = (typeof LEVELS)[number];
|
|
12
|
+
|
|
13
|
+
const DEFAULT_LEVEL: Level = 'warn';
|
|
14
|
+
|
|
15
|
+
const SILENT = 'silent';
|
|
16
|
+
|
|
17
|
+
const oneLine = (s: string): string => s.replace(/\s+/g, ' ').trim();
|
|
18
|
+
|
|
19
|
+
function logLevel(): Level | typeof SILENT {
|
|
20
|
+
const raw = process.env.METRO_WHATSAPP_LOG_LEVEL?.trim().toLowerCase();
|
|
21
|
+
if (raw === SILENT) return SILENT;
|
|
22
|
+
return LEVELS.find((l) => l === raw) ?? DEFAULT_LEVEL;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function detail(obj: unknown): string {
|
|
26
|
+
if (obj === undefined || obj === null) return '';
|
|
27
|
+
if (obj instanceof Error) return ` ${oneLine(obj.message)}`;
|
|
28
|
+
if (typeof obj === 'string') return ` ${oneLine(obj)}`;
|
|
29
|
+
try {
|
|
30
|
+
return ` ${oneLine(JSON.stringify(obj) ?? '').slice(0, MAX_DETAIL)}`;
|
|
31
|
+
} catch {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function baileysLogger(accountId: string): BaileysLogger {
|
|
37
|
+
const level = logLevel();
|
|
38
|
+
const noop = (): void => undefined;
|
|
39
|
+
const write =
|
|
40
|
+
(at: Level) =>
|
|
41
|
+
(obj: unknown, msg?: string): void => {
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
`whatsapp[${accountId}] baileys ${at}: ${oneLine(msg ?? '')}${detail(obj)}\n`,
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
const method = (at: Level): ((obj: unknown, msg?: string) => void) =>
|
|
47
|
+
level !== SILENT && LEVELS.indexOf(at) >= LEVELS.indexOf(level)
|
|
48
|
+
? write(at)
|
|
49
|
+
: noop;
|
|
50
|
+
const logger: BaileysLogger = {
|
|
51
|
+
level,
|
|
52
|
+
child: () => logger,
|
|
53
|
+
trace: method('trace'),
|
|
54
|
+
debug: method('debug'),
|
|
55
|
+
info: method('info'),
|
|
56
|
+
warn: method('warn'),
|
|
57
|
+
error: method('error'),
|
|
58
|
+
};
|
|
59
|
+
return logger;
|
|
60
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import makeWASocket, {
|
|
2
|
+
Browsers,
|
|
3
|
+
DisconnectReason,
|
|
4
|
+
fetchLatestWaWebVersion,
|
|
5
|
+
type WASocket,
|
|
6
|
+
} from 'baileys';
|
|
7
|
+
import { errMsg, log } from '@metro-labs/mcp/log';
|
|
8
|
+
import { inMemoryAuthState } from './auth-state.js';
|
|
9
|
+
|
|
10
|
+
export class WhatsappLoginError extends Error {}
|
|
11
|
+
|
|
12
|
+
export interface WhatsappLoginEvents {
|
|
13
|
+
onQr: (qr: string) => void;
|
|
14
|
+
onPairingCode: (code: string) => void;
|
|
15
|
+
onPaired: (result: {
|
|
16
|
+
config: { phone: string; credentials: unknown };
|
|
17
|
+
identity: { phone: string };
|
|
18
|
+
}) => void;
|
|
19
|
+
onFailed: (message: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PHONE_RE = /^[0-9]{6,15}$/;
|
|
23
|
+
const RESTART_REQUIRED = 515;
|
|
24
|
+
const MAX_REOPENS = 6;
|
|
25
|
+
|
|
26
|
+
export function normalizePhone(raw: unknown): string {
|
|
27
|
+
const phone = typeof raw === 'string' ? raw.replace(/[^0-9]/g, '') : '';
|
|
28
|
+
if (phone !== '' && !PHONE_RE.test(phone))
|
|
29
|
+
throw new WhatsappLoginError(
|
|
30
|
+
'phone must be an international number in digits, for example 447700900123',
|
|
31
|
+
);
|
|
32
|
+
return phone;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function closeReason(err: unknown): number | undefined {
|
|
36
|
+
return (err as { output?: { statusCode?: number } } | undefined)?.output
|
|
37
|
+
?.statusCode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class WhatsappLogin {
|
|
41
|
+
private auth = inMemoryAuthState();
|
|
42
|
+
private sock: WASocket | null = null;
|
|
43
|
+
private pairingRequested = false;
|
|
44
|
+
private settled = false;
|
|
45
|
+
private reopens = 0;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private phone: string,
|
|
49
|
+
private events: WhatsappLoginEvents,
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
get mode(): 'pairing-code' | 'qr' {
|
|
53
|
+
return this.phone === '' ? 'qr' : 'pairing-code';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async start(): Promise<void> {
|
|
57
|
+
const { version, error } = await fetchLatestWaWebVersion({});
|
|
58
|
+
if (error)
|
|
59
|
+
throw new WhatsappLoginError(
|
|
60
|
+
'could not reach WhatsApp to start the pairing',
|
|
61
|
+
);
|
|
62
|
+
this.open(version);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private open(version: [number, number, number]): void {
|
|
66
|
+
if (this.settled) return;
|
|
67
|
+
const sock = makeWASocket({
|
|
68
|
+
version,
|
|
69
|
+
auth: this.auth.state,
|
|
70
|
+
browser: Browsers.macOS('Safari'),
|
|
71
|
+
markOnlineOnConnect: false,
|
|
72
|
+
syncFullHistory: false,
|
|
73
|
+
printQRInTerminal: false,
|
|
74
|
+
});
|
|
75
|
+
this.sock = sock;
|
|
76
|
+
sock.ev.on('connection.update', (update) => {
|
|
77
|
+
const { connection, qr, lastDisconnect } = update;
|
|
78
|
+
if (typeof qr === 'string') this.onQr(sock, qr);
|
|
79
|
+
if (connection === 'open') this.finish();
|
|
80
|
+
if (connection === 'close')
|
|
81
|
+
this.onClose(version, closeReason(lastDisconnect?.error));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private onQr(sock: WASocket, qr: string): void {
|
|
86
|
+
if (this.settled) return;
|
|
87
|
+
if (this.mode === 'qr') {
|
|
88
|
+
this.events.onQr(qr);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (this.pairingRequested || this.auth.state.creds.registered) return;
|
|
92
|
+
this.pairingRequested = true;
|
|
93
|
+
void sock
|
|
94
|
+
.requestPairingCode(this.phone)
|
|
95
|
+
.then((code) => {
|
|
96
|
+
if (!this.settled) this.events.onPairingCode(code);
|
|
97
|
+
})
|
|
98
|
+
.catch(() => {
|
|
99
|
+
this.fail('WhatsApp refused to issue a pairing code for that number');
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private onClose(
|
|
104
|
+
version: [number, number, number],
|
|
105
|
+
statusCode: number | undefined,
|
|
106
|
+
): void {
|
|
107
|
+
if (this.settled) return;
|
|
108
|
+
if (statusCode === DisconnectReason.loggedOut) {
|
|
109
|
+
this.fail('WhatsApp ended the pairing, start again');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.reopens += 1;
|
|
113
|
+
if (this.reopens > MAX_REOPENS && statusCode !== RESTART_REQUIRED) {
|
|
114
|
+
this.fail('WhatsApp kept dropping the pairing connection, start again');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.open(version);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private pairedPhone(): string {
|
|
121
|
+
const linked = this.auth.state.creds.me?.id.split(':')[0] ?? '';
|
|
122
|
+
return PHONE_RE.test(linked) ? linked : this.phone;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private finish(): void {
|
|
126
|
+
if (this.settled) return;
|
|
127
|
+
const phone = this.pairedPhone();
|
|
128
|
+
if (phone === '') {
|
|
129
|
+
this.fail(
|
|
130
|
+
'WhatsApp paired but did not say which number it linked, so Metro has nothing to store',
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const credentials = this.auth.serialize();
|
|
135
|
+
this.settled = true;
|
|
136
|
+
this.closeSocket()
|
|
137
|
+
.then(() => {
|
|
138
|
+
this.events.onPaired({
|
|
139
|
+
config: { phone, credentials },
|
|
140
|
+
identity: { phone },
|
|
141
|
+
});
|
|
142
|
+
})
|
|
143
|
+
.catch((err: unknown) => {
|
|
144
|
+
log.warn({ err: errMsg(err) }, 'whatsapp login: pairing report failed');
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private fail(message: string): void {
|
|
149
|
+
if (this.settled) return;
|
|
150
|
+
this.settled = true;
|
|
151
|
+
this.closeSocket()
|
|
152
|
+
.then(() => {
|
|
153
|
+
this.events.onFailed(message);
|
|
154
|
+
})
|
|
155
|
+
.catch((err: unknown) => {
|
|
156
|
+
log.warn({ err: errMsg(err) }, 'whatsapp login: failure report failed');
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async closeSocket(): Promise<void> {
|
|
161
|
+
const sock = this.sock;
|
|
162
|
+
this.sock = null;
|
|
163
|
+
if (!sock) return;
|
|
164
|
+
try {
|
|
165
|
+
sock.ev.removeAllListeners('connection.update');
|
|
166
|
+
await sock.end(undefined);
|
|
167
|
+
} catch {
|
|
168
|
+
log.warn({}, 'whatsapp login: pairing socket did not close cleanly');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async cancel(): Promise<void> {
|
|
173
|
+
this.settled = true;
|
|
174
|
+
await this.closeSocket();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { proto } from 'baileys';
|
|
2
|
+
import { extFromMime } from '@metro-labs/mcp/stations/attachments';
|
|
3
|
+
|
|
4
|
+
export type WAMediaKind =
|
|
5
|
+
| 'image'
|
|
6
|
+
| 'video'
|
|
7
|
+
| 'voice'
|
|
8
|
+
| 'audio'
|
|
9
|
+
| 'document'
|
|
10
|
+
| 'sticker';
|
|
11
|
+
|
|
12
|
+
export interface WAMediaRef {
|
|
13
|
+
kind: WAMediaKind;
|
|
14
|
+
mime?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
bytes?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Long {
|
|
20
|
+
toNumber(): number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface MediaNode {
|
|
24
|
+
mimetype?: string | null;
|
|
25
|
+
fileName?: string | null;
|
|
26
|
+
fileLength?: number | Long | null;
|
|
27
|
+
ptt?: boolean | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MEDIA_NODES: [keyof proto.IMessage, WAMediaKind][] = [
|
|
31
|
+
['imageMessage', 'image'],
|
|
32
|
+
['videoMessage', 'video'],
|
|
33
|
+
['audioMessage', 'audio'],
|
|
34
|
+
['documentMessage', 'document'],
|
|
35
|
+
['stickerMessage', 'sticker'],
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const EXT_BY_KIND: Record<WAMediaKind, string> = {
|
|
39
|
+
image: 'jpg',
|
|
40
|
+
video: 'mp4',
|
|
41
|
+
voice: 'ogg',
|
|
42
|
+
audio: 'mp3',
|
|
43
|
+
document: 'bin',
|
|
44
|
+
sticker: 'webp',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function toBytes(v: number | Long | null | undefined): number | undefined {
|
|
48
|
+
if (typeof v === 'number') return v;
|
|
49
|
+
if (v && typeof v.toNumber === 'function') return v.toNumber();
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function baseMime(v: string | null | undefined): string | undefined {
|
|
54
|
+
const first = typeof v === 'string' ? (v.split(';')[0] ?? '').trim() : '';
|
|
55
|
+
return first || undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fallbackName(kind: WAMediaKind, mime: string | undefined): string {
|
|
59
|
+
const ext = extFromMime(mime) ?? EXT_BY_KIND[kind];
|
|
60
|
+
return kind === 'voice' ? `voice-message.${ext}` : `${kind}.${ext}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function mediaRefIn(inner: proto.IMessage): WAMediaRef | undefined {
|
|
64
|
+
for (const [key, base] of MEDIA_NODES) {
|
|
65
|
+
const node = inner[key] as MediaNode | null | undefined;
|
|
66
|
+
if (!node) continue;
|
|
67
|
+
const kind = base === 'audio' && node.ptt === true ? 'voice' : base;
|
|
68
|
+
const mime = baseMime(node.mimetype);
|
|
69
|
+
const bytes = toBytes(node.fileLength);
|
|
70
|
+
return {
|
|
71
|
+
kind,
|
|
72
|
+
...(mime ? { mime } : {}),
|
|
73
|
+
name: node.fileName ?? fallbackName(kind, mime),
|
|
74
|
+
...(bytes === undefined ? {} : { bytes }),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function mediaTag(ref: WAMediaRef | undefined): string | undefined {
|
|
81
|
+
if (!ref) return undefined;
|
|
82
|
+
if (ref.kind === 'document') return `[document: ${ref.name ?? 'document'}]`;
|
|
83
|
+
return `[${ref.kind}]`;
|
|
84
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Normalized } from '@metro-labs/mcp/stations/messaging-normalize';
|
|
2
|
+
|
|
3
|
+
type Args = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export function normalizeWhatsApp(action: string, env: Args): Normalized {
|
|
6
|
+
if (action === 'reply') {
|
|
7
|
+
return {
|
|
8
|
+
action: 'send',
|
|
9
|
+
args: {
|
|
10
|
+
line: env.line,
|
|
11
|
+
text: env.text,
|
|
12
|
+
replyTo: env.replyTo ?? env.messageId,
|
|
13
|
+
account: env.account,
|
|
14
|
+
attachments: env.attachments,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (action === 'unreact') {
|
|
19
|
+
return { action: 'react', args: { ...env, emoji: '' } };
|
|
20
|
+
}
|
|
21
|
+
return { action, args: env };
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WAMessageKey, proto } from 'baileys';
|
|
2
|
+
|
|
3
|
+
const MAX_TRACKED_SENDS = 256;
|
|
4
|
+
|
|
5
|
+
export interface Outbox {
|
|
6
|
+
remember(
|
|
7
|
+
key: WAMessageKey | null | undefined,
|
|
8
|
+
message: proto.IMessage | null | undefined,
|
|
9
|
+
): void;
|
|
10
|
+
lookup(key: WAMessageKey): proto.IMessage | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const slotOf = (jid: string, id: string): string => `${jid}|${id}`;
|
|
14
|
+
|
|
15
|
+
export function makeOutbox(max: number = MAX_TRACKED_SENDS): Outbox {
|
|
16
|
+
const sent = new Map<string, proto.IMessage>();
|
|
17
|
+
return {
|
|
18
|
+
remember(key, message) {
|
|
19
|
+
const id = key?.id;
|
|
20
|
+
const jid = key?.remoteJid;
|
|
21
|
+
if (!id || !jid || !message || key?.fromMe !== true) return;
|
|
22
|
+
const slot = slotOf(jid, id);
|
|
23
|
+
sent.delete(slot);
|
|
24
|
+
sent.set(slot, message);
|
|
25
|
+
while (sent.size > max) {
|
|
26
|
+
const oldest = sent.keys().next().value;
|
|
27
|
+
if (oldest === undefined) break;
|
|
28
|
+
sent.delete(oldest);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
lookup(key) {
|
|
32
|
+
const id = key.id;
|
|
33
|
+
const jid = key.remoteJid;
|
|
34
|
+
if (!id || !jid) return undefined;
|
|
35
|
+
return sent.get(slotOf(jid, id));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { WAMessage, WAMessageKey, proto } from 'baileys';
|
|
2
|
+
import { mediaRefIn, mediaTag, type WAMediaRef } from './media.js';
|
|
3
|
+
import type { InboundMessage, ReactionInput } from './format.js';
|
|
4
|
+
|
|
5
|
+
export const isGroupJid = (jid: string): boolean => jid.endsWith('@g.us');
|
|
6
|
+
const isLidJid = (jid: string): boolean => jid.endsWith('@lid');
|
|
7
|
+
export const isPrivateJid = (jid: string): boolean =>
|
|
8
|
+
jid.endsWith('@s.whatsapp.net') || isLidJid(jid);
|
|
9
|
+
|
|
10
|
+
type Timestamp = number | { toNumber(): number } | null | undefined;
|
|
11
|
+
|
|
12
|
+
export function tsToDate(ts: Timestamp): Date {
|
|
13
|
+
if (typeof ts === 'number') return new Date(ts * 1000);
|
|
14
|
+
if (ts && typeof ts.toNumber === 'function')
|
|
15
|
+
return new Date(ts.toNumber() * 1000);
|
|
16
|
+
return new Date();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Content = proto.IMessage | null | undefined;
|
|
20
|
+
|
|
21
|
+
const UNWRAPPERS: ((m: proto.IMessage) => Content)[] = [
|
|
22
|
+
(m) => m.ephemeralMessage?.message,
|
|
23
|
+
(m) => m.viewOnceMessage?.message,
|
|
24
|
+
(m) => m.viewOnceMessageV2?.message,
|
|
25
|
+
(m) => m.viewOnceMessageV2Extension?.message,
|
|
26
|
+
(m) => m.deviceSentMessage?.message,
|
|
27
|
+
(m) => m.documentWithCaptionMessage?.message,
|
|
28
|
+
(m) => m.editedMessage?.message,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function unwrapOnce(m: proto.IMessage): Content {
|
|
32
|
+
for (const pick of UNWRAPPERS) {
|
|
33
|
+
const inner = pick(m);
|
|
34
|
+
if (inner) return inner;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function unwrap(message: Content): proto.IMessage | undefined {
|
|
40
|
+
let current: Content = message;
|
|
41
|
+
for (let depth = 0; current && depth < 8; depth += 1) {
|
|
42
|
+
const inner = unwrapOnce(current);
|
|
43
|
+
if (!inner) return current;
|
|
44
|
+
current = inner;
|
|
45
|
+
}
|
|
46
|
+
return current ?? undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function captionOf(message: proto.IMessage): string {
|
|
50
|
+
const caption =
|
|
51
|
+
message.imageMessage?.caption ??
|
|
52
|
+
message.videoMessage?.caption ??
|
|
53
|
+
message.documentMessage?.caption;
|
|
54
|
+
return typeof caption === 'string' ? caption : '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function extractText(message: Content): string {
|
|
58
|
+
const inner = unwrap(message);
|
|
59
|
+
if (!inner) return '';
|
|
60
|
+
if (typeof inner.conversation === 'string') return inner.conversation;
|
|
61
|
+
const ext = inner.extendedTextMessage?.text;
|
|
62
|
+
if (typeof ext === 'string') return ext;
|
|
63
|
+
return captionOf(inner);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function mediaRefOf(message: Content): WAMediaRef | undefined {
|
|
67
|
+
const inner = unwrap(message);
|
|
68
|
+
return inner ? mediaRefIn(inner) : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function projectText(
|
|
72
|
+
message: proto.IMessage | undefined,
|
|
73
|
+
ref: WAMediaRef | undefined,
|
|
74
|
+
): string {
|
|
75
|
+
return [extractText(message), mediaTag(ref)].filter(Boolean).join(' ');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function senderJidOf(key: WAMessageKey, chatJid: string): string {
|
|
79
|
+
if (isGroupJid(chatJid)) return key.participant ?? chatJid;
|
|
80
|
+
return chatJid;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function toInbound(
|
|
84
|
+
accountId: string,
|
|
85
|
+
m: WAMessage,
|
|
86
|
+
): InboundMessage | undefined {
|
|
87
|
+
const chatJid = m.key.remoteJid;
|
|
88
|
+
const messageId = m.key.id;
|
|
89
|
+
if (!chatJid || !messageId) return undefined;
|
|
90
|
+
const inner = unwrap(m.message);
|
|
91
|
+
if (!inner || inner.reactionMessage) return undefined;
|
|
92
|
+
const media = mediaRefOf(inner);
|
|
93
|
+
const text = projectText(inner, media);
|
|
94
|
+
if (!text && !media) return undefined;
|
|
95
|
+
return {
|
|
96
|
+
accountId,
|
|
97
|
+
chatJid,
|
|
98
|
+
senderJid: senderJidOf(m.key, chatJid),
|
|
99
|
+
messageId,
|
|
100
|
+
text,
|
|
101
|
+
date: tsToDate(m.messageTimestamp),
|
|
102
|
+
isPrivate: isPrivateJid(chatJid),
|
|
103
|
+
...(m.pushName ? { pushName: m.pushName } : {}),
|
|
104
|
+
...(media ? { media } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ReactionEvent {
|
|
109
|
+
key: WAMessageKey;
|
|
110
|
+
reaction: proto.IReaction;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function reactorJid(
|
|
114
|
+
reactorKey: WAMessageKey | undefined,
|
|
115
|
+
chatJid: string,
|
|
116
|
+
): string {
|
|
117
|
+
return reactorKey?.participant ?? reactorKey?.remoteJid ?? chatJid;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function toReaction(
|
|
121
|
+
accountId: string,
|
|
122
|
+
event: ReactionEvent,
|
|
123
|
+
): ReactionInput | undefined {
|
|
124
|
+
const chatJid = event.key.remoteJid;
|
|
125
|
+
const messageId = event.key.id;
|
|
126
|
+
if (!chatJid || !messageId) return undefined;
|
|
127
|
+
const reactorKey = event.reaction.key ?? undefined;
|
|
128
|
+
if (reactorKey?.fromMe) return undefined;
|
|
129
|
+
const senderJid = reactorJid(reactorKey, chatJid);
|
|
130
|
+
const emoji = event.reaction.text ?? '';
|
|
131
|
+
return {
|
|
132
|
+
accountId,
|
|
133
|
+
chatJid,
|
|
134
|
+
senderJid,
|
|
135
|
+
messageId,
|
|
136
|
+
emoji,
|
|
137
|
+
date: new Date(),
|
|
138
|
+
isPrivate: isPrivateJid(chatJid),
|
|
139
|
+
removed: emoji === '',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Station, Verb } from '@metro-labs/mcp/stations/types';
|
|
2
|
+
|
|
3
|
+
export const whatsappStation: Station = {
|
|
4
|
+
name: 'whatsapp',
|
|
5
|
+
hasAccounts: true,
|
|
6
|
+
hasTrain: true,
|
|
7
|
+
messageVerbs: new Set<Verb>([
|
|
8
|
+
'send',
|
|
9
|
+
'reply',
|
|
10
|
+
'react',
|
|
11
|
+
'unreact',
|
|
12
|
+
'edit',
|
|
13
|
+
'delete',
|
|
14
|
+
]),
|
|
15
|
+
attachmentMode: 'canonical',
|
|
16
|
+
tools: [],
|
|
17
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readJson, writeSecure } from '@metro-labs/mcp/secure-fs';
|
|
4
|
+
import { errMsg } from '@metro-labs/mcp/log';
|
|
5
|
+
|
|
6
|
+
export const PERSISTED_KEY_TYPES = ['tctoken', 'lid-mapping'] as const;
|
|
7
|
+
|
|
8
|
+
export type KeyTable = Record<string, Record<string, unknown>>;
|
|
9
|
+
|
|
10
|
+
const PERSISTED = new Set<string>(PERSISTED_KEY_TYPES);
|
|
11
|
+
|
|
12
|
+
const WRITE_DEBOUNCE_MS = 1000;
|
|
13
|
+
|
|
14
|
+
export const isPersistedKeyType = (type: string): boolean =>
|
|
15
|
+
PERSISTED.has(type);
|
|
16
|
+
|
|
17
|
+
const safeSegment = (s: string): string => s.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
18
|
+
|
|
19
|
+
export function tokenStorePath(accountId: string): string {
|
|
20
|
+
const dir = process.env.WHATSAPP_TOKEN_DIR ?? join(homedir(), '.metro');
|
|
21
|
+
return join(dir, `whatsapp-tokens-${safeSegment(accountId)}.json`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function persistedSubset(table: KeyTable): KeyTable {
|
|
25
|
+
const out: KeyTable = {};
|
|
26
|
+
for (const type of PERSISTED_KEY_TYPES) {
|
|
27
|
+
const bucket = table[type];
|
|
28
|
+
if (!bucket) continue;
|
|
29
|
+
const kept: Record<string, unknown> = {};
|
|
30
|
+
for (const id of Object.keys(bucket)) {
|
|
31
|
+
const value = bucket[id];
|
|
32
|
+
if (value === undefined || value === null) continue;
|
|
33
|
+
kept[id] = value;
|
|
34
|
+
}
|
|
35
|
+
if (Object.keys(kept).length) out[type] = kept;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TokenStore {
|
|
41
|
+
path: string;
|
|
42
|
+
load(): KeyTable;
|
|
43
|
+
scheduleSave(table: KeyTable): void;
|
|
44
|
+
saveNow(table: KeyTable): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function makeTokenStore(
|
|
48
|
+
accountId: string,
|
|
49
|
+
onError: (message: string) => void,
|
|
50
|
+
): TokenStore {
|
|
51
|
+
const path = tokenStorePath(accountId);
|
|
52
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
53
|
+
let pending: KeyTable | undefined;
|
|
54
|
+
let written = '';
|
|
55
|
+
const saveNow = (table: KeyTable): void => {
|
|
56
|
+
const body = JSON.stringify(persistedSubset(table));
|
|
57
|
+
if (body === written) return;
|
|
58
|
+
try {
|
|
59
|
+
writeSecure(path, body);
|
|
60
|
+
written = body;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
onError(`could not write ${path}: ${errMsg(err)}`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
path,
|
|
67
|
+
load() {
|
|
68
|
+
const table = persistedSubset(readJson<KeyTable>(path, {}));
|
|
69
|
+
written = JSON.stringify(table);
|
|
70
|
+
return table;
|
|
71
|
+
},
|
|
72
|
+
scheduleSave(table) {
|
|
73
|
+
pending = table;
|
|
74
|
+
if (timer) return;
|
|
75
|
+
timer = setTimeout(() => {
|
|
76
|
+
timer = undefined;
|
|
77
|
+
if (pending) saveNow(pending);
|
|
78
|
+
pending = undefined;
|
|
79
|
+
}, WRITE_DEBOUNCE_MS);
|
|
80
|
+
timer.unref?.();
|
|
81
|
+
},
|
|
82
|
+
saveNow,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const stores = new Map<string, TokenStore>();
|
|
87
|
+
|
|
88
|
+
export function tokenStoreFor(
|
|
89
|
+
accountId: string,
|
|
90
|
+
onError: (message: string) => void,
|
|
91
|
+
): TokenStore {
|
|
92
|
+
const path = tokenStorePath(accountId);
|
|
93
|
+
const existing = stores.get(path);
|
|
94
|
+
if (existing) return existing;
|
|
95
|
+
const store = makeTokenStore(accountId, onError);
|
|
96
|
+
stores.set(path, store);
|
|
97
|
+
return store;
|
|
98
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@metro-labs/xmtp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/station.ts",
|
|
8
|
+
"./train": "./src/index.ts",
|
|
9
|
+
"./verify": "./src/verify.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"test": "tsc --noEmit && DYLD_FALLBACK_LIBRARY_PATH=/usr/lib bun test test/"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@metro-labs/mcp": "workspace:*",
|
|
17
|
+
"@xmtp/content-type-primitives": "^3.0.1",
|
|
18
|
+
"@xmtp/content-type-reaction": "^2.0.2",
|
|
19
|
+
"@xmtp/content-type-remote-attachment": "^2.0.4",
|
|
20
|
+
"@xmtp/content-type-reply": "^2.0.3",
|
|
21
|
+
"@xmtp/content-type-wallet-send-calls": "^2.0.0",
|
|
22
|
+
"@xmtp/node-bindings": "1.9.1",
|
|
23
|
+
"@xmtp/node-sdk": "^6.0.0",
|
|
24
|
+
"viem": "^2.52.2"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@stage-labs/config": "0.1.0-beta.2",
|
|
28
|
+
"eslint": "^10.3.0",
|
|
29
|
+
"knip": "^6.15.0",
|
|
30
|
+
"madge": "^8.0.0",
|
|
31
|
+
"typescript": "^5"
|
|
32
|
+
}
|
|
33
|
+
}
|