@mastra/factory 0.25.0-alpha.4 → 0.25.0-alpha.5
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/auth.js +1 -2
- package/dist/boards/define-board.js +1 -2
- package/dist/boards/index.js +1 -2
- package/dist/boards/registry.js +1 -2
- package/dist/boards/relocate.js +1 -2
- package/dist/boards/review.js +1 -2
- package/dist/boards/semantics.js +1 -2
- package/dist/boards/transition-policy.js +1 -2
- package/dist/boards/work-tool-rules.js +1 -2
- package/dist/boards/work-transition-policy.js +1 -2
- package/dist/boards/work.js +1 -2
- package/dist/environment/build-runner.js +1 -2
- package/dist/environment/build-schedule.js +1 -2
- package/dist/environment/build-workflow.js +1 -2
- package/dist/environment/build.js +1 -2
- package/dist/environment/heads.js +1 -2
- package/dist/factory.d.ts +4 -2
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +43 -16
- package/dist/factory.js.map +1 -1
- package/dist/feed-events.js +1 -2
- package/dist/index.js +2 -2
- package/dist/integrations/channel-thread-announcements.d.ts +8 -0
- package/dist/integrations/channel-thread-announcements.d.ts.map +1 -0
- package/dist/integrations/channel-thread-announcements.js +16 -0
- package/dist/integrations/channel-thread-announcements.js.map +1 -0
- package/dist/integrations/github/acceptance-labels.js +1 -2
- package/dist/integrations/github/app-identity.js +1 -2
- package/dist/integrations/github/commits.js +1 -2
- package/dist/integrations/github/config.js +1 -2
- package/dist/integrations/github/default-rules.js +2 -3
- package/dist/integrations/github/integration.js +3 -4
- package/dist/integrations/github/issue-reconciler.js +1 -2
- package/dist/integrations/github/pat.js +1 -2
- package/dist/integrations/github/provenance.d.ts.map +1 -1
- package/dist/integrations/github/provenance.js +2 -5
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +1 -2
- package/dist/integrations/github/reconciliation-config.js +1 -2
- package/dist/integrations/github/routes.js +4 -5
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +14 -3
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox-release.js +1 -2
- package/dist/integrations/github/sandbox.js +1 -2
- package/dist/integrations/github/session-subscriptions.js +1 -2
- package/dist/integrations/github/settle-or-abort.js +1 -2
- package/dist/integrations/github/stale-reviews.js +1 -2
- package/dist/integrations/github/subscriptions.js +1 -2
- package/dist/integrations/github/token-refresh.js +1 -2
- package/dist/integrations/github/webhook.d.ts.map +1 -1
- package/dist/integrations/github/webhook.js +17 -3
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/gitlab/agent-tools.js +1 -2
- package/dist/integrations/gitlab/api.js +1 -2
- package/dist/integrations/gitlab/default-rules.js +1 -2
- package/dist/integrations/gitlab/integration.js +2 -3
- package/dist/integrations/gitlab/issue-reconciler.js +1 -2
- package/dist/integrations/gitlab/merge-request-reconciler.js +1 -2
- package/dist/integrations/gitlab/reconciler.js +1 -2
- package/dist/integrations/gitlab/reconciliation-config.js +1 -2
- package/dist/integrations/gitlab/routes.js +1 -2
- package/dist/integrations/gitlab/rules.js +1 -2
- package/dist/integrations/gitlab/session-subscriptions.js +1 -2
- package/dist/integrations/gitlab/subscriptions.js +1 -2
- package/dist/integrations/gitlab/version-control.js +1 -2
- package/dist/integrations/gitlab/webhook-dispatch.js +1 -2
- package/dist/integrations/gitlab/webhook.js +1 -2
- package/dist/integrations/incidentio/agent-tools.js +1 -2
- package/dist/integrations/incidentio/api.js +1 -2
- package/dist/integrations/incidentio/claim.js +1 -2
- package/dist/integrations/incidentio/default-rules.js +1 -2
- package/dist/integrations/incidentio/intake.js +1 -2
- package/dist/integrations/incidentio/integration.js +1 -2
- package/dist/integrations/incidentio/issue-reconciler.js +1 -2
- package/dist/integrations/incidentio/reconciliation-config.js +1 -2
- package/dist/integrations/incidentio/routes.js +1 -2
- package/dist/integrations/incidentio/rules.js +1 -2
- package/dist/integrations/issue-reconcile-worker.js +1 -2
- package/dist/integrations/issue-reconciler.js +1 -2
- package/dist/integrations/jira/adf.js +1 -2
- package/dist/integrations/jira/agent-tools.js +1 -2
- package/dist/integrations/jira/api.js +1 -2
- package/dist/integrations/jira/claim.js +1 -2
- package/dist/integrations/jira/default-rules.js +1 -2
- package/dist/integrations/jira/integration.js +1 -2
- package/dist/integrations/jira/issue-reconciler.js +1 -2
- package/dist/integrations/jira/reconciliation-config.js +1 -2
- package/dist/integrations/jira/routes.js +1 -2
- package/dist/integrations/jira/rules.js +1 -2
- package/dist/integrations/linear/agent-tools.d.ts +1 -1
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
- package/dist/integrations/linear/agent-tools.js +5 -5
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/claim.js +1 -2
- package/dist/integrations/linear/default-rules.js +1 -2
- package/dist/integrations/linear/integration.d.ts +2 -0
- package/dist/integrations/linear/integration.d.ts.map +1 -1
- package/dist/integrations/linear/integration.js +2 -2
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/issue-reconciler.js +1 -2
- package/dist/integrations/linear/reconciliation-config.js +1 -2
- package/dist/integrations/linear/routes.js +1 -2
- package/dist/integrations/linear/rules.js +1 -2
- package/dist/integrations/platform/api-client.js +1 -2
- package/dist/integrations/platform/connect/routes.js +1 -2
- package/dist/integrations/platform/github/event-worker.js +6 -2
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +1 -2
- package/dist/integrations/platform/gitlab/event-worker.js +1 -2
- package/dist/integrations/platform/gitlab/integration.js +1 -2
- package/dist/integrations/platform/incidentio/integration.js +1 -2
- package/dist/integrations/platform/jira/agent-tools.js +1 -2
- package/dist/integrations/platform/jira/integration.js +1 -2
- package/dist/integrations/platform/jira/routes.js +1 -2
- package/dist/integrations/platform/linear/event-worker.js +1 -2
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
- package/dist/integrations/platform/linear/integration.js +2 -2
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/pull-request-head.js +1 -2
- package/dist/integrations/reconciliation-config.js +1 -2
- package/dist/integrations/slack/channel-reply-processor.d.ts.map +1 -1
- package/dist/integrations/slack/channel-reply-processor.js +11 -5
- package/dist/integrations/slack/channel-reply-processor.js.map +1 -1
- package/dist/integrations/slack/channel-session-link-processor.js +1 -2
- package/dist/integrations/slack/connect-route.js +1 -2
- package/dist/integrations/slack/emoji-shortcodes.generated.js +1 -2
- package/dist/integrations/slack/emoji.js +1 -2
- package/dist/integrations/slack/feed-publisher.js +1 -2
- package/dist/integrations/slack/integration.d.ts +5 -0
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +13 -2
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/reply-reminder-processor.js +1 -2
- package/dist/integrations/slack/reply-tool.js +1 -2
- package/dist/integrations/slack/share-file-tool.js +1 -2
- package/dist/integrations/slack/slack.js +2 -3
- package/dist/integrations/subscription-session.js +1 -2
- package/dist/integrations/workos/integration.js +1 -2
- package/dist/mcp/disconnect.js +1 -2
- package/dist/mcp/events.js +2 -3
- package/dist/mcp/routes.js +1 -2
- package/dist/packages/_internals/workspace/dist/index.js +1 -2
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/platform-auth-host.js +1 -2
- package/dist/provider-display-name.js +1 -2
- package/dist/review-verdict.d.ts +1 -0
- package/dist/review-verdict.d.ts.map +1 -1
- package/dist/review-verdict.js +9 -3
- package/dist/review-verdict.js.map +1 -1
- package/dist/routes/attention-activity.js +1 -2
- package/dist/routes/attention-parked.js +1 -2
- package/dist/routes/attention-proposed.js +1 -2
- package/dist/routes/attention-providers.js +1 -2
- package/dist/routes/attention.js +1 -2
- package/dist/routes/automation-runs.js +1 -2
- package/dist/routes/config.d.ts +4 -0
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +7 -2
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.js +1 -2
- package/dist/routes/custom-provider-source.js +1 -2
- package/dist/routes/fs.js +1 -2
- package/dist/routes/intake.js +1 -2
- package/dist/routes/knowledge.js +1 -2
- package/dist/routes/oauth.js +1 -2
- package/dist/routes/projects.js +1 -2
- package/dist/routes/provider-credentials.js +1 -2
- package/dist/routes/route.js +1 -2
- package/dist/routes/session-access.js +1 -2
- package/dist/routes/skills.js +1 -2
- package/dist/routes/source-control-sessions.js +5 -6
- package/dist/routes/source-control-settings.js +1 -2
- package/dist/routes/surface.js +2 -3
- package/dist/routes/telemetry.js +1 -2
- package/dist/routes/tenant-credentials.js +1 -2
- package/dist/routes/work-items.js +2 -3
- package/dist/rules/binding-context.js +1 -2
- package/dist/rules/decision-applicability.js +1 -2
- package/dist/rules/dispatch-errors.js +1 -2
- package/dist/rules/dispatcher.js +1 -2
- package/dist/rules/event-rules.js +1 -2
- package/dist/rules/processor.js +1 -2
- package/dist/rules/resolve.js +1 -2
- package/dist/rules/review-source-tool.js +1 -2
- package/dist/rules/start-coordinator.js +1 -2
- package/dist/rules/terminal-cleanup.js +1 -2
- package/dist/rules/tools.js +1 -2
- package/dist/rules/transition-service.js +1 -2
- package/dist/rules/types.d.ts +4 -1
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +1 -2
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.js +1 -2
- package/dist/sandbox/callback-factory-sandbox.js +1 -2
- package/dist/sandbox/git-ref.js +1 -2
- package/dist/sandbox/local-factory-sandbox.d.ts +12 -4
- package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js +32 -7
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/sandbox/materialization.js +1 -2
- package/dist/sandbox/session-retirement.js +1 -2
- package/dist/sandbox/session-sandbox.js +1 -2
- package/dist/sandbox/workdir.js +1 -2
- package/dist/secret-encryption.js +1 -2
- package/dist/server-error.js +1 -2
- package/dist/session/credential-policy.js +1 -2
- package/dist/session/default-model-hydration.js +1 -2
- package/dist/session/environment-repositories.js +1 -2
- package/dist/session/environment-state-processor.d.ts +0 -1
- package/dist/session/environment-state-processor.d.ts.map +1 -1
- package/dist/session/environment-state-processor.js +2 -3
- package/dist/session/environment-state-processor.js.map +1 -1
- package/dist/session/factory-session.js +1 -2
- package/dist/session/filesystem-capture.js +2 -3
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/first-exec-capture.js +2 -3
- package/dist/session/first-exec-capture.js.map +1 -1
- package/dist/session/first-message-capture.js +1 -2
- package/dist/session/live-sessions.js +1 -2
- package/dist/session/memory-settings-hydration.js +1 -2
- package/dist/session/org-seed.js +1 -2
- package/dist/session/reply-credential-processor.js +1 -2
- package/dist/session/resource-migration.js +1 -2
- package/dist/session/run-audit.js +1 -2
- package/dist/session/session-threads.js +1 -2
- package/dist/session/session-title.js +1 -2
- package/dist/session/shell-commands.js +1 -2
- package/dist/session/source-control-tools.js +2 -3
- package/dist/session/thinking-default.js +1 -2
- package/dist/session/thread-session-stamp.js +1 -2
- package/dist/session/thread-title-mirror.js +1 -2
- package/dist/session/thread-title-mirror.js.map +1 -1
- package/dist/session/work-item-repository.js +1 -2
- package/dist/session-owner-signal.js +1 -2
- package/dist/skills/catalog.js +1 -2
- package/dist/skills/service.js +1 -2
- package/dist/spa-static.js +1 -2
- package/dist/state-signing.js +1 -2
- package/dist/storage/domains/audit/actions.js +1 -2
- package/dist/storage/domains/audit/actors.js +1 -2
- package/dist/storage/domains/audit/agent-audit.js +1 -2
- package/dist/storage/domains/audit/base.js +1 -2
- package/dist/storage/domains/audit/domain.js +1 -2
- package/dist/storage/domains/audit/wire.js +1 -2
- package/dist/storage/domains/channel-identity/base.js +1 -2
- package/dist/storage/domains/comments/activity.js +1 -2
- package/dist/storage/domains/comments/actor.js +1 -2
- package/dist/storage/domains/comments/base.js +1 -2
- package/dist/storage/domains/comments/domain.js +1 -2
- package/dist/storage/domains/comments/feed-context.js +1 -2
- package/dist/storage/domains/comments/parse.js +1 -2
- package/dist/storage/domains/comments/routes.js +1 -2
- package/dist/storage/domains/comments/schema.js +1 -2
- package/dist/storage/domains/comments/wire.js +1 -2
- package/dist/storage/domains/credentials/base.d.ts +4 -1
- package/dist/storage/domains/credentials/base.d.ts.map +1 -1
- package/dist/storage/domains/credentials/base.js +40 -44
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +1 -2
- package/dist/storage/domains/filesystem/base.js +1 -2
- package/dist/storage/domains/intake/base.js +1 -2
- package/dist/storage/domains/integrations/base.js +1 -2
- package/dist/storage/domains/memory-settings/base.js +1 -2
- package/dist/storage/domains/model-defaults/base.js +1 -2
- package/dist/storage/domains/projects/base.js +1 -2
- package/dist/storage/domains/queue-health/base.js +1 -2
- package/dist/storage/domains/source-control/base.js +1 -2
- package/dist/storage/domains/source-control/inmemory.js +1 -2
- package/dist/storage/domains/work-items/base.js +1 -2
- package/dist/storage/domains/work-items/board-version.js +1 -2
- package/dist/storage/domains/work-items/metrics.js +1 -2
- package/dist/telemetry-types.js +1 -2
- package/dist/telemetry.js +1 -2
- package/dist/timing.js +1 -2
- package/dist/tools/annotations.js +1 -2
- package/dist/tools/approval-tools.js +1 -2
- package/dist/tools/deliver.js +2 -3
- package/dist/tools/index.js +1 -2
- package/dist/tools/messaging-tools.js +1 -2
- package/dist/tools/permissions.js +1 -2
- package/dist/tools/read-tools.js +1 -2
- package/dist/tools/reports.js +1 -2
- package/dist/tools/run-watch.js +1 -2
- package/dist/tools/sandbox-tools.js +1 -2
- package/dist/tools/scope.js +2 -3
- package/dist/tools/session-tools.js +1 -2
- package/dist/tools/sessions-state-processor.js +1 -2
- package/dist/tools/side-sessions.js +1 -2
- package/dist/tools/work-item-tools.js +1 -2
- package/dist/work-item-branch.js +1 -2
- package/dist/work-item-branch.js.map +1 -1
- package/dist/work-items/create-work-item.js +1 -2
- package/dist/work-items/decision-actions.js +1 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +1 -3
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-rereview/SKILL.md +12 -2
- package/factory-skills/factory-review/SKILL.md +12 -2
- package/factory-skills/factory-triage/SKILL.md +7 -7
- package/package.json +19 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.js","names":[],"sources":["../../../src/integrations/slack/integration.ts"],"sourcesContent":["/**\n * `SlackIntegration` — Slack as a `FactoryIntegration`.\n *\n * Slack contributes two things to a factory: the chat channels that carry\n * inbound messages into agent runs (`channels()`), and the browser-facing\n * account-link routes that bind a Slack sender to a Mastra tenant (`routes()`).\n * Both used to be assembled by hand in the deploy entry, which had to reach\n * into the prepared agent controller by string key, resolve storage domains\n * itself, mint its own state signer, and splice routes onto the factory's\n * assembled server config. Implementing the integration interface instead means\n * the factory does all of that the same way it already does for GitHub and\n * Linear, and the entry's job shrinks to reading Slack's env vars once.\n *\n * Slack ships as a factory built-in alongside GitHub and Linear (it originally\n * lived in `mastracode/web` to keep `@mastra/slack`/`chat` out of this package;\n * the team reversed that so `create-factory` consumers get Slack channels out\n * of the box). The integration interface remains open — third parties still add\n * capabilities by implementing `FactoryIntegration` from outside the package.\n */\n\nimport type { AgentChannels, TypingStatusFn } from '@mastra/core/channels';\nimport type { InputProcessor, OutputProcessor } from '@mastra/core/processors';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\n\nimport type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';\n\nimport type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext, IntegrationTools } from '../base.js';\nimport { FactoryChannelReplyProcessor } from './channel-reply-processor.js';\nimport { FactoryChannelSessionLinkProcessor } from './channel-session-link-processor.js';\nimport { createSlackConnectRoutes } from './connect-route.js';\nimport { SlackFeedPublisher } from './feed-publisher.js';\nimport { FactoryReplyReminderProcessor } from './reply-reminder-processor.js';\nimport { createReplyTool, FactoryReplyStreamProcessor, REPLY_TOOL_NAME } from './reply-tool.js';\nimport { createShareFileTool, isSlackThread } from './share-file-tool.js';\nimport type { ShareFileToolDeps } from './share-file-tool.js';\nimport { createSlackChannelsConfig } from './slack.js';\n\n/**\n * Slack app credentials, read from env ONCE by the deploy entry. `signingSecret`\n * is required because the Slack adapter validates it at construction — an\n * integration constructed without it would throw during `prepare()` rather than\n * reporting itself unconfigured.\n */\nexport interface SlackIntegrationConfig {\n /** Verifies inbound Slack request signatures. Required. */\n signingSecret: string;\n /** Bot token used to post replies and ephemeral cards. */\n botToken?: string;\n /**\n * OAuth client credentials. Required for account linking: \"Sign in with\n * Slack\" (OIDC) is the only flow that writes a link, so without these the\n * settings surface reports `canConnect: false` and no sender can link.\n */\n clientId?: string;\n clientSecret?: string;\n /**\n * HTTPS origin Slack redirects back to. Slack requires HTTPS, so locally this\n * is the tunnel origin rather than the app's own public URL.\n */\n oidcRedirectBaseUrl?: string;\n /** SPA origin the post-connect redirect returns to. */\n uiOrigin?: string;\n /**\n * Overrides for the Slack channel adapter entry.\n *\n * @example\n * ```ts\n * new SlackIntegration({\n * signingSecret,\n * adapterOptions: {\n * streaming: true,\n * toolDisplay: 'grouped',\n * },\n * });\n * ```\n */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\n/** Slack renders the status inline next to the app name; keep it to one short line. */\nconst MAX_STATUS_LENGTH = 80;\n\n/**\n * Turns a tool's `description` arg (\"Listing the repo root\") into a status\n * line that reads after the app name (\"<App> is listing the repo root…\").\n * Only the first line counts: issue/PR creators carry whole bodies there.\n */\nfunction describeToolCall(args: unknown): string | undefined {\n if (!args || typeof args !== 'object') return undefined;\n const { description } = args as { description?: unknown };\n if (typeof description !== 'string') return undefined;\n const line = description\n .split('\\n')\n .map(part => part.replace(/\\s+/g, ' ').trim())\n .find(Boolean);\n if (!line) return undefined;\n const text = line.length > MAX_STATUS_LENGTH ? `${line.slice(0, MAX_STATUS_LENGTH - 1)}…` : line;\n // Drop the sentence-case capital (\"Listing\" → \"listing\") but leave acronyms\n // and identifiers (\"PR lookup\", \"CI status\") alone.\n return `is ${/^[A-Z][a-z]/.test(text) ? text.charAt(0).toLowerCase() + text.slice(1) : text}`;\n}\n\n/**\n * With reply text hidden, streamed text never reaches the thread, so it gets\n * no status: an \"is typing...\" set after the reply tool's post would sit on\n * the thread with nothing coming, and race the clear at the end of the run.\n */\nconst factoryTypingStatus =\n (textDisplay: SlackAdapterChannelConfig['textDisplay']): TypingStatusFn =>\n chunk => {\n switch (chunk.type) {\n case 'reasoning-delta':\n return 'is thinking...';\n case 'text-delta':\n return textDisplay === 'hidden' ? undefined : 'is typing...';\n // The reply tool's text streams into the thread as the model writes it.\n case 'tool-call-input-streaming-start':\n return chunk.payload.toolName === REPLY_TOOL_NAME ? 'is typing...' : undefined;\n case 'tool-call':\n // Still finishing the streamed reply; don't flip to \"is working...\".\n if (chunk.payload.toolName === REPLY_TOOL_NAME) return 'is typing...';\n return describeToolCall(chunk.payload.args) ?? 'is working...';\n default:\n return undefined;\n }\n};\n\n/**\n * Drops keys the caller set to `undefined` so spreading the overrides cannot\n * erase the Factory defaults they sit on top of.\n */\nfunction adapterOverrides(options: SlackAdapterChannelConfig | undefined): SlackAdapterChannelConfig {\n return Object.fromEntries(\n Object.entries(options ?? {}).filter(([, value]) => value !== undefined),\n ) as SlackAdapterChannelConfig;\n}\n\n/**\n * Factory defaults under the caller's overrides. Tool calls stay out of the\n * thread: a Factory session runs hundreds of them, and a row per call (or a\n * card per call) buries the reply. Progress shows through the typing status\n * instead, which names the tool's current step; the session link carries the\n * full trace. `grouped` remains available via `adapterOptions` for callers\n * who want Slack's \"Thinking Steps\" widget.\n */\nfunction factoryAdapterOptions(overrides: SlackAdapterChannelConfig): SlackAdapterChannelConfig {\n const textDisplay = overrides.textDisplay ?? 'hidden';\n return { typingStatus: factoryTypingStatus(textDisplay), toolDisplay: 'hidden', textDisplay, ...overrides };\n}\n\nexport class SlackIntegration implements FactoryIntegration {\n readonly id = 'slack';\n /**\n * The OIDC connect flow round-trips a signed `state` through Slack, so the\n * replica handling the callback must be able to verify a state a different\n * replica signed.\n */\n readonly requiresStableStateSigner = true;\n\n readonly #config: SlackIntegrationConfig;\n readonly #adapterOptions: SlackAdapterChannelConfig;\n /** Bound in `initialize()`; the controller mounts after integrations are built. */\n #channels: ShareFileToolDeps['channels'];\n readonly #deps: ShareFileToolDeps = { channels: () => this.#channels?.() };\n /**\n * With reply text hidden (the Factory default) the agent speaks through\n * `reply_in_thread`, so the prompt, reminder and live streaming for that\n * tool come with it. The setting is fixed for the deployment, so it is\n * decided once here rather than on every run.\n */\n readonly processors: { input: InputProcessor[]; output: OutputProcessor[] };\n /**\n * Whether `channels()` found a source-control owner on the context and wired\n * repo-backed sessions. Set at the channels() attach path, which runs once at\n * boot before diagnostics are served.\n */\n #repoBackedSessions = false;\n\n constructor(config: SlackIntegrationConfig) {\n if (!config.signingSecret) {\n throw new Error(\n \"SlackIntegration: 'signingSecret' is required — Slack cannot verify inbound requests without it.\",\n );\n }\n this.#config = config;\n this.#adapterOptions = factoryAdapterOptions(adapterOverrides(config.adapterOptions));\n const textHidden = this.#textHidden;\n this.processors = {\n input: [\n new FactoryChannelReplyProcessor(),\n ...(textHidden ? [new FactoryReplyReminderProcessor()] : []),\n ],\n output: [\n new FactoryChannelSessionLinkProcessor(),\n ...(textHidden ? [new FactoryReplyStreamProcessor(this.#deps)] : []),\n ],\n };\n }\n\n get #textHidden(): boolean {\n return this.#adapterOptions.textDisplay === 'hidden';\n }\n\n initialize(args: { channels?: () => AgentChannels | null | undefined }): void {\n this.#channels = args.channels;\n }\n\n /**\n * `attach_file`, and `reply_in_thread` when reply text is hidden, on runs\n * whose session is linked to a Slack thread. The tools find the thread to\n * post into when they are called.\n */\n async agentTools({ requestContext }: { requestContext: RequestContext }): Promise<IntegrationTools> {\n if (!(await isSlackThread(requestContext, this.#deps))) return {};\n return {\n ...createShareFileTool(requestContext, this.#deps),\n ...(this.#textHidden ? createReplyTool(requestContext, this.#deps) : {}),\n };\n }\n\n channels(ctx: IntegrationContext): FactoryChannelsConfig {\n // Repo-backed sessions select the source-control provider from the linked\n // repository on each Factory project.\n const sourceControls =\n ctx.storage.sourceControls ?? (ctx.storage.sourceControlOwner ? [ctx.storage.sourceControlOwner] : []);\n this.#repoBackedSessions = sourceControls.length > 0;\n return createSlackChannelsConfig({\n slack: {\n clientId: this.#config.clientId,\n clientSecret: this.#config.clientSecret,\n signingSecret: this.#config.signingSecret,\n botToken: this.#config.botToken,\n },\n accountLinks: ctx.storage.channelIdentity,\n projects: ctx.storage.projects,\n sourceControls,\n modelDefaults: ctx.storage.modelDefaults,\n workItems: ctx.runtime?.workItems,\n feed: ctx.feed,\n adapterOptions: this.#adapterOptions,\n });\n }\n\n feedPublisher(ctx: IntegrationContext): WorkItemFeedPublisher {\n return new SlackFeedPublisher({ controller: ctx.controller });\n }\n\n routes(ctx: IntegrationContext): ApiRoute[] {\n const { clientId, clientSecret, oidcRedirectBaseUrl, uiOrigin } = this.#config;\n return createSlackConnectRoutes({\n auth: ctx.auth,\n accountLinks: ctx.storage.channelIdentity,\n tenantStateSigner: ctx.stateSigner,\n oidc:\n clientId && clientSecret && oidcRedirectBaseUrl\n ? { clientId, clientSecret, redirectBaseUrl: oidcRedirectBaseUrl, uiOrigin }\n : undefined,\n projects: ctx.storage.projects,\n });\n }\n\n diagnostics(): Record<string, unknown> {\n const { clientId, clientSecret, botToken, oidcRedirectBaseUrl } = this.#config;\n return {\n configured: true,\n botTokenConfigured: Boolean(botToken),\n oidcConfigured: Boolean(clientId && clientSecret && oidcRedirectBaseUrl),\n repoBackedSessions: this.#repoBackedSessions,\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"integration.js","names":[],"sources":["../../../src/integrations/slack/integration.ts"],"sourcesContent":["/**\n * `SlackIntegration` — Slack as a `FactoryIntegration`.\n *\n * Slack contributes two things to a factory: the chat channels that carry\n * inbound messages into agent runs (`channels()`), and the browser-facing\n * account-link routes that bind a Slack sender to a Mastra tenant (`routes()`).\n * Both used to be assembled by hand in the deploy entry, which had to reach\n * into the prepared agent controller by string key, resolve storage domains\n * itself, mint its own state signer, and splice routes onto the factory's\n * assembled server config. Implementing the integration interface instead means\n * the factory does all of that the same way it already does for GitHub and\n * Linear, and the entry's job shrinks to reading Slack's env vars once.\n *\n * Slack ships as a factory built-in alongside GitHub and Linear (it originally\n * lived in `mastracode/web` to keep `@mastra/slack`/`chat` out of this package;\n * the team reversed that so `create-factory` consumers get Slack channels out\n * of the box). The integration interface remains open — third parties still add\n * capabilities by implementing `FactoryIntegration` from outside the package.\n */\n\nimport type { AgentChannels, TypingStatusFn } from '@mastra/core/channels';\nimport type { InputProcessor, OutputProcessor } from '@mastra/core/processors';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\n\nimport type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';\n\nimport type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext, IntegrationTools } from '../base.js';\nimport { announcePullRequest } from '../channel-thread-announcements.js';\nimport { parseCreatedPullRequest } from '../github/session-subscriptions.js';\nimport { FactoryChannelReplyProcessor } from './channel-reply-processor.js';\nimport { FactoryChannelSessionLinkProcessor } from './channel-session-link-processor.js';\nimport { createSlackConnectRoutes } from './connect-route.js';\nimport { SlackFeedPublisher } from './feed-publisher.js';\nimport { FactoryReplyReminderProcessor } from './reply-reminder-processor.js';\nimport { createReplyTool, FactoryReplyStreamProcessor, REPLY_TOOL_NAME } from './reply-tool.js';\nimport { createShareFileTool, isSlackThread } from './share-file-tool.js';\nimport type { ShareFileToolDeps } from './share-file-tool.js';\nimport { createSlackChannelsConfig } from './slack.js';\n\n/**\n * Slack app credentials, read from env ONCE by the deploy entry. `signingSecret`\n * is required because the Slack adapter validates it at construction — an\n * integration constructed without it would throw during `prepare()` rather than\n * reporting itself unconfigured.\n */\nexport interface SlackIntegrationConfig {\n /** Verifies inbound Slack request signatures. Required. */\n signingSecret: string;\n /** Bot token used to post replies and ephemeral cards. */\n botToken?: string;\n /**\n * OAuth client credentials. Required for account linking: \"Sign in with\n * Slack\" (OIDC) is the only flow that writes a link, so without these the\n * settings surface reports `canConnect: false` and no sender can link.\n */\n clientId?: string;\n clientSecret?: string;\n /**\n * HTTPS origin Slack redirects back to. Slack requires HTTPS, so locally this\n * is the tunnel origin rather than the app's own public URL.\n */\n oidcRedirectBaseUrl?: string;\n /** SPA origin the post-connect redirect returns to. */\n uiOrigin?: string;\n /**\n * Overrides for the Slack channel adapter entry.\n *\n * @example\n * ```ts\n * new SlackIntegration({\n * signingSecret,\n * adapterOptions: {\n * streaming: true,\n * toolDisplay: 'grouped',\n * },\n * });\n * ```\n */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\n/** Slack renders the status inline next to the app name; keep it to one short line. */\nconst MAX_STATUS_LENGTH = 80;\n\n/**\n * Turns a tool's `description` arg (\"Listing the repo root\") into a status\n * line that reads after the app name (\"<App> is listing the repo root…\").\n * Only the first line counts: issue/PR creators carry whole bodies there.\n */\nfunction describeToolCall(args: unknown): string | undefined {\n if (!args || typeof args !== 'object') return undefined;\n const { description } = args as { description?: unknown };\n if (typeof description !== 'string') return undefined;\n const line = description\n .split('\\n')\n .map(part => part.replace(/\\s+/g, ' ').trim())\n .find(Boolean);\n if (!line) return undefined;\n const text = line.length > MAX_STATUS_LENGTH ? `${line.slice(0, MAX_STATUS_LENGTH - 1)}…` : line;\n // Drop the sentence-case capital (\"Listing\" → \"listing\") but leave acronyms\n // and identifiers (\"PR lookup\", \"CI status\") alone.\n return `is ${/^[A-Z][a-z]/.test(text) ? text.charAt(0).toLowerCase() + text.slice(1) : text}`;\n}\n\n/**\n * With reply text hidden, streamed text never reaches the thread, so it gets\n * no status: an \"is typing...\" set after the reply tool's post would sit on\n * the thread with nothing coming, and race the clear at the end of the run.\n */\nconst factoryTypingStatus =\n (textDisplay: SlackAdapterChannelConfig['textDisplay']): TypingStatusFn =>\n chunk => {\n switch (chunk.type) {\n case 'reasoning-delta':\n return 'is thinking...';\n case 'text-delta':\n return textDisplay === 'hidden' ? undefined : 'is typing...';\n // The reply tool's text streams into the thread as the model writes it.\n case 'tool-call-input-streaming-start':\n return chunk.payload.toolName === REPLY_TOOL_NAME ? 'is typing...' : undefined;\n case 'tool-call':\n // Still finishing the streamed reply; don't flip to \"is working...\".\n if (chunk.payload.toolName === REPLY_TOOL_NAME) return 'is typing...';\n return describeToolCall(chunk.payload.args) ?? 'is working...';\n default:\n return undefined;\n }\n};\n\n/**\n * Drops keys the caller set to `undefined` so spreading the overrides cannot\n * erase the Factory defaults they sit on top of.\n */\nfunction adapterOverrides(options: SlackAdapterChannelConfig | undefined): SlackAdapterChannelConfig {\n return Object.fromEntries(\n Object.entries(options ?? {}).filter(([, value]) => value !== undefined),\n ) as SlackAdapterChannelConfig;\n}\n\n/**\n * Factory defaults under the caller's overrides. Tool calls stay out of the\n * thread: a Factory session runs hundreds of them, and a row per call (or a\n * card per call) buries the reply. Progress shows through the typing status\n * instead, which names the tool's current step; the session link carries the\n * full trace. `grouped` remains available via `adapterOptions` for callers\n * who want Slack's \"Thinking Steps\" widget.\n */\nfunction factoryAdapterOptions(overrides: SlackAdapterChannelConfig): SlackAdapterChannelConfig {\n const textDisplay = overrides.textDisplay ?? 'hidden';\n return { typingStatus: factoryTypingStatus(textDisplay), toolDisplay: 'hidden', textDisplay, ...overrides };\n}\n\nexport class SlackIntegration implements FactoryIntegration {\n readonly id = 'slack';\n /**\n * The OIDC connect flow round-trips a signed `state` through Slack, so the\n * replica handling the callback must be able to verify a state a different\n * replica signed.\n */\n readonly requiresStableStateSigner = true;\n\n readonly #config: SlackIntegrationConfig;\n readonly #adapterOptions: SlackAdapterChannelConfig;\n /** Bound in `initialize()`; the controller mounts after integrations are built. */\n #channels: ShareFileToolDeps['channels'];\n readonly #deps: ShareFileToolDeps = { channels: () => this.#channels?.() };\n /**\n * With reply text hidden (the Factory default) the agent speaks through\n * `reply_in_thread`, so the prompt, reminder and live streaming for that\n * tool come with it. The setting is fixed for the deployment, so it is\n * decided once here rather than on every run.\n */\n readonly processors: { input: InputProcessor[]; output: OutputProcessor[] };\n /**\n * Whether `channels()` found a source-control owner on the context and wired\n * repo-backed sessions. Set at the channels() attach path, which runs once at\n * boot before diagnostics are served.\n */\n #repoBackedSessions = false;\n\n constructor(config: SlackIntegrationConfig) {\n if (!config.signingSecret) {\n throw new Error(\n \"SlackIntegration: 'signingSecret' is required — Slack cannot verify inbound requests without it.\",\n );\n }\n this.#config = config;\n this.#adapterOptions = factoryAdapterOptions(adapterOverrides(config.adapterOptions));\n const textHidden = this.#textHidden;\n this.processors = {\n input: [\n new FactoryChannelReplyProcessor(),\n ...(textHidden ? [new FactoryReplyReminderProcessor()] : []),\n ],\n output: [\n new FactoryChannelSessionLinkProcessor(),\n ...(textHidden ? [new FactoryReplyStreamProcessor(this.#deps)] : []),\n ],\n };\n }\n\n get #textHidden(): boolean {\n return this.#adapterOptions.textDisplay === 'hidden';\n }\n\n initialize(args: { channels?: () => AgentChannels | null | undefined }): void {\n this.#channels = args.channels;\n }\n\n /**\n * `attach_file`, and `reply_in_thread` when reply text is hidden, on runs\n * whose session is linked to a Slack thread. The tools find the thread to\n * post into when they are called.\n */\n async agentTools({ requestContext }: { requestContext: RequestContext }): Promise<IntegrationTools> {\n if (!(await isSlackThread(requestContext, this.#deps))) return {};\n return {\n ...createShareFileTool(requestContext, this.#deps),\n ...(this.#textHidden ? createReplyTool(requestContext, this.#deps) : {}),\n };\n }\n\n /**\n * When a run opens a pull request, post its link into the session's Slack\n * thread.\n */\n async postToolObserver({\n toolContext,\n requestContext,\n }: Parameters<NonNullable<FactoryIntegration['postToolObserver']>>[0]): Promise<void> {\n const url = parseCreatedPullRequest(toolContext);\n const threadId = (requestContext?.get('controller') as { threadId?: string } | undefined)?.threadId;\n if (!url || !requestContext || !threadId || !(await isSlackThread(requestContext, this.#deps))) return;\n // Not awaited: the tool call must not wait on Slack.\n void announcePullRequest(this.#channels?.(), threadId, 'opened', url).catch(error =>\n console.warn('[slack] pull request opened announcement failed', error),\n );\n }\n\n channels(ctx: IntegrationContext): FactoryChannelsConfig {\n // Repo-backed sessions select the source-control provider from the linked\n // repository on each Factory project.\n const sourceControls =\n ctx.storage.sourceControls ?? (ctx.storage.sourceControlOwner ? [ctx.storage.sourceControlOwner] : []);\n this.#repoBackedSessions = sourceControls.length > 0;\n return createSlackChannelsConfig({\n slack: {\n clientId: this.#config.clientId,\n clientSecret: this.#config.clientSecret,\n signingSecret: this.#config.signingSecret,\n botToken: this.#config.botToken,\n },\n accountLinks: ctx.storage.channelIdentity,\n projects: ctx.storage.projects,\n sourceControls,\n modelDefaults: ctx.storage.modelDefaults,\n workItems: ctx.runtime?.workItems,\n feed: ctx.feed,\n adapterOptions: this.#adapterOptions,\n });\n }\n\n feedPublisher(ctx: IntegrationContext): WorkItemFeedPublisher {\n return new SlackFeedPublisher({ controller: ctx.controller });\n }\n\n routes(ctx: IntegrationContext): ApiRoute[] {\n const { clientId, clientSecret, oidcRedirectBaseUrl, uiOrigin } = this.#config;\n return createSlackConnectRoutes({\n auth: ctx.auth,\n accountLinks: ctx.storage.channelIdentity,\n tenantStateSigner: ctx.stateSigner,\n oidc:\n clientId && clientSecret && oidcRedirectBaseUrl\n ? { clientId, clientSecret, redirectBaseUrl: oidcRedirectBaseUrl, uiOrigin }\n : undefined,\n projects: ctx.storage.projects,\n });\n }\n\n diagnostics(): Record<string, unknown> {\n const { clientId, clientSecret, botToken, oidcRedirectBaseUrl } = this.#config;\n return {\n configured: true,\n botTokenConfigured: Boolean(botToken),\n oidcConfigured: Boolean(clientId && clientSecret && oidcRedirectBaseUrl),\n repoBackedSessions: this.#repoBackedSessions,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { REPLY_TOOL_NAME } from "./reply-tool.js";
|
|
2
|
-
|
|
2
|
+
// src/integrations/slack/reply-reminder-processor.ts
|
|
3
3
|
const ID = "factory-reply-reminder";
|
|
4
4
|
/** How long a run may work without posting before it is nudged. */
|
|
5
5
|
const REPLY_REMINDER_AFTER_MS = 3e5;
|
|
@@ -52,7 +52,6 @@ function replyCalls(messages) {
|
|
|
52
52
|
}
|
|
53
53
|
return ids;
|
|
54
54
|
}
|
|
55
|
-
//#endregion
|
|
56
55
|
export { FactoryReplyReminderProcessor, REPLY_REMINDER_AFTER_MS };
|
|
57
56
|
|
|
58
57
|
//# sourceMappingURL=reply-reminder-processor.js.map
|
|
@@ -5,7 +5,7 @@ import { postTarget, resolveRenderContext } from "./share-file-tool.js";
|
|
|
5
5
|
import "@mastra/core/channels";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { createTool } from "@mastra/core/tools";
|
|
8
|
-
|
|
8
|
+
// src/integrations/slack/reply-tool.ts
|
|
9
9
|
const REPLY_TOOL_NAME = "reply_in_thread";
|
|
10
10
|
/** Set once the first reply of a new session carried the session link. */
|
|
11
11
|
const LINKED_KEY = "factory__replyLinked";
|
|
@@ -197,7 +197,6 @@ function partialStringField(json, field) {
|
|
|
197
197
|
}
|
|
198
198
|
return out;
|
|
199
199
|
}
|
|
200
|
-
//#endregion
|
|
201
200
|
export { FactoryReplyStreamProcessor, REPLY_TOOL_NAME, createReplyTool, partialStringField };
|
|
202
201
|
|
|
203
202
|
//# sourceMappingURL=reply-tool.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CHAT_CHANNEL_RENDER_CONTEXT_KEY } from "@mastra/core/channels";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { createTool } from "@mastra/core/tools";
|
|
4
|
-
|
|
4
|
+
// src/integrations/slack/share-file-tool.ts
|
|
5
5
|
/** Slack rejects larger uploads well before this; keep the sandbox read bounded too. */
|
|
6
6
|
const SHARE_FILE_MAX_BYTES = 52428800;
|
|
7
7
|
/**
|
|
@@ -100,7 +100,6 @@ function createShareFileTool(requestContext, deps = {}) {
|
|
|
100
100
|
}
|
|
101
101
|
}) };
|
|
102
102
|
}
|
|
103
|
-
//#endregion
|
|
104
103
|
export { SHARE_FILE_MAX_BYTES, createShareFileTool, isSlackThread, postTarget, resolveRenderContext };
|
|
105
104
|
|
|
106
105
|
//# sourceMappingURL=share-file-tool.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { actorFromChannelAuthor } from "../../storage/domains/comments/actor.js";
|
|
2
2
|
import { FACTORY_INTERACTIVE_KEY } from "../../auth.js";
|
|
3
|
-
import { isRunBound, memoryResourceOf, sessionStampCounts } from "../../session/session-threads.js";
|
|
4
3
|
import { primeTenantCredentialsForRequestContext, primeTenantCredentialsForTenant } from "../../routes/tenant-credentials.js";
|
|
5
4
|
import { readRequestContextOrgId, seedSessionOrg } from "../../session/org-seed.js";
|
|
6
5
|
import { prepareSessionRunContext } from "../subscription-session.js";
|
|
7
6
|
import { FactorySourceControlConflictError, createSourceControlSessionLookup, hydrateFactorySession, resolveFactorySessionModel, resolveFactorySourceControl, resolvePrimaryEnvironmentRepository } from "../../session/factory-session.js";
|
|
7
|
+
import { isRunBound, memoryResourceOf, sessionStampCounts } from "../../session/session-threads.js";
|
|
8
8
|
import { FACTORY_SLACK_SENDER_KEY } from "../../tools/scope.js";
|
|
9
9
|
import { markNewChannelSession } from "./channel-session-link-processor.js";
|
|
10
10
|
import { resolveEmojiShortcodes } from "./emoji.js";
|
|
@@ -13,7 +13,7 @@ import { ChannelSessionRejectedError } from "@mastra/core/channels";
|
|
|
13
13
|
import { migratePersistedModelSelection } from "@mastra/core/agent-controller";
|
|
14
14
|
import { createSlackAdapter } from "@mastra/slack";
|
|
15
15
|
import { Actions, Card, CardText, LinkButton } from "chat";
|
|
16
|
-
|
|
16
|
+
// src/integrations/slack/slack.ts
|
|
17
17
|
const SLACK_REQUEST_TIMEOUT_MS = 15e3;
|
|
18
18
|
var SlackSessionStartError = class extends Error {};
|
|
19
19
|
function configuredSourceControls(deps) {
|
|
@@ -771,7 +771,6 @@ function createSlackChannelsConfig(deps) {
|
|
|
771
771
|
onSessionStart: createChannelSessionStartHook(deps)
|
|
772
772
|
};
|
|
773
773
|
}
|
|
774
|
-
//#endregion
|
|
775
774
|
export { createChannelResourceIdResolver, createChannelSessionResolver, createChannelSessionStartHook, createChannelThreadIdResolver, createHandlers, createSlackChannelsConfig, resolveFactoryForLink, resolveLinkedSender, slackThreadSource };
|
|
776
775
|
|
|
777
776
|
//# sourceMappingURL=slack.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { primeTenantCredentialsForRequestContext } from "../routes/tenant-credentials.js";
|
|
2
2
|
import { hasResolvedOrg, seedSessionOrg } from "../session/org-seed.js";
|
|
3
3
|
import { RequestContext } from "@mastra/core/request-context";
|
|
4
|
-
|
|
4
|
+
// src/integrations/subscription-session.ts
|
|
5
5
|
/**
|
|
6
6
|
* The request context a provider-triggered run executes under: the user who
|
|
7
7
|
* subscribed the thread, in the subscription's organization. A webhook carries
|
|
@@ -112,7 +112,6 @@ async function resolveSubscriptionSession(controller, subscription, options) {
|
|
|
112
112
|
if (session.thread.getId() !== threadId) throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);
|
|
113
113
|
return session;
|
|
114
114
|
}
|
|
115
|
-
//#endregion
|
|
116
115
|
export { createSessionAsRowOwner, prepareSessionRunContext, resolveSubscriptionSession, subscriptionRunContext };
|
|
117
116
|
|
|
118
117
|
//# sourceMappingURL=subscription-session.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WorkOSAdminPortal } from "@mastra/auth-workos";
|
|
2
2
|
import { registerApiRoute } from "@mastra/core/server";
|
|
3
|
-
|
|
3
|
+
// src/integrations/workos/integration.ts
|
|
4
4
|
const UNKNOWN_LOCATION = "unknown";
|
|
5
5
|
function loose(c) {
|
|
6
6
|
return c;
|
|
@@ -85,7 +85,6 @@ var WorkOSAuditIntegration = class {
|
|
|
85
85
|
return { configured: true };
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
|
-
//#endregion
|
|
89
88
|
export { WorkOSAuditIntegration, toWorkOSEvent };
|
|
90
89
|
|
|
91
90
|
//# sourceMappingURL=integration.js.map
|
package/dist/mcp/disconnect.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/mcp/disconnect.ts
|
|
2
2
|
/**
|
|
3
3
|
* Emits `close` on the `toReqRes` response when the HTTP client cancels the
|
|
4
4
|
* body, so the MCP transport aborts instead of writing into a closed stream.
|
|
@@ -38,7 +38,6 @@ function propagateClientDisconnect(fetchResponse, res) {
|
|
|
38
38
|
headers: fetchResponse.headers
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
-
//#endregion
|
|
42
41
|
export { propagateClientDisconnect };
|
|
43
42
|
|
|
44
43
|
//# sourceMappingURL=disconnect.js.map
|
package/dist/mcp/events.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { canViewSourceControlSession } from "../routes/session-access.js";
|
|
2
1
|
import { ACTOR_PROFILE_METADATA_KEY, auditCursorAt, decodeAuditAfterCursor, encodeAuditCursor } from "../storage/domains/audit/base.js";
|
|
2
|
+
import { canViewSourceControlSession } from "../routes/session-access.js";
|
|
3
3
|
import { parseAuditAction } from "../storage/domains/audit/actions.js";
|
|
4
4
|
import { AUDIT_ACTOR_TYPES } from "../storage/domains/audit/actors.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { defineEvent } from "@mastra/mcp";
|
|
7
|
-
|
|
7
|
+
// src/mcp/events.ts
|
|
8
8
|
const activityArgs = z.object({
|
|
9
9
|
workItemId: z.string().min(1).optional().describe("Only activity on this work item."),
|
|
10
10
|
actions: z.array(z.string().min(1)).max(50).optional().describe("Only these actions, e.g. `factory.work_item.stage_moved`.")
|
|
@@ -152,7 +152,6 @@ function factoryActivityEvent(deps) {
|
|
|
152
152
|
}
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
-
//#endregion
|
|
156
155
|
export { ACTIVITY_SETTLE_MS, factoryActivityEvent };
|
|
157
156
|
|
|
158
157
|
//# sourceMappingURL=events.js.map
|
package/dist/mcp/routes.js
CHANGED
|
@@ -12,7 +12,7 @@ import { z } from "zod";
|
|
|
12
12
|
import { createTool } from "@mastra/core/tools";
|
|
13
13
|
import { MCPServer } from "@mastra/mcp";
|
|
14
14
|
import { toFetchResponse, toReqRes } from "fetch-to-node";
|
|
15
|
-
|
|
15
|
+
// src/mcp/routes.ts
|
|
16
16
|
const FACTORY_MCP_PATH = "/web/factory/projects/:projectId/mcp";
|
|
17
17
|
const PROTECTED_RESOURCE_PREFIX = "/.well-known/oauth-protected-resource";
|
|
18
18
|
/**
|
|
@@ -264,7 +264,6 @@ function buildFactoryMcpRoutes(deps) {
|
|
|
264
264
|
}
|
|
265
265
|
})];
|
|
266
266
|
}
|
|
267
|
-
//#endregion
|
|
268
267
|
export { FACTORY_MCP_PATH, PLATFORM_MCP_AUTHORIZATION_SERVER, buildFactoryMcpRoutes, connectorSessionId, createFactoryMcpTools, resolveMcpAuthorizationServers };
|
|
269
268
|
|
|
270
269
|
//# sourceMappingURL=routes.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "@mastra/core/workspace";
|
|
2
2
|
import "path";
|
|
3
3
|
import { createHash } from "crypto";
|
|
4
|
-
|
|
4
|
+
// ../../packages/_internals/workspace/dist/index.js
|
|
5
5
|
/** Directory, relative to the template's build cwd, that holds every marker below. */
|
|
6
6
|
const SETUP_MARKER_DIR = ".mastra-sandbox";
|
|
7
7
|
/**
|
|
@@ -59,7 +59,6 @@ const FILE_COMMAND_EXIT = {
|
|
|
59
59
|
destIsDirectory: 24
|
|
60
60
|
};
|
|
61
61
|
const { notFound, isDirectory, exists, notDirectory, destIsDirectory } = FILE_COMMAND_EXIT;
|
|
62
|
-
//#endregion
|
|
63
62
|
export { FILE_COMMAND_EXIT, SETUP_FAILED_MARKER_PATH, SETUP_MARKER_DIR, SETUP_MARKER_PATH, WORKSPACE_SETUP_MARKER_PATH, normalizeSetupCommands, repoSetupMarkerPath, setupMarkerContent };
|
|
64
63
|
|
|
65
64
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../../packages/_internals/workspace/dist/index.js"],"sourcesContent":["import { createHash } from \"crypto\";\nimport { posix } from \"path\";\nimport { Buffer } from \"buffer\";\nimport { DirectoryNotEmptyError, DirectoryNotFoundError, FileExistsError, FileNotFoundError, IsDirectoryError, MastraFilesystem, NotDirectoryError, ProcessHandle, SandboxProcessManager, UnsupportedStdinCloseError } from \"@mastra/core/workspace\";\n//#region src/setup-marker.ts\n/** Directory, relative to the template's build cwd, that holds every marker below. */\nconst SETUP_MARKER_DIR = \".mastra-sandbox\";\n/**\n* Setup completion marker shared by repo templates and their consumers.\n*\n* A repo template writes this file beside the checkout as its last build\n* step, so it exists only in images where every setup command succeeded. Its\n* content is a digest of the setup commands the image ran, letting a sandbox\n* booted from the image tell whether the setup it is about to run already\n* happened. Relative to the template's build cwd, which is also the runtime\n* working directory the repo was cloned into.\n*\n* This is the single-repository marker. Templates built from a repository\n* list write one marker per repository instead ({@link repoSetupMarkerPath}).\n*/\nconst SETUP_MARKER_PATH = \".mastra-sandbox/setup\";\n/**\n* Marker for the workspace-level setup of a multi-repository template: the\n* commands that ran at the working directory after every repository was set\n* up. Written even when there were no such commands, so a consumer can tell\n* \"ran nothing\" from \"never built\".\n*/\nconst WORKSPACE_SETUP_MARKER_PATH = \".mastra-sandbox/workspace-setup\";\n/**\n* One line per repository directory whose setup failed while the build was\n* told to continue. Absent when nothing failed. Consumers check this before\n* trusting a per-repository marker: that marker means every step ran, not\n* that every step succeeded.\n*/\nconst SETUP_FAILED_MARKER_PATH = \".mastra-sandbox/setup-failed\";\n/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */\nfunction repoSetupMarkerPath(repoDir) {\n\treturn `${SETUP_MARKER_DIR}/repos/${repoDir}`;\n}\n/** Blank entries never become build steps, so they never count toward the digest either. */\nfunction normalizeSetupCommands(setupCommand) {\n\treturn (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== \"\");\n}\n/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */\nfunction setupMarkerContent(setupCommand) {\n\treturn `sha256:${createHash(\"sha256\").update(normalizeSetupCommands(setupCommand).join(\"\\n\")).digest(\"hex\")}`;\n}\n/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */\nfunction setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {\n\treturn `mkdir -p \"$(dirname \"${markerPath}\")\" && printf '%s' '${content}' > \"${markerPath}\"`;\n}\n/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */\nconst REPO_DIR_PATTERN = /^[\\w.-]+$/;\n/**\n* Shell step that runs one setup command inside a repository clone.\n*\n* Without the guard this is the plain `cd \"<dir>\" && <cmd>` step. With it,\n* the `cd` sits inside the parenthesized group so that the failure branch\n* runs back at the build cwd and the failure list lands at the workspace\n* level, never inside the repository.\n*/\nfunction guardedSetupCommand({ repoDir, command, continueOnFailure }) {\n\tif (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory \"${repoDir}\" must match ${REPO_DIR_PATTERN}.`);\n\tif (!continueOnFailure) return `cd \"${repoDir}\" && ${command}`;\n\treturn `( cd \"${repoDir}\" && sh -c ${shellQuote$2(command)} ) || { mkdir -p \"${SETUP_MARKER_DIR}\" && grep -qxF -- '${repoDir}' \"${SETUP_FAILED_MARKER_PATH}\" 2>/dev/null || printf '%s\\\\n' '${repoDir}' >> \"${SETUP_FAILED_MARKER_PATH}\"; }`;\n}\nfunction shellQuote$2(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\n//#region src/repo-clone.ts\nfunction repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {\n\treturn `git ${tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : \"\"}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote$1(branch)} ` : \"\"}${shellQuote$1(cloneUrl)} ${shellQuote$1(destination)}`;\n}\n/** Per-invocation auth header; `-c` config never reaches `.git/config`. */\nfunction gitAuthFlag(tokenEnv) {\n\treturn `-c http.extraheader=\"AUTHORIZATION: basic $(printf 'x-access-token:%s' \"$${tokenEnv}\" | base64 -w0)\"`;\n}\nfunction shellQuote$1(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\n//#region src/file-commands.ts\n/**\n* Shell scripts for the file operations a sandbox can only do atomically (or\n* without moving file bytes over the network) by running a command inside the\n* VM. Each builder takes absolute paths and returns one POSIX `sh` script; the\n* caller runs it with `sh -c` and maps the exit codes below to typed errors.\n*/\n/** Exit codes the scripts use for outcomes the caller turns into typed errors. Any other non-zero code is a plain failure. */\nconst FILE_COMMAND_EXIT = {\n\tnotFound: 20,\n\tisDirectory: 21,\n\texists: 22,\n\tnotDirectory: 23,\n\tdestIsDirectory: 24\n};\n/** Single-quote a string for safe POSIX shell interpolation. */\nfunction shellQuote(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\nconst { notFound, isDirectory, exists, notDirectory, destIsDirectory } = FILE_COMMAND_EXIT;\n/** `[ -e ]` is false for a broken symlink, which still occupies the path. */\nconst present = (variable) => `{ [ -e \"$${variable}\" ] || [ -L \"$${variable}\" ]; }`;\n/**\n* A sibling temp path for `path`, so a later `ln` / `mv` onto `path` stays on\n* one filesystem. Hidden, and unique per call.\n*/\nfunction siblingTempPath(path) {\n\tconst suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;\n\treturn posix.join(posix.dirname(path), `.${posix.basename(path)}.mastra-tmp-${suffix}`);\n}\n/**\n* Hard-link `$tmp` to `$dest` and delete `$tmp`, failing with `exists` when\n* something is at `$dest`. `ln` onto a directory (or a link to one) would\n* create the link inside it and succeed, so that case is undone and reported\n* as taken too.\n*/\nfunction linkTempIntoPlace() {\n\treturn [\n\t\t`ln \"$tmp\" \"$dest\" 2>/dev/null || { rm -f \"$tmp\"; ${present(\"dest\")} && exit ${exists}; exit 1; }`,\n\t\t`if [ -d \"$dest\" ]; then rm -f \"$dest/$(basename \"$tmp\")\" \"$tmp\"; exit ${exists}; fi`,\n\t\t`rm -f \"$tmp\"`\n\t];\n}\n/**\n* Copy `src` to `dest` inside the sandbox, creating `dest`'s parent.\n*\n* A file source copies its content (a symlinked source is followed). A\n* directory source with `recursive` copies its contents into `dest`, keeping\n* symlinks inside the tree as links. With `overwrite: false` the destination\n* is claimed atomically: a directory with an exclusive `mkdir`, a file by\n* copying to a temp name and hard-linking it into place (`link(2)` fails when\n* the destination exists).\n*\n* Exits: `notFound` (no source), `isDirectory` (directory source without\n* `recursive`), `destIsDirectory` (a file onto an existing directory or a link\n* to one), `exists`.\n*/\nfunction copyCommand(src, dest, options = {}) {\n\tconst noClobber = options.overwrite === false;\n\treturn [\n\t\t`src=${shellQuote(src)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`[ -e \"$src\" ] || exit ${notFound}`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" || exit 1`,\n\t\t`if [ -d \"$src\" ]; then`,\n\t\toptions.recursive ? ` :` : ` exit ${isDirectory}`,\n\t\tnoClobber ? ` mkdir \"$dest\" 2>/dev/null || exit ${exists}` : ` mkdir -p \"$dest\" || exit 1`,\n\t\t` cp -R -P \"$src\"/. \"$dest\"/ || exit 1`,\n\t\t` exit 0`,\n\t\t`fi`,\n\t\tnoClobber ? `${present(\"dest\")} && exit ${exists}` : `if [ -d \"$dest\" ]; then exit ${destIsDirectory}; fi`,\n\t\t...noClobber ? [\n\t\t\t`tmp=${shellQuote(siblingTempPath(dest))}`,\n\t\t\t`cp \"$src\" \"$tmp\" || { rm -f \"$tmp\"; exit 1; }`,\n\t\t\t...linkTempIntoPlace()\n\t\t] : [`cp \"$src\" \"$dest\" || exit 1`]\n\t].join(\"\\n\");\n}\n/**\n* Move `src` to `dest` without replacing anything at `dest`, creating `dest`'s\n* parent. A skipped `mv -n` is detected by the source surviving, whatever\n* its exit code. If a directory appears at `dest` between the check and the\n* move, `mv` moves `src` into it (BSD `mv` has no `-T`), and that is reported\n* as success; the no-clobber rename itself is atomic.\n*\n* Exits: `notFound` (no source), `exists`.\n*/\nfunction moveNoClobberCommand(src, dest) {\n\treturn [\n\t\t`src=${shellQuote(src)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`${present(\"src\")} || exit ${notFound}`,\n\t\t`${present(\"dest\")} && exit ${exists}`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" || exit 1`,\n\t\t`mv -n \"$src\" \"$dest\" 2>/dev/null || { ${present(\"src\")} && ${present(\"dest\")} && exit ${exists}; exit 1; }`,\n\t\t`${present(\"src\")} && exit ${exists}`,\n\t\t`exit 0`\n\t].join(\"\\n\");\n}\n/**\n* Append the bytes already written to `tmp` onto `dest`, then delete `tmp`.\n* The content travels as a file rather than a command argument, so it has no\n* size limit. Creates `dest` (and its parent) when missing.\n*\n* Exits: `isDirectory` (dest is a directory).\n*/\nfunction appendFromCommand(tmp, dest) {\n\treturn [\n\t\t`tmp=${shellQuote(tmp)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`if [ -d \"$dest\" ]; then rm -f \"$tmp\"; exit ${isDirectory}; fi`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" && cat \"$tmp\" >> \"$dest\"`,\n\t\t`status=$?`,\n\t\t`rm -f \"$tmp\"`,\n\t\t`exit $status`\n\t].join(\"\\n\");\n}\n/**\n* Publish the file already written to `tmp` at `dest` only if nothing is\n* there, then delete `tmp`. `ln` (`link(2)`) is the atomic exclusivity check;\n* `tmp` must be on the same filesystem, e.g. from {@link siblingTempPath}.\n*\n* Exits: `exists`, `notDirectory` (dest's parent is not a directory).\n*/\nfunction linkNoClobberCommand(tmp, dest) {\n\treturn [\n\t\t`tmp=${shellQuote(tmp)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`if [ ! -d \"$(dirname \"$dest\")\" ]; then rm -f \"$tmp\"; exit ${notDirectory}; fi`,\n\t\t`if ${present(\"dest\")}; then rm -f \"$tmp\"; exit ${exists}; fi`,\n\t\t...linkTempIntoPlace()\n\t].join(\"\\n\");\n}\n//#endregion\n//#region src/files-api-filesystem.ts\n/** Default recursion depth for `readdir({ recursive: true })`, matching `LocalFilesystem`. */\nconst DEFAULT_MAX_DEPTH = 100;\nconst toAbsolute = (path) => posix.resolve(\"/\", path);\n/** The entry's own type: a symlink is a link even when it points at a directory. */\nconst isSymlink = (info) => info.symlinkTarget !== void 0;\nfunction toWriteData(content) {\n\tif (typeof content === \"string\") return content;\n\treturn content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);\n}\nfunction matchesExtension(name, extension) {\n\tif (!extension) return true;\n\tconst ext = posix.extname(name);\n\treturn (Array.isArray(extension) ? extension : [extension]).some((e) => e === ext || e === ext.slice(1));\n}\n/**\n* A sandbox's disk, read and written through E2B's envd files API, with the\n* operations the files API can't do in one call (copy, append, no-clobber\n* writes and moves) run as one shell command inside the VM so they are atomic\n* and file bytes never round-trip through the host. Paths are absolute inside\n* the sandbox; this class does not confine paths or symlinks to any root.\n*\n* Providers supply how to reach the files API and the shell, and how to read\n* their SDK's errors.\n*\n* If a provider's `runCommand` re-runs a command whose first run already\n* finished (e.g. a retry after a dropped connection), the second run sees the\n* first's result: a no-clobber copy, write or move reports `FileExistsError`\n* or `FileNotFoundError`, and an append fails, even though the work was done.\n*/\nvar FilesApiFilesystem = class extends MastraFilesystem {\n\tasync readFile(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst bytes = await this.runFiles(\"readFile\", async (files) => {\n\t\t\ttry {\n\t\t\t\treturn await files.read(abs, { format: \"bytes\" });\n\t\t\t} catch (error) {\n\t\t\t\tconst kind = this.errorKind(error);\n\t\t\t\tif (kind === \"not-found\") throw new FileNotFoundError(path);\n\t\t\t\tif (kind === \"invalid-argument\") throw new IsDirectoryError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t\tconst buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\t\treturn options?.encoding ? buffer.toString(options.encoding) : buffer;\n\t}\n\tasync writeFile(path, content, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tif (options?.overwrite !== false) {\n\t\t\tawait this.runFiles(\"writeFile\", async (files) => {\n\t\t\t\tif (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\t\tawait this.#write(files, abs, path, toWriteData(content));\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst tmp = siblingTempPath(abs);\n\t\tconst upload = async (files) => {\n\t\t\tif (options.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\tawait this.#write(files, tmp, path, toWriteData(content));\n\t\t};\n\t\tawait this.#publishTemp(tmp, \"writeFile\", upload, linkNoClobberCommand(tmp, abs), path, {\n\t\t\texists: () => new FileExistsError(path),\n\t\t\tnotDirectory: () => new NotDirectoryError(posix.dirname(path))\n\t\t});\n\t}\n\tasync appendFile(path, content) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst tmp = siblingTempPath(abs);\n\t\tconst upload = (files) => this.#write(files, tmp, path, toWriteData(content));\n\t\tawait this.#publishTemp(tmp, \"appendFile\", upload, appendFromCommand(tmp, abs), path, { isDirectory: () => new IsDirectoryError(path) });\n\t}\n\tasync deleteFile(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"deleteFile\", async (files) => {\n\t\t\tconst info = await this.#info(files, abs);\n\t\t\tif (!info) {\n\t\t\t\tif (options?.force) return;\n\t\t\t\tthrow new FileNotFoundError(path);\n\t\t\t}\n\t\t\tif (!isSymlink(info) && info.type === \"dir\") throw new IsDirectoryError(path);\n\t\t\tawait files.remove(abs);\n\t\t});\n\t}\n\t/**\n\t* One `cp` inside the VM. A recursive copy keeps symlinks inside the tree as\n\t* links; `overwrite: false` claims the destination atomically.\n\t*/\n\tasync copyFile(src, dest, options) {\n\t\tconst command = copyCommand(toAbsolute(src), toAbsolute(dest), {\n\t\t\trecursive: options?.recursive,\n\t\t\toverwrite: options?.overwrite\n\t\t});\n\t\tawait this.#command(\"copyFile\", command, `${src} -> ${dest}`, {\n\t\t\tnotFound: () => new FileNotFoundError(src),\n\t\t\tisDirectory: () => new IsDirectoryError(src),\n\t\t\tdestIsDirectory: () => new IsDirectoryError(dest),\n\t\t\texists: () => new FileExistsError(dest)\n\t\t});\n\t}\n\tasync moveFile(src, dest, options) {\n\t\tconst srcAbs = toAbsolute(src);\n\t\tconst destAbs = toAbsolute(dest);\n\t\tif (options?.overwrite === false) {\n\t\t\tawait this.#command(\"moveFile\", moveNoClobberCommand(srcAbs, destAbs), `${src} -> ${dest}`, {\n\t\t\t\tnotFound: () => new FileNotFoundError(src),\n\t\t\t\texists: () => new FileExistsError(dest)\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tawait this.runFiles(\"moveFile\", async (files) => {\n\t\t\ttry {\n\t\t\t\tawait files.rename(srcAbs, destAbs);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorKind(error) === \"not-found\") throw new FileNotFoundError(src);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\tasync mkdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"mkdir\", async (files) => {\n\t\t\tif (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\ttry {\n\t\t\t\tawait files.makeDir(abs);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorKind(error) === \"invalid-argument\") throw new FileExistsError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\tasync rmdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"rmdir\", async (files) => {\n\t\t\tconst info = await this.#info(files, abs);\n\t\t\tif (!info) {\n\t\t\t\tif (options?.force) return;\n\t\t\t\tthrow new DirectoryNotFoundError(path);\n\t\t\t}\n\t\t\tif (!options?.recursive) {\n\t\t\t\tif (isSymlink(info) || info.type !== \"dir\") throw new NotDirectoryError(path);\n\t\t\t\tif ((await files.list(abs)).length > 0) throw new DirectoryNotEmptyError(path);\n\t\t\t} else if (!isSymlink(info) && info.type !== \"dir\") throw new NotDirectoryError(path);\n\t\t\tawait files.remove(abs);\n\t\t});\n\t}\n\tasync readdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst depth = options?.recursive ? (options.maxDepth ?? DEFAULT_MAX_DEPTH) + 1 : 1;\n\t\tconst entries = await this.runFiles(\"readdir\", async (files) => {\n\t\t\ttry {\n\t\t\t\treturn await files.list(abs, { depth });\n\t\t\t} catch (error) {\n\t\t\t\tconst kind = this.errorKind(error);\n\t\t\t\tif (kind === \"not-found\") throw new DirectoryNotFoundError(path);\n\t\t\t\tif (kind === \"invalid-argument\") throw new NotDirectoryError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t\tconst result = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.type) continue;\n\t\t\tconst name = posix.relative(abs, entry.path);\n\t\t\tconst type = entry.type === \"dir\" ? \"directory\" : \"file\";\n\t\t\tif (type === \"file\" && !matchesExtension(name, options?.extension)) continue;\n\t\t\tconst link = isSymlink(entry);\n\t\t\tresult.push({\n\t\t\t\tname,\n\t\t\t\ttype,\n\t\t\t\t...type === \"file\" && !link && { size: entry.size },\n\t\t\t\t...link && {\n\t\t\t\t\tisSymlink: true,\n\t\t\t\t\tsymlinkTarget: entry.symlinkTarget\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t}\n\tasync exists(path) {\n\t\tconst abs = toAbsolute(path);\n\t\treturn this.runFiles(\"exists\", async (files) => Boolean((await this.#info(files, abs))?.type));\n\t}\n\tasync stat(path) {\n\t\tconst abs = toAbsolute(path);\n\t\treturn this.runFiles(\"stat\", async (files) => {\n\t\t\tlet info = await this.#info(files, abs);\n\t\t\tif (info?.type && isSymlink(info)) info = await this.#info(files, posix.resolve(posix.dirname(abs), info.symlinkTarget));\n\t\t\tif (!info?.type) throw new FileNotFoundError(path);\n\t\t\tconst modifiedAt = info.modifiedTime ?? /* @__PURE__ */ new Date(0);\n\t\t\treturn {\n\t\t\t\tname: posix.basename(abs),\n\t\t\t\tpath: abs,\n\t\t\t\ttype: info.type === \"dir\" ? \"directory\" : \"file\",\n\t\t\t\tsize: info.type === \"dir\" ? 0 : info.size,\n\t\t\t\tcreatedAt: modifiedAt,\n\t\t\t\tmodifiedAt\n\t\t\t};\n\t\t});\n\t}\n\t/** `getInfo` without following the entry itself, or `undefined` when nothing is there. */\n\tasync #info(files, abs) {\n\t\ttry {\n\t\t\treturn await files.getInfo(abs);\n\t\t} catch (error) {\n\t\t\tif (this.errorKind(error) === \"not-found\") return void 0;\n\t\t\tthrow error;\n\t\t}\n\t}\n\tasync #assertParentDirectory(files, abs, path) {\n\t\tconst parent = await this.#info(files, posix.dirname(abs));\n\t\tif (!parent?.type) throw new DirectoryNotFoundError(posix.dirname(path));\n\t\tif (parent.type !== \"dir\") throw new NotDirectoryError(posix.dirname(path));\n\t}\n\tasync #write(files, abs, path, data) {\n\t\ttry {\n\t\t\tawait files.write(abs, data);\n\t\t} catch (error) {\n\t\t\tif (this.errorKind(error) === \"invalid-argument\") throw new IsDirectoryError(path);\n\t\t\tthrow error;\n\t\t}\n\t}\n\t/**\n\t* Upload the temp file `tmp`, then run the command that consumes it. The\n\t* script deletes it on every path it controls; if the upload or the command\n\t* fails part-way, delete it here so it doesn't linger in the user's tree.\n\t*/\n\tasync #publishTemp(tmp, operation, upload, script, subject, errors) {\n\t\ttry {\n\t\t\tawait this.runFiles(operation, upload);\n\t\t\tawait this.#command(operation, script, subject, errors);\n\t\t} catch (error) {\n\t\t\tawait this.runFiles(operation, (files) => files.remove(tmp)).catch(() => {});\n\t\t\tthrow error;\n\t\t}\n\t}\n\t/** Run a file command and turn its known exit codes into typed errors. */\n\tasync #command(operation, script, subject, errors) {\n\t\tconst result = await this.runCommand(operation, script);\n\t\tif (result.exitCode === 0) return;\n\t\tfor (const [outcome, code] of Object.entries(FILE_COMMAND_EXIT)) {\n\t\t\tconst toError = errors[outcome];\n\t\t\tif (code === result.exitCode && toError) throw toError();\n\t\t}\n\t\tthrow new Error(`${operation} ${subject} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);\n\t}\n};\n//#endregion\n//#region src/commands-api-process-manager.ts\n/**\n* Attaching sets no deadline of its own: without one the SDK cuts the stream\n* after 60s, and the process's own timeout was set when it was spawned.\n*/\nconst ATTACH_TIMEOUT_MS = 0;\n/**\n* Output callbacks for `run`/`connect` that hold output until the handle\n* exists. The SDK's handle reads events from construction, before `run()`\n* returns, so output that arrives with the start event reaches the\n* callbacks first.\n*/\nfunction outputUntilHandled() {\n\tlet handle;\n\tconst early = [];\n\treturn {\n\t\tonStdout: (data) => {\n\t\t\tif (handle) handle.emitStdout(data);\n\t\t\telse early.push({\n\t\t\t\tstream: \"stdout\",\n\t\t\t\tdata\n\t\t\t});\n\t\t},\n\t\tonStderr: (data) => {\n\t\t\tif (handle) handle.emitStderr(data);\n\t\t\telse early.push({\n\t\t\t\tstream: \"stderr\",\n\t\t\t\tdata\n\t\t\t});\n\t\t},\n\t\thandledBy(created) {\n\t\t\tfor (const { stream, data } of early) if (stream === \"stdout\") created.emitStdout(data);\n\t\t\telse created.emitStderr(data);\n\t\t\tearly.length = 0;\n\t\t\thandle = created;\n\t\t\treturn created;\n\t\t}\n\t};\n}\n/** envd runs every command as `/bin/bash -l -c <command>`; show the command itself. */\nfunction commandOf(proc) {\n\tconst [flag, c, command] = proc.args;\n\tif (proc.cmd === \"/bin/bash\" && flag === \"-l\" && c === \"-c\" && command !== void 0 && proc.args.length === 3) return command;\n\treturn [proc.cmd, ...proc.args].join(\" \");\n}\n/**\n* A process started or attached through a {@link CommandsApi}. `kill` and\n* `sendStdin` go back through the manager, so they use whatever commands\n* handle the provider currently supplies.\n*/\nvar CommandsApiProcessHandle = class extends ProcessHandle {\n\tinner;\n\tmanager;\n\tstartTime;\n\tstdinMode;\n\tpid;\n\t/** The exit code `wait()` saw, for a handle that doesn't update its own. */\n\twaitedExitCode;\n\tconstructor(inner, manager, startTime, stdinMode, options) {\n\t\tsuper(options);\n\t\tthis.inner = inner;\n\t\tthis.manager = manager;\n\t\tthis.startTime = startTime;\n\t\tthis.stdinMode = stdinMode;\n\t\tthis.pid = String(inner.pid);\n\t}\n\t/** Read from the commands handle, so it reflects server-side state without `wait()`. */\n\tget exitCode() {\n\t\treturn this.inner.exitCode ?? this.waitedExitCode;\n\t}\n\t/** Wait for the process to exit. Providers whose output stream can drop while the process runs override this. */\n\twaitForExit() {\n\t\treturn this.inner.wait();\n\t}\n\tasync wait() {\n\t\ttry {\n\t\t\tconst result = await this.waitForExit();\n\t\t\tthis.waitedExitCode = result.exitCode;\n\t\t\treturn {\n\t\t\t\tsuccess: result.exitCode === 0,\n\t\t\t\texitCode: result.exitCode,\n\t\t\t\tstdout: this.stdout,\n\t\t\t\tstderr: this.stderr,\n\t\t\t\texecutionTimeMs: Date.now() - this.startTime\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tconst errorObj = error;\n\t\t\tconst reportedExitCode = errorObj.result?.exitCode ?? errorObj.exitCode;\n\t\t\tif (reportedExitCode !== void 0) this.waitedExitCode = reportedExitCode;\n\t\t\tconst exitCode = reportedExitCode ?? this.exitCode ?? 1;\n\t\t\tconst attachedStdout = errorObj.result?.stdout || errorObj.stdout;\n\t\t\tconst attachedStderr = errorObj.result?.stderr || errorObj.stderr;\n\t\t\tif (!this.stdout && !this.stdoutTruncated && attachedStdout) this.emitStdout(attachedStdout);\n\t\t\tif (!this.stderr && !this.stderrTruncated && attachedStderr) this.emitStderr(attachedStderr);\n\t\t\tconst stdout = this.stdout;\n\t\t\tconst stderr = this.stderr;\n\t\t\tconst terminalError = errorObj.result?.error || errorObj.error || (error instanceof Error ? error.message : String(error));\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\texitCode,\n\t\t\t\tstdout,\n\t\t\t\tstderr: [stderr, terminalError && !stderr.includes(terminalError) ? `Error: ${terminalError}` : \"\"].filter(Boolean).join(\"\\n\"),\n\t\t\t\texecutionTimeMs: Date.now() - this.startTime\n\t\t\t};\n\t\t}\n\t}\n\tasync kill() {\n\t\tif (this.exitCode !== void 0) return false;\n\t\treturn this.manager.runCommands(\"kill\", (commands) => commands.kill(this.inner.pid));\n\t}\n\tasync sendStdin(data) {\n\t\tif (this.exitCode !== void 0) throw new Error(`Process ${this.pid} has already exited with code ${this.exitCode}`);\n\t\tif (this.stdinMode === \"ignore\") throw new Error(`Process ${this.pid} was not started with stdin support`);\n\t\tawait this.manager.runCommands(\"sendStdin\", (commands) => commands.sendStdin(this.inner.pid, data));\n\t}\n\tasync closeStdin() {\n\t\tthrow new UnsupportedStdinCloseError(\"The commands API does not expose a way to close stdin for a running command\");\n\t}\n};\n/**\n* A process manager over E2B's envd commands API. Providers supply the\n* commands handle through {@link runCommands}; everything else is shared.\n*/\nvar CommandsApiProcessManager = class extends SandboxProcessManager {\n\tdefaultTimeout;\n\tconstructor({ defaultTimeout, ...options } = {}) {\n\t\tsuper(options);\n\t\tthis.defaultTimeout = defaultTimeout;\n\t}\n\t/** Wrap a started or attached command. Providers override this to supply their own handle. */\n\tcreateHandle(inner, stdinMode, options) {\n\t\treturn new CommandsApiProcessHandle(inner, this, Date.now(), stdinMode, options);\n\t}\n\t/** Where a command runs when the caller gives no `cwd`. */\n\tdefaultCwd() {\n\t\treturn this.sandbox.workingDirectory;\n\t}\n\tasync spawn(command, options = {}) {\n\t\tconst envs = Object.fromEntries(Object.entries({ ...options.env }).filter((entry) => entry[1] !== void 0));\n\t\treturn this.runCommands(\"spawn\", async (commands) => {\n\t\t\tconst output = outputUntilHandled();\n\t\t\tconst inner = await commands.run(command, {\n\t\t\t\tbackground: true,\n\t\t\t\tstdin: options.stdinMode !== \"ignore\",\n\t\t\t\tcwd: options.cwd ?? this.defaultCwd(),\n\t\t\t\tenvs,\n\t\t\t\ttimeoutMs: options.timeout ?? this.defaultTimeout,\n\t\t\t\tonStdout: output.onStdout,\n\t\t\t\tonStderr: output.onStderr\n\t\t\t});\n\t\t\tconst handle = output.handledBy(this.createHandle(inner, options.stdinMode, options));\n\t\t\tthis._tracked.set(handle.pid, handle);\n\t\t\treturn handle;\n\t\t});\n\t}\n\t/**\n\t* Running processes as the VM reports them, plus exited processes this\n\t* manager still holds, so a finished command stays visible until it is released.\n\t*/\n\tasync list() {\n\t\tconst infos = (await this.runCommands(\"list\", (commands) => commands.list())).map((proc) => ({\n\t\t\tpid: String(proc.pid),\n\t\t\tcommand: commandOf(proc),\n\t\t\trunning: true\n\t\t}));\n\t\tconst listed = new Set(infos.map((info) => info.pid));\n\t\tfor (const [pid, handle] of this._tracked) {\n\t\t\tif (handle.exitCode === void 0 || listed.has(pid)) continue;\n\t\t\tinfos.push({\n\t\t\t\tpid,\n\t\t\t\tcommand: handle.command,\n\t\t\t\trunning: false,\n\t\t\t\texitCode: handle.exitCode\n\t\t\t});\n\t\t}\n\t\treturn infos;\n\t}\n\t/**\n\t* A tracked process, or one this manager didn't start, attached through\n\t* `connect`. Output from before attaching is not available.\n\t*/\n\tasync get(pid, _options) {\n\t\tconst tracked = this._tracked.get(pid);\n\t\tif (tracked) return tracked;\n\t\tconst numericPid = /^\\d+$/.test(pid) ? Number(pid) : void 0;\n\t\tif (numericPid === void 0) return void 0;\n\t\tconst output = outputUntilHandled();\n\t\ttry {\n\t\t\tconst inner = await this.runCommands(\"connect\", (commands) => commands.connect(numericPid, {\n\t\t\t\tonStdout: output.onStdout,\n\t\t\t\tonStderr: output.onStderr,\n\t\t\t\ttimeoutMs: 0\n\t\t\t}));\n\t\t\tconst handle = output.handledBy(this.createHandle(inner, void 0));\n\t\t\tthis._tracked.set(handle.pid, handle);\n\t\t\treturn handle;\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t}\n};\n//#endregion\nexport { ATTACH_TIMEOUT_MS, CommandsApiProcessHandle, CommandsApiProcessManager, FILE_COMMAND_EXIT, FilesApiFilesystem, SETUP_FAILED_MARKER_PATH, SETUP_MARKER_DIR, SETUP_MARKER_PATH, WORKSPACE_SETUP_MARKER_PATH, appendFromCommand, copyCommand, guardedSetupCommand, linkNoClobberCommand, moveNoClobberCommand, normalizeSetupCommands, repoCloneCommand, repoSetupMarkerPath, setupMarkerCommand, setupMarkerContent, shellQuote, siblingTempPath };\n\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;AAMA,MAAM,mBAAmB;;;;;;;;;;;;;;AAczB,MAAM,oBAAoB;;;;;;;AAO1B,MAAM,8BAA8B;;;;;;;AAOpC,MAAM,2BAA2B;;AAEjC,SAAS,oBAAoB,SAAS;CACrC,OAAO,GAAG,iBAAiB,SAAS;AACrC;;AAEA,SAAS,uBAAuB,cAAc;CAC7C,QAAQ,iBAAiB,KAAK,IAAI,CAAC,IAAI,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,YAAY,EAAA,CAAG,QAAQ,YAAY,QAAQ,KAAK,MAAM,EAAE;AAC9I;;AAEA,SAAS,mBAAmB,cAAc;CACzC,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,uBAAuB,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK;AAC3G;;;;;;;;AA4CA,MAAM,oBAAoB;CACzB,UAAU;CACV,aAAa;CACb,QAAQ;CACR,cAAc;CACd,iBAAiB;AAClB;AAKA,MAAM,EAAE,UAAU,aAAa,QAAQ,cAAc,oBAAoB"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../../packages/_internals/workspace/dist/index.js"],"sourcesContent":["import { createHash } from \"crypto\";\nimport { posix } from \"path\";\nimport { Buffer } from \"buffer\";\nimport { DirectoryNotEmptyError, DirectoryNotFoundError, FileExistsError, FileNotFoundError, IsDirectoryError, MastraFilesystem, NotDirectoryError, ProcessHandle, SandboxProcessManager, UnsupportedStdinCloseError, getMimeType } from \"@mastra/core/workspace\";\n// src/setup-marker.ts\n/** Directory, relative to the template's build cwd, that holds every marker below. */\nconst SETUP_MARKER_DIR = \".mastra-sandbox\";\n/**\n* Setup completion marker shared by repo templates and their consumers.\n*\n* A repo template writes this file beside the checkout as its last build\n* step, so it exists only in images where every setup command succeeded. Its\n* content is a digest of the setup commands the image ran, letting a sandbox\n* booted from the image tell whether the setup it is about to run already\n* happened. Relative to the template's build cwd, which is also the runtime\n* working directory the repo was cloned into.\n*\n* This is the single-repository marker. Templates built from a repository\n* list write one marker per repository instead ({@link repoSetupMarkerPath}).\n*/\nconst SETUP_MARKER_PATH = \".mastra-sandbox/setup\";\n/**\n* Marker for the workspace-level setup of a multi-repository template: the\n* commands that ran at the working directory after every repository was set\n* up. Written even when there were no such commands, so a consumer can tell\n* \"ran nothing\" from \"never built\".\n*/\nconst WORKSPACE_SETUP_MARKER_PATH = \".mastra-sandbox/workspace-setup\";\n/**\n* One line per repository directory whose setup failed while the build was\n* told to continue. Absent when nothing failed. Consumers check this before\n* trusting a per-repository marker: that marker means every step ran, not\n* that every step succeeded.\n*/\nconst SETUP_FAILED_MARKER_PATH = \".mastra-sandbox/setup-failed\";\n/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */\nfunction repoSetupMarkerPath(repoDir) {\n\treturn `${SETUP_MARKER_DIR}/repos/${repoDir}`;\n}\n/** Blank entries never become build steps, so they never count toward the digest either. */\nfunction normalizeSetupCommands(setupCommand) {\n\treturn (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== \"\");\n}\n/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */\nfunction setupMarkerContent(setupCommand) {\n\treturn `sha256:${createHash(\"sha256\").update(normalizeSetupCommands(setupCommand).join(\"\\n\")).digest(\"hex\")}`;\n}\n/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */\nfunction setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {\n\treturn `mkdir -p \"$(dirname \"${markerPath}\")\" && printf '%s' '${content}' > \"${markerPath}\"`;\n}\n/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */\nconst REPO_DIR_PATTERN = /^[\\w.-]+$/;\n/**\n* Shell step that runs one setup command inside a repository clone.\n*\n* Without the guard this is the plain `cd \"<dir>\" && <cmd>` step. With it,\n* the `cd` sits inside the parenthesized group so that the failure branch\n* runs back at the build cwd and the failure list lands at the workspace\n* level, never inside the repository.\n*/\nfunction guardedSetupCommand({ repoDir, command, continueOnFailure }) {\n\tif (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory \"${repoDir}\" must match ${REPO_DIR_PATTERN}.`);\n\tif (!continueOnFailure) return `cd \"${repoDir}\" && ${command}`;\n\treturn `( cd \"${repoDir}\" && sh -c ${shellQuote$2(command)} ) || { mkdir -p \"${SETUP_MARKER_DIR}\" && grep -qxF -- '${repoDir}' \"${SETUP_FAILED_MARKER_PATH}\" 2>/dev/null || printf '%s\\\\n' '${repoDir}' >> \"${SETUP_FAILED_MARKER_PATH}\"; }`;\n}\nfunction shellQuote$2(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n// src/repo-clone.ts\nfunction repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {\n\treturn `git ${tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : \"\"}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote$1(branch)} ` : \"\"}${shellQuote$1(cloneUrl)} ${shellQuote$1(destination)}`;\n}\n/** Per-invocation auth header; `-c` config never reaches `.git/config`. */\nfunction gitAuthFlag(tokenEnv) {\n\treturn `-c http.extraheader=\"AUTHORIZATION: basic $(printf 'x-access-token:%s' \"$${tokenEnv}\" | base64 -w0)\"`;\n}\nfunction shellQuote$1(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n// src/file-commands.ts\n/**\n* Shell scripts for the file operations a sandbox can only do atomically (or\n* without moving file bytes over the network) by running a command inside the\n* VM. Each builder takes absolute paths and returns one POSIX `sh` script; the\n* caller runs it with `sh -c` and maps the exit codes below to typed errors.\n*/\n/** Exit codes the scripts use for outcomes the caller turns into typed errors. Any other non-zero code is a plain failure. */\nconst FILE_COMMAND_EXIT = {\n\tnotFound: 20,\n\tisDirectory: 21,\n\texists: 22,\n\tnotDirectory: 23,\n\tdestIsDirectory: 24\n};\n/** Single-quote a string for safe POSIX shell interpolation. */\nfunction shellQuote(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\nconst { notFound, isDirectory, exists, notDirectory, destIsDirectory } = FILE_COMMAND_EXIT;\n/** `[ -e ]` is false for a broken symlink, which still occupies the path. */\nconst present = (variable) => `{ [ -e \"$${variable}\" ] || [ -L \"$${variable}\" ]; }`;\n/**\n* A sibling temp path for `path`, so a later `ln` / `mv` onto `path` stays on\n* one filesystem. Hidden, and unique per call.\n*/\nfunction siblingTempPath(path) {\n\tconst suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;\n\treturn posix.join(posix.dirname(path), `.${posix.basename(path)}.mastra-tmp-${suffix}`);\n}\n/**\n* Hard-link `$tmp` to `$dest` and delete `$tmp`, failing with `exists` when\n* something is at `$dest`. `ln` onto a directory (or a link to one) would\n* create the link inside it and succeed, so that case is undone and reported\n* as taken too.\n*/\nfunction linkTempIntoPlace() {\n\treturn [\n\t\t`ln \"$tmp\" \"$dest\" 2>/dev/null || { rm -f \"$tmp\"; ${present(\"dest\")} && exit ${exists}; exit 1; }`,\n\t\t`if [ -d \"$dest\" ]; then rm -f \"$dest/$(basename \"$tmp\")\" \"$tmp\"; exit ${exists}; fi`,\n\t\t`rm -f \"$tmp\"`\n\t];\n}\n/**\n* Copy `src` to `dest` inside the sandbox, creating `dest`'s parent.\n*\n* A file source copies its content (a symlinked source is followed). A\n* directory source with `recursive` copies its contents into `dest`, keeping\n* symlinks inside the tree as links. With `overwrite: false` the destination\n* is claimed atomically: a directory with an exclusive `mkdir`, a file by\n* copying to a temp name and hard-linking it into place (`link(2)` fails when\n* the destination exists).\n*\n* Exits: `notFound` (no source), `isDirectory` (directory source without\n* `recursive`), `destIsDirectory` (a file onto an existing directory or a link\n* to one), `exists`.\n*/\nfunction copyCommand(src, dest, options = {}) {\n\tconst noClobber = options.overwrite === false;\n\treturn [\n\t\t`src=${shellQuote(src)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`[ -e \"$src\" ] || exit ${notFound}`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" || exit 1`,\n\t\t`if [ -d \"$src\" ]; then`,\n\t\toptions.recursive ? ` :` : ` exit ${isDirectory}`,\n\t\tnoClobber ? ` mkdir \"$dest\" 2>/dev/null || exit ${exists}` : ` mkdir -p \"$dest\" || exit 1`,\n\t\t` cp -R -P \"$src\"/. \"$dest\"/ || exit 1`,\n\t\t` exit 0`,\n\t\t`fi`,\n\t\tnoClobber ? `${present(\"dest\")} && exit ${exists}` : `if [ -d \"$dest\" ]; then exit ${destIsDirectory}; fi`,\n\t\t...noClobber ? [\n\t\t\t`tmp=${shellQuote(siblingTempPath(dest))}`,\n\t\t\t`cp \"$src\" \"$tmp\" || { rm -f \"$tmp\"; exit 1; }`,\n\t\t\t...linkTempIntoPlace()\n\t\t] : [`cp \"$src\" \"$dest\" || exit 1`]\n\t].join(\"\\n\");\n}\n/**\n* Move `src` to `dest` without replacing anything at `dest`, creating `dest`'s\n* parent. A skipped `mv -n` is detected by the source surviving, whatever\n* its exit code. If a directory appears at `dest` between the check and the\n* move, `mv` moves `src` into it (BSD `mv` has no `-T`), and that is reported\n* as success; the no-clobber rename itself is atomic.\n*\n* Exits: `notFound` (no source), `exists`.\n*/\nfunction moveNoClobberCommand(src, dest) {\n\treturn [\n\t\t`src=${shellQuote(src)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`${present(\"src\")} || exit ${notFound}`,\n\t\t`${present(\"dest\")} && exit ${exists}`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" || exit 1`,\n\t\t`mv -n \"$src\" \"$dest\" 2>/dev/null || { ${present(\"src\")} && ${present(\"dest\")} && exit ${exists}; exit 1; }`,\n\t\t`${present(\"src\")} && exit ${exists}`,\n\t\t`exit 0`\n\t].join(\"\\n\");\n}\n/**\n* Append the bytes already written to `tmp` onto `dest`, then delete `tmp`.\n* The content travels as a file rather than a command argument, so it has no\n* size limit. Creates `dest` (and its parent) when missing.\n*\n* Exits: `isDirectory` (dest is a directory).\n*/\nfunction appendFromCommand(tmp, dest) {\n\treturn [\n\t\t`tmp=${shellQuote(tmp)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`if [ -d \"$dest\" ]; then rm -f \"$tmp\"; exit ${isDirectory}; fi`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" && cat \"$tmp\" >> \"$dest\"`,\n\t\t`status=$?`,\n\t\t`rm -f \"$tmp\"`,\n\t\t`exit $status`\n\t].join(\"\\n\");\n}\n/**\n* Publish the file already written to `tmp` at `dest` only if nothing is\n* there, then delete `tmp`. `ln` (`link(2)`) is the atomic exclusivity check;\n* `tmp` must be on the same filesystem, e.g. from {@link siblingTempPath}.\n*\n* Exits: `exists`, `notDirectory` (dest's parent is not a directory).\n*/\nfunction linkNoClobberCommand(tmp, dest) {\n\treturn [\n\t\t`tmp=${shellQuote(tmp)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`if [ ! -d \"$(dirname \"$dest\")\" ]; then rm -f \"$tmp\"; exit ${notDirectory}; fi`,\n\t\t`if ${present(\"dest\")}; then rm -f \"$tmp\"; exit ${exists}; fi`,\n\t\t...linkTempIntoPlace()\n\t].join(\"\\n\");\n}\n// src/files-api-filesystem.ts\n/** Default recursion depth for `readdir({ recursive: true })`, matching `LocalFilesystem`. */\nconst DEFAULT_MAX_DEPTH = 100;\nconst toAbsolute = (path) => posix.resolve(\"/\", path);\n/** The entry's own type: a symlink is a link even when it points at a directory. */\nconst isSymlink = (info) => info.symlinkTarget !== void 0;\nfunction toWriteData(content) {\n\tif (typeof content === \"string\") return content;\n\treturn content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);\n}\nfunction matchesExtension(name, extension) {\n\tif (!extension) return true;\n\tconst ext = posix.extname(name);\n\treturn (Array.isArray(extension) ? extension : [extension]).some((e) => e === ext || e === ext.slice(1));\n}\n/**\n* A sandbox's disk, read and written through E2B's envd files API, with the\n* operations the files API can't do in one call (copy, append, no-clobber\n* writes and moves) run as one shell command inside the VM so they are atomic\n* and file bytes never round-trip through the host. Paths are absolute inside\n* the sandbox; this class does not confine paths or symlinks to any root.\n*\n* Providers supply how to reach the files API and the shell, and how to read\n* their SDK's errors.\n*\n* If a provider's `runCommand` re-runs a command whose first run already\n* finished (e.g. a retry after a dropped connection), the second run sees the\n* first's result: a no-clobber copy, write or move reports `FileExistsError`\n* or `FileNotFoundError`, and an append fails, even though the work was done.\n*/\nvar FilesApiFilesystem = class extends MastraFilesystem {\n\tasync readFile(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst bytes = await this.runFiles(\"readFile\", async (files) => {\n\t\t\ttry {\n\t\t\t\treturn await files.read(abs, { format: \"bytes\" });\n\t\t\t} catch (error) {\n\t\t\t\tconst kind = this.errorKind(error);\n\t\t\t\tif (kind === \"not-found\") throw new FileNotFoundError(path);\n\t\t\t\tif (kind === \"invalid-argument\") throw new IsDirectoryError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t\tconst buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\t\treturn options?.encoding ? buffer.toString(options.encoding) : buffer;\n\t}\n\tasync writeFile(path, content, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tif (options?.overwrite !== false) {\n\t\t\tawait this.runFiles(\"writeFile\", async (files) => {\n\t\t\t\tif (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\t\tawait this.#write(files, abs, path, toWriteData(content));\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst tmp = siblingTempPath(abs);\n\t\tconst upload = async (files) => {\n\t\t\tif (options.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\tawait this.#write(files, tmp, path, toWriteData(content));\n\t\t};\n\t\tawait this.#publishTemp(tmp, \"writeFile\", upload, linkNoClobberCommand(tmp, abs), path, {\n\t\t\texists: () => new FileExistsError(path),\n\t\t\tnotDirectory: () => new NotDirectoryError(posix.dirname(path))\n\t\t});\n\t}\n\tasync appendFile(path, content) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst tmp = siblingTempPath(abs);\n\t\tconst upload = (files) => this.#write(files, tmp, path, toWriteData(content));\n\t\tawait this.#publishTemp(tmp, \"appendFile\", upload, appendFromCommand(tmp, abs), path, { isDirectory: () => new IsDirectoryError(path) });\n\t}\n\tasync deleteFile(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"deleteFile\", async (files) => {\n\t\t\tconst info = await this.#info(files, abs);\n\t\t\tif (!info) {\n\t\t\t\tif (options?.force) return;\n\t\t\t\tthrow new FileNotFoundError(path);\n\t\t\t}\n\t\t\tif (!isSymlink(info) && info.type === \"dir\") throw new IsDirectoryError(path);\n\t\t\tawait files.remove(abs);\n\t\t});\n\t}\n\t/**\n\t* One `cp` inside the VM. A recursive copy keeps symlinks inside the tree as\n\t* links; `overwrite: false` claims the destination atomically.\n\t*/\n\tasync copyFile(src, dest, options) {\n\t\tconst command = copyCommand(toAbsolute(src), toAbsolute(dest), {\n\t\t\trecursive: options?.recursive,\n\t\t\toverwrite: options?.overwrite\n\t\t});\n\t\tawait this.#command(\"copyFile\", command, `${src} -> ${dest}`, {\n\t\t\tnotFound: () => new FileNotFoundError(src),\n\t\t\tisDirectory: () => new IsDirectoryError(src),\n\t\t\tdestIsDirectory: () => new IsDirectoryError(dest),\n\t\t\texists: () => new FileExistsError(dest)\n\t\t});\n\t}\n\tasync moveFile(src, dest, options) {\n\t\tconst srcAbs = toAbsolute(src);\n\t\tconst destAbs = toAbsolute(dest);\n\t\tif (options?.overwrite === false) {\n\t\t\tawait this.#command(\"moveFile\", moveNoClobberCommand(srcAbs, destAbs), `${src} -> ${dest}`, {\n\t\t\t\tnotFound: () => new FileNotFoundError(src),\n\t\t\t\texists: () => new FileExistsError(dest)\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tawait this.runFiles(\"moveFile\", async (files) => {\n\t\t\ttry {\n\t\t\t\tawait files.rename(srcAbs, destAbs);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorKind(error) === \"not-found\") throw new FileNotFoundError(src);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\tasync mkdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"mkdir\", async (files) => {\n\t\t\tif (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\ttry {\n\t\t\t\tawait files.makeDir(abs);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorKind(error) === \"invalid-argument\") throw new FileExistsError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\tasync rmdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"rmdir\", async (files) => {\n\t\t\tconst info = await this.#info(files, abs);\n\t\t\tif (!info) {\n\t\t\t\tif (options?.force) return;\n\t\t\t\tthrow new DirectoryNotFoundError(path);\n\t\t\t}\n\t\t\tif (!options?.recursive) {\n\t\t\t\tif (isSymlink(info) || info.type !== \"dir\") throw new NotDirectoryError(path);\n\t\t\t\tif ((await files.list(abs)).length > 0) throw new DirectoryNotEmptyError(path);\n\t\t\t} else if (!isSymlink(info) && info.type !== \"dir\") throw new NotDirectoryError(path);\n\t\t\tawait files.remove(abs);\n\t\t});\n\t}\n\tasync readdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst depth = options?.recursive ? (options.maxDepth ?? DEFAULT_MAX_DEPTH) + 1 : 1;\n\t\tconst entries = await this.runFiles(\"readdir\", async (files) => {\n\t\t\ttry {\n\t\t\t\treturn await files.list(abs, { depth });\n\t\t\t} catch (error) {\n\t\t\t\tconst kind = this.errorKind(error);\n\t\t\t\tif (kind === \"not-found\") throw new DirectoryNotFoundError(path);\n\t\t\t\tif (kind === \"invalid-argument\") throw new NotDirectoryError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t\tconst result = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.type) continue;\n\t\t\tconst name = posix.relative(abs, entry.path);\n\t\t\tconst type = entry.type === \"dir\" ? \"directory\" : \"file\";\n\t\t\tif (type === \"file\" && !matchesExtension(name, options?.extension)) continue;\n\t\t\tconst link = isSymlink(entry);\n\t\t\tresult.push({\n\t\t\t\tname,\n\t\t\t\ttype,\n\t\t\t\t...type === \"file\" && !link && { size: entry.size },\n\t\t\t\t...link && {\n\t\t\t\t\tisSymlink: true,\n\t\t\t\t\tsymlinkTarget: entry.symlinkTarget\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t}\n\tasync exists(path) {\n\t\tconst abs = toAbsolute(path);\n\t\treturn this.runFiles(\"exists\", async (files) => Boolean((await this.#info(files, abs))?.type));\n\t}\n\tasync stat(path) {\n\t\tconst abs = toAbsolute(path);\n\t\treturn this.runFiles(\"stat\", async (files) => {\n\t\t\tlet info = await this.#info(files, abs);\n\t\t\tif (info?.type && isSymlink(info)) info = await this.#info(files, posix.resolve(posix.dirname(abs), info.symlinkTarget));\n\t\t\tif (!info?.type) throw new FileNotFoundError(path);\n\t\t\tconst modifiedAt = info.modifiedTime ?? /* @__PURE__ */ new Date(0);\n\t\t\treturn {\n\t\t\t\tname: posix.basename(abs),\n\t\t\t\tpath: abs,\n\t\t\t\ttype: info.type === \"dir\" ? \"directory\" : \"file\",\n\t\t\t\tsize: info.type === \"dir\" ? 0 : info.size,\n\t\t\t\tcreatedAt: modifiedAt,\n\t\t\t\tmodifiedAt,\n\t\t\t\t...info.type !== \"dir\" && { mimeType: getMimeType(abs) }\n\t\t\t};\n\t\t});\n\t}\n\t/** `getInfo` without following the entry itself, or `undefined` when nothing is there. */\n\tasync #info(files, abs) {\n\t\ttry {\n\t\t\treturn await files.getInfo(abs);\n\t\t} catch (error) {\n\t\t\tif (this.errorKind(error) === \"not-found\") return void 0;\n\t\t\tthrow error;\n\t\t}\n\t}\n\tasync #assertParentDirectory(files, abs, path) {\n\t\tconst parent = await this.#info(files, posix.dirname(abs));\n\t\tif (!parent?.type) throw new DirectoryNotFoundError(posix.dirname(path));\n\t\tif (parent.type !== \"dir\") throw new NotDirectoryError(posix.dirname(path));\n\t}\n\tasync #write(files, abs, path, data) {\n\t\ttry {\n\t\t\tawait files.write(abs, data);\n\t\t} catch (error) {\n\t\t\tif (this.errorKind(error) === \"invalid-argument\") throw new IsDirectoryError(path);\n\t\t\tthrow error;\n\t\t}\n\t}\n\t/**\n\t* Upload the temp file `tmp`, then run the command that consumes it. The\n\t* script deletes it on every path it controls; if the upload or the command\n\t* fails part-way, delete it here so it doesn't linger in the user's tree.\n\t*/\n\tasync #publishTemp(tmp, operation, upload, script, subject, errors) {\n\t\ttry {\n\t\t\tawait this.runFiles(operation, upload);\n\t\t\tawait this.#command(operation, script, subject, errors);\n\t\t} catch (error) {\n\t\t\tawait this.runFiles(operation, (files) => files.remove(tmp)).catch(() => {});\n\t\t\tthrow error;\n\t\t}\n\t}\n\t/** Run a file command and turn its known exit codes into typed errors. */\n\tasync #command(operation, script, subject, errors) {\n\t\tconst result = await this.runCommand(operation, script);\n\t\tif (result.exitCode === 0) return;\n\t\tfor (const [outcome, code] of Object.entries(FILE_COMMAND_EXIT)) {\n\t\t\tconst toError = errors[outcome];\n\t\t\tif (code === result.exitCode && toError) throw toError();\n\t\t}\n\t\tthrow new Error(`${operation} ${subject} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);\n\t}\n};\n// src/commands-api-process-manager.ts\n/**\n* Attaching sets no deadline of its own: without one the SDK cuts the stream\n* after 60s, and the process's own timeout was set when it was spawned.\n*/\nconst ATTACH_TIMEOUT_MS = 0;\n/**\n* Output callbacks for `run`/`connect` that hold output until the handle\n* exists. The SDK's handle reads events from construction, before `run()`\n* returns, so output that arrives with the start event reaches the\n* callbacks first.\n*/\nfunction outputUntilHandled() {\n\tlet handle;\n\tconst early = [];\n\treturn {\n\t\tonStdout: (data) => {\n\t\t\tif (handle) handle.emitStdout(data);\n\t\t\telse early.push({\n\t\t\t\tstream: \"stdout\",\n\t\t\t\tdata\n\t\t\t});\n\t\t},\n\t\tonStderr: (data) => {\n\t\t\tif (handle) handle.emitStderr(data);\n\t\t\telse early.push({\n\t\t\t\tstream: \"stderr\",\n\t\t\t\tdata\n\t\t\t});\n\t\t},\n\t\thandledBy(created) {\n\t\t\tfor (const { stream, data } of early) if (stream === \"stdout\") created.emitStdout(data);\n\t\t\telse created.emitStderr(data);\n\t\t\tearly.length = 0;\n\t\t\thandle = created;\n\t\t\treturn created;\n\t\t}\n\t};\n}\n/** envd runs every command as `/bin/bash -l -c <command>`; show the command itself. */\nfunction commandOf(proc) {\n\tconst [flag, c, command] = proc.args;\n\tif (proc.cmd === \"/bin/bash\" && flag === \"-l\" && c === \"-c\" && command !== void 0 && proc.args.length === 3) return command;\n\treturn [proc.cmd, ...proc.args].join(\" \");\n}\n/**\n* A process started or attached through a {@link CommandsApi}. `kill` and\n* `sendStdin` go back through the manager, so they use whatever commands\n* handle the provider currently supplies.\n*/\nvar CommandsApiProcessHandle = class extends ProcessHandle {\n\tinner;\n\tmanager;\n\tstartTime;\n\tstdinMode;\n\tpid;\n\t/** The exit code `wait()` saw, for a handle that doesn't update its own. */\n\twaitedExitCode;\n\tconstructor(inner, manager, startTime, stdinMode, options) {\n\t\tsuper(options);\n\t\tthis.inner = inner;\n\t\tthis.manager = manager;\n\t\tthis.startTime = startTime;\n\t\tthis.stdinMode = stdinMode;\n\t\tthis.pid = String(inner.pid);\n\t}\n\t/** Read from the commands handle, so it reflects server-side state without `wait()`. */\n\tget exitCode() {\n\t\treturn this.inner.exitCode ?? this.waitedExitCode;\n\t}\n\t/** Wait for the process to exit. Providers whose output stream can drop while the process runs override this. */\n\twaitForExit() {\n\t\treturn this.inner.wait();\n\t}\n\tasync wait() {\n\t\ttry {\n\t\t\tconst result = await this.waitForExit();\n\t\t\tthis.waitedExitCode = result.exitCode;\n\t\t\treturn {\n\t\t\t\tsuccess: result.exitCode === 0,\n\t\t\t\texitCode: result.exitCode,\n\t\t\t\tstdout: this.stdout,\n\t\t\t\tstderr: this.stderr,\n\t\t\t\texecutionTimeMs: Date.now() - this.startTime\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tconst errorObj = error;\n\t\t\tconst reportedExitCode = errorObj.result?.exitCode ?? errorObj.exitCode;\n\t\t\tif (reportedExitCode !== void 0) this.waitedExitCode = reportedExitCode;\n\t\t\tconst exitCode = reportedExitCode ?? this.exitCode ?? 1;\n\t\t\tconst attachedStdout = errorObj.result?.stdout || errorObj.stdout;\n\t\t\tconst attachedStderr = errorObj.result?.stderr || errorObj.stderr;\n\t\t\tif (!this.stdout && !this.stdoutTruncated && attachedStdout) this.emitStdout(attachedStdout);\n\t\t\tif (!this.stderr && !this.stderrTruncated && attachedStderr) this.emitStderr(attachedStderr);\n\t\t\tconst stdout = this.stdout;\n\t\t\tconst stderr = this.stderr;\n\t\t\tconst terminalError = errorObj.result?.error || errorObj.error || (error instanceof Error ? error.message : String(error));\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\texitCode,\n\t\t\t\tstdout,\n\t\t\t\tstderr: [stderr, terminalError && !stderr.includes(terminalError) ? `Error: ${terminalError}` : \"\"].filter(Boolean).join(\"\\n\"),\n\t\t\t\texecutionTimeMs: Date.now() - this.startTime\n\t\t\t};\n\t\t}\n\t}\n\tasync kill() {\n\t\tif (this.exitCode !== void 0) return false;\n\t\treturn this.manager.runCommands(\"kill\", (commands) => commands.kill(this.inner.pid));\n\t}\n\tasync sendStdin(data) {\n\t\tif (this.exitCode !== void 0) throw new Error(`Process ${this.pid} has already exited with code ${this.exitCode}`);\n\t\tif (this.stdinMode === \"ignore\") throw new Error(`Process ${this.pid} was not started with stdin support`);\n\t\tawait this.manager.runCommands(\"sendStdin\", (commands) => commands.sendStdin(this.inner.pid, data));\n\t}\n\tasync closeStdin() {\n\t\tthrow new UnsupportedStdinCloseError(\"The commands API does not expose a way to close stdin for a running command\");\n\t}\n};\n/**\n* A process manager over E2B's envd commands API. Providers supply the\n* commands handle through {@link runCommands}; everything else is shared.\n*/\nvar CommandsApiProcessManager = class extends SandboxProcessManager {\n\tdefaultTimeout;\n\tconstructor({ defaultTimeout, ...options } = {}) {\n\t\tsuper(options);\n\t\tthis.defaultTimeout = defaultTimeout;\n\t}\n\t/** Wrap a started or attached command. Providers override this to supply their own handle. */\n\tcreateHandle(inner, stdinMode, options) {\n\t\treturn new CommandsApiProcessHandle(inner, this, Date.now(), stdinMode, options);\n\t}\n\t/** Where a command runs when the caller gives no `cwd`. */\n\tdefaultCwd() {\n\t\treturn this.sandbox.workingDirectory;\n\t}\n\tasync spawn(command, options = {}) {\n\t\tconst envs = Object.fromEntries(Object.entries({ ...options.env }).filter((entry) => entry[1] !== void 0));\n\t\treturn this.runCommands(\"spawn\", async (commands) => {\n\t\t\tconst output = outputUntilHandled();\n\t\t\tconst inner = await commands.run(command, {\n\t\t\t\tbackground: true,\n\t\t\t\tstdin: options.stdinMode !== \"ignore\",\n\t\t\t\tcwd: options.cwd ?? this.defaultCwd(),\n\t\t\t\tenvs,\n\t\t\t\ttimeoutMs: options.timeout ?? this.defaultTimeout,\n\t\t\t\tonStdout: output.onStdout,\n\t\t\t\tonStderr: output.onStderr\n\t\t\t});\n\t\t\tconst handle = output.handledBy(this.createHandle(inner, options.stdinMode, options));\n\t\t\tthis._tracked.set(handle.pid, handle);\n\t\t\treturn handle;\n\t\t});\n\t}\n\t/**\n\t* Running processes as the VM reports them, plus exited processes this\n\t* manager still holds, so a finished command stays visible until it is released.\n\t*/\n\tasync list() {\n\t\tconst infos = (await this.runCommands(\"list\", (commands) => commands.list())).map((proc) => ({\n\t\t\tpid: String(proc.pid),\n\t\t\tcommand: commandOf(proc),\n\t\t\trunning: true\n\t\t}));\n\t\tconst listed = new Set(infos.map((info) => info.pid));\n\t\tfor (const [pid, handle] of this._tracked) {\n\t\t\tif (handle.exitCode === void 0 || listed.has(pid)) continue;\n\t\t\tinfos.push({\n\t\t\t\tpid,\n\t\t\t\tcommand: handle.command,\n\t\t\t\trunning: false,\n\t\t\t\texitCode: handle.exitCode\n\t\t\t});\n\t\t}\n\t\treturn infos;\n\t}\n\t/**\n\t* A tracked process, or one this manager didn't start, attached through\n\t* `connect`. Output from before attaching is not available.\n\t*/\n\tasync get(pid, _options) {\n\t\tconst tracked = this._tracked.get(pid);\n\t\tif (tracked) return tracked;\n\t\tconst numericPid = /^\\d+$/.test(pid) ? Number(pid) : void 0;\n\t\tif (numericPid === void 0) return void 0;\n\t\tconst output = outputUntilHandled();\n\t\ttry {\n\t\t\tconst inner = await this.runCommands(\"connect\", (commands) => commands.connect(numericPid, {\n\t\t\t\tonStdout: output.onStdout,\n\t\t\t\tonStderr: output.onStderr,\n\t\t\t\ttimeoutMs: 0\n\t\t\t}));\n\t\t\tconst handle = output.handledBy(this.createHandle(inner, void 0));\n\t\t\tthis._tracked.set(handle.pid, handle);\n\t\t\treturn handle;\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t}\n};\nexport { ATTACH_TIMEOUT_MS, CommandsApiProcessHandle, CommandsApiProcessManager, FILE_COMMAND_EXIT, FilesApiFilesystem, SETUP_FAILED_MARKER_PATH, SETUP_MARKER_DIR, SETUP_MARKER_PATH, WORKSPACE_SETUP_MARKER_PATH, appendFromCommand, copyCommand, guardedSetupCommand, linkNoClobberCommand, moveNoClobberCommand, normalizeSetupCommands, repoCloneCommand, repoSetupMarkerPath, setupMarkerCommand, setupMarkerContent, shellQuote, siblingTempPath };\n\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;AAMA,MAAM,mBAAmB;;;;;;;;;;;;;;AAczB,MAAM,oBAAoB;;;;;;;AAO1B,MAAM,8BAA8B;;;;;;;AAOpC,MAAM,2BAA2B;;AAEjC,SAAS,oBAAoB,SAAS;CACrC,OAAO,GAAG,iBAAiB,SAAS;AACrC;;AAEA,SAAS,uBAAuB,cAAc;CAC7C,QAAQ,iBAAiB,KAAK,IAAI,CAAC,IAAI,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,YAAY,EAAA,CAAG,QAAQ,YAAY,QAAQ,KAAK,MAAM,EAAE;AAC9I;;AAEA,SAAS,mBAAmB,cAAc;CACzC,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,uBAAuB,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK;AAC3G;;;;;;;;AA0CA,MAAM,oBAAoB;CACzB,UAAU;CACV,aAAa;CACb,QAAQ;CACR,cAAc;CACd,iBAAiB;AAClB;AAKA,MAAM,EAAE,UAAU,aAAa,QAAQ,cAAc,oBAAoB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/platform-auth-host.ts
|
|
2
2
|
const PLATFORM_AUTH_PROVIDER = "mastra-studio";
|
|
3
3
|
const CUSTOM_DOMAIN_UNSUPPORTED_ERROR = "custom_domain_unsupported";
|
|
4
4
|
const PLATFORM_AUTH_DOMAINS = ["mastra.cloud", "mastra.ai"];
|
|
@@ -12,7 +12,6 @@ function isPlatformAuthSupportedHost(hostname) {
|
|
|
12
12
|
if (LOOPBACK_HOSTS.has(normalizedHostname) || normalizedHostname.endsWith(".localhost")) return true;
|
|
13
13
|
return PLATFORM_AUTH_DOMAINS.some((domain) => normalizedHostname === domain || normalizedHostname.endsWith(`.${domain}`));
|
|
14
14
|
}
|
|
15
|
-
//#endregion
|
|
16
15
|
export { CUSTOM_DOMAIN_UNSUPPORTED_ERROR, PLATFORM_AUTH_PROVIDER, isPlatformAuthSupportedHost };
|
|
17
16
|
|
|
18
17
|
//# sourceMappingURL=platform-auth-host.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/provider-display-name.ts
|
|
2
2
|
const PROVIDER_DISPLAY_NAMES = {
|
|
3
3
|
anthropic: "Anthropic",
|
|
4
4
|
"github-copilot": "GitHub Copilot",
|
|
@@ -10,7 +10,6 @@ const PROVIDER_DISPLAY_NAMES = {
|
|
|
10
10
|
function providerDisplayName(provider) {
|
|
11
11
|
return PROVIDER_DISPLAY_NAMES[provider] ?? provider.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
12
12
|
}
|
|
13
|
-
//#endregion
|
|
14
13
|
export { providerDisplayName };
|
|
15
14
|
|
|
16
15
|
//# sourceMappingURL=provider-display-name.js.map
|
package/dist/review-verdict.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Normalizes the first non-empty line of a review body so its `Verdict:` line can be read
|
|
3
3
|
* regardless of the markdown it is wrapped in (`**Verdict:** request changes`, `# Verdict: ...`).
|
|
4
|
+
* A leading proxy attribution line is skipped.
|
|
4
5
|
*/
|
|
5
6
|
export declare function normalizedVerdictLine(body: string | undefined): string | undefined;
|
|
6
7
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-verdict.d.ts","sourceRoot":"","sources":["../src/review-verdict.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"review-verdict.d.ts","sourceRoot":"","sources":["../src/review-verdict.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAWlF;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAOxE"}
|
package/dist/review-verdict.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
// src/review-verdict.ts
|
|
2
|
+
/**
|
|
3
|
+
* The Platform GitHub proxy prefixes bodies it writes for a session with
|
|
4
|
+
* `> Opened by @<actor>`, which pushes the skill's verdict off the first line.
|
|
5
|
+
*/
|
|
6
|
+
const ACTOR_ATTRIBUTION_LINE = /^>\s*Opened by\s+\S+$/;
|
|
2
7
|
/**
|
|
3
8
|
* Normalizes the first non-empty line of a review body so its `Verdict:` line can be read
|
|
4
9
|
* regardless of the markdown it is wrapped in (`**Verdict:** request changes`, `# Verdict: ...`).
|
|
10
|
+
* A leading proxy attribution line is skipped.
|
|
5
11
|
*/
|
|
6
12
|
function normalizedVerdictLine(body) {
|
|
7
|
-
const
|
|
13
|
+
const lines = body?.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
14
|
+
const firstLine = lines?.[0] && ACTOR_ATTRIBUTION_LINE.test(lines[0]) ? lines[1] : lines?.[0];
|
|
8
15
|
if (!firstLine) return void 0;
|
|
9
16
|
return firstLine.replaceAll(/[*_`#>\s]+/g, " ").trim().toLowerCase();
|
|
10
17
|
}
|
|
@@ -23,7 +30,6 @@ function requestsChangesVerdict(body) {
|
|
|
23
30
|
if (!normalized) return false;
|
|
24
31
|
return /^verdict: ?(request changes|changes requested)$/.test(normalized);
|
|
25
32
|
}
|
|
26
|
-
//#endregion
|
|
27
33
|
export { normalizedVerdictLine, requestsChangesVerdict };
|
|
28
34
|
|
|
29
35
|
//# sourceMappingURL=review-verdict.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-verdict.js","names":[],"sources":["../src/review-verdict.ts"],"sourcesContent":["/**\n * Normalizes the first non-empty line of a review body so its `Verdict:` line can be read\n * regardless of the markdown it is wrapped in (`**Verdict:** request changes`, `# Verdict: ...`).\n */\nexport function normalizedVerdictLine(body: string | undefined): string | undefined {\n const
|
|
1
|
+
{"version":3,"file":"review-verdict.js","names":[],"sources":["../src/review-verdict.ts"],"sourcesContent":["/**\n * The Platform GitHub proxy prefixes bodies it writes for a session with\n * `> Opened by @<actor>`, which pushes the skill's verdict off the first line.\n */\nconst ACTOR_ATTRIBUTION_LINE = /^>\\s*Opened by\\s+\\S+$/;\n\n/**\n * Normalizes the first non-empty line of a review body so its `Verdict:` line can be read\n * regardless of the markdown it is wrapped in (`**Verdict:** request changes`, `# Verdict: ...`).\n * A leading proxy attribution line is skipped.\n */\nexport function normalizedVerdictLine(body: string | undefined): string | undefined {\n const lines = body\n ?.split('\\n')\n .map(line => line.trim())\n .filter(line => line.length > 0);\n const firstLine = lines?.[0] && ACTOR_ATTRIBUTION_LINE.test(lines[0]) ? lines[1] : lines?.[0];\n if (!firstLine) return undefined;\n return firstLine\n .replaceAll(/[*_`#>\\s]+/g, ' ')\n .trim()\n .toLowerCase();\n}\n\n/**\n * Detects the `factory-review` handoff verdict in a comment body.\n *\n * GitHub forbids an app from reviewing a pull request it authored, so on\n * Factory-authored PRs the review skill falls back to posting its verdict as a\n * plain comment. That comment is the only signal the authoring agent gets, so\n * it has to be readable back out. The skill's handoff contract puts the verdict\n * on the first line (`Verdict: request changes`), so only that line is\n * inspected — a verdict quoted later in the findings must not count.\n */\nexport function requestsChangesVerdict(body: string | undefined): boolean {\n // Tolerate the markdown the skill wraps the line in (`**Verdict: ...**`).\n const normalized = normalizedVerdictLine(body);\n if (!normalized) return false;\n // Match the verdict exactly so negated phrasings (\"Verdict: do not request\n // changes\") cannot wake the author.\n return /^verdict: ?(request changes|changes requested)$/.test(normalized);\n}\n"],"mappings":";;;;;AAIA,MAAM,yBAAyB;;;;;;AAO/B,SAAgB,sBAAsB,MAA8C;CAClF,MAAM,QAAQ,MACV,MAAM,IAAI,CAAC,CACZ,KAAI,SAAQ,KAAK,KAAK,CAAC,CAAC,CACxB,QAAO,SAAQ,KAAK,SAAS,CAAC;CACjC,MAAM,YAAY,QAAQ,MAAM,uBAAuB,KAAK,MAAM,EAAE,IAAI,MAAM,KAAK,QAAQ;CAC3F,IAAI,CAAC,WAAW,OAAO,KAAA;CACvB,OAAO,UACJ,WAAW,eAAe,GAAG,CAAC,CAC9B,KAAK,CAAC,CACN,YAAY;AACjB;;;;;;;;;;;AAYA,SAAgB,uBAAuB,MAAmC;CAExE,MAAM,aAAa,sBAAsB,IAAI;CAC7C,IAAI,CAAC,YAAY,OAAO;CAGxB,OAAO,kDAAkD,KAAK,UAAU;AAC1E"}
|
|
@@ -2,7 +2,7 @@ import { boardForWorkItem } from "../boards/semantics.js";
|
|
|
2
2
|
import "../boards/index.js";
|
|
3
3
|
import { factoryActivityAttentionIdentity, factoryAttentionKey } from "../storage/domains/work-items/base.js";
|
|
4
4
|
import { collectPage, markScanRead, matchesView, receiptScope, scanBatches } from "./attention-providers.js";
|
|
5
|
-
|
|
5
|
+
// src/routes/attention-activity.ts
|
|
6
6
|
/** Projects `activity.ts` fan-out rows. */
|
|
7
7
|
var ActivityAttentionProvider = class {
|
|
8
8
|
kind = "activity";
|
|
@@ -149,7 +149,6 @@ function toActivityItem(scope, resolved) {
|
|
|
149
149
|
}
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
|
-
//#endregion
|
|
153
152
|
export { ActivityAttentionProvider };
|
|
154
153
|
|
|
155
154
|
//# sourceMappingURL=attention-activity.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { factoryAgentWaitingAttentionIdentity, factoryAttentionKey } from "../storage/domains/work-items/base.js";
|
|
2
2
|
import { matchesView, receiptScope } from "./attention-providers.js";
|
|
3
|
-
|
|
3
|
+
// src/routes/attention-parked.ts
|
|
4
4
|
function parkedRunLabel(toolName) {
|
|
5
5
|
return toolName === "submit_plan" ? "Plan waiting for review" : "Agent is waiting for an answer";
|
|
6
6
|
}
|
|
@@ -130,7 +130,6 @@ var ParkedRunAttentionProvider = class {
|
|
|
130
130
|
return { hasMore: false };
|
|
131
131
|
}
|
|
132
132
|
};
|
|
133
|
-
//#endregion
|
|
134
133
|
export { ParkedRunAttentionProvider };
|
|
135
134
|
|
|
136
135
|
//# sourceMappingURL=attention-parked.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { factoryProposalAttentionIdentity } from "../storage/domains/work-items/base.js";
|
|
2
2
|
import { factoryDecisionType } from "./attention-providers.js";
|
|
3
|
-
|
|
3
|
+
// src/routes/attention-proposed.ts
|
|
4
4
|
function proposedAction(decision) {
|
|
5
5
|
const { type, stage, role } = decision.decision;
|
|
6
6
|
if (type === "transition" && typeof stage === "string") return `move to ${stage.slice(0, 64)}`;
|
|
@@ -15,7 +15,6 @@ const proposedDecisionAttentionSpec = {
|
|
|
15
15
|
detail: (decision) => `Waiting for approval to ${proposedAction(decision)}`,
|
|
16
16
|
matches: (decision, item, search) => item?.title.toLowerCase().includes(search) === true || proposedAction(decision).toLowerCase().includes(search) || factoryDecisionType(decision).toLowerCase().includes(search)
|
|
17
17
|
};
|
|
18
|
-
//#endregion
|
|
19
18
|
export { proposedDecisionAttentionSpec };
|
|
20
19
|
|
|
21
20
|
//# sourceMappingURL=attention-proposed.js.map
|
|
@@ -3,7 +3,7 @@ import "../boards/index.js";
|
|
|
3
3
|
import { factoryDispatchFailureMetadata } from "../rules/dispatch-errors.js";
|
|
4
4
|
import { factoryAttentionKey, factoryDecisionAttentionIdentity, factoryMentionAttentionIdentity } from "../storage/domains/work-items/base.js";
|
|
5
5
|
import { decisionRetryable, overtakenDecisionIds } from "../rules/decision-applicability.js";
|
|
6
|
-
|
|
6
|
+
// src/routes/attention-providers.ts
|
|
7
7
|
/**
|
|
8
8
|
* Per-kind attention providers. Each kind owns its counts, its bounded page
|
|
9
9
|
* scan, and its wire item shape; the route layer k-way merges provider pages
|
|
@@ -391,7 +391,6 @@ function receiptScope(scope) {
|
|
|
391
391
|
userId: scope.userId
|
|
392
392
|
};
|
|
393
393
|
}
|
|
394
|
-
//#endregion
|
|
395
394
|
export { DecisionAttentionProvider, MentionAttentionProvider, SCAN_PAGE_SIZE, collectPage, factoryDecisionType, failedDecisionAttentionSpec, markScanRead, matchesView, receiptScope, scanBatches };
|
|
396
395
|
|
|
397
396
|
//# sourceMappingURL=attention-providers.js.map
|
package/dist/routes/attention.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ActivityAttentionProvider } from "./attention-activity.js";
|
|
|
5
5
|
import { ParkedRunAttentionProvider } from "./attention-parked.js";
|
|
6
6
|
import { proposedDecisionAttentionSpec } from "./attention-proposed.js";
|
|
7
7
|
import { registerApiRoute } from "@mastra/core/server";
|
|
8
|
-
|
|
8
|
+
// src/routes/attention.ts
|
|
9
9
|
function encodeAttentionCursor(cursors) {
|
|
10
10
|
const wire = {};
|
|
11
11
|
for (const [kind, position] of cursors) wire[kind] = position ? [position.occurredAt.toISOString(), position.id] : null;
|
|
@@ -212,7 +212,6 @@ function buildAttentionRoutes(dependencies) {
|
|
|
212
212
|
receiptRoute(dependencies, "restore", "restore")
|
|
213
213
|
];
|
|
214
214
|
}
|
|
215
|
-
//#endregion
|
|
216
215
|
export { buildAttentionRoutes, factoryDecisionType };
|
|
217
216
|
|
|
218
217
|
//# sourceMappingURL=attention.js.map
|
|
@@ -2,7 +2,7 @@ import { FACTORY_ROUTE_CONTRACTS } from "./contracts.js";
|
|
|
2
2
|
import { Route } from "./route.js";
|
|
3
3
|
import { auditRequestContext } from "../storage/domains/audit/domain.js";
|
|
4
4
|
import { registerApiRoute } from "@mastra/core/server";
|
|
5
|
-
|
|
5
|
+
// src/routes/automation-runs.ts
|
|
6
6
|
function loose(c) {
|
|
7
7
|
return c;
|
|
8
8
|
}
|
|
@@ -190,7 +190,6 @@ var AutomationRunRoutes = class extends Route {
|
|
|
190
190
|
function buildAutomationRunRoutes(deps) {
|
|
191
191
|
return new AutomationRunRoutes(deps).routes();
|
|
192
192
|
}
|
|
193
|
-
//#endregion
|
|
194
193
|
export { AutomationRunRoutes, buildAutomationRunRoutes };
|
|
195
194
|
|
|
196
195
|
//# sourceMappingURL=automation-runs.js.map
|