@stage-labs/metro 0.1.0-beta.7 → 0.1.0-beta.8

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.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: metro
3
- description: Run the metro Telegram/Discord bridge in this session — launch `metro` in the background, watch its stdout for inbound JSON events, and act on each. Use when the user asks to start/run/launch metro, when you see JSON lines on stdout shaped `{"kind":"inbound","station":...,"line":"metro://...","messageId":...,"text":...}`, or when handling a chat reply/edit/react/send/download/fetch/notify.
3
+ description: Run the metro Telegram/Discord/webhook bridge in this session — launch `metro` in the background, watch its stdout for inbound JSON events, and act on each. Use when the user asks to start/run/launch metro, when you see JSON lines on stdout shaped `{"kind":"inbound","station":...,"line":"metro://...","messageId":...,"text":...}`, or when handling a chat/webhook reply/edit/react/send/download/fetch/notify.
4
4
  ---
5
5
 
6
- # Metro — running the Telegram & Discord bridge
6
+ # Metro — running the Telegram / Discord / webhook bridge
7
7
 
8
- Metro is a CLI bridge between this agent session and Telegram/Discord. You launch `metro` once when the user asks, then act on each inbound JSON line via `metro <subcommand>`.
8
+ Metro is a CLI bridge between this agent session and external sources: Telegram, Discord, and HTTP webhooks from third parties (GitHub, Intercom, Fireflies, …). You launch `metro` once when the user asks, then act on each inbound JSON line via `metro <subcommand>`.
9
9
 
10
10
  ## Starting the bridge
11
11
 
@@ -44,8 +44,8 @@ Every line on stdout is one **history entry** — the same record appended to `h
44
44
  - `kind` — `"inbound"`, `"notification"`, `"outbound"`, `"edit"`, or `"react"`
45
45
  - `id` (`msg_…`) — universal message ID minted by metro
46
46
  - `ts` — ISO timestamp
47
- - `station` — `"discord"`, `"telegram"`, `"claude"`, `"codex"`
48
- - `line` — conversation URI; `lineName?` is the channel/topic display name
47
+ - `station` — `"discord"`, `"telegram"`, `"claude"`, `"codex"`, `"webhook"`
48
+ - `line` — conversation URI; `lineName?` is the channel/topic display name (for webhooks: the label you gave it)
49
49
  - `from` / `fromName?` — sender participant URI + optional display name
50
50
  - `to` — recipient participant URI (agent for inbound, line for notification, original sender for replies/reacts)
51
51
  - `text` — universal display projection. Includes `[image]`/`[file: …]`/`[voice]`/`[audio]` tags inline.
@@ -53,11 +53,11 @@ Every line on stdout is one **history entry** — the same record appended to `h
53
53
  - `payload?` — raw platform-native message object. Set on inbound only. Shape varies per `station`.
54
54
 
55
55
  ```json
56
- {"kind":"inbound","id":"msg_aB3xY7zP","ts":"2026-05-14T12:00:00Z","station":"telegram","line":"metro://telegram/-100…/247","lineName":"infra","from":"metro://telegram/user/12345","fromName":"@alice","to":"metro://claude/agent","messageId":"4567","text":"hi [image]","payload":{"message_id":4567,"chat":{"id":-100,"type":"supergroup","is_forum":true},"from":{"id":12345,"username":"alice"},"text":"hi","photo":[{"file_id":"…"}],"reply_to_message":{"message_id":4500,"text":"earlier","from":{"id":99,"username":"bob"}}}}
56
+ {"kind":"inbound","id":"msg_aB3xY7zP","ts":"2026-05-14T12:00:00Z","station":"telegram","line":"metro://telegram/-100…/247","lineName":"infra","from":"metro://telegram/user/12345","fromName":"@alice","to":"metro://claude/user/9bfc7af0-…","messageId":"4567","text":"hi [image]","payload":{"message_id":4567,"chat":{"id":-100,"type":"supergroup","is_forum":true},"from":{"id":12345,"username":"alice"},"text":"hi","photo":[{"file_id":"…"}],"reply_to_message":{"message_id":4500,"text":"earlier","from":{"id":99,"username":"bob"}}}}
57
57
  ```
58
58
 
