@stage-labs/metro 0.1.0-beta.13 → 0.1.0-beta.130
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/senders.ts +38 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +207 -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,239 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
5
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
6
|
+
import { readJson, writeJson } from '@metro-labs/core/secure-fs';
|
|
7
|
+
import { agentsDir } from '../agents/files.js';
|
|
8
|
+
import { claudeDir } from './files.js';
|
|
9
|
+
import { stagedMarketplaceDir } from './plugin-install.js';
|
|
10
|
+
import { readModelConfig, type ModelConfig } from '../gateway/model-config.js';
|
|
11
|
+
|
|
12
|
+
export const PRIVACY_ENV: Record<string, string> = {
|
|
13
|
+
DISABLE_TELEMETRY: '1',
|
|
14
|
+
DISABLE_ERROR_REPORTING: '1',
|
|
15
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
16
|
+
CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF: '1',
|
|
17
|
+
};
|
|
18
|
+
export const RETENTION_DAYS = 7;
|
|
19
|
+
export const SKILL_NAME = 'metro-orchestrator';
|
|
20
|
+
const STATE_FILE = 'claude-setup.json';
|
|
21
|
+
const GUIDANCE = 'orchestrator.md';
|
|
22
|
+
|
|
23
|
+
export const WORKER_AGENT = `---
|
|
24
|
+
name: worker
|
|
25
|
+
description: Default execution agent for delegated work. Full tool access and maximum reasoning effort. Use for any substantive task the orchestrator main thread cannot perform itself - reading and writing code, running commands, research, analysis.
|
|
26
|
+
effort: xhigh
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
You are a worker agent. The main thread that dispatched you is an orchestrator with no file, shell or network access; it can only delegate and relay messages. That means you are where the actual work happens, and the quality of your output is the quality of the result.
|
|
30
|
+
|
|
31
|
+
Work to completion. Do not hand back a partial answer with a suggestion that someone else finish it; there is no one else. If part of the task is genuinely blocked, complete every other part in full and say plainly what you left undone and why.
|
|
32
|
+
|
|
33
|
+
Never call AskUserQuestion or ExitPlanMode. They are blocked by policy, because nobody is watching the terminal around the clock and a blocking prompt stalls indefinitely. When you hit an ambiguity: do everything that does not depend on it, then choose the most reasonable interpretation, state the assumption explicitly in your report, and continue.
|
|
34
|
+
|
|
35
|
+
Your final message is a report to the orchestrator, not a message to a human. It gets relayed onward, so make it self-contained and concise. Lead with the outcome. Include concrete evidence, command output, file paths with line numbers, test results, for anything you claim to have verified. Report failures faithfully: if a test failed, say so and show the output; if you skipped a step, say that too.
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
export interface SetupDeps {
|
|
39
|
+
dir?: string;
|
|
40
|
+
agents?: string;
|
|
41
|
+
guidance?: string;
|
|
42
|
+
env?: NodeJS.ProcessEnv;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type Placed = 'written' | 'present' | 'missing';
|
|
46
|
+
export type SettingsOutcome = 'written' | 'unchanged' | 'unreadable';
|
|
47
|
+
|
|
48
|
+
export interface SetupReport {
|
|
49
|
+
privacy: boolean;
|
|
50
|
+
guard: 'plugin';
|
|
51
|
+
worker: Placed;
|
|
52
|
+
skill: Placed;
|
|
53
|
+
settings: SettingsOutcome;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SetupStatus {
|
|
57
|
+
privacy: boolean;
|
|
58
|
+
permissionMode: PermissionMode;
|
|
59
|
+
guard: 'plugin';
|
|
60
|
+
worker: boolean;
|
|
61
|
+
skill: boolean;
|
|
62
|
+
privacyApplied: boolean;
|
|
63
|
+
retentionDays: number | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const statePath = (agents: string): string => join(agents, STATE_FILE);
|
|
67
|
+
|
|
68
|
+
export const PERMISSION_MODES = ['auto', 'bypass'] as const;
|
|
69
|
+
export type PermissionMode = (typeof PERMISSION_MODES)[number];
|
|
70
|
+
|
|
71
|
+
function readState(agents: string): Record<string, unknown> {
|
|
72
|
+
const raw = readJson<unknown>(statePath(agents), null);
|
|
73
|
+
return isRecord(raw) ? raw : {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeState(agents: string, patch: Record<string, unknown>): void {
|
|
77
|
+
mkdirSync(agents, { recursive: true });
|
|
78
|
+
writeJson(statePath(agents), { ...readState(agents), ...patch });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const privacyEnabled = (agents = agentsDir()): boolean => readState(agents).privacy !== false;
|
|
82
|
+
|
|
83
|
+
export function setPrivacy(enabled: boolean, agents = agentsDir()): void {
|
|
84
|
+
writeState(agents, { privacy: enabled });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const isPermissionMode = (value: unknown): value is PermissionMode => PERMISSION_MODES.some((m) => m === value);
|
|
88
|
+
|
|
89
|
+
export function permissionMode(agents = agentsDir()): PermissionMode {
|
|
90
|
+
const mode = readState(agents).permissionMode;
|
|
91
|
+
return isPermissionMode(mode) ? mode : 'auto';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function setPermissionMode(mode: PermissionMode, agents = agentsDir()): void {
|
|
95
|
+
writeState(agents, { permissionMode: mode });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function guidancePath(env: NodeJS.ProcessEnv = process.env): string {
|
|
99
|
+
const staged = stagedMarketplaceDir(env);
|
|
100
|
+
if (staged !== null) {
|
|
101
|
+
const path = join(staged, 'plugin', GUIDANCE);
|
|
102
|
+
if (existsSync(path)) return path;
|
|
103
|
+
}
|
|
104
|
+
return fileURLToPath(new URL(`../../../../plugin/${GUIDANCE}`, import.meta.url));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ensureFile(path: string, text: string): Placed {
|
|
108
|
+
if (existsSync(path)) return 'present';
|
|
109
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
110
|
+
writeFileSync(path, text, { mode: 0o644 });
|
|
111
|
+
return 'written';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readSettings(path: string): Record<string, unknown> | null {
|
|
115
|
+
if (!existsSync(path)) return {};
|
|
116
|
+
try {
|
|
117
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
118
|
+
return isRecord(parsed) ? parsed : null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function withPrivacy(settings: Record<string, unknown>, enabled: boolean): Record<string, unknown> {
|
|
125
|
+
const current = isRecord(settings.env) ? settings.env : {};
|
|
126
|
+
if (enabled)
|
|
127
|
+
return { ...settings, env: { ...current, ...PRIVACY_ENV }, cleanupPeriodDays: settings.cleanupPeriodDays ?? RETENTION_DAYS };
|
|
128
|
+
const env = Object.fromEntries(Object.entries(current).filter(([key]) => !(key in PRIVACY_ENV)));
|
|
129
|
+
const rest = Object.fromEntries(Object.entries(settings).filter(([key]) => key !== 'env'));
|
|
130
|
+
return Object.keys(env).length === 0 ? rest : { ...rest, env };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function applyPrivacy(dir: string, enabled: boolean): SettingsOutcome {
|
|
134
|
+
const path = join(dir, 'settings.json');
|
|
135
|
+
const current = readSettings(path);
|
|
136
|
+
if (current === null) return 'unreadable';
|
|
137
|
+
const next = withPrivacy(current, enabled);
|
|
138
|
+
if (existsSync(path) && JSON.stringify(next) === JSON.stringify(current)) return 'unchanged';
|
|
139
|
+
const text = `${JSON.stringify(next, null, 2)}\n`;
|
|
140
|
+
mkdirSync(dir, { recursive: true });
|
|
141
|
+
const tmp = `${path}.metro-${String(process.pid)}.tmp`;
|
|
142
|
+
writeFileSync(tmp, text, { mode: 0o644 });
|
|
143
|
+
renameSync(tmp, path);
|
|
144
|
+
return 'written';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const skillPath = (dir: string): string => join(dir, 'skills', SKILL_NAME, 'SKILL.md');
|
|
148
|
+
const workerPath = (dir: string): string => join(dir, 'agents', 'worker.md');
|
|
149
|
+
|
|
150
|
+
function placeSkill(dir: string, guidance: string): Placed {
|
|
151
|
+
if (!existsSync(guidance)) return 'missing';
|
|
152
|
+
return ensureFile(skillPath(dir), readFileSync(guidance, 'utf8'));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function ensureClaudeSetup(deps: SetupDeps = {}): SetupReport {
|
|
156
|
+
const dir = deps.dir ?? claudeDir();
|
|
157
|
+
const agents = deps.agents ?? agentsDir();
|
|
158
|
+
const privacy = privacyEnabled(agents);
|
|
159
|
+
const report: SetupReport = {
|
|
160
|
+
privacy,
|
|
161
|
+
guard: 'plugin',
|
|
162
|
+
worker: ensureFile(workerPath(dir), WORKER_AGENT),
|
|
163
|
+
skill: placeSkill(dir, deps.guidance ?? guidancePath(deps.env)),
|
|
164
|
+
settings: applyPrivacy(dir, privacy),
|
|
165
|
+
};
|
|
166
|
+
syncAvailableModelsQuietly({ ...deps, dir, agents });
|
|
167
|
+
if (report.worker === 'written' || report.skill === 'written' || report.settings === 'written')
|
|
168
|
+
log.info(report, 'claude-setup: applied the Claude Code setup for a metro box');
|
|
169
|
+
if (report.settings === 'unreadable') log.warn({ path: join(dir, 'settings.json') }, 'claude-setup: settings.json is not valid JSON, so the privacy settings were not written');
|
|
170
|
+
return report;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function claudeSetupStatus(deps: SetupDeps = {}): SetupStatus {
|
|
174
|
+
const dir = deps.dir ?? claudeDir();
|
|
175
|
+
const settings = readSettings(join(dir, 'settings.json'));
|
|
176
|
+
const env = settings !== null && isRecord(settings.env) ? settings.env : {};
|
|
177
|
+
const days = settings?.cleanupPeriodDays;
|
|
178
|
+
return {
|
|
179
|
+
privacy: privacyEnabled(deps.agents ?? agentsDir()),
|
|
180
|
+
permissionMode: permissionMode(deps.agents ?? agentsDir()),
|
|
181
|
+
guard: 'plugin',
|
|
182
|
+
worker: existsSync(workerPath(dir)),
|
|
183
|
+
skill: existsSync(skillPath(dir)),
|
|
184
|
+
privacyApplied: Object.keys(PRIVACY_ENV).every((key) => env[key] === '1'),
|
|
185
|
+
retentionDays: typeof days === 'number' ? days : null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function tryClaudeSetup(deps: SetupDeps = {}): void {
|
|
190
|
+
try {
|
|
191
|
+
ensureClaudeSetup(deps);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
log.warn({ err: errMsg(err) }, 'claude-setup: could not apply the Claude Code setup');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const MODELS_KEY = 'availableModels';
|
|
198
|
+
const ENFORCE_KEY = 'enforceAvailableModels';
|
|
199
|
+
|
|
200
|
+
export function routeOf(cfg: ModelConfig): string | null {
|
|
201
|
+
if (cfg.provider === 'anthropic') return null;
|
|
202
|
+
const model = cfg[cfg.provider].model;
|
|
203
|
+
return model === '' ? null : `${cfg.provider}:${model}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function withAvailableModels(settings: Record<string, unknown>, route: string | null, metroWrote: boolean): Record<string, unknown> {
|
|
207
|
+
if (route !== null) return { ...settings, [MODELS_KEY]: [route], [ENFORCE_KEY]: true };
|
|
208
|
+
if (!metroWrote) return settings;
|
|
209
|
+
return Object.fromEntries(Object.entries(settings).filter(([key]) => key !== MODELS_KEY && key !== ENFORCE_KEY));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function syncAvailableModels(cfg: ModelConfig, deps: SetupDeps = {}): SettingsOutcome {
|
|
213
|
+
const dir = deps.dir ?? claudeDir();
|
|
214
|
+
const agents = deps.agents ?? agentsDir();
|
|
215
|
+
const route = routeOf(cfg);
|
|
216
|
+
const state = readJson<unknown>(statePath(agents), null);
|
|
217
|
+
const metroWrote = isRecord(state) && state.modelsByMetro === true;
|
|
218
|
+
const path = join(dir, 'settings.json');
|
|
219
|
+
const current = readSettings(path);
|
|
220
|
+
if (current === null) return 'unreadable';
|
|
221
|
+
const next = withAvailableModels(current, route, metroWrote);
|
|
222
|
+
mkdirSync(agents, { recursive: true });
|
|
223
|
+
writeJson(statePath(agents), { ...(isRecord(state) ? state : {}), modelsByMetro: route !== null });
|
|
224
|
+
if (existsSync(path) && JSON.stringify(next) === JSON.stringify(current)) return 'unchanged';
|
|
225
|
+
const text = `${JSON.stringify(next, null, 2)}\n`;
|
|
226
|
+
mkdirSync(dir, { recursive: true });
|
|
227
|
+
const tmp = `${path}.metro-${String(process.pid)}.tmp`;
|
|
228
|
+
writeFileSync(tmp, text, { mode: 0o644 });
|
|
229
|
+
renameSync(tmp, path);
|
|
230
|
+
return 'written';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function syncAvailableModelsQuietly(deps: SetupDeps = {}, cfg?: ModelConfig): void {
|
|
234
|
+
try {
|
|
235
|
+
syncAvailableModels(cfg ?? readModelConfig(deps.agents ?? agentsDir()), deps);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
log.warn({ err: errMsg(err) }, 'claude-setup: could not sync the model allowlist');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
import { ApiError } from '@metro-labs/http/api-error';
|
|
4
|
+
import { claudeDir, listClaudeProjects } from './files.js';
|
|
5
|
+
|
|
6
|
+
export const SKILL_MAX = 256 * 1024;
|
|
7
|
+
export const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
8
|
+
const USER_SCOPE = 'user';
|
|
9
|
+
const FILE = 'SKILL.md';
|
|
10
|
+
const DEFAULT_MODE = 0o644;
|
|
11
|
+
const SUMMARY_MAX = 300;
|
|
12
|
+
|
|
13
|
+
export type SkillScope = 'user' | 'project';
|
|
14
|
+
|
|
15
|
+
export interface ClaudeSkill {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
scope: SkillScope;
|
|
21
|
+
where: string;
|
|
22
|
+
path: string;
|
|
23
|
+
editable: boolean;
|
|
24
|
+
updatedAt: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const frontmatterField = (text: string, field: string): string => {
|
|
28
|
+
const head = text.startsWith('---') ? text.slice(3, text.indexOf('\n---', 3)) : '';
|
|
29
|
+
for (const line of head.split('\n')) {
|
|
30
|
+
const at = line.indexOf(':');
|
|
31
|
+
if (at > 0 && line.slice(0, at).trim() === field) return line.slice(at + 1).trim().replace(/^["']|["']$/g, '').slice(0, SUMMARY_MAX);
|
|
32
|
+
}
|
|
33
|
+
return '';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function entryOf(id: string, name: string, scope: SkillScope, where: string, path: string): ClaudeSkill {
|
|
37
|
+
const stat = statSync(path);
|
|
38
|
+
const editable = stat.size <= SKILL_MAX;
|
|
39
|
+
const text = editable ? readFileSync(path, 'utf8') : '';
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
name,
|
|
43
|
+
title: frontmatterField(text, 'name') || name,
|
|
44
|
+
description: frontmatterField(text, 'description'),
|
|
45
|
+
scope,
|
|
46
|
+
where,
|
|
47
|
+
path,
|
|
48
|
+
editable,
|
|
49
|
+
updatedAt: stat.mtime.toISOString(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function skillsIn(root: string, prefix: string, scope: SkillScope, where: string): ClaudeSkill[] {
|
|
54
|
+
if (!existsSync(root)) return [];
|
|
55
|
+
const out: ClaudeSkill[] = [];
|
|
56
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
57
|
+
if (!entry.isDirectory() || !SKILL_NAME_RE.test(entry.name)) continue;
|
|
58
|
+
const path = join(root, entry.name, FILE);
|
|
59
|
+
if (existsSync(path)) out.push(entryOf(`${prefix}:${entry.name}`, entry.name, scope, where, path));
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const userSkillsRoot = (dir: string): string => join(dir, 'skills');
|
|
65
|
+
|
|
66
|
+
export interface SkillHome {
|
|
67
|
+
prefix: string;
|
|
68
|
+
scope: SkillScope;
|
|
69
|
+
where: string;
|
|
70
|
+
root: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function skillHomes(dir = claudeDir()): SkillHome[] {
|
|
74
|
+
const homes: SkillHome[] = [{ prefix: USER_SCOPE, scope: USER_SCOPE, where: 'This machine', root: userSkillsRoot(dir) }];
|
|
75
|
+
for (const project of listClaudeProjects(dir)) {
|
|
76
|
+
const cwd = project.cwd;
|
|
77
|
+
if (cwd === null || !existsSync(cwd)) continue;
|
|
78
|
+
homes.push({ prefix: project.id, scope: 'project', where: cwd, root: join(cwd, '.claude', 'skills') });
|
|
79
|
+
}
|
|
80
|
+
return homes;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function listClaudeSkills(dir = claudeDir()): ClaudeSkill[] {
|
|
84
|
+
const out = skillHomes(dir).flatMap((home) => skillsIn(home.root, home.prefix, home.scope, home.where));
|
|
85
|
+
return out.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? '') || a.name.localeCompare(b.name));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function found(id: string, dir: string): ClaudeSkill {
|
|
89
|
+
const skill = listClaudeSkills(dir).find((entry) => entry.id === id);
|
|
90
|
+
if (skill === undefined) throw new ApiError('no such skill', 404);
|
|
91
|
+
return skill;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function readClaudeSkill(id: string, dir = claudeDir()): ClaudeSkill & { text: string } {
|
|
95
|
+
const skill = found(id, dir);
|
|
96
|
+
return { ...skill, text: skill.editable ? readFileSync(skill.path, 'utf8') : '' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function writeAtomic(path: string, text: string): void {
|
|
100
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
101
|
+
const tmp = `${path}.metro-${String(process.pid)}`;
|
|
102
|
+
writeFileSync(tmp, text, { mode: DEFAULT_MODE });
|
|
103
|
+
renameSync(tmp, path);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function assertText(text: string): void {
|
|
107
|
+
if (text.length > SKILL_MAX) throw new ApiError('that is more text than a skill may hold', 413);
|
|
108
|
+
if (text.trim() === '') throw new ApiError('a skill needs some text', 400);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function writeClaudeSkill(
|
|
112
|
+
id: string,
|
|
113
|
+
text: string,
|
|
114
|
+
seenAt: string | null | undefined,
|
|
115
|
+
dir = claudeDir(),
|
|
116
|
+
): ClaudeSkill {
|
|
117
|
+
const skill = found(id, dir);
|
|
118
|
+
if (!skill.editable) throw new ApiError('that skill is too large to edit here', 409);
|
|
119
|
+
assertText(text);
|
|
120
|
+
if (seenAt !== undefined && seenAt !== skill.updatedAt)
|
|
121
|
+
throw new ApiError('that skill changed on disk since you opened it; reload it before saving', 409);
|
|
122
|
+
writeAtomic(skill.path, text);
|
|
123
|
+
return entryOf(skill.id, skill.name, skill.scope, skill.where, skill.path);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const skillTemplate = (name: string): string =>
|
|
127
|
+
['---', `name: ${name}`, 'description: what this skill does, and when Claude should reach for it', '---', '', `# ${name}`, '', 'Write the instructions here.', ''].join('\n');
|
|
128
|
+
|
|
129
|
+
export function createClaudeSkill(
|
|
130
|
+
name: string,
|
|
131
|
+
scope: string | undefined,
|
|
132
|
+
text: string | undefined,
|
|
133
|
+
dir = claudeDir(),
|
|
134
|
+
): ClaudeSkill {
|
|
135
|
+
if (typeof name !== 'string' || !SKILL_NAME_RE.test(name))
|
|
136
|
+
throw new ApiError('a skill name is lowercase letters, digits and dashes, up to 64 characters', 400);
|
|
137
|
+
const prefix = scope === undefined || scope === '' ? USER_SCOPE : scope;
|
|
138
|
+
const home = skillHomes(dir).find((h) => h.prefix === prefix);
|
|
139
|
+
if (home === undefined) throw new ApiError('no such place to keep a skill', 404);
|
|
140
|
+
const path = join(home.root, name, FILE);
|
|
141
|
+
if (existsSync(path)) throw new ApiError('a skill by that name already lives there', 409);
|
|
142
|
+
const body = text === undefined || text.trim() === '' ? skillTemplate(name) : text;
|
|
143
|
+
assertText(body);
|
|
144
|
+
writeAtomic(path, body);
|
|
145
|
+
return entryOf(`${home.prefix}:${name}`, name, home.scope, home.where, path);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function deleteClaudeSkill(id: string, dir = claudeDir()): string {
|
|
149
|
+
const skill = found(id, dir);
|
|
150
|
+
const folder = dirname(skill.path);
|
|
151
|
+
if (basename(folder) !== skill.name) throw new ApiError('that skill does not live in its own folder', 409);
|
|
152
|
+
rmSync(folder, { recursive: true, force: true });
|
|
153
|
+
return id;
|
|
154
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
6
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
7
|
+
import { sessionRunning, stopSession, type SessionDeps } from './session.js';
|
|
8
|
+
|
|
9
|
+
const LATEST_URL = 'https://registry.npmjs.org/@anthropic-ai/claude-code/latest';
|
|
10
|
+
const VERSION_RE = /(\d+)\.(\d+)\.(\d+)/;
|
|
11
|
+
const CACHE_MS = 10 * 60_000;
|
|
12
|
+
const INSTALL_TIMEOUT_MS = 5 * 60_000;
|
|
13
|
+
const TAIL = 400;
|
|
14
|
+
|
|
15
|
+
export interface VersionDeps {
|
|
16
|
+
claude?: string;
|
|
17
|
+
fetchImpl?: typeof fetch;
|
|
18
|
+
session?: SessionDeps;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ClaudeVersion {
|
|
23
|
+
installed: string | null;
|
|
24
|
+
latest: string | null;
|
|
25
|
+
newer: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let cached: { latest: string | null; at: number } | null = null;
|
|
29
|
+
|
|
30
|
+
const candidates = (deps: VersionDeps): string[] => (deps.claude === undefined ? ['claude', join(homedir(), '.local', 'bin', 'claude')] : [deps.claude]);
|
|
31
|
+
|
|
32
|
+
const parse = (text: string): string | null => {
|
|
33
|
+
const m = VERSION_RE.exec(text);
|
|
34
|
+
return m === null ? null : `${m[1] ?? ''}.${m[2] ?? ''}.${m[3] ?? ''}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function probe(bin: string): string | null {
|
|
38
|
+
const run = spawnSync(bin, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
39
|
+
return run.error === undefined && run.status === 0 && typeof run.stdout === 'string' ? parse(run.stdout) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function located(deps: VersionDeps): { bin: string; installed: string } | null {
|
|
43
|
+
for (const bin of candidates(deps)) {
|
|
44
|
+
const installed = probe(bin);
|
|
45
|
+
if (installed !== null) return { bin, installed };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function newerThan(a: string, b: string): boolean {
|
|
51
|
+
const left = a.split('.').map(Number);
|
|
52
|
+
const right = b.split('.').map(Number);
|
|
53
|
+
for (let i = 0; i < 3; i += 1) {
|
|
54
|
+
const d = (left[i] ?? 0) - (right[i] ?? 0);
|
|
55
|
+
if (d !== 0) return d > 0;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function latestClaudeVersion(deps: VersionDeps = {}): Promise<string | null> {
|
|
61
|
+
const now = (deps.now ?? Date.now)();
|
|
62
|
+
if (cached !== null && now - cached.at < CACHE_MS) return cached.latest;
|
|
63
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
64
|
+
let latest: string | null = null;
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetchImpl(LATEST_URL, { headers: { accept: 'application/json' }, redirect: 'manual' });
|
|
67
|
+
const body: unknown = res.ok ? await res.json() : null;
|
|
68
|
+
latest = isRecord(body) && typeof body.version === 'string' ? parse(body.version) : null;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.warn({ err: errMsg(err) }, 'claude-version: could not read the latest version from npm');
|
|
71
|
+
}
|
|
72
|
+
cached = { latest, at: now };
|
|
73
|
+
return latest;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function forgetClaudeVersion(): void {
|
|
77
|
+
cached = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function claudeVersion(deps: VersionDeps = {}): Promise<ClaudeVersion> {
|
|
81
|
+
const installed = located(deps)?.installed ?? null;
|
|
82
|
+
const latest = await latestClaudeVersion(deps);
|
|
83
|
+
return { installed, latest, newer: installed !== null && latest !== null && newerThan(latest, installed) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function install(bin: string): void {
|
|
87
|
+
const run = spawnSync(bin, ['install', 'latest'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: INSTALL_TIMEOUT_MS });
|
|
88
|
+
if (run.error !== undefined) throw new ApiError(`claude install latest failed: ${errMsg(run.error)}`, 502);
|
|
89
|
+
if (run.status === 0) return;
|
|
90
|
+
const said = `${run.stderr ?? ''}${run.stdout ?? ''}`.trim().slice(-TAIL);
|
|
91
|
+
throw new ApiError(`claude install latest failed: ${said}`, 502);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function restartIfChanged(changed: boolean, session: SessionDeps): boolean {
|
|
95
|
+
if (!changed || !sessionRunning(session.tmux ?? 'tmux')) return false;
|
|
96
|
+
stopSession(session);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function updateClaude(deps: VersionDeps = {}): Promise<ClaudeVersion & { restarted: boolean }> {
|
|
101
|
+
const found = located(deps);
|
|
102
|
+
if (found === null) throw new ApiError('Claude Code is not installed on this machine', 404);
|
|
103
|
+
install(found.bin);
|
|
104
|
+
forgetClaudeVersion();
|
|
105
|
+
const after = await claudeVersion(deps);
|
|
106
|
+
const restarted = restartIfChanged(after.installed !== null && after.installed !== found.installed, deps.session ?? {});
|
|
107
|
+
log.info({ from: found.installed, to: after.installed, restarted }, 'claude-version: Claude Code updated');
|
|
108
|
+
return { ...after, restarted };
|
|
109
|
+
}
|