@mastra/factory 0.25.0-alpha.3 → 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.
Files changed (301) hide show
  1. package/dist/auth.js +1 -2
  2. package/dist/boards/define-board.js +1 -2
  3. package/dist/boards/index.js +1 -2
  4. package/dist/boards/registry.js +1 -2
  5. package/dist/boards/relocate.js +1 -2
  6. package/dist/boards/review.js +1 -2
  7. package/dist/boards/semantics.js +1 -2
  8. package/dist/boards/transition-policy.js +1 -2
  9. package/dist/boards/work-tool-rules.js +1 -2
  10. package/dist/boards/work-transition-policy.js +1 -2
  11. package/dist/boards/work.js +1 -2
  12. package/dist/environment/build-runner.js +1 -2
  13. package/dist/environment/build-schedule.js +1 -2
  14. package/dist/environment/build-workflow.js +1 -2
  15. package/dist/environment/build.js +1 -2
  16. package/dist/environment/heads.js +1 -2
  17. package/dist/factory.d.ts +4 -2
  18. package/dist/factory.d.ts.map +1 -1
  19. package/dist/factory.js +43 -16
  20. package/dist/factory.js.map +1 -1
  21. package/dist/feed-events.js +1 -2
  22. package/dist/index.js +2 -2
  23. package/dist/integrations/channel-thread-announcements.d.ts +8 -0
  24. package/dist/integrations/channel-thread-announcements.d.ts.map +1 -0
  25. package/dist/integrations/channel-thread-announcements.js +16 -0
  26. package/dist/integrations/channel-thread-announcements.js.map +1 -0
  27. package/dist/integrations/github/acceptance-labels.js +1 -2
  28. package/dist/integrations/github/app-identity.js +1 -2
  29. package/dist/integrations/github/commits.js +1 -2
  30. package/dist/integrations/github/config.js +1 -2
  31. package/dist/integrations/github/default-rules.js +2 -3
  32. package/dist/integrations/github/integration.js +3 -4
  33. package/dist/integrations/github/issue-reconciler.js +1 -2
  34. package/dist/integrations/github/pat.js +1 -2
  35. package/dist/integrations/github/provenance.d.ts.map +1 -1
  36. package/dist/integrations/github/provenance.js +2 -5
  37. package/dist/integrations/github/provenance.js.map +1 -1
  38. package/dist/integrations/github/reconcile-worker.js +1 -2
  39. package/dist/integrations/github/reconciliation-config.js +1 -2
  40. package/dist/integrations/github/routes.js +4 -5
  41. package/dist/integrations/github/rules.d.ts.map +1 -1
  42. package/dist/integrations/github/rules.js +14 -3
  43. package/dist/integrations/github/rules.js.map +1 -1
  44. package/dist/integrations/github/sandbox-release.js +1 -2
  45. package/dist/integrations/github/sandbox.js +1 -2
  46. package/dist/integrations/github/session-subscriptions.js +1 -2
  47. package/dist/integrations/github/settle-or-abort.js +1 -2
  48. package/dist/integrations/github/stale-reviews.js +1 -2
  49. package/dist/integrations/github/subscriptions.js +1 -2
  50. package/dist/integrations/github/token-refresh.js +1 -2
  51. package/dist/integrations/github/webhook.d.ts.map +1 -1
  52. package/dist/integrations/github/webhook.js +17 -3
  53. package/dist/integrations/github/webhook.js.map +1 -1
  54. package/dist/integrations/gitlab/agent-tools.js +1 -2
  55. package/dist/integrations/gitlab/api.js +1 -2
  56. package/dist/integrations/gitlab/default-rules.js +1 -2
  57. package/dist/integrations/gitlab/integration.js +2 -3
  58. package/dist/integrations/gitlab/issue-reconciler.js +1 -2
  59. package/dist/integrations/gitlab/merge-request-reconciler.js +1 -2
  60. package/dist/integrations/gitlab/reconciler.js +1 -2
  61. package/dist/integrations/gitlab/reconciliation-config.js +1 -2
  62. package/dist/integrations/gitlab/routes.js +1 -2
  63. package/dist/integrations/gitlab/rules.js +1 -2
  64. package/dist/integrations/gitlab/session-subscriptions.js +1 -2
  65. package/dist/integrations/gitlab/subscriptions.js +1 -2
  66. package/dist/integrations/gitlab/version-control.js +1 -2
  67. package/dist/integrations/gitlab/webhook-dispatch.js +1 -2
  68. package/dist/integrations/gitlab/webhook.js +1 -2
  69. package/dist/integrations/incidentio/agent-tools.js +1 -2
  70. package/dist/integrations/incidentio/api.js +1 -2
  71. package/dist/integrations/incidentio/claim.js +1 -2
  72. package/dist/integrations/incidentio/default-rules.js +1 -2
  73. package/dist/integrations/incidentio/intake.js +1 -2
  74. package/dist/integrations/incidentio/integration.js +1 -2
  75. package/dist/integrations/incidentio/issue-reconciler.js +1 -2
  76. package/dist/integrations/incidentio/reconciliation-config.js +1 -2
  77. package/dist/integrations/incidentio/routes.js +1 -2
  78. package/dist/integrations/incidentio/rules.js +1 -2
  79. package/dist/integrations/issue-reconcile-worker.js +1 -2
  80. package/dist/integrations/issue-reconciler.js +1 -2
  81. package/dist/integrations/jira/adf.js +1 -2
  82. package/dist/integrations/jira/agent-tools.js +1 -2
  83. package/dist/integrations/jira/api.js +1 -2
  84. package/dist/integrations/jira/claim.js +1 -2
  85. package/dist/integrations/jira/default-rules.js +1 -2
  86. package/dist/integrations/jira/integration.js +1 -2
  87. package/dist/integrations/jira/issue-reconciler.js +1 -2
  88. package/dist/integrations/jira/reconciliation-config.js +1 -2
  89. package/dist/integrations/jira/routes.js +1 -2
  90. package/dist/integrations/jira/rules.js +1 -2
  91. package/dist/integrations/linear/agent-tools.d.ts +1 -1
  92. package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
  93. package/dist/integrations/linear/agent-tools.js +5 -5
  94. package/dist/integrations/linear/agent-tools.js.map +1 -1
  95. package/dist/integrations/linear/claim.js +1 -2
  96. package/dist/integrations/linear/default-rules.js +1 -2
  97. package/dist/integrations/linear/integration.d.ts +2 -0
  98. package/dist/integrations/linear/integration.d.ts.map +1 -1
  99. package/dist/integrations/linear/integration.js +2 -2
  100. package/dist/integrations/linear/integration.js.map +1 -1
  101. package/dist/integrations/linear/issue-reconciler.js +1 -2
  102. package/dist/integrations/linear/reconciliation-config.js +1 -2
  103. package/dist/integrations/linear/routes.js +1 -2
  104. package/dist/integrations/linear/rules.js +1 -2
  105. package/dist/integrations/platform/api-client.js +1 -2
  106. package/dist/integrations/platform/connect/routes.js +1 -2
  107. package/dist/integrations/platform/github/event-worker.js +6 -2
  108. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  109. package/dist/integrations/platform/github/integration.js +1 -2
  110. package/dist/integrations/platform/gitlab/event-worker.js +1 -2
  111. package/dist/integrations/platform/gitlab/integration.js +1 -2
  112. package/dist/integrations/platform/incidentio/integration.js +1 -2
  113. package/dist/integrations/platform/jira/agent-tools.js +1 -2
  114. package/dist/integrations/platform/jira/integration.js +1 -2
  115. package/dist/integrations/platform/jira/routes.js +1 -2
  116. package/dist/integrations/platform/linear/event-worker.js +1 -2
  117. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  118. package/dist/integrations/platform/linear/integration.js +2 -2
  119. package/dist/integrations/platform/linear/integration.js.map +1 -1
  120. package/dist/integrations/pull-request-head.js +1 -2
  121. package/dist/integrations/reconciliation-config.js +1 -2
  122. package/dist/integrations/slack/channel-reply-processor.d.ts.map +1 -1
  123. package/dist/integrations/slack/channel-reply-processor.js +11 -5
  124. package/dist/integrations/slack/channel-reply-processor.js.map +1 -1
  125. package/dist/integrations/slack/channel-session-link-processor.js +1 -2
  126. package/dist/integrations/slack/connect-route.js +1 -2
  127. package/dist/integrations/slack/emoji-shortcodes.generated.js +1 -2
  128. package/dist/integrations/slack/emoji.js +1 -2
  129. package/dist/integrations/slack/feed-publisher.js +1 -2
  130. package/dist/integrations/slack/integration.d.ts +5 -0
  131. package/dist/integrations/slack/integration.d.ts.map +1 -1
  132. package/dist/integrations/slack/integration.js +13 -2
  133. package/dist/integrations/slack/integration.js.map +1 -1
  134. package/dist/integrations/slack/reply-reminder-processor.js +1 -2
  135. package/dist/integrations/slack/reply-tool.js +1 -2
  136. package/dist/integrations/slack/share-file-tool.js +1 -2
  137. package/dist/integrations/slack/slack.js +2 -3
  138. package/dist/integrations/subscription-session.js +1 -2
  139. package/dist/integrations/workos/integration.js +1 -2
  140. package/dist/mcp/disconnect.js +1 -2
  141. package/dist/mcp/events.js +2 -3
  142. package/dist/mcp/routes.js +1 -2
  143. package/dist/packages/_internals/workspace/dist/index.js +1 -2
  144. package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
  145. package/dist/platform-auth-host.js +1 -2
  146. package/dist/provider-display-name.js +1 -2
  147. package/dist/review-verdict.d.ts +1 -0
  148. package/dist/review-verdict.d.ts.map +1 -1
  149. package/dist/review-verdict.js +9 -3
  150. package/dist/review-verdict.js.map +1 -1
  151. package/dist/routes/attention-activity.js +1 -2
  152. package/dist/routes/attention-parked.js +1 -2
  153. package/dist/routes/attention-proposed.js +1 -2
  154. package/dist/routes/attention-providers.js +1 -2
  155. package/dist/routes/attention.js +1 -2
  156. package/dist/routes/automation-runs.js +1 -2
  157. package/dist/routes/config.d.ts +4 -0
  158. package/dist/routes/config.d.ts.map +1 -1
  159. package/dist/routes/config.js +7 -2
  160. package/dist/routes/config.js.map +1 -1
  161. package/dist/routes/contracts.js +1 -2
  162. package/dist/routes/custom-provider-source.js +1 -2
  163. package/dist/routes/fs.js +1 -2
  164. package/dist/routes/intake.js +1 -2
  165. package/dist/routes/knowledge.js +1 -2
  166. package/dist/routes/oauth.js +1 -2
  167. package/dist/routes/projects.js +1 -2
  168. package/dist/routes/provider-credentials.js +1 -2
  169. package/dist/routes/route.js +1 -2
  170. package/dist/routes/session-access.js +1 -2
  171. package/dist/routes/skills.js +1 -2
  172. package/dist/routes/source-control-sessions.js +5 -6
  173. package/dist/routes/source-control-settings.js +1 -2
  174. package/dist/routes/surface.js +2 -3
  175. package/dist/routes/telemetry.js +1 -2
  176. package/dist/routes/tenant-credentials.js +1 -2
  177. package/dist/routes/work-items.js +2 -3
  178. package/dist/rules/binding-context.js +1 -2
  179. package/dist/rules/decision-applicability.js +1 -2
  180. package/dist/rules/dispatch-errors.js +1 -2
  181. package/dist/rules/dispatcher.js +1 -2
  182. package/dist/rules/event-rules.js +1 -2
  183. package/dist/rules/processor.js +1 -2
  184. package/dist/rules/resolve.js +1 -2
  185. package/dist/rules/review-source-tool.js +1 -2
  186. package/dist/rules/start-coordinator.js +1 -2
  187. package/dist/rules/terminal-cleanup.js +1 -2
  188. package/dist/rules/tools.js +1 -2
  189. package/dist/rules/transition-service.js +1 -2
  190. package/dist/rules/types.d.ts +4 -1
  191. package/dist/rules/types.d.ts.map +1 -1
  192. package/dist/rules/types.js +1 -2
  193. package/dist/rules/types.js.map +1 -1
  194. package/dist/rules/validation.js +1 -2
  195. package/dist/sandbox/callback-factory-sandbox.js +1 -2
  196. package/dist/sandbox/git-ref.js +1 -2
  197. package/dist/sandbox/local-factory-sandbox.d.ts +12 -4
  198. package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
  199. package/dist/sandbox/local-factory-sandbox.js +32 -7
  200. package/dist/sandbox/local-factory-sandbox.js.map +1 -1
  201. package/dist/sandbox/materialization.js +1 -2
  202. package/dist/sandbox/session-retirement.js +1 -2
  203. package/dist/sandbox/session-sandbox.js +1 -2
  204. package/dist/sandbox/workdir.js +1 -2
  205. package/dist/secret-encryption.js +1 -2
  206. package/dist/server-error.js +1 -2
  207. package/dist/session/credential-policy.js +1 -2
  208. package/dist/session/default-model-hydration.js +1 -2
  209. package/dist/session/environment-repositories.js +1 -2
  210. package/dist/session/environment-state-processor.d.ts +0 -1
  211. package/dist/session/environment-state-processor.d.ts.map +1 -1
  212. package/dist/session/environment-state-processor.js +2 -3
  213. package/dist/session/environment-state-processor.js.map +1 -1
  214. package/dist/session/factory-session.js +1 -2
  215. package/dist/session/filesystem-capture.js +2 -3
  216. package/dist/session/filesystem-capture.js.map +1 -1
  217. package/dist/session/first-exec-capture.js +2 -3
  218. package/dist/session/first-exec-capture.js.map +1 -1
  219. package/dist/session/first-message-capture.js +1 -2
  220. package/dist/session/live-sessions.js +1 -2
  221. package/dist/session/memory-settings-hydration.js +1 -2
  222. package/dist/session/org-seed.js +1 -2
  223. package/dist/session/reply-credential-processor.js +1 -2
  224. package/dist/session/resource-migration.js +1 -2
  225. package/dist/session/run-audit.js +1 -2
  226. package/dist/session/session-threads.js +1 -2
  227. package/dist/session/session-title.js +1 -2
  228. package/dist/session/shell-commands.js +1 -2
  229. package/dist/session/source-control-tools.js +2 -3
  230. package/dist/session/thinking-default.js +1 -2
  231. package/dist/session/thread-session-stamp.js +1 -2
  232. package/dist/session/thread-title-mirror.js +1 -2
  233. package/dist/session/thread-title-mirror.js.map +1 -1
  234. package/dist/session/work-item-repository.js +1 -2
  235. package/dist/session-owner-signal.js +1 -2
  236. package/dist/skills/catalog.js +1 -2
  237. package/dist/skills/service.js +1 -2
  238. package/dist/spa-static.js +1 -2
  239. package/dist/state-signing.js +1 -2
  240. package/dist/storage/domains/audit/actions.js +1 -2
  241. package/dist/storage/domains/audit/actors.js +1 -2
  242. package/dist/storage/domains/audit/agent-audit.js +1 -2
  243. package/dist/storage/domains/audit/base.js +1 -2
  244. package/dist/storage/domains/audit/domain.js +1 -2
  245. package/dist/storage/domains/audit/wire.js +1 -2
  246. package/dist/storage/domains/channel-identity/base.js +1 -2
  247. package/dist/storage/domains/comments/activity.js +1 -2
  248. package/dist/storage/domains/comments/actor.js +1 -2
  249. package/dist/storage/domains/comments/base.js +1 -2
  250. package/dist/storage/domains/comments/domain.js +1 -2
  251. package/dist/storage/domains/comments/feed-context.js +1 -2
  252. package/dist/storage/domains/comments/parse.js +1 -2
  253. package/dist/storage/domains/comments/routes.js +1 -2
  254. package/dist/storage/domains/comments/schema.js +1 -2
  255. package/dist/storage/domains/comments/wire.js +1 -2
  256. package/dist/storage/domains/credentials/base.d.ts +4 -1
  257. package/dist/storage/domains/credentials/base.d.ts.map +1 -1
  258. package/dist/storage/domains/credentials/base.js +40 -44
  259. package/dist/storage/domains/credentials/base.js.map +1 -1
  260. package/dist/storage/domains/custom-providers/base.js +1 -2
  261. package/dist/storage/domains/filesystem/base.js +1 -2
  262. package/dist/storage/domains/intake/base.js +1 -2
  263. package/dist/storage/domains/integrations/base.js +1 -2
  264. package/dist/storage/domains/memory-settings/base.js +1 -2
  265. package/dist/storage/domains/model-defaults/base.js +1 -2
  266. package/dist/storage/domains/projects/base.js +1 -2
  267. package/dist/storage/domains/queue-health/base.js +1 -2
  268. package/dist/storage/domains/source-control/base.js +1 -2
  269. package/dist/storage/domains/source-control/inmemory.js +1 -2
  270. package/dist/storage/domains/work-items/base.js +1 -2
  271. package/dist/storage/domains/work-items/board-version.js +1 -2
  272. package/dist/storage/domains/work-items/metrics.js +1 -2
  273. package/dist/telemetry-types.js +1 -2
  274. package/dist/telemetry.js +1 -2
  275. package/dist/timing.js +1 -2
  276. package/dist/tools/annotations.js +1 -2
  277. package/dist/tools/approval-tools.js +1 -2
  278. package/dist/tools/deliver.js +2 -3
  279. package/dist/tools/index.js +1 -2
  280. package/dist/tools/messaging-tools.js +1 -2
  281. package/dist/tools/permissions.js +1 -2
  282. package/dist/tools/read-tools.js +1 -2
  283. package/dist/tools/reports.js +1 -2
  284. package/dist/tools/run-watch.js +1 -2
  285. package/dist/tools/sandbox-tools.js +1 -2
  286. package/dist/tools/scope.js +2 -3
  287. package/dist/tools/session-tools.js +1 -2
  288. package/dist/tools/sessions-state-processor.js +1 -2
  289. package/dist/tools/side-sessions.js +1 -2
  290. package/dist/tools/work-item-tools.js +1 -2
  291. package/dist/work-item-branch.js +1 -2
  292. package/dist/work-item-branch.js.map +1 -1
  293. package/dist/work-items/create-work-item.js +1 -2
  294. package/dist/work-items/decision-actions.js +1 -2
  295. package/dist/workspace.d.ts.map +1 -1
  296. package/dist/workspace.js +1 -3
  297. package/dist/workspace.js.map +1 -1
  298. package/factory-skills/factory-rereview/SKILL.md +12 -2
  299. package/factory-skills/factory-review/SKILL.md +12 -2
  300. package/factory-skills/factory-triage/SKILL.md +7 -7
  301. 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