59
59
  ```json
60
- {"kind":"notification","id":"msg_pQ4r5sT0","ts":"…","station":"claude","line":"metro://claude/deploys","from":"metro://codex/ci","to":"metro://claude/deploys","text":"deploy green"}
60
+ {"kind":"notification","id":"msg_pQ4r5sT0","ts":"…","station":"claude","line":"metro://claude/9bfc7af0-…/50b00d11-…","from":"metro://codex/user/8119ecb1-…","to":"metro://claude/9bfc7af0-…/50b00d11-…","text":"deploy green"}
61
61
  ```
62
62
 
63
63
  ### `payload` by station
@@ -66,6 +66,7 @@ Every line on stdout is one **history entry** — the same record appended to `h
66
66
 
67
67
  - **`discord`** — discord.js `Message.toJSON()`: camelCase fields (`channelId`, `guildId`, `content`, `author`, `mentions: { users[], roles[], everyone }`, `attachments[]`, `reference`, …). Collections come back as **arrays of IDs**. `referencedMessage` is added inline on replies (auto-fetched).
68
68
  - **`telegram`** — raw Bot API `Message` (snake_case): `{ message_id, chat, from, text, caption, entities[], photo[], document, voice, audio, reply_to_message, … }`. `reply_to_message` is inline on replies.
69
+ - **`webhook`** — `{ headers, body }`. The provider lives in `headers['x-github-event']`, `headers['x-intercom-topic']`, etc. Full event payload is `body` (parsed JSON when possible). `text` is a short summary; always narrow on `body` for real routing.
69
70
 
70
71
  Use `payload` for anything the envelope doesn't surface — mentions, reply chains, embeds, entities.
71
72
 
@@ -75,15 +76,18 @@ Derive from `payload`. Bot id per station is cached in `$METRO_STATE_DIR/bot-ids
75
76
 
76
77
  - **discord** — DM when `payload.guildId == null`; otherwise pinged when `payload.mentions.users.includes(<bot-id>)`.
77
78
  - **telegram** — DM when `payload.chat.type === 'private'`; otherwise pinged when any entity in `payload.entities` (or `caption_entities`) is `{type:"mention"}` matching `@<bot-username>` or `{type:"text_mention", user:{id:<bot-id>}}`.
