@stage-labs/metro 0.1.0-beta.13 → 0.1.0-beta.131
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 +14 -0
- package/dist/claude-settings.js +40 -0
- package/dist/claude.js +176 -0
- package/dist/cli.js +101 -0
- package/dist/control.js +74 -0
- package/dist/hold.js +211 -0
- package/dist/local.js +93 -0
- package/dist/mcp-config.js +25 -0
- package/dist/node-name.js +71 -0
- package/dist/onboarding.js +43 -0
- package/dist/plugin.js +120 -0
- package/dist/provider-flags.js +9 -0
- package/dist/route.js +38 -0
- package/dist/runtime-install.js +97 -0
- package/dist/runtime.js +74 -0
- package/dist/serve.js +150 -0
- package/dist/service.js +242 -0
- package/dist/tail.js +119 -0
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -27
- package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +18 -0
- package/runtime/marketplace/plugin/.mcp.json +1 -0
- package/runtime/marketplace/plugin/bin/guard.mjs +55 -0
- package/runtime/marketplace/plugin/bin/metro-plugin.mjs +122 -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 +16 -0
- package/runtime/marketplace/plugin/skills/refresh/SKILL.md +15 -0
- package/runtime/node_modules/@metro-labs/core/package.json +43 -0
- package/runtime/node_modules/@metro-labs/core/src/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/events.ts +120 -0
- package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +87 -0
- package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/protocol.ts +226 -0
- package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -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/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +108 -0
- package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -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/agents/account-attach.ts +19 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +365 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +43 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +300 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +122 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +371 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +155 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +45 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +95 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/seed.ts +33 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +209 -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 +14 -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 +25 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +377 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -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 +309 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +358 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +169 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +56 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +140 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +253 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +117 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +239 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +154 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +109 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +310 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/cli-api.ts +63 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +175 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/json.ts +21 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +77 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +166 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +139 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +171 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +155 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +122 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +296 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +286 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +98 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +332 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/watch.ts +54 -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-serve.ts +121 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +294 -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 +259 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +231 -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 +288 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +203 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +231 -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 +187 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +229 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +269 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +207 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +65 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +17 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +229 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +151 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +216 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +29 -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 +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +21 -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 +191 -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-relay.ts +66 -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/request-identity.ts +47 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +215 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +215 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +400 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +237 -0
- package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +389 -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 +372 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/identity.ts +54 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +223 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +60 -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 +64 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +61 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +116 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +202 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +280 -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 +261 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +37 -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/terminal/api.ts +96 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +40 -0
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -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 +220 -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 +127 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -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 +19 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/http/package.json +33 -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 +95 -0
- package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/http/src/identity-key.ts +33 -0
- package/runtime/node_modules/@metro-labs/http/src/identity-registry.ts +13 -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/signed-identity.ts +36 -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 +254 -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 +156 -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 +78 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -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/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -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 +120 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -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 +201 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +58 -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 +3 -0
- package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
- package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +100 -0
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +219 -0
- package/runtime/node_modules/@metro-labs/threema/src/api.ts +130 -0
- package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +132 -0
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +123 -0
- package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/threema/src/index.ts +29 -0
- package/runtime/node_modules/@metro-labs/threema/src/station.ts +10 -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 +190 -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 +347 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -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 +185 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +18 -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 +112 -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 +335 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -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 +53 -0
- package/runtime/runtime.json +3 -0
- package/runtime/server.ts +1 -0
- package/runtime/stations.json +39 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -252
- package/dist/broker.js +0 -202
- package/dist/cache.js +0 -69
- package/dist/cli/actions.js +0 -206
- package/dist/cli/config.js +0 -185
- package/dist/cli/index.js +0 -188
- package/dist/cli/skill.js +0 -62
- package/dist/cli/tail.js +0 -164
- package/dist/cli/util.js +0 -72
- package/dist/cli/webhook.js +0 -81
- package/dist/codex-rc.js +0 -245
- package/dist/dispatcher.js +0 -128
- package/dist/history.js +0 -121
- package/dist/ipc.js +0 -72
- package/dist/log.js +0 -15
- package/dist/monitor.js +0 -194
- package/dist/paths.js +0 -68
- package/dist/registry.js +0 -48
- package/dist/stations/claude.js +0 -45
- package/dist/stations/codex.js +0 -68
- package/dist/stations/discord.js +0 -216
- package/dist/stations/index.js +0 -129
- package/dist/stations/telegram-md.js +0 -34
- package/dist/stations/telegram-upload.js +0 -113
- package/dist/stations/telegram.js +0 -234
- package/dist/stations/webhook.js +0 -103
- package/dist/tunnel.js +0 -64
- package/dist/webhooks.js +0 -41
- package/docs/broker.md +0 -214
- package/docs/monitor.md +0 -163
- package/docs/uri-scheme.md +0 -124
- package/docs/users.md +0 -226
- package/skills/metro/SKILL.md +0 -256
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
6
|
+
import { log } from '@metro-labs/core/log';
|
|
7
|
+
|
|
8
|
+
const MARKETPLACE = 'metro';
|
|
9
|
+
const PLUGIN = 'metro@metro';
|
|
10
|
+
const STEP_MS = 120_000;
|
|
11
|
+
const SAID_MAX = 300;
|
|
12
|
+
|
|
13
|
+
export interface Answer {
|
|
14
|
+
status: number;
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Run = (args: string[]) => Promise<Answer>;
|
|
20
|
+
|
|
21
|
+
export interface PluginInstallDeps {
|
|
22
|
+
run: Run | null;
|
|
23
|
+
marketplaceDir: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type PluginOutcome = 'skipped' | 'unchanged' | 'installed' | 'updated' | 'failed';
|
|
27
|
+
|
|
28
|
+
export function stagedMarketplaceDir(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
29
|
+
const store = env.METRO_RUNTIME_STORE?.trim() ?? '';
|
|
30
|
+
if (store === '') return null;
|
|
31
|
+
const dir = join(store, 'marketplace');
|
|
32
|
+
return existsSync(join(dir, '.claude-plugin', 'marketplace.json')) ? dir : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function stagedPluginVersion(dir: string): string | null {
|
|
36
|
+
try {
|
|
37
|
+
const parsed: unknown = JSON.parse(readFileSync(join(dir, 'plugin', '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
38
|
+
return isRecord(parsed) && typeof parsed.version === 'string' ? parsed.version : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const runner =
|
|
45
|
+
(bin: string): Run =>
|
|
46
|
+
(args) =>
|
|
47
|
+
new Promise((resolve) => {
|
|
48
|
+
execFile(bin, args, { encoding: 'utf8', timeout: STEP_MS, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
49
|
+
const status = err === null ? 0 : typeof (err as { code?: unknown }).code === 'number' ? (err as { code: number }).code : 1;
|
|
50
|
+
resolve({ status, stdout, stderr });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export async function findClaude(candidates = ['claude', join(homedir(), '.local', 'bin', 'claude')]): Promise<Run | null> {
|
|
55
|
+
for (const bin of candidates) {
|
|
56
|
+
const run = runner(bin);
|
|
57
|
+
if ((await run(['--version'])).status === 0) return run;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function listOf(stdout: string): Record<string, unknown>[] {
|
|
63
|
+
try {
|
|
64
|
+
const parsed: unknown = JSON.parse(stdout);
|
|
65
|
+
return Array.isArray(parsed) ? parsed.filter(isRecord) : [];
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const realish = (path: string): string => {
|
|
72
|
+
try {
|
|
73
|
+
return realpathSync(path);
|
|
74
|
+
} catch {
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const samePath = (a: string | null, b: string): boolean => a !== null && realish(a) === realish(b);
|
|
80
|
+
|
|
81
|
+
async function step(run: Run, args: string[]): Promise<boolean> {
|
|
82
|
+
const answer = await run(args);
|
|
83
|
+
if (answer.status === 0) {
|
|
84
|
+
log.info({ args }, 'plugin: claude');
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
log.warn({ args, said: `${answer.stderr}${answer.stdout}`.trim().slice(0, SAID_MAX) }, 'plugin: claude refused');
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function marketplacePath(run: Run): Promise<string | null | undefined> {
|
|
92
|
+
const mine = listOf((await run(['plugin', 'marketplace', 'list', '--json'])).stdout).find((m) => m.name === MARKETPLACE);
|
|
93
|
+
if (mine === undefined) return undefined;
|
|
94
|
+
const at = mine.installLocation ?? mine.path;
|
|
95
|
+
return typeof at === 'string' ? at : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function ensureMarketplace(run: Run, dir: string): Promise<boolean> {
|
|
99
|
+
const at = await marketplacePath(run);
|
|
100
|
+
if (at !== undefined && samePath(at, dir)) return true;
|
|
101
|
+
if (at !== undefined) await step(run, ['plugin', 'marketplace', 'remove', MARKETPLACE]);
|
|
102
|
+
return step(run, ['plugin', 'marketplace', 'add', dir]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function installedVersion(run: Run): Promise<string | null> {
|
|
106
|
+
const mine = listOf((await run(['plugin', 'list', '--json'])).stdout).find((p) => p.id === PLUGIN);
|
|
107
|
+
return mine !== undefined && typeof mine.version === 'string' ? mine.version : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function ensureInstalled(run: Run, wanted: string | null): Promise<PluginOutcome> {
|
|
111
|
+
const have = await installedVersion(run);
|
|
112
|
+
if (have === null) return (await step(run, ['plugin', 'install', PLUGIN, '-y', '--scope', 'user'])) ? 'installed' : 'failed';
|
|
113
|
+
if (wanted === null || have === wanted) return 'unchanged';
|
|
114
|
+
await step(run, ['plugin', 'marketplace', 'update', MARKETPLACE]);
|
|
115
|
+
return (await step(run, ['plugin', 'update', PLUGIN, '-y'])) ? 'updated' : 'failed';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function explainMissingMarketplace(env: NodeJS.ProcessEnv = process.env): void {
|
|
119
|
+
const store = env.METRO_RUNTIME_STORE?.trim() ?? '';
|
|
120
|
+
if (store === '') return;
|
|
121
|
+
log.warn(
|
|
122
|
+
{ store },
|
|
123
|
+
'plugin: the runtime store holds no plugin marketplace, so metro cannot install its Claude Code plugin; a `metro serve` started before beta.97 never copied it, restart the service once',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function ensureMetroPlugin(deps?: Partial<PluginInstallDeps>): Promise<PluginOutcome> {
|
|
128
|
+
const dir = deps?.marketplaceDir === undefined ? stagedMarketplaceDir() : deps.marketplaceDir;
|
|
129
|
+
if (dir === null) {
|
|
130
|
+
if (deps?.marketplaceDir === undefined) explainMissingMarketplace();
|
|
131
|
+
return 'skipped';
|
|
132
|
+
}
|
|
133
|
+
const run = deps?.run === undefined ? await findClaude() : deps.run;
|
|
134
|
+
if (run === null) {
|
|
135
|
+
log.info('plugin: claude is not on this machine, so metro leaves its Claude Code plugin alone');
|
|
136
|
+
return 'skipped';
|
|
137
|
+
}
|
|
138
|
+
if (!(await ensureMarketplace(run, dir))) return 'failed';
|
|
139
|
+
return ensureInstalled(run, stagedPluginVersion(dir));
|
|
140
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import type { Readable } from 'node:stream';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
6
|
+
import { claudeDir, PROJECT_RE, safeName, SESSION_RE } from './files.js';
|
|
7
|
+
|
|
8
|
+
export const SESSION_FILE_MAX = 512 * 1024 * 1024;
|
|
9
|
+
const FILE_MODE = 0o644;
|
|
10
|
+
|
|
11
|
+
export interface SessionFile {
|
|
12
|
+
id: string;
|
|
13
|
+
bytes: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const transcriptPath = (project: string, session: string, dir: string): string =>
|
|
17
|
+
join(dir, 'projects', safeName(project, PROJECT_RE, 'Claude project id'), `${safeName(session, SESSION_RE, 'session id')}.jsonl`);
|
|
18
|
+
|
|
19
|
+
export function sessionFilePath(project: string, session: string, dir = claudeDir()): string {
|
|
20
|
+
const path = transcriptPath(project, session, dir);
|
|
21
|
+
if (!existsSync(path)) throw new ApiError('no such session', 404);
|
|
22
|
+
if (statSync(path).size > SESSION_FILE_MAX) throw new ApiError(`a session file is at most ${String(SESSION_FILE_MAX)} bytes`, 400);
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readSessionFile(project: string, session: string, dir = claudeDir()): string {
|
|
27
|
+
return readFileSync(sessionFilePath(project, session, dir), 'utf8');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const HEAD_BYTES = 64 * 1024;
|
|
31
|
+
|
|
32
|
+
async function spool(body: Readable, tmp: string): Promise<string> {
|
|
33
|
+
const out = createWriteStream(tmp, { mode: FILE_MODE });
|
|
34
|
+
const head: Buffer[] = [];
|
|
35
|
+
let total = 0;
|
|
36
|
+
let kept = 0;
|
|
37
|
+
for await (const chunk of body) {
|
|
38
|
+
const buf = chunk as Buffer;
|
|
39
|
+
total += buf.length;
|
|
40
|
+
if (total > SESSION_FILE_MAX) {
|
|
41
|
+
out.destroy();
|
|
42
|
+
rmSync(tmp, { force: true });
|
|
43
|
+
throw new ApiError(`a session file is at most ${String(SESSION_FILE_MAX)} bytes`, 413);
|
|
44
|
+
}
|
|
45
|
+
if (kept < HEAD_BYTES) {
|
|
46
|
+
head.push(buf.subarray(0, HEAD_BYTES - kept));
|
|
47
|
+
kept += buf.length;
|
|
48
|
+
}
|
|
49
|
+
if (!out.write(buf)) await new Promise<void>((r) => out.once('drain', r));
|
|
50
|
+
}
|
|
51
|
+
await new Promise<void>((resolve, reject) => {
|
|
52
|
+
out.once('error', reject);
|
|
53
|
+
out.end(resolve);
|
|
54
|
+
});
|
|
55
|
+
return Buffer.concat(head).toString('utf8');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function receiveSessionFile(project: string, session: string, body: Readable, dir = claudeDir()): Promise<SessionFile> {
|
|
59
|
+
const path = transcriptPath(project, session, dir);
|
|
60
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
61
|
+
const tmp = `${path}.metro-${String(process.pid)}`;
|
|
62
|
+
const head = await spool(body, tmp);
|
|
63
|
+
if (!firstLineIsJson(head)) {
|
|
64
|
+
rmSync(tmp, { force: true });
|
|
65
|
+
throw new ApiError('a session file is JSON lines, one Claude Code entry per line', 400);
|
|
66
|
+
}
|
|
67
|
+
renameSync(tmp, path);
|
|
68
|
+
return { id: safeName(session, SESSION_RE, 'session id'), bytes: statSync(path).size };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function firstLineIsJson(text: string): boolean {
|
|
72
|
+
const first = text.split('\n').find((line) => line.trim() !== '');
|
|
73
|
+
if (first === undefined) return false;
|
|
74
|
+
try {
|
|
75
|
+
return isRecord(JSON.parse(first));
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function writeSessionFile(project: string, session: string, text: string, dir = claudeDir()): SessionFile {
|
|
82
|
+
if (!firstLineIsJson(text)) throw new ApiError('a session file is JSON lines, one Claude Code entry per line', 400);
|
|
83
|
+
if (Buffer.byteLength(text, 'utf8') > SESSION_FILE_MAX) throw new ApiError(`a session file is at most ${String(SESSION_FILE_MAX)} bytes`, 400);
|
|
84
|
+
const path = transcriptPath(project, session, dir);
|
|
85
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
86
|
+
const tmp = `${path}.metro-${String(process.pid)}`;
|
|
87
|
+
writeFileSync(tmp, text, { mode: FILE_MODE });
|
|
88
|
+
renameSync(tmp, path);
|
|
89
|
+
return { id: safeName(session, SESSION_RE, 'session id'), bytes: statSync(path).size };
|
|
90
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
6
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
7
|
+
import { readJson, writeJson } from '@metro-labs/core/secure-fs';
|
|
8
|
+
import { METRO_VERSION } from '@metro-labs/core/version';
|
|
9
|
+
import { agentsDir, listAgentFiles } from '../agents/files.js';
|
|
10
|
+
import { notReady, readModelConfig } from '../gateway/model-config.js';
|
|
11
|
+
import { claudeDir, listClaudeProjects } from './files.js';
|
|
12
|
+
import { claudeAccount, claudeInstalled } from './login.js';
|
|
13
|
+
import { trustFolder } from './onboarding.js';
|
|
14
|
+
|
|
15
|
+
export const SESSION_NAME = 'metro';
|
|
16
|
+
const STATE_FILE = 'claude-session.json';
|
|
17
|
+
const WARNING = 'WARNING: Loading development channels';
|
|
18
|
+
const CONFIRM_POLL_MS = 500;
|
|
19
|
+
const CONFIRM_WAIT_MS = 60_000;
|
|
20
|
+
const WATCH_MS = 15_000;
|
|
21
|
+
const STRIKE_WINDOW_MS = 60_000;
|
|
22
|
+
const MAX_STRIKES = 5;
|
|
23
|
+
const PAUSE_MS = 30 * 60_000;
|
|
24
|
+
|
|
25
|
+
export interface SessionDeps {
|
|
26
|
+
tmux?: string;
|
|
27
|
+
metro?: string[];
|
|
28
|
+
home?: string;
|
|
29
|
+
agents?: string;
|
|
30
|
+
signedIn?: () => boolean;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
version?: string;
|
|
33
|
+
continues?: (home: string) => boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SessionStatus {
|
|
37
|
+
name: string;
|
|
38
|
+
running: boolean;
|
|
39
|
+
autostart: boolean;
|
|
40
|
+
blocked: string | null;
|
|
41
|
+
lastStartedAt: string | null;
|
|
42
|
+
lastError: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Memory {
|
|
46
|
+
lastStartedAt: number | null;
|
|
47
|
+
lastError: string | null;
|
|
48
|
+
strikes: number;
|
|
49
|
+
pausedUntil: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const memory: Memory = { lastStartedAt: null, lastError: null, strikes: 0, pausedUntil: 0 };
|
|
53
|
+
|
|
54
|
+
const statePath = (agents: string): string => join(agents, STATE_FILE);
|
|
55
|
+
|
|
56
|
+
function readState(agents: string): Record<string, unknown> {
|
|
57
|
+
const raw = readJson<unknown>(statePath(agents), null);
|
|
58
|
+
return isRecord(raw) ? raw : {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const writeState = (agents: string, patch: Record<string, unknown>): void => {
|
|
62
|
+
writeJson(statePath(agents), { ...readState(agents), ...patch });
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const autostartEnabled = (agents = agentsDir()): boolean => readState(agents).autostart !== false;
|
|
66
|
+
|
|
67
|
+
export function setAutostart(enabled: boolean, agents = agentsDir()): void {
|
|
68
|
+
writeState(agents, { autostart: enabled });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function startedVersion(agents = agentsDir()): string | null {
|
|
72
|
+
const version = readState(agents).version;
|
|
73
|
+
return typeof version === 'string' ? version : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function tmuxOk(tmux: string, args: string[]): boolean {
|
|
77
|
+
const run = spawnSync(tmux, args, { stdio: 'ignore' });
|
|
78
|
+
return run.error === undefined && run.status === 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const sessionRunning = (tmux = 'tmux'): boolean => tmuxOk(tmux, ['has-session', '-t', SESSION_NAME]);
|
|
82
|
+
|
|
83
|
+
function realDir(dir: string): string {
|
|
84
|
+
try {
|
|
85
|
+
return realpathSync(dir);
|
|
86
|
+
} catch {
|
|
87
|
+
return dir;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function hasConversation(home: string, dir = claudeDir()): boolean {
|
|
92
|
+
const cwd = realDir(home);
|
|
93
|
+
return listClaudeProjects(dir).some((project) => project.sessions > 0 && project.cwd !== null && realDir(project.cwd) === cwd);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function metroCommand(deps: SessionDeps, home: string): string[] {
|
|
97
|
+
const bin = process.env.METRO_CLI_BIN?.trim() ?? '';
|
|
98
|
+
const base = deps.metro ?? (bin === '' ? ['metro', 'claude'] : [process.execPath, bin, 'claude']);
|
|
99
|
+
const continues = (deps.continues ?? hasConversation)(home);
|
|
100
|
+
return continues ? [...base, '-c'] : base;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function credentialReady(deps: SessionDeps): string | null {
|
|
104
|
+
const signedIn = deps.signedIn ?? (() => claudeAccount().signedIn);
|
|
105
|
+
if (signedIn()) return null;
|
|
106
|
+
try {
|
|
107
|
+
const cfg = readModelConfig(deps.agents ?? agentsDir());
|
|
108
|
+
if (cfg.provider !== 'anthropic' && notReady(cfg) === null) return null;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return `the Model page is not readable (${errMsg(err)})`;
|
|
111
|
+
}
|
|
112
|
+
return 'Claude Code is not signed in and the Model page routes nowhere yet';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function sessionBlocked(deps: SessionDeps = {}): string | null {
|
|
116
|
+
const agents = deps.agents ?? agentsDir();
|
|
117
|
+
if (listAgentFiles(agents).length === 0) return 'no agent on this machine yet';
|
|
118
|
+
if (deps.metro === undefined && !claudeInstalled()) return 'Claude Code is not installed on this machine';
|
|
119
|
+
if (!tmuxOk(deps.tmux ?? 'tmux', ['-V'])) return 'tmux is not installed on this machine';
|
|
120
|
+
return credentialReady(deps);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function capturePane(tmux: string): string {
|
|
124
|
+
const run = spawnSync(tmux, ['capture-pane', '-p', '-t', SESSION_NAME], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
125
|
+
return run.error === undefined && run.status === 0 ? run.stdout : '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function confirmChannels(tmux: string, until: number): void {
|
|
129
|
+
const tick = (): void => {
|
|
130
|
+
const pane = capturePane(tmux);
|
|
131
|
+
if (pane.includes(WARNING) && pane.includes('server:metro')) {
|
|
132
|
+
spawnSync(tmux, ['send-keys', '-t', SESSION_NAME, 'Enter'], { stdio: 'ignore' });
|
|
133
|
+
log.info('claude-session: confirmed the development channels dialog for server:metro');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (Date.now() < until && sessionRunning(tmux)) setTimeout(tick, CONFIRM_POLL_MS).unref();
|
|
137
|
+
};
|
|
138
|
+
setTimeout(tick, CONFIRM_POLL_MS).unref();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function recordStart(deps: SessionDeps, tmux: string, now: number, run: { error?: Error; status: number | null; stderr: string }): void {
|
|
142
|
+
memory.lastStartedAt = now;
|
|
143
|
+
if (run.error !== undefined || run.status !== 0) {
|
|
144
|
+
memory.lastError = run.error === undefined ? `tmux new-session exited ${String(run.status)}: ${run.stderr.trim()}` : errMsg(run.error);
|
|
145
|
+
log.warn({ err: memory.lastError }, 'claude-session: tmux could not start');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
memory.lastError = null;
|
|
149
|
+
writeState(deps.agents ?? agentsDir(), { version: deps.version ?? METRO_VERSION });
|
|
150
|
+
confirmChannels(tmux, now + CONFIRM_WAIT_MS);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function startSession(deps: SessionDeps = {}): SessionStatus {
|
|
154
|
+
const tmux = deps.tmux ?? 'tmux';
|
|
155
|
+
const home = deps.home ?? homedir();
|
|
156
|
+
const now = (deps.now ?? Date.now)();
|
|
157
|
+
const trusted = trustFolder(home);
|
|
158
|
+
const [command = 'metro', ...args] = metroCommand(deps, home);
|
|
159
|
+
const run = spawnSync(tmux, ['new-session', '-d', '-s', SESSION_NAME, '-c', home, '-x', '200', '-y', '50', command, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], cwd: home });
|
|
160
|
+
recordStart(deps, tmux, now, run);
|
|
161
|
+
if (memory.lastError === null) log.info({ home, trusted, command: [command, ...args].join(' ') }, 'claude-session: started Claude Code in tmux');
|
|
162
|
+
return sessionStatus(deps);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function stopSession(deps: SessionDeps = {}): SessionStatus {
|
|
166
|
+
spawnSync(deps.tmux ?? 'tmux', ['kill-session', '-t', SESSION_NAME], { stdio: 'ignore' });
|
|
167
|
+
return sessionStatus(deps);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function sessionStatus(deps: SessionDeps = {}): SessionStatus {
|
|
171
|
+
return {
|
|
172
|
+
name: SESSION_NAME,
|
|
173
|
+
running: sessionRunning(deps.tmux ?? 'tmux'),
|
|
174
|
+
autostart: autostartEnabled(deps.agents ?? agentsDir()),
|
|
175
|
+
blocked: sessionBlocked(deps),
|
|
176
|
+
lastStartedAt: memory.lastStartedAt === null ? null : new Date(memory.lastStartedAt).toISOString(),
|
|
177
|
+
lastError: memory.lastError,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function strike(now: number): boolean {
|
|
182
|
+
if (memory.lastStartedAt !== null && now - memory.lastStartedAt < STRIKE_WINDOW_MS) memory.strikes += 1;
|
|
183
|
+
else memory.strikes = 1;
|
|
184
|
+
if (memory.strikes < MAX_STRIKES) return false;
|
|
185
|
+
memory.pausedUntil = now + PAUSE_MS;
|
|
186
|
+
memory.strikes = 0;
|
|
187
|
+
log.warn({ pauseMinutes: PAUSE_MS / 60_000 }, 'claude-session: Claude Code keeps exiting right after starting; not restarting it for a while');
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type Ensured = 'started' | 'restarted' | 'running' | 'blocked' | 'off' | 'paused';
|
|
192
|
+
|
|
193
|
+
function holdsBack(deps: SessionDeps, now: number): Ensured | null {
|
|
194
|
+
if (now < memory.pausedUntil) return 'paused';
|
|
195
|
+
if (sessionBlocked(deps) !== null) return 'blocked';
|
|
196
|
+
return strike(now) ? 'paused' : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface Resolved {
|
|
200
|
+
now: number;
|
|
201
|
+
agents: string;
|
|
202
|
+
version: string;
|
|
203
|
+
tmux: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const resolved = (deps: SessionDeps): Resolved => ({
|
|
207
|
+
now: (deps.now ?? Date.now)(),
|
|
208
|
+
agents: deps.agents ?? agentsDir(),
|
|
209
|
+
version: deps.version ?? METRO_VERSION,
|
|
210
|
+
tmux: deps.tmux ?? 'tmux',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
function situation(r: Resolved): 'running' | 'stale' | 'absent' {
|
|
214
|
+
if (!sessionRunning(r.tmux)) return 'absent';
|
|
215
|
+
return startedVersion(r.agents) === r.version ? 'running' : 'stale';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function ensureSession(deps: SessionDeps = {}): Ensured {
|
|
219
|
+
const r = resolved(deps);
|
|
220
|
+
if (!autostartEnabled(r.agents)) return 'off';
|
|
221
|
+
const found = situation(r);
|
|
222
|
+
if (found === 'running') return 'running';
|
|
223
|
+
const held = holdsBack(deps, r.now);
|
|
224
|
+
if (held !== null) return held;
|
|
225
|
+
if (found === 'stale') {
|
|
226
|
+
log.info({ was: startedVersion(r.agents), now: r.version }, 'claude-session: restarting the session an older metro started, so the new plugin and flags load');
|
|
227
|
+
stopSession(deps);
|
|
228
|
+
}
|
|
229
|
+
startSession(deps);
|
|
230
|
+
return found === 'stale' ? 'restarted' : 'started';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let watcher: ReturnType<typeof setInterval> | null = null;
|
|
234
|
+
|
|
235
|
+
export function watchSession(deps: SessionDeps = {}, everyMs = WATCH_MS): void {
|
|
236
|
+
if (watcher !== null) return;
|
|
237
|
+
const tick = (): void => {
|
|
238
|
+
try {
|
|
239
|
+
const outcome = ensureSession(deps);
|
|
240
|
+
if (outcome === 'started' || outcome === 'restarted') log.info({ outcome }, 'claude-session: Claude Code is up');
|
|
241
|
+
} catch (err) {
|
|
242
|
+
log.warn({ err: errMsg(err) }, 'claude-session: check failed');
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
watcher = setInterval(tick, everyMs);
|
|
246
|
+
watcher.unref();
|
|
247
|
+
setTimeout(tick, 1_000).unref();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function unwatchSession(): void {
|
|
251
|
+
if (watcher !== null) clearInterval(watcher);
|
|
252
|
+
watcher = null;
|
|
253
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { ApiError } from '@metro-labs/http/api-error';
|
|
4
|
+
import { claudeDir, listClaudeProjects } from './files.js';
|
|
5
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
6
|
+
import { errMsg } from '@metro-labs/core/log';
|
|
7
|
+
|
|
8
|
+
export const SETTINGS_MAX = 256 * 1024;
|
|
9
|
+
const USER_ID = 'user';
|
|
10
|
+
const LOCAL_SUFFIX = '.local';
|
|
11
|
+
const DEFAULT_MODE = 0o644;
|
|
12
|
+
|
|
13
|
+
export type SettingsScope = 'user' | 'project' | 'local';
|
|
14
|
+
|
|
15
|
+
export interface ClaudeSettingsFile {
|
|
16
|
+
id: string;
|
|
17
|
+
scope: SettingsScope;
|
|
18
|
+
label: string;
|
|
19
|
+
path: string;
|
|
20
|
+
exists: boolean;
|
|
21
|
+
editable: boolean;
|
|
22
|
+
text: string;
|
|
23
|
+
modifiedAt: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const missing = (id: string, scope: SettingsScope, label: string, path: string): ClaudeSettingsFile => ({
|
|
27
|
+
id,
|
|
28
|
+
scope,
|
|
29
|
+
label,
|
|
30
|
+
path,
|
|
31
|
+
exists: false,
|
|
32
|
+
editable: true,
|
|
33
|
+
text: '',
|
|
34
|
+
modifiedAt: null,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function entryOf(id: string, scope: SettingsScope, label: string, path: string): ClaudeSettingsFile {
|
|
38
|
+
if (!existsSync(path)) return missing(id, scope, label, path);
|
|
39
|
+
const stat = statSync(path);
|
|
40
|
+
const editable = stat.size <= SETTINGS_MAX;
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
scope,
|
|
44
|
+
label,
|
|
45
|
+
path,
|
|
46
|
+
exists: true,
|
|
47
|
+
editable,
|
|
48
|
+
text: editable ? readFileSync(path, 'utf8') : '',
|
|
49
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const PROJECT_FILES: [string, SettingsScope, string][] = [
|
|
54
|
+
['', 'project', 'settings.json'],
|
|
55
|
+
[LOCAL_SUFFIX, 'local', 'settings.local.json'],
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function projectEntries(dir: string): ClaudeSettingsFile[] {
|
|
59
|
+
const out: ClaudeSettingsFile[] = [];
|
|
60
|
+
for (const project of listClaudeProjects(dir)) {
|
|
61
|
+
const cwd = project.cwd;
|
|
62
|
+
if (cwd === null) continue;
|
|
63
|
+
for (const [suffix, scope, name] of PROJECT_FILES) {
|
|
64
|
+
const path = join(cwd, '.claude', name);
|
|
65
|
+
if (existsSync(path)) out.push(entryOf(`${project.id}${suffix}`, scope, cwd, path));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function listClaudeSettings(dir = claudeDir()): ClaudeSettingsFile[] {
|
|
72
|
+
return [entryOf(USER_ID, 'user', 'This machine', join(dir, 'settings.json')), ...projectEntries(dir)];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function assertSettingsJson(text: string): void {
|
|
76
|
+
if (text.length > SETTINGS_MAX) throw new ApiError('that is more text than a settings file may hold', 413);
|
|
77
|
+
let parsed: unknown;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(text);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new ApiError(`that is not valid JSON: ${errMsg(err)}`, 400);
|
|
82
|
+
}
|
|
83
|
+
if (!isRecord(parsed)) throw new ApiError('Claude Code settings must be a JSON object', 400);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function modeOf(path: string): number {
|
|
87
|
+
try {
|
|
88
|
+
return statSync(path).mode & 0o777;
|
|
89
|
+
} catch {
|
|
90
|
+
return DEFAULT_MODE;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function writeAtomic(path: string, text: string): void {
|
|
95
|
+
const mode = existsSync(path) ? modeOf(path) : DEFAULT_MODE;
|
|
96
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
97
|
+
const tmp = `${path}.metro-${String(process.pid)}`;
|
|
98
|
+
writeFileSync(tmp, text, { mode });
|
|
99
|
+
chmodSync(tmp, mode);
|
|
100
|
+
renameSync(tmp, path);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function writeClaudeSettings(
|
|
104
|
+
id: string,
|
|
105
|
+
text: string,
|
|
106
|
+
seenAt: string | null | undefined,
|
|
107
|
+
dir = claudeDir(),
|
|
108
|
+
): ClaudeSettingsFile {
|
|
109
|
+
const target = listClaudeSettings(dir).find((file) => file.id === id);
|
|
110
|
+
if (target === undefined) throw new ApiError('no such settings file', 404);
|
|
111
|
+
if (!target.editable) throw new ApiError('that settings file is too large to edit here', 409);
|
|
112
|
+
assertSettingsJson(text);
|
|
113
|
+
if (seenAt !== undefined && seenAt !== target.modifiedAt)
|
|
114
|
+
throw new ApiError('that file changed on disk since you opened it; reload it before saving', 409);
|
|
115
|
+
writeAtomic(target.path, text);
|
|
116
|
+
return entryOf(target.id, target.scope, target.label, target.path);
|
|
117
|
+
}
|