@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,212 @@
|
|
|
1
|
+
import type { StationTool } from '@metro-labs/mcp/stations/types';
|
|
2
|
+
import {
|
|
3
|
+
str,
|
|
4
|
+
createChannel,
|
|
5
|
+
setChannelMetadata,
|
|
6
|
+
xmtpSendAttachments,
|
|
7
|
+
} from './tools-handlers.js';
|
|
8
|
+
|
|
9
|
+
export { xmtpSendAttachments };
|
|
10
|
+
|
|
11
|
+
const lineProp = {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description:
|
|
14
|
+
'The metro:// line (from the inbound <channel> tag). The station is derived from it.',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const XMTP_TOOLS: StationTool[] = [
|
|
18
|
+
{
|
|
19
|
+
name: 'create_channel',
|
|
20
|
+
description:
|
|
21
|
+
'Create a new XMTP group conversation (channel). Args: addresses (required, array of ' +
|
|
22
|
+
'Ethereum 0x addresses to add as members), name (required, the group name), labels? ' +
|
|
23
|
+
'(optional string[] status labels applied after creation), account? ' +
|
|
24
|
+
'(defaults to your only XMTP account). Calls the daemon xmtp `newGroup`, then ' +
|
|
25
|
+
'`updateChannelMeta` if labels ' +
|
|
26
|
+
'are given. Returns the new metro:// line and convId. This is an xmtp-only operation. ' +
|
|
27
|
+
'NOTE: there is no add-members verb on the daemon, so members must be supplied at ' +
|
|
28
|
+
'creation time.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
addresses: {
|
|
33
|
+
type: 'array',
|
|
34
|
+
description: 'Ethereum 0x addresses to add as group members.',
|
|
35
|
+
items: { type: 'string' },
|
|
36
|
+
},
|
|
37
|
+
name: { type: 'string', description: 'The group/channel name.' },
|
|
38
|
+
labels: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
description: 'Optional status labels to apply after creation.',
|
|
41
|
+
items: { type: 'string' },
|
|
42
|
+
},
|
|
43
|
+
account: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description:
|
|
46
|
+
'XMTP account to create under. Omit when you have only one.',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
required: ['addresses', 'name'],
|
|
50
|
+
},
|
|
51
|
+
handle: (a, ctx) => createChannel(a, ctx),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'ask',
|
|
55
|
+
description:
|
|
56
|
+
'Ask a question as a poll in an XMTP conversation (mirrors Claude AskUserQuestion). ' +
|
|
57
|
+
'Single-question form: question (required), options? (string[]), header?, multiSelect?, ' +
|
|
58
|
+
'open? (true => free-text answer, options optional). Multi-question form: questions ' +
|
|
59
|
+
'(array of {question, options?, header?, multiSelect?, open?}). Args: line (required) + ' +
|
|
60
|
+
'the above. xmtp-only (the daemon `ask` action). Returns the poll messageId + pollId.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
line: lineProp,
|
|
65
|
+
question: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'The question text (single-question form).',
|
|
68
|
+
},
|
|
69
|
+
options: {
|
|
70
|
+
type: 'array',
|
|
71
|
+
description: 'Answer options for a single question.',
|
|
72
|
+
items: { type: 'string' },
|
|
73
|
+
},
|
|
74
|
+
header: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: 'Optional header/title for the poll.',
|
|
77
|
+
},
|
|
78
|
+
multiSelect: {
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
description: 'Allow selecting multiple options.',
|
|
81
|
+
},
|
|
82
|
+
open: {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
description: 'Free-text answer (options optional).',
|
|
85
|
+
},
|
|
86
|
+
questions: {
|
|
87
|
+
type: 'array',
|
|
88
|
+
description:
|
|
89
|
+
'Multiple questions (multi-question form). Each is {question, options?, header?, multiSelect?, open?}.',
|
|
90
|
+
items: { type: 'object' },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
required: ['line'],
|
|
94
|
+
},
|
|
95
|
+
async handle(a, ctx) {
|
|
96
|
+
const line = str(a.line);
|
|
97
|
+
if (!line) return ctx.err('ask requires `line`');
|
|
98
|
+
const args: Record<string, unknown> = { line };
|
|
99
|
+
for (const k of [
|
|
100
|
+
'question',
|
|
101
|
+
'options',
|
|
102
|
+
'header',
|
|
103
|
+
'multiSelect',
|
|
104
|
+
'open',
|
|
105
|
+
'questions',
|
|
106
|
+
] as const) {
|
|
107
|
+
if (a[k] !== undefined) args[k] = a[k];
|
|
108
|
+
}
|
|
109
|
+
if (a.question === undefined && a.questions === undefined) {
|
|
110
|
+
return ctx.err(
|
|
111
|
+
'ask requires `question` (single) or `questions` (multi)',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return ctx.okJson(await ctx.call('ask', args));
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'dm',
|
|
119
|
+
description:
|
|
120
|
+
'Open (or reuse) a 1:1 XMTP DM with an Ethereum address. Args: address (required, 0x...), ' +
|
|
121
|
+
'account? (defaults to your only XMTP account). Returns the new metro:// line and ' +
|
|
122
|
+
'convId. xmtp-only ' +
|
|
123
|
+
'(daemon `newDm`). Use this instead of create_channel when there is a single recipient.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
address: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'Recipient Ethereum 0x address.',
|
|
130
|
+
},
|
|
131
|
+
account: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: 'XMTP account to DM from. Omit when you have only one.',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
required: ['address'],
|
|
137
|
+
},
|
|
138
|
+
async handle(a, ctx) {
|
|
139
|
+
const address = str(a.address);
|
|
140
|
+
if (!address) return ctx.err('dm requires `address`');
|
|
141
|
+
const dmArgs: Record<string, unknown> = { address };
|
|
142
|
+
if (a.account) dmArgs.account = str(a.account);
|
|
143
|
+
return ctx.okJson(await ctx.call('newDm', dmArgs));
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'group_info',
|
|
148
|
+
description:
|
|
149
|
+
"Read an XMTP channel's current metadata + membership. Args: line (required). Returns " +
|
|
150
|
+
'{line, id, account, version (dm|group), name, memberCount, labels, github, preview, ' +
|
|
151
|
+
'members:[{inboxId, address}]}. xmtp-only (daemon `groupInfo`). Use before ' +
|
|
152
|
+
'set_channel_metadata/add_members to see current state.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: { line: lineProp },
|
|
156
|
+
required: ['line'],
|
|
157
|
+
},
|
|
158
|
+
async handle(a, ctx) {
|
|
159
|
+
const line = str(a.line);
|
|
160
|
+
if (!line) return ctx.err('group_info requires `line`');
|
|
161
|
+
return ctx.okJson(await ctx.call('groupInfo', { line }));
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'close_channel',
|
|
166
|
+
description:
|
|
167
|
+
'Archive/close an XMTP group (removes members). Args: line (required). xmtp-only ' +
|
|
168
|
+
'(daemon `closeGroup`). Irreversible-ish: members are removed from the group.',
|
|
169
|
+
inputSchema: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: { line: lineProp },
|
|
172
|
+
required: ['line'],
|
|
173
|
+
},
|
|
174
|
+
async handle(a, ctx) {
|
|
175
|
+
const line = str(a.line);
|
|
176
|
+
if (!line) return ctx.err('close_channel requires `line`');
|
|
177
|
+
return ctx.okJson(await ctx.call('closeGroup', { line }));
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'set_channel_metadata',
|
|
182
|
+
description:
|
|
183
|
+
"Update an existing channel's metadata. Args: line (required, the metro:// line), and " +
|
|
184
|
+
'any of labels? (string[]), github? (url), preview? (url), name? (string). All provided ' +
|
|
185
|
+
'fields are applied together in one atomic updateChannelMeta daemon call (a single ' +
|
|
186
|
+
'appData merge plus name update). xmtp-only ' +
|
|
187
|
+
'(channel metadata lives on xmtp groups). Returns the updated channel info.',
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
line: lineProp,
|
|
192
|
+
labels: {
|
|
193
|
+
type: 'array',
|
|
194
|
+
description: 'Status labels to set.',
|
|
195
|
+
items: { type: 'string' },
|
|
196
|
+
},
|
|
197
|
+
github: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
description: 'Linked GitHub URL ("" to clear).',
|
|
200
|
+
},
|
|
201
|
+
preview: {
|
|
202
|
+
type: 'string',
|
|
203
|
+
description: 'Linked preview URL ("" to clear).',
|
|
204
|
+
},
|
|
205
|
+
name: { type: 'string', description: 'New channel name.' },
|
|
206
|
+
},
|
|
207
|
+
required: ['line'],
|
|
208
|
+
},
|
|
209
|
+
handle: (a, ctx) => setChannelMetadata(a, ctx),
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { errMsg } from '@metro-labs/mcp/log';
|
|
2
|
+
import { mintId, SELF_URI } from './wire.js';
|
|
3
|
+
|
|
4
|
+
type EmitInbound = (accountId: string, e: Record<string, unknown>) => void;
|
|
5
|
+
|
|
6
|
+
const WHISPER_BIN = process.env.METRO_WHISPER_BIN ?? 'whisper-cli';
|
|
7
|
+
const WHISPER_MODEL =
|
|
8
|
+
process.env.METRO_WHISPER_MODEL ??
|
|
9
|
+
`${process.env.HOME}/.cache/whisper-cpp/ggml-base.bin`;
|
|
10
|
+
const FFMPEG_BIN = process.env.METRO_FFMPEG_BIN ?? 'ffmpeg';
|
|
11
|
+
|
|
12
|
+
export async function transcribeAndEmit(
|
|
13
|
+
audio: Uint8Array,
|
|
14
|
+
line: string,
|
|
15
|
+
accountId: string,
|
|
16
|
+
sourceMsgId: string,
|
|
17
|
+
emitInbound: EmitInbound,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const {
|
|
20
|
+
existsSync: ex,
|
|
21
|
+
readFileSync: rf,
|
|
22
|
+
writeFileSync: wf,
|
|
23
|
+
unlinkSync,
|
|
24
|
+
mkdtempSync,
|
|
25
|
+
} = await import('node:fs');
|
|
26
|
+
const { tmpdir } = await import('node:os');
|
|
27
|
+
const path = await import('node:path');
|
|
28
|
+
const j = (...parts: string[]): string => path.join(...parts);
|
|
29
|
+
const { spawn } = await import('node:child_process');
|
|
30
|
+
if (!ex(WHISPER_MODEL)) return;
|
|
31
|
+
const dir = mkdtempSync(j(tmpdir(), 'xmtp-tx-'));
|
|
32
|
+
const inFile = j(dir, 'in.m4a');
|
|
33
|
+
const wav = j(dir, 'in.wav');
|
|
34
|
+
const out = j(dir, 'in');
|
|
35
|
+
const run = (bin: string, args: string[]): Promise<void> =>
|
|
36
|
+
new Promise((res, rej) => {
|
|
37
|
+
const p = spawn(bin, args, { stdio: 'ignore' });
|
|
38
|
+
p.on('error', rej);
|
|
39
|
+
p.on('exit', (c) => {
|
|
40
|
+
if (c === 0) res();
|
|
41
|
+
else rej(new Error(`${bin} ${String(c)}`));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
wf(inFile, audio);
|
|
46
|
+
await run(FFMPEG_BIN, [
|
|
47
|
+
'-y',
|
|
48
|
+
'-i',
|
|
49
|
+
inFile,
|
|
50
|
+
'-ar',
|
|
51
|
+
'16000',
|
|
52
|
+
'-ac',
|
|
53
|
+
'1',
|
|
54
|
+
wav,
|
|
55
|
+
]);
|
|
56
|
+
await run(WHISPER_BIN, [
|
|
57
|
+
'-m',
|
|
58
|
+
WHISPER_MODEL,
|
|
59
|
+
'-f',
|
|
60
|
+
wav,
|
|
61
|
+
'--output-txt',
|
|
62
|
+
'-of',
|
|
63
|
+
out,
|
|
64
|
+
]);
|
|
65
|
+
const text = rf(`${out}.txt`, 'utf8').trim();
|
|
66
|
+
if (!text) return;
|
|
67
|
+
emitInbound(accountId, {
|
|
68
|
+
id: mintId(),
|
|
69
|
+
ts: new Date().toISOString(),
|
|
70
|
+
station: 'xmtp',
|
|
71
|
+
line,
|
|
72
|
+
from: SELF_URI,
|
|
73
|
+
text: `🎙️ ${text}`,
|
|
74
|
+
payload: {
|
|
75
|
+
contentType: 'transcript',
|
|
76
|
+
transcribeFor: sourceMsgId,
|
|
77
|
+
transcript: text,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
process.stderr.write(`xmtp transcribe failed: ${errMsg(err)}\n`);
|
|
82
|
+
} finally {
|
|
83
|
+
for (const f of [inFile, wav, `${out}.txt`]) {
|
|
84
|
+
try {
|
|
85
|
+
unlinkSync(f);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { Client, type ClientOptions, type Signer } from '@xmtp/node-sdk';
|
|
4
|
+
import { expandHome, signerFor, XMTP_ENV } from './identity.js';
|
|
5
|
+
|
|
6
|
+
export class XmtpVerifyError extends Error {}
|
|
7
|
+
|
|
8
|
+
export interface XmtpVerified {
|
|
9
|
+
inboxId: string;
|
|
10
|
+
address: string;
|
|
11
|
+
installationId: string;
|
|
12
|
+
dbPath: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RegisteredClient {
|
|
16
|
+
inboxId: string;
|
|
17
|
+
installationId: string;
|
|
18
|
+
isRegistered: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CreateXmtpClient = (
|
|
22
|
+
signer: Signer,
|
|
23
|
+
dbPath: string,
|
|
24
|
+
) => Promise<RegisteredClient>;
|
|
25
|
+
|
|
26
|
+
const openInbox: CreateXmtpClient = (signer, dbPath) => {
|
|
27
|
+
const options: ClientOptions = { env: XMTP_ENV, dbPath };
|
|
28
|
+
return Client.create(signer, options);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const reason = (err: unknown): string =>
|
|
32
|
+
err instanceof Error && err.message !== '' ? err.message : 'no reason given';
|
|
33
|
+
|
|
34
|
+
export async function verifyXmtpKey(
|
|
35
|
+
privateKey: string,
|
|
36
|
+
dbPath: string,
|
|
37
|
+
create: CreateXmtpClient = openInbox,
|
|
38
|
+
): Promise<XmtpVerified> {
|
|
39
|
+
let signer: Signer;
|
|
40
|
+
let address: string;
|
|
41
|
+
try {
|
|
42
|
+
({ signer, address } = signerFor(privateKey));
|
|
43
|
+
} catch {
|
|
44
|
+
throw new XmtpVerifyError('that is not a usable XMTP private key');
|
|
45
|
+
}
|
|
46
|
+
const resolved = expandHome(dbPath);
|
|
47
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
48
|
+
const client = await create(signer, resolved).catch((err: unknown) => {
|
|
49
|
+
throw new XmtpVerifyError(
|
|
50
|
+
`XMTP would not open an inbox for that key: ${reason(err)}`,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
if (!client.isRegistered)
|
|
54
|
+
throw new XmtpVerifyError('XMTP did not register an inbox for that key');
|
|
55
|
+
if (client.inboxId === '')
|
|
56
|
+
throw new XmtpVerifyError('XMTP returned no inbox id for that key');
|
|
57
|
+
return {
|
|
58
|
+
inboxId: client.inboxId,
|
|
59
|
+
address,
|
|
60
|
+
installationId: client.installationId,
|
|
61
|
+
dbPath: resolved,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function runCli(): Promise<void> {
|
|
66
|
+
let out: Record<string, unknown>;
|
|
67
|
+
try {
|
|
68
|
+
const input = JSON.parse(readFileSync(0, 'utf8')) as {
|
|
69
|
+
privateKey?: unknown;
|
|
70
|
+
dbPath?: unknown;
|
|
71
|
+
};
|
|
72
|
+
if (typeof input.privateKey !== 'string' || typeof input.dbPath !== 'string')
|
|
73
|
+
throw new XmtpVerifyError('xmtp verify needs a privateKey and a dbPath');
|
|
74
|
+
out = { ok: true, ...(await verifyXmtpKey(input.privateKey, input.dbPath)) };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
out = { ok: false, error: reason(err) };
|
|
77
|
+
}
|
|
78
|
+
writeFileSync(1, `${JSON.stringify(out)}\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (import.meta.main) {
|
|
82
|
+
await runCli();
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export { emit, respond, mintId } from '@metro-labs/mcp/stations/station-runtime';
|
|
2
|
+
|
|
3
|
+
export const SELF_URI = process.env.METRO_SELF_URI ?? '';
|
|
4
|
+
|
|
5
|
+
const UID_MAP_MAX = 5000;
|
|
6
|
+
const uidToXmtp = new Map<string, string>();
|
|
7
|
+
|
|
8
|
+
const INBOX_ETH_CACHE_MAX = 5000;
|
|
9
|
+
export const inboxEthCache = new Map<string, string>();
|
|
10
|
+
|
|
11
|
+
export function cacheInboxEth(inboxId: string, eth: string): void {
|
|
12
|
+
inboxEthCache.set(inboxId, eth);
|
|
13
|
+
if (inboxEthCache.size > INBOX_ETH_CACHE_MAX) {
|
|
14
|
+
const oldest = inboxEthCache.keys().next().value;
|
|
15
|
+
if (oldest !== undefined) inboxEthCache.delete(oldest);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function rememberUid(
|
|
20
|
+
uid: string | undefined,
|
|
21
|
+
xmtpId: string | undefined,
|
|
22
|
+
): void {
|
|
23
|
+
if (!uid || !xmtpId || !uid.startsWith('msg_')) return;
|
|
24
|
+
uidToXmtp.set(uid, xmtpId);
|
|
25
|
+
if (uidToXmtp.size > UID_MAP_MAX) {
|
|
26
|
+
const oldest = uidToXmtp.keys().next().value;
|
|
27
|
+
if (oldest !== undefined) uidToXmtp.delete(oldest);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveMsgId(rawId: string): string {
|
|
32
|
+
if (!rawId.startsWith('msg_')) return rawId;
|
|
33
|
+
const mapped = uidToXmtp.get(rawId);
|
|
34
|
+
if (mapped) return mapped;
|
|
35
|
+
throw new Error(
|
|
36
|
+
`could not resolve universal id ${rawId} to an xmtp message id ` +
|
|
37
|
+
'(not seen by this train; pass the raw xmtp message_id)',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
File without changes
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Stage Labs
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/README.md
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Metro
|
|
2
|
-
|
|
3
|
-
Chat with your Claude Code or Codex agent over Telegram and Discord.
|
|
4
|
-
|
|
5
|
-
## Quickstart
|
|
6
|
-
|
|
7
|
-
In your shell:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install -g @stage-labs/metro@beta # or: bun add -g @stage-labs/metro@beta
|
|
11
|
-
|
|
12
|
-
metro setup telegram <token> # https://t.me/BotFather
|
|
13
|
-
metro setup discord <token> # https://discord.com/developers/applications
|
|
14
|
-
|
|
15
|
-
metro setup skill # writes SKILL.md so Claude Code + Codex auto-onboard
|
|
16
|
-
metro doctor # verify
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
> **Discord setup:** toggle **Message Content Intent** in Developer Portal → Bot → Privileged Gateway Intents.
|
|
20
|
-
|
|
21
|
-
### Run with Claude Code
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
claude
|
|
25
|
-
> Run metro in the background.
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Then DM your bot. The bundled skill auto-triggers — the agent launches metro via Bash + Monitor, watches stdout, and replies.
|
|
29
|
-
|
|
30
|
-
### Run with Codex
|
|
31
|
-
|
|
32
|
-
Codex's `unified_exec` is poll-only ([#4751](https://github.com/openai/codex/issues/4751)) — there's no Monitor equivalent. Metro instead pushes each inbound into the agent's history via JSON-RPC. Two terminals plus a prompt — the TUI's `--remote` flag only accepts `ws://`, so daemon and TUI share one URL:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
# Terminal 1 — daemon (must be running first)
|
|
36
|
-
codex app-server --listen ws://127.0.0.1:8421
|
|
37
|
-
|
|
38
|
-
# Terminal 2 — TUI attached to the daemon
|
|
39
|
-
codex --remote ws://127.0.0.1:8421
|
|
40
|
-
> Run metro in the background.
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
The agent launches `metro` (with `METRO_CODEX_RC=ws://127.0.0.1:8421` set) via its shell tool. Metro connects to the daemon and pushes each inbound as a `turn/start` on the active thread — the agent in terminal 2 reacts on its next turn. `codex remote-control` is stdio-only (no listener), so don't use it for this flow.
|
|
44
|
-
|
|
45
|
-
Bare `codex` (no `--remote`) can't work with metro — the agent has no daemon to push to. The TUI must be attached to a running app-server.
|
|
46
|
-
|
|
47
|
-
`METRO_CODEX_RC` accepts `ws://host:port` (required for use with the codex TUI) or `unix:///abs/path` (headless only — the daemon supports UDS but the TUI doesn't).
|
|
48
|
-
|
|
49
|
-
## Config
|
|
50
|
-
|
|
51
|
-
| Variable | Default | Description |
|
|
52
|
-
|---|---|---|
|
|
53
|
-
| `TELEGRAM_BOT_TOKEN`, `DISCORD_BOT_TOKEN` | — | Bot tokens. `metro setup` writes them here. |
|
|
54
|
-
| `METRO_CONFIG_DIR` | `~/.config/metro` | Where the global `.env` lives. |
|
|
55
|
-
| `METRO_STATE_DIR` | `~/.cache/metro` | Lockfile, attachment cache, default download dir. |
|
|
56
|
-
| `METRO_LOG_LEVEL` | `info` | `trace` / `debug` / `info` / `warn` / `error` / `fatal`. |
|
|
57
|
-
| `METRO_CODEX_RC` | — | Codex app-server URL (e.g. `ws://127.0.0.1:8421`). When set, metro pushes each inbound into the agent's history via JSON-RPC `turn/start` — the Codex equivalent of Claude Code's Monitor. Accepts `ws://host:port` (required for use with the codex TUI) or `unix:///abs/path` (headless only). See [Codex setup](#codex-setup). |
|
|
58
|
-
|
|
59
|
-
Token precedence: process env → `./.env` → `$METRO_CONFIG_DIR/.env`. Logs to stderr.
|
|
60
|
-
|
|
61
|
-
## Reference
|
|
62
|
-
|
|
63
|
-
- `metro --help` — command surface
|
|
64
|
-
- `metro doctor` — health check
|
|
65
|
-
- [SKILL.md](skills/metro/SKILL.md) — agent-facing flow
|
|
66
|
-
|
|
67
|
-
## Uninstall
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
metro setup clear; metro setup skill --clear
|
|
71
|
-
rm -rf ~/.cache/metro/
|
|
72
|
-
npm uninstall -g @stage-labs/metro
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Caveats
|
|
76
|
-
|
|
77
|
-
- **No allowlist.** Anyone who can DM your bot or @-mention it can talk to your session. Run against bots you own.
|
|
78
|
-
- **Latency.** Inbounds surface at the next agent decision boundary — sub-second on Claude Code, longer on Codex turns.
|
package/dist/channels/discord.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { Client, Events, GatewayIntentBits, Partials } from 'discord.js';
|
|
2
|
-
import { errMsg, log } from '../log.js';
|
|
3
|
-
// Receive path: discord.js gateway, used by tail.ts only.
|
|
4
|
-
// Send path: raw REST against discord.com/api — no gateway login required,
|
|
5
|
-
// so cli.ts subcommands stay one-shot and fast.
|
|
6
|
-
const API_BASE = 'https://discord.com/api/v10';
|
|
7
|
-
function token() {
|
|
8
|
-
const t = process.env.DISCORD_BOT_TOKEN;
|
|
9
|
-
if (!t)
|
|
10
|
-
throw new Error('DISCORD_BOT_TOKEN is not set');
|
|
11
|
-
return t;
|
|
12
|
-
}
|
|
13
|
-
async function rest(method, path, body, timeoutMs = 30_000) {
|
|
14
|
-
const res = await fetch(`${API_BASE}${path}`, {
|
|
15
|
-
method,
|
|
16
|
-
headers: {
|
|
17
|
-
'Authorization': `Bot ${token()}`,
|
|
18
|
-
'User-Agent': 'metro (https://github.com/bonustrack/metro, dev)',
|
|
19
|
-
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
20
|
-
},
|
|
21
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
22
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
23
|
-
});
|
|
24
|
-
if (!res.ok) {
|
|
25
|
-
const text = await res.text().catch(() => '');
|
|
26
|
-
throw new Error(`discord ${method} ${path}: ${res.status} ${text}`);
|
|
27
|
-
}
|
|
28
|
-
// 204 No Content for typing/reactions/clear.
|
|
29
|
-
if (res.status === 204)
|
|
30
|
-
return undefined;
|
|
31
|
-
return (await res.json());
|
|
32
|
-
}
|
|
33
|
-
// ---------- Receive path (gateway, discord.js) -----------------------------
|
|
34
|
-
let client = null;
|
|
35
|
-
function getClient() {
|
|
36
|
-
if (client)
|
|
37
|
-
return client;
|
|
38
|
-
client = new Client({
|
|
39
|
-
intents: [
|
|
40
|
-
GatewayIntentBits.DirectMessages,
|
|
41
|
-
GatewayIntentBits.Guilds,
|
|
42
|
-
GatewayIntentBits.GuildMessages,
|
|
43
|
-
GatewayIntentBits.MessageContent,
|
|
44
|
-
],
|
|
45
|
-
// DM channels arrive partial; without this messageCreate never fires.
|
|
46
|
-
partials: [Partials.Channel],
|
|
47
|
-
});
|
|
48
|
-
return client;
|
|
49
|
-
}
|
|
50
|
-
let onInboundHandler = () => { };
|
|
51
|
-
export function onInbound(handler) {
|
|
52
|
-
onInboundHandler = handler;
|
|
53
|
-
}
|
|
54
|
-
export async function shutdownGateway() {
|
|
55
|
-
if (!client)
|
|
56
|
-
return;
|
|
57
|
-
await client.destroy();
|
|
58
|
-
client = null;
|
|
59
|
-
}
|
|
60
|
-
export async function startGateway() {
|
|
61
|
-
const c = getClient();
|
|
62
|
-
c.on(Events.MessageCreate, m => {
|
|
63
|
-
if (m.author.bot)
|
|
64
|
-
return;
|
|
65
|
-
// Guild messages: only forward when the bot is mentioned. DMs always pass.
|
|
66
|
-
// The bot's own @-mention is preserved in `m.content` — stripping it would
|
|
67
|
-
// lose mid-sentence position ("Wdyt @Metro is this good?" → "Wdyt is this
|
|
68
|
-
// good?") and silently drop bare-mention pings. The agent recognizes
|
|
69
|
-
// `<@bot_id>` and acts on the request as a whole.
|
|
70
|
-
if (m.guildId && c.user && !m.mentions.has(c.user.id))
|
|
71
|
-
return;
|
|
72
|
-
const tags = [...m.attachments.values()]
|
|
73
|
-
.map(a => {
|
|
74
|
-
if (a.contentType?.startsWith('image/'))
|
|
75
|
-
return '[image]';
|
|
76
|
-
if (a.contentType?.startsWith('audio/'))
|
|
77
|
-
return `[audio: ${a.name}]`;
|
|
78
|
-
return `[file: ${a.name}]`;
|
|
79
|
-
})
|
|
80
|
-
.join(' ');
|
|
81
|
-
const text = [m.content, tags].filter(Boolean).join(' ').trim();
|
|
82
|
-
if (!text)
|
|
83
|
-
return;
|
|
84
|
-
onInboundHandler({ channel_id: m.channelId, message_id: m.id, text });
|
|
85
|
-
});
|
|
86
|
-
c.on(Events.Error, err => log.error({ err: errMsg(err) }, 'discord error'));
|
|
87
|
-
await c.login(process.env.DISCORD_BOT_TOKEN);
|
|
88
|
-
await new Promise(r => c.once(Events.ClientReady, () => r()));
|
|
89
|
-
}
|
|
90
|
-
export async function getMe() {
|
|
91
|
-
const me = await rest('GET', '/users/@me');
|
|
92
|
-
return { username: me.username };
|
|
93
|
-
}
|
|
94
|
-
export async function sendMessage(channelId, text) {
|
|
95
|
-
const sent = await rest('POST', `/channels/${channelId}/messages`, { content: text });
|
|
96
|
-
return sent.id;
|
|
97
|
-
}
|
|
98
|
-
export async function replyToMessage(channelId, messageId, text) {
|
|
99
|
-
const sent = await rest('POST', `/channels/${channelId}/messages`, {
|
|
100
|
-
content: text,
|
|
101
|
-
message_reference: { message_id: messageId, fail_if_not_exists: false },
|
|
102
|
-
});
|
|
103
|
-
return sent.id;
|
|
104
|
-
}
|
|
105
|
-
export async function editMessage(channelId, messageId, text) {
|
|
106
|
-
const sent = await rest('PATCH', `/channels/${channelId}/messages/${messageId}`, { content: text });
|
|
107
|
-
return sent.id;
|
|
108
|
-
}
|
|
109
|
-
export async function sendTyping(channelId) {
|
|
110
|
-
await rest('POST', `/channels/${channelId}/typing`);
|
|
111
|
-
}
|
|
112
|
-
export async function setReaction(channelId, messageId, emoji) {
|
|
113
|
-
if (emoji) {
|
|
114
|
-
await rest('PUT', `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
// Clear only the bot's own reactions (matches Telegram's clear semantics).
|
|
118
|
-
const msg = await rest('GET', `/channels/${channelId}/messages/${messageId}`);
|
|
119
|
-
for (const r of msg.reactions ?? []) {
|
|
120
|
-
if (!r.me || !r.emoji.name)
|
|
121
|
-
continue;
|
|
122
|
-
await rest('DELETE', `/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(r.emoji.name)}/@me`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
export async function fetchAttachments(channelId, messageId) {
|
|
126
|
-
const msg = await rest('GET', `/channels/${channelId}/messages/${messageId}`);
|
|
127
|
-
const out = [];
|
|
128
|
-
for (const a of msg.attachments) {
|
|
129
|
-
if (!a.content_type?.startsWith('image/'))
|
|
130
|
-
continue;
|
|
131
|
-
const res = await fetch(a.url, { signal: AbortSignal.timeout(30_000) });
|
|
132
|
-
if (!res.ok)
|
|
133
|
-
throw new Error(`discord: download ${a.url}: ${res.status}`);
|
|
134
|
-
out.push({ data: Buffer.from(await res.arrayBuffer()).toString('base64'), mime: a.content_type });
|
|
135
|
-
}
|
|
136
|
-
return out;
|
|
137
|
-
}
|
|
138
|
-
export async function fetchRecentMessages(channelId, limit) {
|
|
139
|
-
const n = Math.min(Math.max(limit, 1), 100);
|
|
140
|
-
const msgs = await rest('GET', `/channels/${channelId}/messages?limit=${n}`);
|
|
141
|
-
// Discord returns newest-first; reverse for chronological.
|
|
142
|
-
return [...msgs].reverse().map(m => ({
|
|
143
|
-
message_id: m.id,
|
|
144
|
-
author: m.author.username,
|
|
145
|
-
text: m.content,
|
|
146
|
-
timestamp: m.timestamp,
|
|
147
|
-
}));
|
|
148
|
-
}
|