@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.200
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 +11 -0
- package/dist/claude-settings.js +40 -0
- package/dist/claude.js +163 -0
- package/dist/cli.js +77 -567
- package/dist/control.js +74 -0
- package/dist/hold.js +211 -0
- package/dist/local.js +43 -0
- package/dist/mcp-config.js +25 -0
- package/dist/node-name.js +71 -0
- package/dist/onboarding.js +44 -0
- package/dist/provider-flags.js +9 -0
- package/dist/route.js +58 -0
- package/dist/runtime-install.js +97 -0
- package/dist/runtime.js +74 -0
- package/dist/serve.js +158 -0
- package/dist/service.js +243 -0
- package/dist/tail.js +102 -144
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -23
- package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +17 -0
- package/runtime/marketplace/plugin/.mcp.json +1 -0
- package/runtime/marketplace/plugin/bin/guard.mjs +71 -0
- package/runtime/marketplace/plugin/bin/metro-plugin.mjs +30 -0
- package/runtime/marketplace/plugin/bin/policy.mjs +97 -0
- package/runtime/marketplace/plugin/bin/session-start.mjs +31 -0
- package/runtime/marketplace/plugin/hooks/hooks.json +25 -0
- package/runtime/marketplace/plugin/orchestrator.md +24 -0
- package/runtime/node_modules/@metro-labs/core/package.json +47 -0
- package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/events.ts +119 -0
- package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/ids.ts +24 -0
- package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +91 -0
- package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/protocol.ts +227 -0
- package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +74 -0
- package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/account-files.ts +41 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +58 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/sender-profile.ts +64 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +61 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +89 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +123 -0
- package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/core/src/tickets.ts +37 -0
- package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/package.json +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/agent-fs.ts +134 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files-api.ts +12 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +119 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/home-fs.ts +113 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +100 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +252 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +61 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +87 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +55 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +76 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +357 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +30 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +59 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +131 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +132 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +305 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +168 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +35 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +128 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/policy-route.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/sender-cards.ts +86 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/api.ts +55 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +149 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +221 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +21 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +12 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +370 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +78 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +13 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +1 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +355 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +359 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +60 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +149 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +75 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +283 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +109 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +271 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +120 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +113 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +306 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +176 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +72 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +18 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +159 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +135 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +158 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +166 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +131 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-policy.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +348 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +358 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +81 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +106 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-reaper.ts +174 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +278 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +233 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +173 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +194 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +137 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +77 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +237 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +118 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +274 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +160 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +46 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +150 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +139 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +223 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +199 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +299 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +193 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +36 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +18 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/subscription.ts +120 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +15 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +244 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +156 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/attachment-schema.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +226 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +137 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +19 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +87 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +79 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +83 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +79 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +60 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-tool.ts +96 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +87 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +46 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +202 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +88 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-def.ts +9 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +225 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +333 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +61 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +43 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +126 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +165 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/privilege.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +132 -0
- package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +316 -0
- package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +118 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +35 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +365 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +71 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +58 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-driver.ts +42 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +128 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +134 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-outlook.ts +63 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +225 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +288 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +248 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +265 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +84 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +124 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +15 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/api.ts +45 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/config.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +96 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/install.ts +56 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/paths.ts +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +148 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +130 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +61 -0
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +26 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +117 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +240 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +174 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +122 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +68 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +22 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/http/package.json +29 -0
- package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +138 -0
- package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
- package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
- package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +149 -0
- package/runtime/node_modules/@metro-labs/outlook/package.json +25 -0
- package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +107 -0
- package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +20 -0
- package/runtime/node_modules/@metro-labs/outlook/src/attachments.ts +97 -0
- package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +150 -0
- package/runtime/node_modules/@metro-labs/outlook/src/browser.ts +53 -0
- package/runtime/node_modules/@metro-labs/outlook/src/config.ts +36 -0
- package/runtime/node_modules/@metro-labs/outlook/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/outlook/src/graph.ts +60 -0
- package/runtime/node_modules/@metro-labs/outlook/src/html.ts +72 -0
- package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +104 -0
- package/runtime/node_modules/@metro-labs/outlook/src/index.ts +12 -0
- package/runtime/node_modules/@metro-labs/outlook/src/login.ts +150 -0
- package/runtime/node_modules/@metro-labs/outlook/src/me.ts +41 -0
- package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +116 -0
- package/runtime/node_modules/@metro-labs/outlook/src/query.ts +119 -0
- package/runtime/node_modules/@metro-labs/outlook/src/state.ts +43 -0
- package/runtime/node_modules/@metro-labs/outlook/src/station.ts +15 -0
- package/runtime/node_modules/@metro-labs/outlook/src/trust.ts +70 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +256 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +70 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +27 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +246 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +173 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +95 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +65 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +20 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +56 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
- package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +106 -0
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +134 -0
- package/runtime/node_modules/@metro-labs/threema/src/api.ts +152 -0
- package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +100 -0
- package/runtime/node_modules/@metro-labs/threema/src/files.ts +58 -0
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +169 -0
- package/runtime/node_modules/@metro-labs/threema/src/groups.ts +96 -0
- package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +89 -0
- package/runtime/node_modules/@metro-labs/threema/src/index.ts +24 -0
- package/runtime/node_modules/@metro-labs/threema/src/messages.ts +264 -0
- package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +160 -0
- package/runtime/node_modules/@metro-labs/threema/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +213 -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 +388 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +83 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +7 -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/names.ts +28 -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 +185 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +94 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +5 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +114 -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 +101 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +118 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +244 -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-payloads.ts +316 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +69 -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 +47 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +122 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +139 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +61 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +153 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +112 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/smart.ts +72 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +79 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +198 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +88 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +98 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +51 -0
- package/runtime/runtime.json +3 -0
- package/runtime/server.ts +1 -0
- package/runtime/stations.json +37 -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/skills/metro/SKILL.md +0 -122
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createPublicClient, http, namehash, type Hex } from 'viem';
|
|
2
|
+
import { base } from 'viem/chains';
|
|
3
|
+
import { normalize } from 'viem/ens';
|
|
4
|
+
import { makeProfileCache, type SenderProfile as Profile } from '@metro-labs/core/stations/sender-profile';
|
|
5
|
+
import { respond } from './wire.js';
|
|
6
|
+
import { TrainError } from '@metro-labs/core/train-error';
|
|
7
|
+
import { accountForCall, accounts, type Account } from './accounts.js';
|
|
8
|
+
import { resolveAddresses } from './conv-helpers.js';
|
|
9
|
+
import { nameOf } from './names.js';
|
|
10
|
+
import { BASENAME_L2_RESOLVER, BASENAME_REGISTRY, NAME_ABI, REGISTRY_ABI, reverseNodeOf } from './profile.js';
|
|
11
|
+
|
|
12
|
+
export interface SenderProfile {
|
|
13
|
+
address: string;
|
|
14
|
+
name: string | null;
|
|
15
|
+
displayName: string | null;
|
|
16
|
+
about: string | null;
|
|
17
|
+
avatar: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const BASE_RPC = 'https://rpc.brovider.xyz/8453';
|
|
21
|
+
const ZERO = '0x0000000000000000000000000000000000000000';
|
|
22
|
+
const AVATAR_RE = /^(https?:\/\/|ipfs:\/\/|data:image\/)/i;
|
|
23
|
+
|
|
24
|
+
const TEXT_ABI = [
|
|
25
|
+
{ name: 'addr', type: 'function', stateMutability: 'view', inputs: [{ name: 'node', type: 'bytes32' }], outputs: [{ name: '', type: 'address' }] },
|
|
26
|
+
{
|
|
27
|
+
name: 'text',
|
|
28
|
+
type: 'function',
|
|
29
|
+
stateMutability: 'view',
|
|
30
|
+
inputs: [{ name: 'node', type: 'bytes32' }, { name: 'key', type: 'string' }],
|
|
31
|
+
outputs: [{ name: '', type: 'string' }],
|
|
32
|
+
},
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export const baseRpc = (): string => process.env.METRO_BASE_RPC ?? BASE_RPC;
|
|
36
|
+
|
|
37
|
+
const makeReader = (rpc: string) => createPublicClient({ chain: base, transport: http(rpc), batch: { multicall: true } });
|
|
38
|
+
|
|
39
|
+
export type Reader = ReturnType<typeof makeReader>;
|
|
40
|
+
|
|
41
|
+
let reader: Reader | null = null;
|
|
42
|
+
const readerFor = (): Reader => (reader ??= makeReader(baseRpc()));
|
|
43
|
+
|
|
44
|
+
const trimmed = (v: string): string | null => (v.trim() === '' ? null : v.trim());
|
|
45
|
+
|
|
46
|
+
async function resolverOf(client: Reader, node: Hex): Promise<Hex> {
|
|
47
|
+
const found: Hex = await client.readContract({ address: BASENAME_REGISTRY, abi: REGISTRY_ABI, functionName: 'resolver', args: [node] });
|
|
48
|
+
return found === ZERO ? BASENAME_L2_RESOLVER : found;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function primaryName(client: Reader, address: string): Promise<string | null> {
|
|
52
|
+
const node = reverseNodeOf(address);
|
|
53
|
+
const found = await client.readContract({ address: await resolverOf(client, node), abi: NAME_ABI, functionName: 'name', args: [node] });
|
|
54
|
+
return found === '' ? null : found;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function recordsOf(client: Reader, name: string, address: string): Promise<SenderProfile | null> {
|
|
58
|
+
const node = namehash(normalize(name));
|
|
59
|
+
const resolver = { address: await resolverOf(client, node), abi: TEXT_ABI } as const;
|
|
60
|
+
const text = (key: string): Promise<string> => client.readContract({ ...resolver, functionName: 'text', args: [node, key] });
|
|
61
|
+
const [forward, displayName, about, avatar] = await Promise.all([
|
|
62
|
+
client.readContract({ ...resolver, functionName: 'addr', args: [node] }),
|
|
63
|
+
text('name'),
|
|
64
|
+
text('description'),
|
|
65
|
+
text('avatar'),
|
|
66
|
+
]);
|
|
67
|
+
if (forward.toLowerCase() !== address.toLowerCase()) return null;
|
|
68
|
+
return { address, name, displayName: trimmed(displayName), about: trimmed(about), avatar: AVATAR_RE.test(avatar.trim()) ? avatar.trim() : null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function profileOf(address: string, client: Reader = readerFor(), fetchImpl: typeof fetch = fetch): Promise<SenderProfile> {
|
|
72
|
+
const bare: SenderProfile = { address, name: null, displayName: null, about: null, avatar: null };
|
|
73
|
+
const name = (await primaryName(client, address)) ?? (await nameOf(address, fetchImpl));
|
|
74
|
+
if (name === null) return bare;
|
|
75
|
+
return (await recordsOf(client, name, address)) ?? bare;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const key = (acct: Account, inboxId: string): string => `${acct.cfg.id}:${inboxId}`;
|
|
79
|
+
|
|
80
|
+
const senders = makeProfileCache<SenderProfile>(
|
|
81
|
+
async (k) => {
|
|
82
|
+
const [accountId, inboxId] = k.split(':', 2) as [string, string];
|
|
83
|
+
const acct = accounts.get(accountId);
|
|
84
|
+
if (acct === undefined) return null;
|
|
85
|
+
const address = (await resolveAddresses(acct, [inboxId]))[inboxId];
|
|
86
|
+
return address === undefined ? null : profileOf(address);
|
|
87
|
+
},
|
|
88
|
+
{ onError: (k, err) => process.stderr.write(`xmtp: could not resolve the sender ${k}: ${err instanceof Error ? err.message : String(err)}\n`) },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
export const senderFields = (p: SenderProfile | null): Record<string, string> =>
|
|
92
|
+
p === null ? {} : { from_name: p.name ?? p.address, ...(p.displayName === null ? {} : { from_display_name: p.displayName }) };
|
|
93
|
+
|
|
94
|
+
export const senderFieldsNow = (acct: Account, inboxId: string): Record<string, string> => senderFields(senders.peek(key(acct, inboxId)));
|
|
95
|
+
|
|
96
|
+
export const senderProfile = (acct: Account, inboxId: string): Promise<SenderProfile | null> => senders.get(key(acct, inboxId));
|
|
97
|
+
|
|
98
|
+
export const profileView = (inboxId: string, p: SenderProfile | null): Profile => ({
|
|
99
|
+
id: inboxId,
|
|
100
|
+
...(p === null ? {} : { address: p.address }),
|
|
101
|
+
...(p?.name == null ? {} : { name: p.name }),
|
|
102
|
+
...(p?.displayName == null ? {} : { display_name: p.displayName }),
|
|
103
|
+
...(p?.about == null ? {} : { about: p.about }),
|
|
104
|
+
...(p?.avatar == null ? {} : { avatar: p.avatar }),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export async function profileAction(id: string, args: Record<string, unknown>): Promise<void> {
|
|
108
|
+
const user = typeof args.user === 'string' ? args.user.trim() : '';
|
|
109
|
+
if (user === '') throw new TrainError('xmtp_user_required', 'profile needs the inbox id of the person', { retryable: false });
|
|
110
|
+
const acct = accountForCall({ account: typeof args.account === 'string' ? args.account : undefined });
|
|
111
|
+
respond(id, { result: profileView(user, await senderProfile(acct, user)) });
|
|
112
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createPublicClient, http, type Hex } from 'viem';
|
|
2
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
3
|
+
import { base } from 'viem/chains';
|
|
4
|
+
import {
|
|
5
|
+
createKernelAccount,
|
|
6
|
+
createKernelAccountClient,
|
|
7
|
+
createZeroDevPaymasterClient,
|
|
8
|
+
getUserOperationGasPrice,
|
|
9
|
+
type KernelAccountClient,
|
|
10
|
+
} from '@zerodev/sdk';
|
|
11
|
+
import { getEntryPoint, KERNEL_V3_1 } from '@zerodev/sdk/constants';
|
|
12
|
+
import { signerToEcdsaValidator } from '@zerodev/ecdsa-validator';
|
|
13
|
+
|
|
14
|
+
export const SMART_CHAIN_ID = 8453;
|
|
15
|
+
const STAGE_ZERODEV_PROJECT = 'c107f734-f7bb-48c1-a5d2-ceb3f70a42f6';
|
|
16
|
+
const ENTRY_POINT = getEntryPoint('0.7');
|
|
17
|
+
|
|
18
|
+
export const zerodevRpc = (): string =>
|
|
19
|
+
`https://rpc.zerodev.app/api/v3/${process.env.METRO_ZERODEV_PROJECT ?? STAGE_ZERODEV_PROJECT}/chain/${String(SMART_CHAIN_ID)}`;
|
|
20
|
+
|
|
21
|
+
export const makePublicClient = (rpc: string) => createPublicClient({ chain: base, transport: http(rpc) });
|
|
22
|
+
|
|
23
|
+
export type BaseClient = ReturnType<typeof makePublicClient>;
|
|
24
|
+
|
|
25
|
+
export interface SmartAccount {
|
|
26
|
+
address: Hex;
|
|
27
|
+
publicClient: BaseClient;
|
|
28
|
+
client: () => KernelAccountClient;
|
|
29
|
+
signMessage: (message: string) => Promise<Hex>;
|
|
30
|
+
deployed: () => Promise<boolean>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function smartAccountFor(privateKey: string, rpc = zerodevRpc()): Promise<SmartAccount> {
|
|
34
|
+
const owner = privateKeyToAccount(privateKey as Hex);
|
|
35
|
+
const publicClient = makePublicClient(rpc);
|
|
36
|
+
const validator = await signerToEcdsaValidator(publicClient, { signer: owner, entryPoint: ENTRY_POINT, kernelVersion: KERNEL_V3_1 });
|
|
37
|
+
const account = await createKernelAccount(publicClient, { plugins: { sudo: validator }, entryPoint: ENTRY_POINT, kernelVersion: KERNEL_V3_1, index: 0n });
|
|
38
|
+
const paymaster = createZeroDevPaymasterClient({ chain: base, transport: http(rpc) });
|
|
39
|
+
const client = (): KernelAccountClient =>
|
|
40
|
+
createKernelAccountClient({
|
|
41
|
+
account,
|
|
42
|
+
chain: base,
|
|
43
|
+
bundlerTransport: http(rpc),
|
|
44
|
+
client: publicClient,
|
|
45
|
+
paymaster: { getPaymasterData: (userOperation) => paymaster.sponsorUserOperation({ userOperation }) },
|
|
46
|
+
userOperation: { estimateFeesPerGas: ({ bundlerClient }) => getUserOperationGasPrice(bundlerClient) },
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
address: account.address,
|
|
50
|
+
publicClient,
|
|
51
|
+
client,
|
|
52
|
+
signMessage: (message) => account.signMessage({ message }),
|
|
53
|
+
deployed: async () => {
|
|
54
|
+
const code = await publicClient.getCode({ address: account.address });
|
|
55
|
+
return code !== undefined && code !== '0x';
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function sendSponsored(smart: SmartAccount, to: Hex, data: Hex): Promise<Hex> {
|
|
61
|
+
const client = smart.client();
|
|
62
|
+
const hash = await client.sendTransaction({ to, data, value: 0n } as Parameters<typeof client.sendTransaction>[0]);
|
|
63
|
+
const receipt = await smart.publicClient.waitForTransactionReceipt({ hash });
|
|
64
|
+
if (receipt.status !== 'success') throw new Error(`the Base transaction ${hash} reverted`);
|
|
65
|
+
return hash;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function ensureDeployed(smart: SmartAccount): Promise<boolean> {
|
|
69
|
+
if (await smart.deployed()) return false;
|
|
70
|
+
await sendSponsored(smart, smart.address, '0x');
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { GroupOp, Station, Verb } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
|
|
3
|
+
import { XMTP_TOOLS, xmtpSendAttachments } from './tools.js';
|
|
4
|
+
|
|
5
|
+
export const xmtpStation: Station = {
|
|
6
|
+
name: 'xmtp',
|
|
7
|
+
hasAccounts: true,
|
|
8
|
+
hasTrain: true,
|
|
9
|
+
messageVerbs: new Set<Verb>(['send', 'reply', 'react', 'unreact', 'read']),
|
|
10
|
+
groupOps: new Set<GroupOp>(['create_group', 'add_members', 'remove_members']),
|
|
11
|
+
attachmentMode: 'native',
|
|
12
|
+
profileFields: new Set<ProfileField>(PROFILE_FIELDS),
|
|
13
|
+
readsProfiles: true,
|
|
14
|
+
claimsName: true,
|
|
15
|
+
sendAttachments: xmtpSendAttachments,
|
|
16
|
+
tools: XMTP_TOOLS,
|
|
17
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CanonicalAttachment, ToolContext } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { TrainError } from '@metro-labs/core/train-error';
|
|
3
|
+
import {
|
|
4
|
+
guessMime,
|
|
5
|
+
isImageMime,
|
|
6
|
+
isImageExt,
|
|
7
|
+
kindOf,
|
|
8
|
+
} from '@metro-labs/core/stations/attachments';
|
|
9
|
+
|
|
10
|
+
export const str = (v: unknown): string => (typeof v === 'string' ? v : '');
|
|
11
|
+
|
|
12
|
+
export async function setChannelMetadata(
|
|
13
|
+
a: Record<string, unknown>,
|
|
14
|
+
ctx: ToolContext,
|
|
15
|
+
) {
|
|
16
|
+
const line = str(a.line);
|
|
17
|
+
if (!line) return ctx.err('set_channel_metadata requires `line`');
|
|
18
|
+
const labels = a.labels as unknown[] | undefined;
|
|
19
|
+
const github = a.github as string | undefined;
|
|
20
|
+
const preview = a.preview as string | undefined;
|
|
21
|
+
const metaName = a.name as string | undefined;
|
|
22
|
+
const appData: Record<string, unknown> = {};
|
|
23
|
+
if (Array.isArray(labels)) appData.labels = labels.map(String);
|
|
24
|
+
if (typeof github === 'string') appData.github = github;
|
|
25
|
+
if (typeof preview === 'string') appData.preview = preview;
|
|
26
|
+
const hasName = typeof metaName === 'string' && metaName.length > 0;
|
|
27
|
+
if (!hasName && Object.keys(appData).length === 0)
|
|
28
|
+
return ctx.err(
|
|
29
|
+
'set_channel_metadata requires at least one of `labels`, `github`, `preview`, `name`',
|
|
30
|
+
);
|
|
31
|
+
const callArgs: Record<string, unknown> = { line, appData };
|
|
32
|
+
if (hasName) callArgs.name = metaName;
|
|
33
|
+
return ctx.okJson(await ctx.call('updateChannelMeta', callArgs));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const XMTP_ATTACH_MAX_BYTES = 190 * 1024;
|
|
37
|
+
|
|
38
|
+
async function sendFileAttachment(
|
|
39
|
+
line: string,
|
|
40
|
+
a: CanonicalAttachment,
|
|
41
|
+
src: string,
|
|
42
|
+
ctx: ToolContext,
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const buf = await ctx.readFile(src);
|
|
45
|
+
if (buf.byteLength > XMTP_ATTACH_MAX_BYTES) {
|
|
46
|
+
throw new TrainError(
|
|
47
|
+
'attachment_too_large',
|
|
48
|
+
`attachment '${src}' is ${(buf.byteLength / 1024).toFixed(0)} KiB; xmtp non-image files ` +
|
|
49
|
+
'over ~190 KiB (256 KiB once base64-encoded) cannot be sent via this MCP path. ' +
|
|
50
|
+
'Send it as an image, host it elsewhere, or use the metro CLI directly.',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
await ctx.call('sendAttachment', {
|
|
54
|
+
line,
|
|
55
|
+
name: a.name ?? src.split('/').pop() ?? 'attachment',
|
|
56
|
+
mime: a.mime ?? guessMime(src),
|
|
57
|
+
dataB64: buf.toString('base64'),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function xmtpSendAttachments(
|
|
62
|
+
line: string,
|
|
63
|
+
atts: CanonicalAttachment[],
|
|
64
|
+
ctx: ToolContext,
|
|
65
|
+
): Promise<string[]> {
|
|
66
|
+
const sent: string[] = [];
|
|
67
|
+
for (const a of atts) {
|
|
68
|
+
const src = a.path ?? a.url ?? '';
|
|
69
|
+
if (!src) continue;
|
|
70
|
+
const mime = a.mime ?? guessMime(src);
|
|
71
|
+
if (isImageMime(mime) || isImageExt(src)) {
|
|
72
|
+
await ctx.call('sendImage', { line, path: src });
|
|
73
|
+
} else {
|
|
74
|
+
await sendFileAttachment(line, a, src, ctx);
|
|
75
|
+
}
|
|
76
|
+
sent.push(kindOf(mime, src));
|
|
77
|
+
}
|
|
78
|
+
return sent;
|
|
79
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { StationTool } from '@metro-labs/core/stations/types';
|
|
2
|
+
import {
|
|
3
|
+
str,
|
|
4
|
+
setChannelMetadata,
|
|
5
|
+
xmtpSendAttachments,
|
|
6
|
+
} from './tools-handlers.js';
|
|
7
|
+
|
|
8
|
+
export { xmtpSendAttachments };
|
|
9
|
+
|
|
10
|
+
const lineProp = {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description:
|
|
13
|
+
'The metro:// line (from the inbound <channel> tag). The station is derived from it.',
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export const XMTP_TOOLS: StationTool[] = [
|
|
17
|
+
{
|
|
18
|
+
name: 'ask',
|
|
19
|
+
group: 'write',
|
|
20
|
+
description:
|
|
21
|
+
'Ask a question as a poll in an XMTP conversation (mirrors Claude AskUserQuestion). ' +
|
|
22
|
+
'Single-question form: question (required), options? (string[]), header?, multiSelect?, ' +
|
|
23
|
+
'open? (true => free-text answer, options optional). Multi-question form: questions ' +
|
|
24
|
+
'(array of {question, options?, header?, multiSelect?, open?}). Args: line (required) + ' +
|
|
25
|
+
'the above. xmtp-only (the daemon `ask` action). Returns the poll messageId + pollId.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
line: lineProp,
|
|
30
|
+
question: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'The question text (single-question form).',
|
|
33
|
+
},
|
|
34
|
+
options: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
description: 'Answer options for a single question.',
|
|
37
|
+
items: { type: 'string' },
|
|
38
|
+
},
|
|
39
|
+
header: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Optional header/title for the poll.',
|
|
42
|
+
},
|
|
43
|
+
multiSelect: {
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
description: 'Allow selecting multiple options.',
|
|
46
|
+
},
|
|
47
|
+
open: {
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
description: 'Free-text answer (options optional).',
|
|
50
|
+
},
|
|
51
|
+
questions: {
|
|
52
|
+
type: 'array',
|
|
53
|
+
description:
|
|
54
|
+
'Multiple questions (multi-question form). Each is {question, options?, header?, multiSelect?, open?}.',
|
|
55
|
+
items: { type: 'object' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
required: ['line'],
|
|
59
|
+
},
|
|
60
|
+
async handle(a, ctx) {
|
|
61
|
+
const line = str(a.line);
|
|
62
|
+
if (!line) return ctx.err('ask requires `line`');
|
|
63
|
+
const args: Record<string, unknown> = { line };
|
|
64
|
+
for (const k of [
|
|
65
|
+
'question',
|
|
66
|
+
'options',
|
|
67
|
+
'header',
|
|
68
|
+
'multiSelect',
|
|
69
|
+
'open',
|
|
70
|
+
'questions',
|
|
71
|
+
] as const) {
|
|
72
|
+
if (a[k] !== undefined) args[k] = a[k];
|
|
73
|
+
}
|
|
74
|
+
if (a.question === undefined && a.questions === undefined) {
|
|
75
|
+
return ctx.err(
|
|
76
|
+
'ask requires `question` (single) or `questions` (multi)',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return ctx.okJson(await ctx.call('ask', args));
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'dm',
|
|
84
|
+
group: 'write',
|
|
85
|
+
description:
|
|
86
|
+
'Open (or reuse) a 1:1 XMTP DM with an Ethereum address. Args: address (required, 0x...), ' +
|
|
87
|
+
'account? (defaults to your only XMTP account). Returns the new metro:// line and ' +
|
|
88
|
+
'convId. xmtp-only ' +
|
|
89
|
+
'(daemon `newDm`). For a group, use create_group.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
address: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Recipient Ethereum 0x address.',
|
|
96
|
+
},
|
|
97
|
+
account: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: 'XMTP account to DM from. Omit when you have only one.',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
required: ['address'],
|
|
103
|
+
},
|
|
104
|
+
async handle(a, ctx) {
|
|
105
|
+
const address = str(a.address);
|
|
106
|
+
if (!address) return ctx.err('dm requires `address`');
|
|
107
|
+
const dmArgs: Record<string, unknown> = { address };
|
|
108
|
+
if (a.account) dmArgs.account = str(a.account);
|
|
109
|
+
return ctx.okJson(await ctx.call('newDm', dmArgs));
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'group_info',
|
|
114
|
+
group: 'read',
|
|
115
|
+
description:
|
|
116
|
+
"Read an XMTP channel's current metadata + membership. Args: line (required). Returns " +
|
|
117
|
+
'{line, id, account, version (dm|group), name, memberCount, labels, github, preview, ' +
|
|
118
|
+
'members:[{inboxId, address}]}. xmtp-only (daemon `groupInfo`). Use before ' +
|
|
119
|
+
'set_channel_metadata/add_members to see current state.',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: { line: lineProp },
|
|
123
|
+
required: ['line'],
|
|
124
|
+
},
|
|
125
|
+
async handle(a, ctx) {
|
|
126
|
+
const line = str(a.line);
|
|
127
|
+
if (!line) return ctx.err('group_info requires `line`');
|
|
128
|
+
return ctx.okJson(await ctx.call('groupInfo', { line }));
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'close_channel',
|
|
133
|
+
group: 'write',
|
|
134
|
+
destructive: true,
|
|
135
|
+
description:
|
|
136
|
+
'Remove members from an XMTP group, and optionally leave it. Args: line (required), ' +
|
|
137
|
+
'removeInboxIds? (inbox ids to remove; your own is ignored here), removeSelf? (true to ' +
|
|
138
|
+
'leave the group yourself). With neither, nothing changes. xmtp-only (daemon ' +
|
|
139
|
+
'`closeGroup`). Returns {removed, leftSelf}.',
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
line: lineProp,
|
|
144
|
+
removeInboxIds: {
|
|
145
|
+
type: 'array',
|
|
146
|
+
description: 'Inbox ids of the members to remove.',
|
|
147
|
+
items: { type: 'string' },
|
|
148
|
+
},
|
|
149
|
+
removeSelf: {
|
|
150
|
+
type: 'boolean',
|
|
151
|
+
description: 'Leave the group after removing the others.',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
required: ['line'],
|
|
155
|
+
},
|
|
156
|
+
async handle(a, ctx) {
|
|
157
|
+
const line = str(a.line);
|
|
158
|
+
if (!line) return ctx.err('close_channel requires `line`');
|
|
159
|
+
const args: Record<string, unknown> = { line };
|
|
160
|
+
if (Array.isArray(a.removeInboxIds)) args.removeInboxIds = a.removeInboxIds.map(String);
|
|
161
|
+
if (a.removeSelf === true) args.removeSelf = true;
|
|
162
|
+
return ctx.okJson(await ctx.call('closeGroup', args));
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'set_channel_metadata',
|
|
167
|
+
group: 'write',
|
|
168
|
+
description:
|
|
169
|
+
"Update an existing channel's metadata. Args: line (required, the metro:// line), and " +
|
|
170
|
+
'any of labels? (string[]), github? (url), preview? (url), name? (string). All provided ' +
|
|
171
|
+
'fields go in one updateChannelMeta call: the name first, then the labels and links ' +
|
|
172
|
+
'merged into the group appData. xmtp-only ' +
|
|
173
|
+
'(channel metadata lives on xmtp groups). Returns the updated channel info.',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
line: lineProp,
|
|
178
|
+
labels: {
|
|
179
|
+
type: 'array',
|
|
180
|
+
description: 'Status labels to set.',
|
|
181
|
+
items: { type: 'string' },
|
|
182
|
+
},
|
|
183
|
+
github: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: 'Linked GitHub URL ("" to clear).',
|
|
186
|
+
},
|
|
187
|
+
preview: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'Linked preview URL ("" to clear).',
|
|
190
|
+
},
|
|
191
|
+
name: { type: 'string', description: 'New channel name.' },
|
|
192
|
+
},
|
|
193
|
+
required: ['line'],
|
|
194
|
+
},
|
|
195
|
+
handle: (a, ctx) => setChannelMetadata(a, ctx),
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { errMsg } from '@metro-labs/core/log';
|
|
2
|
+
import { emitInbound, selfUri } from '@metro-labs/core/stations/train-events';
|
|
3
|
+
import { mintId } from './wire.js';
|
|
4
|
+
|
|
5
|
+
const WHISPER_BIN = process.env.METRO_WHISPER_BIN ?? 'whisper-cli';
|
|
6
|
+
const WHISPER_MODEL =
|
|
7
|
+
process.env.METRO_WHISPER_MODEL ??
|
|
8
|
+
`${process.env.HOME}/.cache/whisper-cpp/ggml-base.bin`;
|
|
9
|
+
const FFMPEG_BIN = process.env.METRO_FFMPEG_BIN ?? 'ffmpeg';
|
|
10
|
+
|
|
11
|
+
export async function transcribeAndEmit(
|
|
12
|
+
audio: Uint8Array,
|
|
13
|
+
line: string,
|
|
14
|
+
accountId: string,
|
|
15
|
+
sourceMsgId: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const {
|
|
18
|
+
existsSync: ex,
|
|
19
|
+
readFileSync: rf,
|
|
20
|
+
writeFileSync: wf,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
mkdtempSync,
|
|
23
|
+
} = await import('node:fs');
|
|
24
|
+
const { tmpdir } = await import('node:os');
|
|
25
|
+
const path = await import('node:path');
|
|
26
|
+
const j = (...parts: string[]): string => path.join(...parts);
|
|
27
|
+
const { spawn } = await import('node:child_process');
|
|
28
|
+
if (!ex(WHISPER_MODEL)) return;
|
|
29
|
+
const dir = mkdtempSync(j(tmpdir(), 'xmtp-tx-'));
|
|
30
|
+
const inFile = j(dir, 'in.m4a');
|
|
31
|
+
const wav = j(dir, 'in.wav');
|
|
32
|
+
const out = j(dir, 'in');
|
|
33
|
+
const run = (bin: string, args: string[]): Promise<void> =>
|
|
34
|
+
new Promise((res, rej) => {
|
|
35
|
+
const p = spawn(bin, args, { stdio: 'ignore' });
|
|
36
|
+
p.on('error', rej);
|
|
37
|
+
p.on('exit', (c) => {
|
|
38
|
+
if (c === 0) res();
|
|
39
|
+
else rej(new Error(`${bin} ${String(c)}`));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
wf(inFile, audio);
|
|
44
|
+
await run(FFMPEG_BIN, [
|
|
45
|
+
'-y',
|
|
46
|
+
'-i',
|
|
47
|
+
inFile,
|
|
48
|
+
'-ar',
|
|
49
|
+
'16000',
|
|
50
|
+
'-ac',
|
|
51
|
+
'1',
|
|
52
|
+
wav,
|
|
53
|
+
]);
|
|
54
|
+
await run(WHISPER_BIN, [
|
|
55
|
+
'-m',
|
|
56
|
+
WHISPER_MODEL,
|
|
57
|
+
'-f',
|
|
58
|
+
wav,
|
|
59
|
+
'--output-txt',
|
|
60
|
+
'-of',
|
|
61
|
+
out,
|
|
62
|
+
]);
|
|
63
|
+
const text = rf(`${out}.txt`, 'utf8').trim();
|
|
64
|
+
if (!text) return;
|
|
65
|
+
emitInbound(accountId, {
|
|
66
|
+
id: mintId(),
|
|
67
|
+
ts: new Date().toISOString(),
|
|
68
|
+
station: 'xmtp',
|
|
69
|
+
line,
|
|
70
|
+
from: selfUri('xmtp', accountId),
|
|
71
|
+
text: `🎙️ ${text}`,
|
|
72
|
+
payload: {
|
|
73
|
+
contentType: 'transcript',
|
|
74
|
+
transcribeFor: sourceMsgId,
|
|
75
|
+
transcript: text,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
process.stderr.write(`xmtp transcribe failed: ${errMsg(err)}\n`);
|
|
80
|
+
} finally {
|
|
81
|
+
for (const f of [inFile, wav, `${out}.txt`]) {
|
|
82
|
+
try {
|
|
83
|
+
unlinkSync(f);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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, identityFor, signerFor, XMTP_ENV } from './identity.js';
|
|
5
|
+
import { ensureDeployed } from './smart.js';
|
|
6
|
+
|
|
7
|
+
export class XmtpVerifyError extends Error {}
|
|
8
|
+
|
|
9
|
+
export interface XmtpVerified {
|
|
10
|
+
inboxId: string;
|
|
11
|
+
address: string;
|
|
12
|
+
installationId: string;
|
|
13
|
+
dbPath: string;
|
|
14
|
+
smart: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RegisteredClient {
|
|
18
|
+
inboxId: string;
|
|
19
|
+
installationId: string;
|
|
20
|
+
isRegistered: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CreateXmtpClient = (
|
|
24
|
+
signer: Signer,
|
|
25
|
+
dbPath: string,
|
|
26
|
+
) => Promise<RegisteredClient>;
|
|
27
|
+
|
|
28
|
+
const openInbox: CreateXmtpClient = (signer, dbPath) => {
|
|
29
|
+
const options: ClientOptions = { env: XMTP_ENV, dbPath };
|
|
30
|
+
return Client.create(signer, options);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const reason = (err: unknown): string =>
|
|
34
|
+
err instanceof Error && err.message !== '' ? err.message : 'no reason given';
|
|
35
|
+
|
|
36
|
+
export async function verifyXmtpKey(
|
|
37
|
+
privateKey: string,
|
|
38
|
+
dbPath: string,
|
|
39
|
+
smart = false,
|
|
40
|
+
create: CreateXmtpClient = openInbox,
|
|
41
|
+
): Promise<XmtpVerified> {
|
|
42
|
+
let identity: Awaited<ReturnType<typeof identityFor>>;
|
|
43
|
+
try {
|
|
44
|
+
signerFor(privateKey);
|
|
45
|
+
} catch {
|
|
46
|
+
throw new XmtpVerifyError('that is not a usable XMTP private key');
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
identity = await identityFor(privateKey, smart);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
throw new XmtpVerifyError(`could not build the smart account on Base: ${reason(err)}`);
|
|
52
|
+
}
|
|
53
|
+
const { signer, address } = identity;
|
|
54
|
+
if (identity.smart !== null)
|
|
55
|
+
await ensureDeployed(identity.smart).catch((err: unknown) => {
|
|
56
|
+
throw new XmtpVerifyError(`could not deploy the smart account on Base (is the ZeroDev sponsorship still on?): ${reason(err)}`);
|
|
57
|
+
});
|
|
58
|
+
const resolved = expandHome(dbPath);
|
|
59
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
60
|
+
const client = await create(signer, resolved).catch((err: unknown) => {
|
|
61
|
+
throw new XmtpVerifyError(
|
|
62
|
+
`XMTP would not open an inbox for that key: ${reason(err)}`,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
if (!client.isRegistered)
|
|
66
|
+
throw new XmtpVerifyError('XMTP did not register an inbox for that key');
|
|
67
|
+
if (client.inboxId === '')
|
|
68
|
+
throw new XmtpVerifyError('XMTP returned no inbox id for that key');
|
|
69
|
+
return {
|
|
70
|
+
inboxId: client.inboxId,
|
|
71
|
+
address,
|
|
72
|
+
installationId: client.installationId,
|
|
73
|
+
dbPath: resolved,
|
|
74
|
+
smart,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function runCli(): Promise<void> {
|
|
79
|
+
let out: Record<string, unknown>;
|
|
80
|
+
try {
|
|
81
|
+
const input = JSON.parse(readFileSync(0, 'utf8')) as {
|
|
82
|
+
privateKey?: unknown;
|
|
83
|
+
dbPath?: unknown;
|
|
84
|
+
smart?: unknown;
|
|
85
|
+
};
|
|
86
|
+
if (typeof input.privateKey !== 'string' || typeof input.dbPath !== 'string')
|
|
87
|
+
throw new XmtpVerifyError('xmtp verify needs a privateKey and a dbPath');
|
|
88
|
+
out = { ok: true, ...(await verifyXmtpKey(input.privateKey, input.dbPath, input.smart === true)) };
|
|
89
|
+
} catch (err) {
|
|
90
|
+
out = { ok: false, error: reason(err) };
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(1, `${JSON.stringify(out)}\n`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (import.meta.main) {
|
|
96
|
+
await runCli();
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|