@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +11 -0
- package/dist/claude-settings.js +40 -0
- package/dist/claude.js +163 -0
- package/dist/cli.js +77 -567
- package/dist/control.js +74 -0
- package/dist/hold.js +211 -0
- package/dist/local.js +43 -0
- package/dist/mcp-config.js +25 -0
- package/dist/node-name.js +71 -0
- package/dist/onboarding.js +44 -0
- package/dist/provider-flags.js +9 -0
- package/dist/route.js +58 -0
- package/dist/runtime-install.js +97 -0
- package/dist/runtime.js +74 -0
- package/dist/serve.js +158 -0
- package/dist/service.js +243 -0
- package/dist/tail.js +102 -144
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -23
- package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +17 -0
- package/runtime/marketplace/plugin/.mcp.json +1 -0
- package/runtime/marketplace/plugin/bin/guard.mjs +71 -0
- package/runtime/marketplace/plugin/bin/metro-plugin.mjs +30 -0
- package/runtime/marketplace/plugin/bin/policy.mjs +97 -0
- package/runtime/marketplace/plugin/bin/session-start.mjs +31 -0
- package/runtime/marketplace/plugin/hooks/hooks.json +25 -0
- package/runtime/marketplace/plugin/orchestrator.md +24 -0
- package/runtime/node_modules/@metro-labs/core/package.json +47 -0
- package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/events.ts +119 -0
- package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/core/src/ids.ts +24 -0
- package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +91 -0
- package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/protocol.ts +227 -0
- package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +74 -0
- package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/account-files.ts +41 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +58 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/sender-profile.ts +64 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +61 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +42 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +89 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +123 -0
- package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/core/src/tickets.ts +37 -0
- package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/package.json +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/agent-fs.ts +134 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files-api.ts +12 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +119 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/home-fs.ts +113 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +100 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +252 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +61 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +87 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +55 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +76 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +357 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +30 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +59 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +131 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +132 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +305 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +168 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +35 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +128 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/policy-route.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/sender-cards.ts +86 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/api.ts +55 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +149 -0
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +221 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +21 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +12 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +370 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +78 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +13 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +1 -0
- package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +355 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +359 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +60 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +149 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +75 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +283 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +109 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +271 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +120 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +113 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +306 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +176 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +72 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +18 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +159 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +135 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +158 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +166 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +131 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-policy.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +348 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +358 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +81 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +106 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-reaper.ts +174 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +278 -0
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +233 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +173 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +194 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +137 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +77 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +237 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +118 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +274 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +160 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +46 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +150 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +139 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +223 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +199 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +299 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +193 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +70 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +36 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +66 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +18 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/subscription.ts +120 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +15 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +244 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +156 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/attachment-schema.ts +91 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +226 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +137 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +19 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +87 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +79 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +83 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +79 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +60 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-tool.ts +96 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +87 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +46 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +202 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +88 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-def.ts +9 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +225 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +333 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +61 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +43 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +126 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +165 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/privilege.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +132 -0
- package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +316 -0
- package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +118 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +35 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +365 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +208 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +71 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +39 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +58 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +90 -0
- package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-driver.ts +42 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +128 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +134 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-outlook.ts +63 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +225 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +288 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +248 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +265 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +84 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +124 -0
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +15 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/api.ts +45 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/config.ts +51 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +48 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +96 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/install.ts +56 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/paths.ts +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +148 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +130 -0
- package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +61 -0
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +26 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +117 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +240 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +174 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +122 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +68 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +22 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/http/package.json +29 -0
- package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +138 -0
- package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
- package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
- package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +149 -0
- package/runtime/node_modules/@metro-labs/outlook/package.json +25 -0
- package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +107 -0
- package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +20 -0
- package/runtime/node_modules/@metro-labs/outlook/src/attachments.ts +97 -0
- package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +150 -0
- package/runtime/node_modules/@metro-labs/outlook/src/browser.ts +53 -0
- package/runtime/node_modules/@metro-labs/outlook/src/config.ts +36 -0
- package/runtime/node_modules/@metro-labs/outlook/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/outlook/src/graph.ts +60 -0
- package/runtime/node_modules/@metro-labs/outlook/src/html.ts +72 -0
- package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +104 -0
- package/runtime/node_modules/@metro-labs/outlook/src/index.ts +12 -0
- package/runtime/node_modules/@metro-labs/outlook/src/login.ts +150 -0
- package/runtime/node_modules/@metro-labs/outlook/src/me.ts +41 -0
- package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +116 -0
- package/runtime/node_modules/@metro-labs/outlook/src/query.ts +119 -0
- package/runtime/node_modules/@metro-labs/outlook/src/state.ts +43 -0
- package/runtime/node_modules/@metro-labs/outlook/src/station.ts +15 -0
- package/runtime/node_modules/@metro-labs/outlook/src/trust.ts +70 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +256 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +70 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +27 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +246 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +173 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +95 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +65 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +20 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +56 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
- package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +106 -0
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +134 -0
- package/runtime/node_modules/@metro-labs/threema/src/api.ts +152 -0
- package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +100 -0
- package/runtime/node_modules/@metro-labs/threema/src/files.ts +58 -0
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +169 -0
- package/runtime/node_modules/@metro-labs/threema/src/groups.ts +96 -0
- package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +89 -0
- package/runtime/node_modules/@metro-labs/threema/src/index.ts +24 -0
- package/runtime/node_modules/@metro-labs/threema/src/messages.ts +264 -0
- package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +160 -0
- package/runtime/node_modules/@metro-labs/threema/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +213 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +388 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +83 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +7 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/names.ts +28 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +185 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +94 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +5 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +114 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +101 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +118 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +244 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +316 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +69 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +47 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +122 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +139 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +61 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +153 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +112 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/smart.ts +72 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +79 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +198 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +88 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +98 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +51 -0
- package/runtime/runtime.json +3 -0
- package/runtime/server.ts +1 -0
- package/runtime/stations.json +37 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -78
- package/dist/channels/discord.js +0 -148
- package/dist/channels/telegram.js +0 -137
- package/dist/lib/address.js +0 -21
- package/dist/lib/codex-rc.js +0 -274
- package/dist/lib/dotenv.js +0 -31
- package/dist/log.js +0 -12
- package/dist/paths.js +0 -45
- package/skills/metro/SKILL.md +0 -122
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from '../agent-user/agent-fs.js';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
5
|
+
import { writeHomeText } from '../agent-user/home-fs.js';
|
|
6
|
+
import { agentUser } from '../agent-user/user.js';
|
|
7
|
+
|
|
8
|
+
const FILE = '.claude.json';
|
|
9
|
+
const FLAG = 'hasCompletedOnboarding';
|
|
10
|
+
|
|
11
|
+
export type OnboardingMark = 'marked' | 'already' | 'unreadable';
|
|
12
|
+
|
|
13
|
+
const given = (value: string | undefined): string | undefined =>
|
|
14
|
+
value !== undefined && value.trim() !== '' ? value.trim() : undefined;
|
|
15
|
+
|
|
16
|
+
export function claudeConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
17
|
+
const user = agentUser();
|
|
18
|
+
if (user !== null) return join(user.home, FILE);
|
|
19
|
+
return join(given(env.CLAUDE_CONFIG_DIR) ?? given(env.HOME) ?? homedir(), FILE);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readConfig(path: string): Record<string, unknown> | null {
|
|
23
|
+
if (!existsSync(path)) return {};
|
|
24
|
+
try {
|
|
25
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
26
|
+
return isRecord(parsed) ? parsed : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function markOnboardingDone(path = claudeConfigPath()): OnboardingMark {
|
|
33
|
+
const config = readConfig(path);
|
|
34
|
+
if (config === null) return 'unreadable';
|
|
35
|
+
if (config[FLAG] === true) return 'already';
|
|
36
|
+
writeHomeText(path, `${JSON.stringify({ ...config, [FLAG]: true }, null, 2)}\n`, 0o600);
|
|
37
|
+
return 'marked';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TRUST = 'hasTrustDialogAccepted';
|
|
41
|
+
|
|
42
|
+
function realDir(dir: string): string {
|
|
43
|
+
try {
|
|
44
|
+
return realpathSync(dir);
|
|
45
|
+
} catch {
|
|
46
|
+
return dir;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function trustFolder(dir: string, path = claudeConfigPath()): OnboardingMark {
|
|
51
|
+
const config = readConfig(path);
|
|
52
|
+
if (config === null) return 'unreadable';
|
|
53
|
+
const key = realDir(dir);
|
|
54
|
+
const projects = isRecord(config.projects) ? config.projects : {};
|
|
55
|
+
const project = isRecord(projects[key]) ? projects[key] : {};
|
|
56
|
+
if (project[TRUST] === true) return 'already';
|
|
57
|
+
const next = { ...config, projects: { ...projects, [key]: { ...project, [TRUST]: true } } };
|
|
58
|
+
writeHomeText(path, `${JSON.stringify(next, null, 2)}\n`, 0o600);
|
|
59
|
+
return 'marked';
|
|
60
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from '../agent-user/agent-fs.js';
|
|
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
|
+
import { agentMarketplaceDir, agentUser, asAgent, claudeBin } from '../agent-user/user.js';
|
|
8
|
+
|
|
9
|
+
const MARKETPLACE = 'metro';
|
|
10
|
+
const PLUGIN = 'metro@metro';
|
|
11
|
+
const STEP_MS = 120_000;
|
|
12
|
+
const SAID_MAX = 300;
|
|
13
|
+
|
|
14
|
+
export interface Answer {
|
|
15
|
+
status: number;
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type Run = (args: string[]) => Promise<Answer>;
|
|
21
|
+
|
|
22
|
+
export interface PluginInstallDeps {
|
|
23
|
+
run: Run | null;
|
|
24
|
+
marketplaceDir: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type PluginOutcome = 'skipped' | 'unchanged' | 'installed' | 'updated' | 'failed';
|
|
28
|
+
|
|
29
|
+
export function storeMarketplaceDir(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
30
|
+
const store = env.METRO_RUNTIME_STORE?.trim() ?? '';
|
|
31
|
+
if (store === '') return null;
|
|
32
|
+
const dir = join(store, 'marketplace');
|
|
33
|
+
return existsSync(join(dir, '.claude-plugin', 'marketplace.json')) ? dir : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function stagedMarketplaceDir(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
37
|
+
const store = storeMarketplaceDir(env);
|
|
38
|
+
const user = agentUser();
|
|
39
|
+
if (store === null || user === null) return store;
|
|
40
|
+
const copy = agentMarketplaceDir(user);
|
|
41
|
+
return existsSync(join(copy, '.claude-plugin', 'marketplace.json')) ? copy : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function stagedPluginVersion(dir: string): string | null {
|
|
45
|
+
try {
|
|
46
|
+
const parsed: unknown = JSON.parse(readFileSync(join(dir, 'plugin', '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
47
|
+
return isRecord(parsed) && typeof parsed.version === 'string' ? parsed.version : null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const runner =
|
|
54
|
+
(bin: string): Run =>
|
|
55
|
+
(args) =>
|
|
56
|
+
new Promise((resolve) => {
|
|
57
|
+
execFile(...asAgent(bin, args), { encoding: 'utf8', timeout: STEP_MS, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
58
|
+
const status = err === null ? 0 : typeof (err as { code?: unknown }).code === 'number' ? (err as { code: number }).code : 1;
|
|
59
|
+
resolve({ status, stdout, stderr });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export async function findClaude(candidates = agentUser() === null ? ['claude', join(homedir(), '.local', 'bin', 'claude')] : [claudeBin()]): Promise<Run | null> {
|
|
64
|
+
for (const bin of candidates) {
|
|
65
|
+
const run = runner(bin);
|
|
66
|
+
if ((await run(['--version'])).status === 0) return run;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function listOf(stdout: string): Record<string, unknown>[] {
|
|
72
|
+
try {
|
|
73
|
+
const parsed: unknown = JSON.parse(stdout);
|
|
74
|
+
return Array.isArray(parsed) ? parsed.filter(isRecord) : [];
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const realish = (path: string): string => {
|
|
81
|
+
try {
|
|
82
|
+
return realpathSync(path);
|
|
83
|
+
} catch {
|
|
84
|
+
return path;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const samePath = (a: string | null, b: string): boolean => a !== null && realish(a) === realish(b);
|
|
89
|
+
|
|
90
|
+
async function step(run: Run, args: string[]): Promise<boolean> {
|
|
91
|
+
const answer = await run(args);
|
|
92
|
+
if (answer.status === 0) {
|
|
93
|
+
log.info({ args }, 'plugin: claude');
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
log.warn({ args, said: `${answer.stderr}${answer.stdout}`.trim().slice(0, SAID_MAX) }, 'plugin: claude refused');
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function marketplacePath(run: Run): Promise<string | null | undefined> {
|
|
101
|
+
const mine = listOf((await run(['plugin', 'marketplace', 'list', '--json'])).stdout).find((m) => m.name === MARKETPLACE);
|
|
102
|
+
if (mine === undefined) return undefined;
|
|
103
|
+
const at = mine.installLocation ?? mine.path;
|
|
104
|
+
return typeof at === 'string' ? at : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function ensureMarketplace(run: Run, dir: string): Promise<boolean> {
|
|
108
|
+
const at = await marketplacePath(run);
|
|
109
|
+
if (at !== undefined && samePath(at, dir)) return true;
|
|
110
|
+
if (at !== undefined) await step(run, ['plugin', 'marketplace', 'remove', MARKETPLACE]);
|
|
111
|
+
return step(run, ['plugin', 'marketplace', 'add', dir]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function installedVersion(run: Run): Promise<string | null> {
|
|
115
|
+
const mine = listOf((await run(['plugin', 'list', '--json'])).stdout).find((p) => p.id === PLUGIN);
|
|
116
|
+
return mine !== undefined && typeof mine.version === 'string' ? mine.version : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function ensureInstalled(run: Run, wanted: string | null): Promise<PluginOutcome> {
|
|
120
|
+
const have = await installedVersion(run);
|
|
121
|
+
if (have === null) return (await step(run, ['plugin', 'install', PLUGIN, '-y', '--scope', 'user'])) ? 'installed' : 'failed';
|
|
122
|
+
if (wanted === null || have === wanted) return 'unchanged';
|
|
123
|
+
await step(run, ['plugin', 'marketplace', 'update', MARKETPLACE]);
|
|
124
|
+
return (await step(run, ['plugin', 'update', PLUGIN, '-y'])) ? 'updated' : 'failed';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function explainMissingMarketplace(env: NodeJS.ProcessEnv = process.env): void {
|
|
128
|
+
const store = env.METRO_RUNTIME_STORE?.trim() ?? '';
|
|
129
|
+
if (store === '') return;
|
|
130
|
+
log.warn(
|
|
131
|
+
{ store },
|
|
132
|
+
'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',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function ensureMetroPlugin(deps?: Partial<PluginInstallDeps>): Promise<PluginOutcome> {
|
|
137
|
+
const dir = deps?.marketplaceDir === undefined ? stagedMarketplaceDir() : deps.marketplaceDir;
|
|
138
|
+
if (dir === null) {
|
|
139
|
+
if (deps?.marketplaceDir === undefined) explainMissingMarketplace();
|
|
140
|
+
return 'skipped';
|
|
141
|
+
}
|
|
142
|
+
const run = deps?.run === undefined ? await findClaude() : deps.run;
|
|
143
|
+
if (run === null) {
|
|
144
|
+
log.info('plugin: claude is not on this machine, so metro leaves its Claude Code plugin alone');
|
|
145
|
+
return 'skipped';
|
|
146
|
+
}
|
|
147
|
+
if (!(await ensureMarketplace(run, dir))) return 'failed';
|
|
148
|
+
return ensureInstalled(run, stagedPluginVersion(dir));
|
|
149
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, statSync } from '../agent-user/agent-fs.js';
|
|
2
|
+
import { Transform, type Readable } from 'node:stream';
|
|
3
|
+
import { moveHome, receiveHomeFile, removeHome } from '../agent-user/home-fs.js';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { ApiError } from '@metro-labs/http/api-error';
|
|
6
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
7
|
+
import { claudeDir, PROJECT_RE, safeName, SESSION_RE } from './files.js';
|
|
8
|
+
|
|
9
|
+
export const SESSION_FILE_MAX = 512 * 1024 * 1024;
|
|
10
|
+
const FILE_MODE = 0o644;
|
|
11
|
+
|
|
12
|
+
export interface SessionFile {
|
|
13
|
+
id: string;
|
|
14
|
+
bytes: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const transcriptPath = (project: string, session: string, dir: string): string =>
|
|
18
|
+
join(dir, 'projects', safeName(project, PROJECT_RE, 'Claude project id'), `${safeName(session, SESSION_RE, 'session id')}.jsonl`);
|
|
19
|
+
|
|
20
|
+
export function sessionFilePath(project: string, session: string, dir = claudeDir()): string {
|
|
21
|
+
const path = transcriptPath(project, session, dir);
|
|
22
|
+
if (!existsSync(path)) throw new ApiError('no such session', 404);
|
|
23
|
+
if (statSync(path).size > SESSION_FILE_MAX) throw new ApiError(`a session file is at most ${String(SESSION_FILE_MAX)} bytes`, 400);
|
|
24
|
+
return path;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const HEAD_BYTES = 64 * 1024;
|
|
28
|
+
|
|
29
|
+
function watched(body: Readable, head: Buffer[]): Transform {
|
|
30
|
+
let total = 0;
|
|
31
|
+
let kept = 0;
|
|
32
|
+
const check = new Transform({
|
|
33
|
+
transform(chunk: Buffer, _enc, done): void {
|
|
34
|
+
total += chunk.length;
|
|
35
|
+
if (total > SESSION_FILE_MAX) {
|
|
36
|
+
done(new ApiError(`a session file is at most ${String(SESSION_FILE_MAX)} bytes`, 413));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (kept < HEAD_BYTES) {
|
|
40
|
+
head.push(chunk.subarray(0, HEAD_BYTES - kept));
|
|
41
|
+
kept += chunk.length;
|
|
42
|
+
}
|
|
43
|
+
done(null, chunk);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return body.pipe(check);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function receiveSessionFile(project: string, session: string, body: Readable, dir = claudeDir()): Promise<SessionFile> {
|
|
50
|
+
const path = transcriptPath(project, session, dir);
|
|
51
|
+
const tmp = `${path}.metro-upload`;
|
|
52
|
+
const head: Buffer[] = [];
|
|
53
|
+
try {
|
|
54
|
+
await receiveHomeFile(tmp, watched(body, head), FILE_MODE);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
removeHome(tmp);
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
if (!firstLineIsJson(Buffer.concat(head).toString('utf8'))) {
|
|
60
|
+
removeHome(tmp);
|
|
61
|
+
throw new ApiError('a session file is JSON lines, one Claude Code entry per line', 400);
|
|
62
|
+
}
|
|
63
|
+
moveHome(tmp, path);
|
|
64
|
+
return { id: safeName(session, SESSION_RE, 'session id'), bytes: statSync(path).size };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function firstLineIsJson(text: string): boolean {
|
|
68
|
+
const first = text.split('\n').find((line) => line.trim() !== '');
|
|
69
|
+
if (first === undefined) return false;
|
|
70
|
+
try {
|
|
71
|
+
return isRecord(JSON.parse(first));
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { realpathSync } from '../agent-user/agent-fs.js';
|
|
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, routedConnection } 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
|
+
import { inSessionScope } from './memory.js';
|
|
15
|
+
import { runningAsMetro } from '../metro-user/privilege.js';
|
|
16
|
+
import { agentExtraEnv, agentUser, agentUserExpected, agentViewDir, asAgent, claudeHome } from '../agent-user/user.js';
|
|
17
|
+
|
|
18
|
+
function sessionEnv(): Record<string, string> {
|
|
19
|
+
const user = agentUser();
|
|
20
|
+
if (user === null) return {};
|
|
21
|
+
const port = process.env.METRO_WEBHOOK_PORT?.trim() ?? '';
|
|
22
|
+
return { ...agentExtraEnv(), METRO_AGENTS_DIR: agentViewDir(user), ...(port === '' ? {} : { METRO_WEBHOOK_PORT: port }) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const SESSION_NAME = 'metro';
|
|
26
|
+
const STATE_FILE = 'claude-session.json';
|
|
27
|
+
const WARNING = 'WARNING: Loading development channels';
|
|
28
|
+
const CONFIRM_POLL_MS = 500;
|
|
29
|
+
const CONFIRM_WAIT_MS = 60_000;
|
|
30
|
+
const WATCH_MS = 15_000;
|
|
31
|
+
const STRIKE_WINDOW_MS = 60_000;
|
|
32
|
+
const MAX_STRIKES = 5;
|
|
33
|
+
const PAUSE_MS = 30 * 60_000;
|
|
34
|
+
|
|
35
|
+
export interface SessionDeps {
|
|
36
|
+
tmux?: string;
|
|
37
|
+
metro?: string[];
|
|
38
|
+
home?: string;
|
|
39
|
+
agents?: string;
|
|
40
|
+
signedIn?: () => boolean;
|
|
41
|
+
now?: () => number;
|
|
42
|
+
version?: string;
|
|
43
|
+
continues?: (home: string) => boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SessionStatus {
|
|
47
|
+
name: string;
|
|
48
|
+
running: boolean;
|
|
49
|
+
autostart: boolean;
|
|
50
|
+
blocked: string | null;
|
|
51
|
+
lastStartedAt: string | null;
|
|
52
|
+
lastError: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Memory {
|
|
56
|
+
lastStartedAt: number | null;
|
|
57
|
+
lastError: string | null;
|
|
58
|
+
strikes: number;
|
|
59
|
+
pausedUntil: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const memory: Memory = { lastStartedAt: null, lastError: null, strikes: 0, pausedUntil: 0 };
|
|
63
|
+
|
|
64
|
+
const statePath = (agents: string): string => join(agents, STATE_FILE);
|
|
65
|
+
|
|
66
|
+
function readState(agents: string): Record<string, unknown> {
|
|
67
|
+
const raw = readJson<unknown>(statePath(agents), null);
|
|
68
|
+
return isRecord(raw) ? raw : {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const writeState = (agents: string, patch: Record<string, unknown>): void => {
|
|
72
|
+
writeJson(statePath(agents), { ...readState(agents), ...patch });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const autostartEnabled = (agents = agentsDir()): boolean => readState(agents).autostart !== false;
|
|
76
|
+
|
|
77
|
+
export function setAutostart(enabled: boolean, agents = agentsDir()): void {
|
|
78
|
+
writeState(agents, { autostart: enabled });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function startedVersion(agents = agentsDir()): string | null {
|
|
82
|
+
const version = readState(agents).version;
|
|
83
|
+
return typeof version === 'string' ? version : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function tmuxOk(tmux: string, args: string[]): boolean {
|
|
87
|
+
const run = spawnSync(...asAgent(tmux, args), { stdio: 'ignore' });
|
|
88
|
+
return run.error === undefined && run.status === 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const sessionRunning = (tmux = 'tmux'): boolean => tmuxOk(tmux, ['has-session', '-t', SESSION_NAME]);
|
|
92
|
+
|
|
93
|
+
export const tmuxServerUp = (tmux = 'tmux'): boolean => tmuxOk(tmux, ['list-sessions']);
|
|
94
|
+
|
|
95
|
+
function realDir(dir: string): string {
|
|
96
|
+
try {
|
|
97
|
+
return realpathSync(dir);
|
|
98
|
+
} catch {
|
|
99
|
+
return dir;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const encodedCwd = (dir: string): string => dir.replace(/[^A-Za-z0-9]/g, '-');
|
|
104
|
+
|
|
105
|
+
export function hasConversation(home: string, dir = claudeDir()): boolean {
|
|
106
|
+
const cwd = realDir(home);
|
|
107
|
+
const folder = encodedCwd(cwd);
|
|
108
|
+
return listClaudeProjects(dir).some(
|
|
109
|
+
(project) => project.sessions > 0 && (project.id === folder || (project.cwd !== null && realDir(project.cwd) === cwd)),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function metroCommand(deps: SessionDeps, home: string): string[] {
|
|
114
|
+
const bin = process.env.METRO_CLI_BIN?.trim() ?? '';
|
|
115
|
+
const runtime = agentUser() === null ? process.execPath : 'node';
|
|
116
|
+
const base = deps.metro ?? (bin === '' ? ['metro', 'claude'] : [runtime, bin, 'claude']);
|
|
117
|
+
const continues = (deps.continues ?? hasConversation)(home);
|
|
118
|
+
const env = Object.entries(sessionEnv()).map(([k, v]) => `${k}=${v}`);
|
|
119
|
+
const command = env.length === 0 || deps.metro !== undefined ? base : ['env', ...env, ...base];
|
|
120
|
+
return continues ? [...command, '-c'] : command;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function credentialReady(deps: SessionDeps): string | null {
|
|
124
|
+
const signedIn = deps.signedIn ?? (() => claudeAccount().signedIn);
|
|
125
|
+
if (signedIn()) return null;
|
|
126
|
+
try {
|
|
127
|
+
const cfg = readModelConfig(deps.agents ?? agentsDir());
|
|
128
|
+
const conn = routedConnection(cfg);
|
|
129
|
+
if (conn !== null && conn.provider !== 'anthropic' && notReady(cfg) === null) return null;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return `the Model page is not readable (${errMsg(err)})`;
|
|
132
|
+
}
|
|
133
|
+
return 'Claude Code is not signed in and the Model page routes nowhere yet';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const agentUserMissing = (deps: SessionDeps): boolean =>
|
|
137
|
+
deps.metro === undefined && agentUserExpected() && agentUser() === null;
|
|
138
|
+
|
|
139
|
+
export function sessionBlocked(deps: SessionDeps = {}): string | null {
|
|
140
|
+
const agents = deps.agents ?? agentsDir();
|
|
141
|
+
if (listAgentFiles(agents).length === 0) return 'no agent on this machine yet';
|
|
142
|
+
if (agentUserMissing(deps))
|
|
143
|
+
return 'Claude Code runs as the user agent here, and that user is not ready yet; see the log';
|
|
144
|
+
if (deps.metro === undefined && !claudeInstalled()) return 'Claude Code is not installed on this machine';
|
|
145
|
+
if (!tmuxOk(deps.tmux ?? 'tmux', ['-V'])) return 'tmux is not installed on this machine';
|
|
146
|
+
return credentialReady(deps);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function capturePane(tmux: string): string {
|
|
150
|
+
const run = spawnSync(...asAgent(tmux, ['capture-pane', '-p', '-t', SESSION_NAME]), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
151
|
+
return run.error === undefined && run.status === 0 ? run.stdout : '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function confirmChannels(tmux: string, until: number): void {
|
|
155
|
+
const tick = (): void => {
|
|
156
|
+
const pane = capturePane(tmux);
|
|
157
|
+
if (pane.includes(WARNING) && pane.includes('server:metro')) {
|
|
158
|
+
spawnSync(...asAgent(tmux, ['send-keys', '-t', SESSION_NAME, 'Enter']), { stdio: 'ignore' });
|
|
159
|
+
log.info('claude-session: confirmed the development channels dialog for server:metro');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (Date.now() < until && sessionRunning(tmux)) setTimeout(tick, CONFIRM_POLL_MS).unref();
|
|
163
|
+
};
|
|
164
|
+
setTimeout(tick, CONFIRM_POLL_MS).unref();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function recordStart(deps: SessionDeps, tmux: string, now: number, run: { error?: Error; status: number | null; stderr: string }): void {
|
|
168
|
+
memory.lastStartedAt = now;
|
|
169
|
+
if (run.error !== undefined || run.status !== 0) {
|
|
170
|
+
memory.lastError = run.error === undefined ? `tmux new-session exited ${String(run.status)}: ${run.stderr.trim()}` : errMsg(run.error);
|
|
171
|
+
log.warn({ err: memory.lastError }, 'claude-session: tmux could not start');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
memory.lastError = null;
|
|
175
|
+
writeState(deps.agents ?? agentsDir(), { version: deps.version ?? METRO_VERSION });
|
|
176
|
+
confirmChannels(tmux, now + CONFIRM_WAIT_MS);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const spawnCwd = (home: string): string => (runningAsMetro() ? '/' : home);
|
|
180
|
+
|
|
181
|
+
export function startSession(deps: SessionDeps = {}): SessionStatus {
|
|
182
|
+
const tmux = deps.tmux ?? 'tmux';
|
|
183
|
+
const home = deps.home ?? claudeHome() ?? homedir();
|
|
184
|
+
const now = (deps.now ?? Date.now)();
|
|
185
|
+
const trusted = trustFolder(home);
|
|
186
|
+
const [command = 'metro', ...args] = metroCommand(deps, home);
|
|
187
|
+
const tmuxArgs = ['new-session', '-d', '-s', SESSION_NAME, '-c', home, '-x', '200', '-y', '50', command, ...args];
|
|
188
|
+
const launch = deps.tmux === undefined && !tmuxServerUp(tmux) ? inSessionScope(asAgent(tmux, tmuxArgs)) : asAgent(tmux, tmuxArgs);
|
|
189
|
+
const run = spawnSync(...launch, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], cwd: spawnCwd(home) });
|
|
190
|
+
recordStart(deps, tmux, now, run);
|
|
191
|
+
if (memory.lastError === null) log.info({ home, trusted, command: [command, ...args].join(' ') }, 'claude-session: started Claude Code in tmux');
|
|
192
|
+
return sessionStatus(deps);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function stopSession(deps: SessionDeps = {}): SessionStatus {
|
|
196
|
+
spawnSync(...asAgent(deps.tmux ?? 'tmux', ['kill-session', '-t', SESSION_NAME]), { stdio: 'ignore' });
|
|
197
|
+
return sessionStatus(deps);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function sessionStatus(deps: SessionDeps = {}): SessionStatus {
|
|
201
|
+
return {
|
|
202
|
+
name: SESSION_NAME,
|
|
203
|
+
running: sessionRunning(deps.tmux ?? 'tmux'),
|
|
204
|
+
autostart: autostartEnabled(deps.agents ?? agentsDir()),
|
|
205
|
+
blocked: sessionBlocked(deps),
|
|
206
|
+
lastStartedAt: memory.lastStartedAt === null ? null : new Date(memory.lastStartedAt).toISOString(),
|
|
207
|
+
lastError: memory.lastError,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function strike(now: number): boolean {
|
|
212
|
+
if (memory.lastStartedAt !== null && now - memory.lastStartedAt < STRIKE_WINDOW_MS) memory.strikes += 1;
|
|
213
|
+
else memory.strikes = 1;
|
|
214
|
+
if (memory.strikes < MAX_STRIKES) return false;
|
|
215
|
+
memory.pausedUntil = now + PAUSE_MS;
|
|
216
|
+
memory.strikes = 0;
|
|
217
|
+
log.warn({ pauseMinutes: PAUSE_MS / 60_000 }, 'claude-session: Claude Code keeps exiting right after starting; not restarting it for a while');
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export type Ensured = 'started' | 'restarted' | 'running' | 'blocked' | 'off' | 'paused';
|
|
222
|
+
|
|
223
|
+
function holdsBack(deps: SessionDeps, now: number): Ensured | null {
|
|
224
|
+
if (now < memory.pausedUntil) return 'paused';
|
|
225
|
+
if (sessionBlocked(deps) !== null) return 'blocked';
|
|
226
|
+
return strike(now) ? 'paused' : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface Resolved {
|
|
230
|
+
now: number;
|
|
231
|
+
agents: string;
|
|
232
|
+
version: string;
|
|
233
|
+
tmux: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const resolved = (deps: SessionDeps): Resolved => ({
|
|
237
|
+
now: (deps.now ?? Date.now)(),
|
|
238
|
+
agents: deps.agents ?? agentsDir(),
|
|
239
|
+
version: deps.version ?? METRO_VERSION,
|
|
240
|
+
tmux: deps.tmux ?? 'tmux',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
function situation(r: Resolved): 'running' | 'stale' | 'absent' {
|
|
244
|
+
if (!sessionRunning(r.tmux)) return 'absent';
|
|
245
|
+
return startedVersion(r.agents) === r.version ? 'running' : 'stale';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function ensureSession(deps: SessionDeps = {}): Ensured {
|
|
249
|
+
const r = resolved(deps);
|
|
250
|
+
if (!autostartEnabled(r.agents)) return 'off';
|
|
251
|
+
const found = situation(r);
|
|
252
|
+
if (found === 'running') return 'running';
|
|
253
|
+
const held = holdsBack(deps, r.now);
|
|
254
|
+
if (held !== null) return held;
|
|
255
|
+
if (found === 'stale') {
|
|
256
|
+
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');
|
|
257
|
+
stopSession(deps);
|
|
258
|
+
}
|
|
259
|
+
startSession(deps);
|
|
260
|
+
return found === 'stale' ? 'restarted' : 'started';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let watcher: ReturnType<typeof setInterval> | null = null;
|
|
264
|
+
|
|
265
|
+
export function watchSession(deps: SessionDeps = {}, everyMs = WATCH_MS): void {
|
|
266
|
+
if (watcher !== null) return;
|
|
267
|
+
const tick = (): void => {
|
|
268
|
+
try {
|
|
269
|
+
const outcome = ensureSession(deps);
|
|
270
|
+
if (outcome === 'started' || outcome === 'restarted') log.info({ outcome }, 'claude-session: Claude Code is up');
|
|
271
|
+
} catch (err) {
|
|
272
|
+
log.warn({ err: errMsg(err) }, 'claude-session: check failed');
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
watcher = setInterval(tick, everyMs);
|
|
276
|
+
watcher.unref();
|
|
277
|
+
setTimeout(tick, 1_000).unref();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function unwatchSession(): void {
|
|
281
|
+
if (watcher !== null) clearInterval(watcher);
|
|
282
|
+
watcher = null;
|
|
283
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from '../agent-user/agent-fs.js';
|
|
2
|
+
import { 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
|
+
import { writeHomeText } from '../agent-user/home-fs.js';
|
|
8
|
+
|
|
9
|
+
export const SETTINGS_MAX = 256 * 1024;
|
|
10
|
+
const USER_ID = 'user';
|
|
11
|
+
const LOCAL_SUFFIX = '.local';
|
|
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
|
+
const sameFile = (a: string, b: string): boolean => {
|
|
72
|
+
try {
|
|
73
|
+
return realpathSync(a) === realpathSync(b);
|
|
74
|
+
} catch {
|
|
75
|
+
return a === b;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function listClaudeSettings(dir = claudeDir()): ClaudeSettingsFile[] {
|
|
80
|
+
const user = entryOf(USER_ID, 'user', 'This machine', join(dir, 'settings.json'));
|
|
81
|
+
return [user, ...projectEntries(dir).filter((p) => !sameFile(p.path, user.path))];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function assertSettingsJson(text: string): void {
|
|
85
|
+
if (text.length > SETTINGS_MAX) throw new ApiError('that is more text than a settings file may hold', 413);
|
|
86
|
+
let parsed: unknown;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(text);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new ApiError(`that is not valid JSON: ${errMsg(err)}`, 400);
|
|
91
|
+
}
|
|
92
|
+
if (!isRecord(parsed)) throw new ApiError('Claude Code settings must be a JSON object', 400);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function writeClaudeSettings(
|
|
96
|
+
id: string,
|
|
97
|
+
text: string,
|
|
98
|
+
seenAt: string | null | undefined,
|
|
99
|
+
dir = claudeDir(),
|
|
100
|
+
): ClaudeSettingsFile {
|
|
101
|
+
const target = listClaudeSettings(dir).find((file) => file.id === id);
|
|
102
|
+
if (target === undefined) throw new ApiError('no such settings file', 404);
|
|
103
|
+
if (!target.editable) throw new ApiError('that settings file is too large to edit here', 409);
|
|
104
|
+
assertSettingsJson(text);
|
|
105
|
+
if (seenAt !== undefined && seenAt !== target.modifiedAt)
|
|
106
|
+
throw new ApiError('that file changed on disk since you opened it; reload it before saving', 409);
|
|
107
|
+
writeHomeText(target.path, text, existsSync(target.path) ? statSync(target.path).mode & 0o777 : 0o644);
|
|
108
|
+
return entryOf(target.id, target.scope, target.label, target.path);
|
|
109
|
+
}
|