- //#region src/integrations/slack/reply-reminder-processor.ts
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
- //#region src/integrations/slack/reply-tool.ts
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
- //#region src/integrations/slack/share-file-tool.ts
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
- //#region src/integrations/slack/slack.ts
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
- //#region src/integrations/subscription-session.ts
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
- //#region src/integrations/workos/integration.ts
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
@@ -1,4 +1,4 @@
1
- //#region src/mcp/disconnect.ts
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
@@ -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
- //#region src/mcp/events.ts
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
@@ -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
- //#region src/mcp/routes.ts
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
- //#region ../../packages/_internals/workspace/dist/index.js
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
- //#region src/platform-auth-host.ts
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
- //#region src/provider-display-name.ts
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
@@ -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":"AAAA;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAUlF;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAOxE"}
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"}
@@ -1,10 +1,17 @@
1
- //#region src/review-verdict.ts
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 firstLine = body?.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
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 firstLine = body\n ?.split('\\n')\n .map(line => line.trim())\n .find(line => line.length > 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,SAAgB,sBAAsB,MAA8C;CAClF,MAAM,YAAY,MACd,MAAM,IAAI,CAAC,CACZ,KAAI,SAAQ,KAAK,KAAK,CAAC,CAAC,CACxB,MAAK,SAAQ,KAAK,SAAS,CAAC;CAC/B,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"}
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
- //#region src/routes/attention-activity.ts
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
- //#region src/routes/attention-parked.ts
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
- //#region src/routes/attention-proposed.ts
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
- //#region src/routes/attention-providers.ts
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
@@ -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
- //#region src/routes/attention.ts
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
- //#region src/routes/automation-runs.ts
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