79
+ - **webhook** — every POST is "for you" by design (it's an endpoint you registered). Route on `payload.headers['x-github-event']` / `x-intercom-topic` etc. to decide which provider event you're handling.
78
80
 
79
- Default: only reply on DM or ping; otherwise stay silent or `metro react` to ack.
81
+ Default for chat: only reply on DM or ping; otherwise stay silent or `metro react` to ack. Webhooks have no "ack" mechanism — just consume the event.
80
82
 
81
83
  Both `from` and `to` are **participant URIs** (the conversation context lives in `line`):
82
84
  - `metro://<station>/user/<id>` — a person on a chat platform
83
- - `metro://claude/<topic>` / `metro://codex/<topic>` — an agent
85
+ - `metro://claude/user/<orgId>` — a Claude Code agent (orgId = stable Anthropic-account UUID, same across devices for the same account)
86
+ - `metro://codex/user/<accountId>` — a Codex agent (accountId = stable ChatGPT-account UUID, same across devices)
87
+ - `metro://webhook/<endpoint-id>` — a webhook endpoint (line + `from` are the same — no HTTP-side user identity)
84
88
  - `metro://<station>/<channelId>` — a channel (used as `to` for fresh sends to a group, where no single recipient)
85
89
 
86
- When **you** send via `metro send`/`reply`/`edit`/`react`, metro auto-stamps `from = metro://claude/agent` (from `$CLAUDECODE`) or `metro://codex/agent` (from `$METRO_CODEX_RC` / `$CODEX_HOME`). Override with `--from=<uri>` or `$METRO_FROM`. When replying/reacting, `to` is automatically the original sender (looked up via the universal id).
90
+ When **you** send via `metro send`/`reply`/`edit`/`react`, metro auto-stamps `from = metro://claude/user/<orgId>` (when `$CLAUDECODE` is set; resolved from `claude auth status --json`) or `metro://codex/user/<accountId>` (from `$METRO_CODEX_RC` / `$CODEX_HOME`; resolved from `$CODEX_HOME/auth.json`). Switching accounts via `claude auth login` / `codex login` flips the id on the next event (within ~5 s for the daemon). Override with `--from=<uri>` or `$METRO_FROM`. When replying/reacting, `to` is automatically the original sender (looked up via the universal id).
87
91
 
88
92
  The `id` is the **canonical handle** for that message across all stations — store it if you want to refer back to it later.
89
93
 
@@ -111,7 +115,10 @@ All take positional args (no `--to=`/`--text=` flags). Append `--json` to any fo
111
115
  | Reaction (empty emoji clears) | `metro react <line> <messageId> <emoji>` |
112
116
  | Download `[image]` attachments → paths | `metro download <line> <messageId> [--out=<dir>]` |
113
117
  | Recent channel history (Discord only) | `metro fetch <line> [--limit=20]` |
114
- | Ping another agent (cross-agent line) | `metro send metro://claude/<topic> <text> [--from=<line>]` |
118
+ | Ping another agent (cross-agent line) | `metro send metro://claude/<agent-id>/<session-id> <text> [--from=<line>]` |
119
+ | Register webhook endpoint | `metro webhook add <label> [--secret=<hmac-secret>]` |
120
+ | List / remove webhook endpoints | `metro webhook list` · `metro webhook remove <id>` |
121
+ | Configure Cloudflare named tunnel | `metro tunnel setup <tunnel-name> <hostname>` |
115
122
 
116
123
  `reply` / `send` / `edit` accept multi-line text via stdin (heredoc).
117
124
 
@@ -152,8 +159,9 @@ Limits / quirks:
152
159
  |------------|-------------------------------------------|--------------------------------------|
153
160
  | `discord` | `metro://discord/<channel-id>` | `metro://discord/1234567890` |
154
161
  | `telegram` | `metro://telegram/<chat-id>[/<topic-id>]` | `metro://telegram/-1001234567890/42` |
155
- | `claude` | `metro://claude/<topic>` | `metro://claude/deploys` |
156
- | `codex` | `metro://codex/<topic>` | `metro://codex/ci` |
162
+ | `claude` | `metro://claude/<agent-id>/<session-id>` | `metro://claude/9bfc7af0-…/50b00d11-…` |
163
+ | `codex` | `metro://codex/<agent-id>/<session-id>` | `metro://codex/8119ecb1-…/01997d4b-…` |
164
+ | `webhook` | `metro://webhook/<endpoint-id>` | `metro://webhook/fwaCgTKJuLAjS2K0` |
157
165
 
158
166
  The `messageId` is **not** part of the URI — it's a separate positional arg for `reply` / `edit` / `react` / `download`.
159
167
 
@@ -174,8 +182,8 @@ When an event's `text` contains `[image]`:
174
182
  Both agents can post to each other's "agent line":
175
183
 
176
184
  ```bash
177
- metro send metro://claude/deploys "build green, ready to ship"
178
- metro send metro://codex/ci "build green" --from=metro://claude/deploys # override sender
185
+ metro send metro://claude/9bfc7af0-…/50b00d11-… "build green, ready to ship"
186
+ metro send metro://codex/8119ecb1-…/01997d4b-… "build green" --from=metro://claude/user/9bfc7af0-… # override sender
179
187
  ```
180
188
 
181
189
  The daemon re-emits the post on its stdout stream (and pushes via codex-rc if configured), so the peer agent sees a `{"kind":"notification",...}` event. Requires the metro daemon to be running on the machine — agent-line sends error with `metro daemon is not running` otherwise.
@@ -187,7 +195,7 @@ The daemon re-emits the post on its stdout stream (and pushes via codex-rc if co
187
195
  - `metro history` — universal message log (every inbound + outbound + notification across all stations). Newest first. Filters:
188
196
  - `--limit=N` (default 50)
189
197
  - `--line=<metro://…>` — only this conversation
190
- - `--station=<discord|telegram|claude|codex>`
198
+ - `--station=<discord|telegram|claude|codex|webhook>`
191
199
  - `--kind=<inbound|outbound|edit|react|notification>`
192
200
  - `--from=<sender>`
193
201
  - `--text=<substring>`