@mastra/factory 0.24.0-alpha.9 → 0.25.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -13
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +18 -4
- package/dist/auth.js.map +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +3 -2
- package/dist/boards/review.js.map +1 -1
- package/dist/environment/build-runner.js +2 -1
- package/dist/environment/build-runner.js.map +1 -1
- package/dist/environment/build-workflow.js +1 -1
- package/dist/environment/build-workflow.js.map +1 -1
- package/dist/environment/build.js +16 -14
- package/dist/environment/build.js.map +1 -1
- package/dist/factory.d.ts +3 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +172 -88
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +15 -2
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/app-identity.js.map +1 -1
- package/dist/integrations/github/integration.js +1 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +1 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +24 -13
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +53 -9
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +2 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +1 -2
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.d.ts +7 -4
- package/dist/integrations/github/subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/subscriptions.js +5 -5
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/integration.js.map +1 -1
- package/dist/integrations/gitlab/reconciliation-config.js +2 -1
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
- package/dist/integrations/gitlab/routes.d.ts.map +1 -1
- package/dist/integrations/gitlab/routes.js +10 -6
- package/dist/integrations/gitlab/routes.js.map +1 -1
- package/dist/integrations/gitlab/rules.js.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.js +1 -2
- package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/subscriptions.d.ts +7 -4
- package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/subscriptions.js +5 -5
- package/dist/integrations/gitlab/subscriptions.js.map +1 -1
- package/dist/integrations/incidentio/api.js.map +1 -1
- package/dist/integrations/incidentio/integration.js.map +1 -1
- package/dist/integrations/incidentio/rules.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.js +1 -1
- package/dist/integrations/issue-reconcile-worker.js.map +1 -1
- package/dist/integrations/issue-reconciler.js +1 -1
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/jira/api.js.map +1 -1
- package/dist/integrations/jira/integration.js.map +1 -1
- package/dist/integrations/jira/rules.js.map +1 -1
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +1 -1
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +2 -2
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
- package/dist/integrations/platform/gitlab/integration.js.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js +2 -1
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/{session → integrations/slack}/channel-reply-processor.d.ts +11 -9
- package/dist/integrations/slack/channel-reply-processor.d.ts.map +1 -0
- package/dist/integrations/slack/channel-reply-processor.js +61 -0
- package/dist/integrations/slack/channel-reply-processor.js.map +1 -0
- package/dist/integrations/slack/channel-session-link-processor.d.ts.map +1 -0
- package/dist/{session → integrations/slack}/channel-session-link-processor.js +14 -4
- package/dist/integrations/slack/channel-session-link-processor.js.map +1 -0
- package/dist/integrations/slack/connect-route.js +1 -1
- package/dist/integrations/slack/connect-route.js.map +1 -1
- package/dist/integrations/slack/feed-publisher.js +1 -1
- package/dist/integrations/slack/feed-publisher.js.map +1 -1
- package/dist/integrations/slack/integration.d.ts +25 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +56 -6
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/reply-reminder-processor.d.ts +23 -0
- package/dist/integrations/slack/reply-reminder-processor.d.ts.map +1 -0
- package/dist/integrations/slack/reply-reminder-processor.js +58 -0
- package/dist/integrations/slack/reply-reminder-processor.js.map +1 -0
- package/dist/integrations/slack/reply-tool.d.ts +31 -0
- package/dist/integrations/slack/reply-tool.d.ts.map +1 -0
- package/dist/integrations/slack/reply-tool.js +203 -0
- package/dist/integrations/slack/reply-tool.js.map +1 -0
- package/dist/integrations/slack/share-file-tool.d.ts +45 -0
- package/dist/integrations/slack/share-file-tool.d.ts.map +1 -0
- package/dist/integrations/slack/share-file-tool.js +106 -0
- package/dist/integrations/slack/share-file-tool.js.map +1 -0
- package/dist/integrations/slack/slack.d.ts +16 -5
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +141 -40
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +0 -1
- package/dist/integrations/subscription-session.d.ts.map +1 -1
- package/dist/integrations/subscription-session.js +1 -4
- package/dist/integrations/subscription-session.js.map +1 -1
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/mcp/events.d.ts +73 -0
- package/dist/mcp/events.d.ts.map +1 -0
- package/dist/mcp/events.js +158 -0
- package/dist/mcp/events.js.map +1 -0
- package/dist/mcp/routes.d.ts +0 -1
- package/dist/mcp/routes.d.ts.map +1 -1
- package/dist/mcp/routes.js +31 -4
- package/dist/mcp/routes.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js +18 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/platform-auth-host.d.ts.map +1 -1
- package/dist/platform-auth-host.js +1 -1
- package/dist/platform-auth-host.js.map +1 -1
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-parked.js.map +1 -1
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/automation-runs.js.map +1 -1
- package/dist/routes/config.d.ts +8 -7
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +70 -28
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.d.ts +2 -0
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +2 -2
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.d.ts +31 -23
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +218 -64
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +2 -1
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/knowledge.js +1 -1
- package/dist/routes/knowledge.js.map +1 -1
- package/dist/routes/oauth.js +3 -2
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.d.ts +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +33 -31
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.d.ts.map +1 -1
- package/dist/routes/provider-credentials.js +6 -1
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/session-access.d.ts +60 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +202 -0
- package/dist/routes/session-access.js.map +1 -0
- package/dist/routes/skills.d.ts +2 -4
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/skills.js +23 -68
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +8 -1
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +239 -19
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +7 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +20 -8
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +11 -9
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +21 -13
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts +2 -2
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +3 -3
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +4 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +36 -4
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +2 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/sandbox/session-retirement.d.ts +11 -0
- package/dist/sandbox/session-retirement.d.ts.map +1 -1
- package/dist/sandbox/session-retirement.js +38 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/sandbox/session-sandbox.d.ts +2 -0
- package/dist/sandbox/session-sandbox.d.ts.map +1 -1
- package/dist/sandbox/session-sandbox.js +5 -1
- package/dist/sandbox/session-sandbox.js.map +1 -1
- package/dist/secret-encryption.d.ts.map +1 -1
- package/dist/secret-encryption.js +65 -25
- package/dist/secret-encryption.js.map +1 -1
- package/dist/session/credential-policy.d.ts +4 -2
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js +18 -7
- package/dist/session/credential-policy.js.map +1 -1
- package/dist/session/default-model-hydration.d.ts +1 -0
- package/dist/session/default-model-hydration.d.ts.map +1 -1
- package/dist/session/default-model-hydration.js +3 -2
- package/dist/session/default-model-hydration.js.map +1 -1
- package/dist/session/environment-state-processor.js +1 -1
- package/dist/session/environment-state-processor.js.map +1 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +14 -1
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +3 -3
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +16 -17
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/first-exec-capture.d.ts +1 -1
- package/dist/session/first-exec-capture.d.ts.map +1 -1
- package/dist/session/first-exec-capture.js +1 -1
- package/dist/session/first-exec-capture.js.map +1 -1
- package/dist/session/first-message-capture.d.ts +1 -1
- package/dist/session/first-message-capture.d.ts.map +1 -1
- package/dist/session/first-message-capture.js +1 -1
- package/dist/session/first-message-capture.js.map +1 -1
- package/dist/session/live-sessions.d.ts +1 -1
- package/dist/session/live-sessions.js +1 -1
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/session/memory-settings-hydration.d.ts +1 -1
- package/dist/session/memory-settings-hydration.d.ts.map +1 -1
- package/dist/session/memory-settings-hydration.js +2 -1
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/resource-migration.d.ts +105 -0
- package/dist/session/resource-migration.d.ts.map +1 -0
- package/dist/session/resource-migration.js +195 -0
- package/dist/session/resource-migration.js.map +1 -0
- package/dist/session/run-audit.d.ts +1 -0
- package/dist/session/run-audit.d.ts.map +1 -1
- package/dist/session/run-audit.js +7 -2
- package/dist/session/run-audit.js.map +1 -1
- package/dist/session/session-threads.d.ts +63 -0
- package/dist/session/session-threads.d.ts.map +1 -0
- package/dist/session/session-threads.js +97 -0
- package/dist/session/session-threads.js.map +1 -0
- package/dist/session/source-control-tools.js +8 -8
- package/dist/session/source-control-tools.js.map +1 -1
- package/dist/session/thinking-default.d.ts +6 -13
- package/dist/session/thinking-default.d.ts.map +1 -1
- package/dist/session/thinking-default.js +19 -24
- package/dist/session/thinking-default.js.map +1 -1
- package/dist/session/thread-session-stamp.d.ts +21 -0
- package/dist/session/thread-session-stamp.d.ts.map +1 -0
- package/dist/session/thread-session-stamp.js +20 -0
- package/dist/session/thread-session-stamp.js.map +1 -0
- package/dist/session/thread-title-mirror.d.ts +1 -1
- package/dist/session/thread-title-mirror.d.ts.map +1 -1
- package/dist/session/thread-title-mirror.js +1 -1
- package/dist/session/thread-title-mirror.js.map +1 -1
- package/dist/session-owner-signal.d.ts +2 -0
- package/dist/session-owner-signal.d.ts.map +1 -0
- package/dist/session-owner-signal.js +6 -0
- package/dist/session-owner-signal.js.map +1 -0
- package/dist/skills/service.d.ts +11 -14
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +12 -22
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +1 -1
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/actions.d.ts +1 -0
- package/dist/storage/domains/audit/actions.d.ts.map +1 -1
- package/dist/storage/domains/audit/actions.js +2 -1
- package/dist/storage/domains/audit/actions.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +2 -2
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.d.ts +18 -2
- package/dist/storage/domains/audit/base.d.ts.map +1 -1
- package/dist/storage/domains/audit/base.js +22 -4
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/channel-identity/base.js.map +1 -1
- package/dist/storage/domains/comments/base.js.map +1 -1
- package/dist/storage/domains/comments/domain.js.map +1 -1
- package/dist/storage/domains/comments/feed-context.js.map +1 -1
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.d.ts +8 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -1
- package/dist/storage/domains/integrations/base.js +7 -0
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-defaults/base.d.ts +8 -0
- package/dist/storage/domains/model-defaults/base.d.ts.map +1 -1
- package/dist/storage/domains/model-defaults/base.js +18 -0
- package/dist/storage/domains/model-defaults/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +44 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +109 -3
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +23 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +35 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +14 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +25 -1
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/board-version.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/tools/approval-tools.js +1 -1
- package/dist/tools/approval-tools.js.map +1 -1
- package/dist/tools/deliver.d.ts +8 -5
- package/dist/tools/deliver.d.ts.map +1 -1
- package/dist/tools/deliver.js +22 -8
- package/dist/tools/deliver.js.map +1 -1
- package/dist/tools/messaging-tools.d.ts +1 -1
- package/dist/tools/messaging-tools.d.ts.map +1 -1
- package/dist/tools/messaging-tools.js +203 -72
- package/dist/tools/messaging-tools.js.map +1 -1
- package/dist/tools/permissions.d.ts +1 -1
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/permissions.js +3 -2
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/reports.d.ts.map +1 -1
- package/dist/tools/reports.js +10 -3
- package/dist/tools/reports.js.map +1 -1
- package/dist/tools/run-watch.d.ts +2 -0
- package/dist/tools/run-watch.d.ts.map +1 -1
- package/dist/tools/run-watch.js +42 -2
- package/dist/tools/run-watch.js.map +1 -1
- package/dist/tools/sandbox-tools.d.ts +12 -0
- package/dist/tools/sandbox-tools.d.ts.map +1 -0
- package/dist/tools/sandbox-tools.js +40 -0
- package/dist/tools/sandbox-tools.js.map +1 -0
- package/dist/tools/scope.d.ts +2 -1
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +5 -7
- package/dist/tools/scope.js.map +1 -1
- package/dist/tools/session-tools.d.ts +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +39 -18
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/sessions-state-processor.js +2 -2
- package/dist/tools/sessions-state-processor.js.map +1 -1
- package/dist/tools/side-sessions.d.ts +1 -1
- package/dist/tools/side-sessions.d.ts.map +1 -1
- package/dist/tools/side-sessions.js +1 -1
- package/dist/tools/side-sessions.js.map +1 -1
- package/dist/work-item-branch.d.ts +11 -3
- package/dist/work-item-branch.d.ts.map +1 -1
- package/dist/work-item-branch.js +34 -9
- package/dist/work-item-branch.js.map +1 -1
- package/dist/workspace.d.ts +7 -3
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +32 -17
- package/dist/workspace.js.map +1 -1
- package/factory-skills/configure-factory-rules/SKILL.md +2 -2
- package/package.json +16 -15
- package/dist/session/channel-reply-processor.d.ts.map +0 -1
- package/dist/session/channel-reply-processor.js +0 -46
- package/dist/session/channel-reply-processor.js.map +0 -1
- package/dist/session/channel-session-link-processor.d.ts.map +0 -1
- package/dist/session/channel-session-link-processor.js.map +0 -1
- /package/dist/{session → integrations/slack}/channel-session-link-processor.d.ts +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { readFactorySessionScope } from "
|
|
1
|
+
import { readFactorySessionScope } from "../../rules/binding-context.js";
|
|
2
2
|
import { SESSION_LINK_LABEL, sessionLink } from "./channel-reply-processor.js";
|
|
3
|
-
|
|
3
|
+
import { CHAT_CHANNEL_RENDER_CONTEXT_KEY } from "@mastra/core/channels";
|
|
4
|
+
//#region src/integrations/slack/channel-session-link-processor.ts
|
|
4
5
|
/**
|
|
5
6
|
* Request-context marker a channel handler sets on the run that opens a new
|
|
6
7
|
* thread. Only that run gets the link; follow-ups in the same thread don't.
|
|
@@ -45,7 +46,7 @@ const LINK_PART_ID = "factory-session-link";
|
|
|
45
46
|
var FactoryChannelSessionLinkProcessor = class {
|
|
46
47
|
id = "factory-channel-session-link";
|
|
47
48
|
async processOutputStream({ part, state, requestContext }) {
|
|
48
|
-
if (state.linked || !requestContext
|
|
49
|
+
if (state.linked || !linksThisRun(requestContext)) return part;
|
|
49
50
|
if (part.type === "text-delta") {
|
|
50
51
|
state.textStarted = true;
|
|
51
52
|
return part;
|
|
@@ -66,7 +67,7 @@ var FactoryChannelSessionLinkProcessor = class {
|
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
69
|
processOutputStep({ messageList, requestContext }) {
|
|
69
|
-
if (!requestContext
|
|
70
|
+
if (!linksThisRun(requestContext)) return messageList;
|
|
70
71
|
const link = sessionLinkFromRequest(requestContext);
|
|
71
72
|
if (!link) return messageList;
|
|
72
73
|
const suffix = linkSuffix(link);
|
|
@@ -81,6 +82,14 @@ var FactoryChannelSessionLinkProcessor = class {
|
|
|
81
82
|
return messageList;
|
|
82
83
|
}
|
|
83
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* With reply text hidden the reply text never reaches the thread, and
|
|
87
|
+
* `reply_in_thread` appends the link to its own first post instead.
|
|
88
|
+
*/
|
|
89
|
+
function linksThisRun(requestContext) {
|
|
90
|
+
if (!requestContext?.get("factory__newChannelSession")) return false;
|
|
91
|
+
return requestContext.get(CHAT_CHANNEL_RENDER_CONTEXT_KEY)?.textDisplay !== "hidden";
|
|
92
|
+
}
|
|
84
93
|
function linkSuffix(link) {
|
|
85
94
|
return `\n\n[${SESSION_LINK_LABEL}](${link})`;
|
|
86
95
|
}
|
|
@@ -89,6 +98,7 @@ function sessionLinkFromRequest(requestContext) {
|
|
|
89
98
|
if (!controller?.threadId) return void 0;
|
|
90
99
|
return sessionLink({
|
|
91
100
|
threadId: controller.threadId,
|
|
101
|
+
sessionId: controller.sessionId,
|
|
92
102
|
resourceId: controller.resourceId,
|
|
93
103
|
factoryProjectId: readFactorySessionScope(requestContext)?.factoryProjectId
|
|
94
104
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-session-link-processor.js","names":[],"sources":["../../../src/integrations/slack/channel-session-link-processor.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport { CHAT_CHANNEL_RENDER_CONTEXT_KEY } from '@mastra/core/channels';\nimport type { ChatChannelRenderContext } from '@mastra/core/channels';\nimport type { Processor, ProcessOutputStepArgs, ProcessOutputStreamArgs } from '@mastra/core/processors';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ChunkType } from '@mastra/core/stream';\n\nimport { readFactorySessionScope } from '../../rules/binding-context.js';\nimport { SESSION_LINK_LABEL, sessionLink } from './channel-reply-processor.js';\n\n/**\n * Request-context marker a channel handler sets on the run that opens a new\n * thread. Only that run gets the link; follow-ups in the same thread don't.\n */\nexport const NEW_CHANNEL_SESSION_KEY = 'factory__newChannelSession';\n\nexport function markNewChannelSession(requestContext: RequestContext): void {\n requestContext.set(NEW_CHANNEL_SESSION_KEY, true);\n}\n\n/**\n * State key the core ProcessorRunner reads to re-drive one extra part through\n * the output chain after the part a processor returned. Internal to core\n * (`processors/stream-reprocess.ts`, used by `BatchPartsProcessor`); mirrored\n * here rather than exported so Factory's one use doesn't widen core's API.\n */\nconst REPROCESS_PART_KEY = '__mastraReprocessPart';\n\n/** Text-part id of the link delta; never collides with a model block id. */\nconst LINK_PART_ID = 'factory-session-link';\n\n/**\n * Emits an `Open session ↗` link right after the first reply of a new channel\n * session, so the link closes the reply rather than leading it. The channel\n * driver ends a post at `text-end`, so Slack shows the link as its own short\n * message after the reply.\n *\n * \"End of the reply\" is not `text-end`: a reply with citations is several\n * text blocks (`text … source … text`), each with its own `text-end`. The\n * reply ends at the first `tool-call` (the opener before tools run) or at\n * `step-finish` (a reply with no tools), whichever comes first after some\n * text. A processor can only return one part, so on that chunk this returns\n * a `text-delta` carrying the link and stashes the chunk itself under\n * `REPROCESS_PART_KEY`, which the runner re-drives through the chain right\n * after — the link lands before it and downstream processors see both.\n * Factory-emitted rather than left to the model so a skipped opener can't\n * drop it. No-op without the new-session marker or a public origin.\n *\n * The streamed text is also what gets persisted, and the link is a Slack\n * display detail: in memory it would show the model its own `[↗](…)` on\n * later turns (inviting it to fabricate links) and render a self-link in\n * the web transcript. The link delta opens its own text part, so\n * `processOutputStep` drops that part from the assistant message before\n * `MessageHistory` saves it.\n */\nexport class FactoryChannelSessionLinkProcessor implements Processor {\n readonly id = 'factory-channel-session-link';\n\n async processOutputStream({ part, state, requestContext }: ProcessOutputStreamArgs): Promise<ChunkType> {\n if (state.linked || !linksThisRun(requestContext)) return part;\n if (part.type === 'text-delta') {\n state.textStarted = true;\n return part;\n }\n if ((part.type !== 'tool-call' && part.type !== 'step-finish') || !state.textStarted) return part;\n state.linked = true;\n const link = sessionLinkFromRequest(requestContext);\n if (!link) return part;\n state[REPROCESS_PART_KEY] = part;\n return {\n type: 'text-delta',\n runId: part.runId,\n from: part.from,\n payload: { id: LINK_PART_ID, text: linkSuffix(link) },\n };\n }\n\n processOutputStep({ messageList, requestContext }: ProcessOutputStepArgs) {\n if (!linksThisRun(requestContext)) return messageList;\n const link = sessionLinkFromRequest(requestContext);\n if (!link) return messageList;\n const suffix = linkSuffix(link);\n for (const message of messageList.get.response.db()) {\n if (message.role !== 'assistant') continue;\n const index = message.content.parts.findIndex(part => part.type === 'text' && part.text === suffix);\n if (index === -1) continue;\n message.content.parts.splice(index, 1);\n if (typeof message.content.content === 'string' && message.content.content.endsWith(suffix)) {\n message.content.content = message.content.content.slice(0, -suffix.length);\n }\n return messageList;\n }\n return messageList;\n }\n}\n\n/**\n * With reply text hidden the reply text never reaches the thread, and\n * `reply_in_thread` appends the link to its own first post instead.\n */\nfunction linksThisRun(requestContext: RequestContext | undefined): requestContext is RequestContext {\n if (!requestContext?.get(NEW_CHANNEL_SESSION_KEY)) return false;\n const render = requestContext.get(CHAT_CHANNEL_RENDER_CONTEXT_KEY) as ChatChannelRenderContext | undefined;\n return render?.textDisplay !== 'hidden';\n}\n\nfunction linkSuffix(link: string): string {\n // Own paragraph: the link closes the message, it is not part of the last sentence.\n return `\\n\\n[${SESSION_LINK_LABEL}](${link})`;\n}\n\nfunction sessionLinkFromRequest(requestContext: RequestContext): string | undefined {\n const controller = requestContext.get('controller') as AgentControllerRequestContext | undefined;\n if (!controller?.threadId) return undefined;\n return sessionLink({\n threadId: controller.threadId,\n sessionId: controller.sessionId,\n resourceId: controller.resourceId,\n factoryProjectId: readFactorySessionScope(requestContext)?.factoryProjectId,\n });\n}\n"],"mappings":";;;;;;;;AAcA,MAAa,0BAA0B;AAEvC,SAAgB,sBAAsB,gBAAsC;CAC1E,eAAe,IAAI,yBAAyB,IAAI;AAClD;;;;;;;AAQA,MAAM,qBAAqB;;AAG3B,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;AA0BrB,IAAa,qCAAb,MAAqE;CACnE,KAAc;CAEd,MAAM,oBAAoB,EAAE,MAAM,OAAO,kBAA+D;EACtG,IAAI,MAAM,UAAU,CAAC,aAAa,cAAc,GAAG,OAAO;EAC1D,IAAI,KAAK,SAAS,cAAc;GAC9B,MAAM,cAAc;GACpB,OAAO;EACT;EACA,IAAK,KAAK,SAAS,eAAe,KAAK,SAAS,iBAAkB,CAAC,MAAM,aAAa,OAAO;EAC7F,MAAM,SAAS;EACf,MAAM,OAAO,uBAAuB,cAAc;EAClD,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,sBAAsB;EAC5B,OAAO;GACL,MAAM;GACN,OAAO,KAAK;GACZ,MAAM,KAAK;GACX,SAAS;IAAE,IAAI;IAAc,MAAM,WAAW,IAAI;GAAE;EACtD;CACF;CAEA,kBAAkB,EAAE,aAAa,kBAAyC;EACxE,IAAI,CAAC,aAAa,cAAc,GAAG,OAAO;EAC1C,MAAM,OAAO,uBAAuB,cAAc;EAClD,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,SAAS,WAAW,IAAI;EAC9B,KAAK,MAAM,WAAW,YAAY,IAAI,SAAS,GAAG,GAAG;GACnD,IAAI,QAAQ,SAAS,aAAa;GAClC,MAAM,QAAQ,QAAQ,QAAQ,MAAM,WAAU,SAAQ,KAAK,SAAS,UAAU,KAAK,SAAS,MAAM;GAClG,IAAI,UAAU,IAAI;GAClB,QAAQ,QAAQ,MAAM,OAAO,OAAO,CAAC;GACrC,IAAI,OAAO,QAAQ,QAAQ,YAAY,YAAY,QAAQ,QAAQ,QAAQ,SAAS,MAAM,GACxF,QAAQ,QAAQ,UAAU,QAAQ,QAAQ,QAAQ,MAAM,GAAG,CAAC,OAAO,MAAM;GAE3E,OAAO;EACT;EACA,OAAO;CACT;AACF;;;;;AAMA,SAAS,aAAa,gBAA8E;CAClG,IAAI,CAAC,gBAAgB,IAAA,4BAA2B,GAAG,OAAO;CAE1D,OADe,eAAe,IAAI,+BACtB,CAAC,EAAE,gBAAgB;AACjC;AAEA,SAAS,WAAW,MAAsB;CAExC,OAAO,QAAQ,mBAAmB,IAAI,KAAK;AAC7C;AAEA,SAAS,uBAAuB,gBAAoD;CAClF,MAAM,aAAa,eAAe,IAAI,YAAY;CAClD,IAAI,CAAC,YAAY,UAAU,OAAO,KAAA;CAClC,OAAO,YAAY;EACjB,UAAU,WAAW;EACrB,WAAW,WAAW;EACtB,YAAY,WAAW;EACvB,kBAAkB,wBAAwB,cAAc,CAAC,EAAE;CAC7D,CAAC;AACH"}
|
|
@@ -5,7 +5,7 @@ const SLACK_TOKEN_URL = "https://slack.com/api/openid.connect.token";
|
|
|
5
5
|
const OIDC_CALLBACK_PATH = "/connect/slack/oidc/callback";
|
|
6
6
|
const SLACK_TOKEN_TIMEOUT_MS = 1e4;
|
|
7
7
|
/** Matches the signer's own `state` lifetime — past it, `verify` rejects anyway. */
|
|
8
|
-
const STATE_REPLAY_WINDOW_MS =
|
|
8
|
+
const STATE_REPLAY_WINDOW_MS = 6e5;
|
|
9
9
|
/**
|
|
10
10
|
* Decode a JWT's payload WITHOUT signature verification. Safe here because the
|
|
11
11
|
* `id_token` arrives directly from Slack's token endpoint over TLS in a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-route.js","names":[],"sources":["../../../src/integrations/slack/connect-route.ts"],"sourcesContent":["import { registerApiRoute } from '@mastra/core/server';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { StateSigner } from '../../state-signing.js';\nimport type { ChannelIdentityStorage } from '../../storage/domains/channel-identity/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\n\n/**\n * Payload shape for the Connections list: the platform sender key +\n * link time, without the tenant ids (the caller IS the tenant).\n */\ninterface ConnectedChannelAccountPayload {\n platform: string;\n externalTeamId: string;\n externalUserId: string;\n /** Display names captured at link time (OIDC profile claims); ids fall back. */\n externalTeamName?: string;\n externalUserName?: string;\n /** Which Factory project this link's channel runs route to (unset = not picked yet). */\n defaultFactoryProjectId?: string;\n linkedAt: string;\n}\n\n/**\n * Config for the web-initiated \"Sign in with Slack\" (OIDC) connect flow. All\n * values come from the env-supplied Slack app. Absent → the OIDC routes\n * respond with an error redirect and the list endpoint reports\n * `canConnect: false` so the UI hides its Connect button.\n */\ninterface SlackOidcConfig {\n clientId: string;\n clientSecret: string;\n /**\n * Public HTTPS origin the OIDC `redirect_uri` is built from (Slack rejects\n * plain-http redirect URLs, so locally this is the tunnel origin). Must also\n * be registered as a redirect URL on the Slack app.\n */\n redirectBaseUrl: string;\n /** Browser-facing origin post-connect redirects land on (the SPA host). */\n uiOrigin?: string;\n}\n\nconst SLACK_AUTHORIZE_URL = 'https://slack.com/openid/connect/authorize';\nconst SLACK_TOKEN_URL = 'https://slack.com/api/openid.connect.token';\nconst OIDC_CALLBACK_PATH = '/connect/slack/oidc/callback';\nconst SLACK_TOKEN_TIMEOUT_MS = 10_000;\n/** Matches the signer's own `state` lifetime — past it, `verify` rejects anyway. */\nconst STATE_REPLAY_WINDOW_MS = 10 * 60 * 1000;\n\n/**\n * Decode a JWT's payload WITHOUT signature verification. Safe here because the\n * `id_token` arrives directly from Slack's token endpoint over TLS in a\n * confidential-client code exchange — the transport authenticates the issuer,\n * which is the standard OIDC allowance for this flow. `iss`/`aud`/`exp` are\n * still checked by the caller.\n */\nfunction decodeJwtPayload(jwt: string): Record<string, unknown> | null {\n const payload = jwt.split('.')[1];\n if (!payload) return null;\n try {\n return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/**\n * Slack account-linking routes.\n *\n * A link is only ever written by the OIDC flow below, where Slack itself\n * asserts the `(team, user)` pair in the id_token — so the web user has to\n * actually control the Slack account they bind. `/connect/slack` is just the\n * Slack-side entry point: it carries no identity and writes nothing, it only\n * sends the visitor to Connections, where the flow starts.\n */\nexport function createSlackConnectRoutes(deps: {\n auth: RouteAuth;\n accountLinks: ChannelIdentityStorage;\n /** Signs the OIDC `state`, binding the round-trip to the initiating tenant. */\n tenantStateSigner?: StateSigner;\n oidc?: SlackOidcConfig;\n /**\n * Factory projects domain, for validating (and listing to) the per-link\n * default factory. Unset → the default-factory PATCH route rejects.\n */\n projects?: FactoryProjectsStorage;\n}): ApiRoute[] {\n const { auth, accountLinks, tenantStateSigner, oidc, projects } = deps;\n const oidcEnabled = Boolean(oidc && tenantStateSigner);\n const uiOrigin = oidc?.uiOrigin?.replace(/\\/$/, '') ?? '';\n\n // A signed `state` stays valid for its whole 10-minute window, so on its own\n // it authorizes the binding repeatedly: anyone who captures a callback URL\n // can re-run the exchange with their own fresh Slack `code` and bind their\n // Slack account to the tenant that started the flow. Burning the nonce makes\n // the state single-use. Scope note: this is per-process, so it does not stop\n // a replay routed to a different replica — narrowing that further needs\n // shared storage, which is out of proportion to a 10-minute window here.\n const consumedNonces = new Map<string, number>();\n const consumeNonce = (nonce: string): boolean => {\n const now = Date.now();\n for (const [seen, expiresAt] of consumedNonces) {\n if (expiresAt <= now) consumedNonces.delete(seen);\n }\n if (consumedNonces.has(nonce)) return false;\n consumedNonces.set(nonce, now + STATE_REPLAY_WINDOW_MS);\n return true;\n };\n\n // mc-web can resolve a different hono version than @mastra/factory, so the\n // registerApiRoute handler `Context` and the factory `RouteAuth` `Context`\n // are structurally incompatible (private `[GET_MATCH_RESULT]` symbol). Cast\n // through `unknown` at the seam — same `loose()` workaround the factory\n // routes use internally.\n type RouteAuthContext = Parameters<RouteAuth['ensureUser']>[0];\n const loose = (c: unknown): RouteAuthContext => c as RouteAuthContext;\n\n return [\n registerApiRoute('/connect/slack', {\n method: 'GET',\n // No auth: nothing here reads a tenant or writes a link. The SPA route\n // this lands on requires a session and bounces through login itself.\n requiresAuth: false,\n handler: async c => c.redirect(`${uiOrigin}/settings/connections`),\n }),\n // Web-initiated connect: \"Sign in with Slack\" (OIDC). The only route that\n // writes a link, because it is the only one that proves the signed-in web\n // user actually CONTROLS the Slack account: Slack itself asserts the\n // (team, user) pair in the id_token.\n registerApiRoute('/connect/slack/oidc/start', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!oidcEnabled) return c.redirect(`${uiOrigin}/?slack=error`);\n\n const factoryProjectId = c.req.query('factoryId')?.trim() || undefined;\n const startPath = factoryProjectId\n ? `/connect/slack/oidc/start?factoryId=${encodeURIComponent(factoryProjectId)}`\n : '/connect/slack/oidc/start';\n\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) {\n return c.redirect(`/auth/login?returnTo=${encodeURIComponent(startPath)}`);\n }\n\n const params = new URLSearchParams({\n response_type: 'code',\n // `profile` adds display-name claims (user name, team name) to the\n // id_token so Connections can show names instead of ids.\n scope: 'openid profile',\n client_id: oidc!.clientId,\n // Personal accounts have no org; the signer requires a string, so an\n // empty org round-trips and is mapped back to undefined on save.\n state: tenantStateSigner!.sign(tenant.orgId ?? '', tenant.userId, { factoryProjectId }),\n redirect_uri: `${oidc!.redirectBaseUrl.replace(/\\/$/, '')}${OIDC_CALLBACK_PATH}`,\n });\n return c.redirect(`${SLACK_AUTHORIZE_URL}?${params.toString()}`);\n },\n }),\n // OIDC callback. Authenticates via the signed `state` (which carries the\n // initiating tenant) rather than the session cookie — the callback may\n // arrive on a different origin (the public tunnel) where the SPA's\n // host-scoped cookie is not sent.\n registerApiRoute(OIDC_CALLBACK_PATH, {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!oidcEnabled) return c.redirect(`${uiOrigin}/?slack=error`);\n\n const tenant = tenantStateSigner!.verify(c.req.query('state'));\n const code = c.req.query('code');\n if (!tenant || !code) return c.redirect(`${uiOrigin}/?slack=error`);\n // Burn the state before spending the code, not after saving: a replay\n // must be refused even if the first attempt fails partway through.\n if (!consumeNonce(tenant.nonce)) return c.redirect(`${uiOrigin}/?slack=error`);\n\n // Bound the exchange: without a deadline a slow token endpoint parks the\n // request until the platform's own timeout, and this route is reachable\n // by anyone holding a signed state.\n const tokenRes = await fetch(SLACK_TOKEN_URL, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n client_id: oidc!.clientId,\n client_secret: oidc!.clientSecret,\n code,\n redirect_uri: `${oidc!.redirectBaseUrl.replace(/\\/$/, '')}${OIDC_CALLBACK_PATH}`,\n }),\n signal: AbortSignal.timeout(SLACK_TOKEN_TIMEOUT_MS),\n }).catch(() => null);\n const token = (await tokenRes?.json().catch(() => null)) as { ok?: boolean; id_token?: string } | null;\n if (!tokenRes?.ok || !token?.ok || typeof token.id_token !== 'string') {\n return c.redirect(`${uiOrigin}/?slack=error`);\n }\n\n const claims = decodeJwtPayload(token.id_token);\n const teamId = claims?.['https://slack.com/team_id'];\n const slackUserId = claims?.['https://slack.com/user_id'];\n // Display-only claims from the `profile` scope — absent claims just\n // mean the settings list falls back to ids.\n const teamName = claims?.['https://slack.com/team_name'];\n const userName = claims?.name;\n // `exp` is seconds since the epoch. A missing or past `exp` means the\n // assertion is not currently valid, so it cannot bind an account.\n const expiresAt = typeof claims?.exp === 'number' ? claims.exp * 1000 : null;\n if (\n !claims ||\n claims.iss !== 'https://slack.com' ||\n claims.aud !== oidc!.clientId ||\n expiresAt === null ||\n expiresAt <= Date.now() ||\n typeof teamId !== 'string' ||\n typeof slackUserId !== 'string'\n ) {\n return c.redirect(`${uiOrigin}/?slack=error`);\n }\n\n await accountLinks.saveAccountLink({\n platform: 'slack',\n externalTeamId: teamId,\n externalUserId: slackUserId,\n orgId: tenant.orgId || undefined,\n userId: tenant.userId,\n externalTeamName: typeof teamName === 'string' ? teamName : undefined,\n externalUserName: typeof userName === 'string' ? userName : undefined,\n });\n\n const successPath = tenant.factoryProjectId\n ? `/factories/${encodeURIComponent(tenant.factoryProjectId)}/settings/connections/slack`\n : '/settings/connections';\n return c.redirect(`${uiOrigin}${successPath}?slack=connected`);\n },\n }),\n // The caller's own linked channel accounts for Connections. Tenant-scoped:\n // you only ever see your own links.\n registerApiRoute('/web/channel-accounts', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const links = await accountLinks.listAccountLinksForUser(tenant.userId);\n const accounts: ConnectedChannelAccountPayload[] = links.map(link => ({\n platform: link.platform,\n externalTeamId: link.externalTeamId,\n externalUserId: link.externalUserId,\n externalTeamName: link.externalTeamName,\n externalUserName: link.externalUserName,\n defaultFactoryProjectId: link.defaultFactoryProjectId,\n linkedAt: link.linkedAt.toISOString(),\n }));\n // `canConnect` tells the settings UI whether the web-initiated OIDC\n // connect flow is configured (vs. Slack-side Connect card only).\n return c.json({ accounts, canConnect: oidcEnabled });\n },\n }),\n // Self-service disconnect. The storage delete is guarded by the caller's\n // tenant userId, so a known sender key alone can never sever someone\n // else's link.\n registerApiRoute('/web/channel-accounts', {\n method: 'DELETE',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const body = (await c.req.json().catch(() => null)) as {\n platform?: string;\n externalTeamId?: string;\n externalUserId?: string;\n } | null;\n if (!body?.platform || !body.externalTeamId || !body.externalUserId) {\n return c.json({ error: 'platform, externalTeamId and externalUserId are required' }, 400);\n }\n\n const deleted = await accountLinks.deleteAccountLinkForUser({\n userId: tenant.userId,\n platform: body.platform,\n externalTeamId: body.externalTeamId,\n externalUserId: body.externalUserId,\n });\n return c.json({ deleted });\n },\n }),\n // Set (or clear, with `factoryProjectId: null`) which Factory project a\n // link's channel runs route to. The storage write is guarded by the\n // caller's tenant userId — a known sender key alone can never repoint\n // someone else's link — and a non-null factory must exist in the caller's\n // org.\n registerApiRoute('/web/channel-accounts/default-factory', {\n method: 'PATCH',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const body = (await c.req.json().catch(() => null)) as {\n platform?: string;\n externalTeamId?: string;\n externalUserId?: string;\n factoryProjectId?: string | null;\n } | null;\n if (!body?.platform || !body.externalTeamId || !body.externalUserId || body.factoryProjectId === undefined) {\n return c.json(\n { error: 'platform, externalTeamId, externalUserId and factoryProjectId (nullable) are required' },\n 400,\n );\n }\n\n if (body.factoryProjectId !== null) {\n if (!projects) return c.json({ error: 'factory routing is not configured' }, 400);\n const factory = await projects.get({ orgId: tenant.orgId ?? '', id: body.factoryProjectId });\n if (!factory) return c.json({ error: 'unknown factory' }, 400);\n }\n\n const updated = await accountLinks.setDefaultFactory({\n userId: tenant.userId,\n platform: body.platform,\n externalTeamId: body.externalTeamId,\n externalUserId: body.externalUserId,\n factoryProjectId: body.factoryProjectId,\n });\n return c.json({ updated });\n },\n }),\n ];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"connect-route.js","names":[],"sources":["../../../src/integrations/slack/connect-route.ts"],"sourcesContent":["import { registerApiRoute } from '@mastra/core/server';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { StateSigner } from '../../state-signing.js';\nimport type { ChannelIdentityStorage } from '../../storage/domains/channel-identity/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\n\n/**\n * Payload shape for the Connections list: the platform sender key +\n * link time, without the tenant ids (the caller IS the tenant).\n */\ninterface ConnectedChannelAccountPayload {\n platform: string;\n externalTeamId: string;\n externalUserId: string;\n /** Display names captured at link time (OIDC profile claims); ids fall back. */\n externalTeamName?: string;\n externalUserName?: string;\n /** Which Factory project this link's channel runs route to (unset = not picked yet). */\n defaultFactoryProjectId?: string;\n linkedAt: string;\n}\n\n/**\n * Config for the web-initiated \"Sign in with Slack\" (OIDC) connect flow. All\n * values come from the env-supplied Slack app. Absent → the OIDC routes\n * respond with an error redirect and the list endpoint reports\n * `canConnect: false` so the UI hides its Connect button.\n */\ninterface SlackOidcConfig {\n clientId: string;\n clientSecret: string;\n /**\n * Public HTTPS origin the OIDC `redirect_uri` is built from (Slack rejects\n * plain-http redirect URLs, so locally this is the tunnel origin). Must also\n * be registered as a redirect URL on the Slack app.\n */\n redirectBaseUrl: string;\n /** Browser-facing origin post-connect redirects land on (the SPA host). */\n uiOrigin?: string;\n}\n\nconst SLACK_AUTHORIZE_URL = 'https://slack.com/openid/connect/authorize';\nconst SLACK_TOKEN_URL = 'https://slack.com/api/openid.connect.token';\nconst OIDC_CALLBACK_PATH = '/connect/slack/oidc/callback';\nconst SLACK_TOKEN_TIMEOUT_MS = 10_000;\n/** Matches the signer's own `state` lifetime — past it, `verify` rejects anyway. */\nconst STATE_REPLAY_WINDOW_MS = 10 * 60 * 1000;\n\n/**\n * Decode a JWT's payload WITHOUT signature verification. Safe here because the\n * `id_token` arrives directly from Slack's token endpoint over TLS in a\n * confidential-client code exchange — the transport authenticates the issuer,\n * which is the standard OIDC allowance for this flow. `iss`/`aud`/`exp` are\n * still checked by the caller.\n */\nfunction decodeJwtPayload(jwt: string): Record<string, unknown> | null {\n const payload = jwt.split('.')[1];\n if (!payload) return null;\n try {\n return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/**\n * Slack account-linking routes.\n *\n * A link is only ever written by the OIDC flow below, where Slack itself\n * asserts the `(team, user)` pair in the id_token — so the web user has to\n * actually control the Slack account they bind. `/connect/slack` is just the\n * Slack-side entry point: it carries no identity and writes nothing, it only\n * sends the visitor to Connections, where the flow starts.\n */\nexport function createSlackConnectRoutes(deps: {\n auth: RouteAuth;\n accountLinks: ChannelIdentityStorage;\n /** Signs the OIDC `state`, binding the round-trip to the initiating tenant. */\n tenantStateSigner?: StateSigner;\n oidc?: SlackOidcConfig;\n /**\n * Factory projects domain, for validating (and listing to) the per-link\n * default factory. Unset → the default-factory PATCH route rejects.\n */\n projects?: FactoryProjectsStorage;\n}): ApiRoute[] {\n const { auth, accountLinks, tenantStateSigner, oidc, projects } = deps;\n const oidcEnabled = Boolean(oidc && tenantStateSigner);\n const uiOrigin = oidc?.uiOrigin?.replace(/\\/$/, '') ?? '';\n\n // A signed `state` stays valid for its whole 10-minute window, so on its own\n // it authorizes the binding repeatedly: anyone who captures a callback URL\n // can re-run the exchange with their own fresh Slack `code` and bind their\n // Slack account to the tenant that started the flow. Burning the nonce makes\n // the state single-use. Scope note: this is per-process, so it does not stop\n // a replay routed to a different replica — narrowing that further needs\n // shared storage, which is out of proportion to a 10-minute window here.\n const consumedNonces = new Map<string, number>();\n const consumeNonce = (nonce: string): boolean => {\n const now = Date.now();\n for (const [seen, expiresAt] of consumedNonces) {\n if (expiresAt <= now) consumedNonces.delete(seen);\n }\n if (consumedNonces.has(nonce)) return false;\n consumedNonces.set(nonce, now + STATE_REPLAY_WINDOW_MS);\n return true;\n };\n\n // mc-web can resolve a different hono version than @mastra/factory, so the\n // registerApiRoute handler `Context` and the factory `RouteAuth` `Context`\n // are structurally incompatible (private `[GET_MATCH_RESULT]` symbol). Cast\n // through `unknown` at the seam — same `loose()` workaround the factory\n // routes use internally.\n type RouteAuthContext = Parameters<RouteAuth['ensureUser']>[0];\n const loose = (c: unknown): RouteAuthContext => c as RouteAuthContext;\n\n return [\n registerApiRoute('/connect/slack', {\n method: 'GET',\n // No auth: nothing here reads a tenant or writes a link. The SPA route\n // this lands on requires a session and bounces through login itself.\n requiresAuth: false,\n handler: async c => c.redirect(`${uiOrigin}/settings/connections`),\n }),\n // Web-initiated connect: \"Sign in with Slack\" (OIDC). The only route that\n // writes a link, because it is the only one that proves the signed-in web\n // user actually CONTROLS the Slack account: Slack itself asserts the\n // (team, user) pair in the id_token.\n registerApiRoute('/connect/slack/oidc/start', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!oidcEnabled) return c.redirect(`${uiOrigin}/?slack=error`);\n\n const factoryProjectId = c.req.query('factoryId')?.trim() || undefined;\n const startPath = factoryProjectId\n ? `/connect/slack/oidc/start?factoryId=${encodeURIComponent(factoryProjectId)}`\n : '/connect/slack/oidc/start';\n\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) {\n return c.redirect(`/auth/login?returnTo=${encodeURIComponent(startPath)}`);\n }\n\n const params = new URLSearchParams({\n response_type: 'code',\n // `profile` adds display-name claims (user name, team name) to the\n // id_token so Connections can show names instead of ids.\n scope: 'openid profile',\n client_id: oidc!.clientId,\n // Personal accounts have no org; the signer requires a string, so an\n // empty org round-trips and is mapped back to undefined on save.\n state: tenantStateSigner!.sign(tenant.orgId ?? '', tenant.userId, { factoryProjectId }),\n redirect_uri: `${oidc!.redirectBaseUrl.replace(/\\/$/, '')}${OIDC_CALLBACK_PATH}`,\n });\n return c.redirect(`${SLACK_AUTHORIZE_URL}?${params.toString()}`);\n },\n }),\n // OIDC callback. Authenticates via the signed `state` (which carries the\n // initiating tenant) rather than the session cookie — the callback may\n // arrive on a different origin (the public tunnel) where the SPA's\n // host-scoped cookie is not sent.\n registerApiRoute(OIDC_CALLBACK_PATH, {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!oidcEnabled) return c.redirect(`${uiOrigin}/?slack=error`);\n\n const tenant = tenantStateSigner!.verify(c.req.query('state'));\n const code = c.req.query('code');\n if (!tenant || !code) return c.redirect(`${uiOrigin}/?slack=error`);\n // Burn the state before spending the code, not after saving: a replay\n // must be refused even if the first attempt fails partway through.\n if (!consumeNonce(tenant.nonce)) return c.redirect(`${uiOrigin}/?slack=error`);\n\n // Bound the exchange: without a deadline a slow token endpoint parks the\n // request until the platform's own timeout, and this route is reachable\n // by anyone holding a signed state.\n const tokenRes = await fetch(SLACK_TOKEN_URL, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n client_id: oidc!.clientId,\n client_secret: oidc!.clientSecret,\n code,\n redirect_uri: `${oidc!.redirectBaseUrl.replace(/\\/$/, '')}${OIDC_CALLBACK_PATH}`,\n }),\n signal: AbortSignal.timeout(SLACK_TOKEN_TIMEOUT_MS),\n }).catch(() => null);\n const token = (await tokenRes?.json().catch(() => null)) as { ok?: boolean; id_token?: string } | null;\n if (!tokenRes?.ok || !token?.ok || typeof token.id_token !== 'string') {\n return c.redirect(`${uiOrigin}/?slack=error`);\n }\n\n const claims = decodeJwtPayload(token.id_token);\n const teamId = claims?.['https://slack.com/team_id'];\n const slackUserId = claims?.['https://slack.com/user_id'];\n // Display-only claims from the `profile` scope — absent claims just\n // mean the settings list falls back to ids.\n const teamName = claims?.['https://slack.com/team_name'];\n const userName = claims?.name;\n // `exp` is seconds since the epoch. A missing or past `exp` means the\n // assertion is not currently valid, so it cannot bind an account.\n const expiresAt = typeof claims?.exp === 'number' ? claims.exp * 1000 : null;\n if (\n !claims ||\n claims.iss !== 'https://slack.com' ||\n claims.aud !== oidc!.clientId ||\n expiresAt === null ||\n expiresAt <= Date.now() ||\n typeof teamId !== 'string' ||\n typeof slackUserId !== 'string'\n ) {\n return c.redirect(`${uiOrigin}/?slack=error`);\n }\n\n await accountLinks.saveAccountLink({\n platform: 'slack',\n externalTeamId: teamId,\n externalUserId: slackUserId,\n orgId: tenant.orgId || undefined,\n userId: tenant.userId,\n externalTeamName: typeof teamName === 'string' ? teamName : undefined,\n externalUserName: typeof userName === 'string' ? userName : undefined,\n });\n\n const successPath = tenant.factoryProjectId\n ? `/factories/${encodeURIComponent(tenant.factoryProjectId)}/settings/connections/slack`\n : '/settings/connections';\n return c.redirect(`${uiOrigin}${successPath}?slack=connected`);\n },\n }),\n // The caller's own linked channel accounts for Connections. Tenant-scoped:\n // you only ever see your own links.\n registerApiRoute('/web/channel-accounts', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const links = await accountLinks.listAccountLinksForUser(tenant.userId);\n const accounts: ConnectedChannelAccountPayload[] = links.map(link => ({\n platform: link.platform,\n externalTeamId: link.externalTeamId,\n externalUserId: link.externalUserId,\n externalTeamName: link.externalTeamName,\n externalUserName: link.externalUserName,\n defaultFactoryProjectId: link.defaultFactoryProjectId,\n linkedAt: link.linkedAt.toISOString(),\n }));\n // `canConnect` tells the settings UI whether the web-initiated OIDC\n // connect flow is configured (vs. Slack-side Connect card only).\n return c.json({ accounts, canConnect: oidcEnabled });\n },\n }),\n // Self-service disconnect. The storage delete is guarded by the caller's\n // tenant userId, so a known sender key alone can never sever someone\n // else's link.\n registerApiRoute('/web/channel-accounts', {\n method: 'DELETE',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const body = (await c.req.json().catch(() => null)) as {\n platform?: string;\n externalTeamId?: string;\n externalUserId?: string;\n } | null;\n if (!body?.platform || !body.externalTeamId || !body.externalUserId) {\n return c.json({ error: 'platform, externalTeamId and externalUserId are required' }, 400);\n }\n\n const deleted = await accountLinks.deleteAccountLinkForUser({\n userId: tenant.userId,\n platform: body.platform,\n externalTeamId: body.externalTeamId,\n externalUserId: body.externalUserId,\n });\n return c.json({ deleted });\n },\n }),\n // Set (or clear, with `factoryProjectId: null`) which Factory project a\n // link's channel runs route to. The storage write is guarded by the\n // caller's tenant userId — a known sender key alone can never repoint\n // someone else's link — and a non-null factory must exist in the caller's\n // org.\n registerApiRoute('/web/channel-accounts/default-factory', {\n method: 'PATCH',\n requiresAuth: false,\n handler: async c => {\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized' }, 401);\n\n const body = (await c.req.json().catch(() => null)) as {\n platform?: string;\n externalTeamId?: string;\n externalUserId?: string;\n factoryProjectId?: string | null;\n } | null;\n if (!body?.platform || !body.externalTeamId || !body.externalUserId || body.factoryProjectId === undefined) {\n return c.json(\n { error: 'platform, externalTeamId, externalUserId and factoryProjectId (nullable) are required' },\n 400,\n );\n }\n\n if (body.factoryProjectId !== null) {\n if (!projects) return c.json({ error: 'factory routing is not configured' }, 400);\n const factory = await projects.get({ orgId: tenant.orgId ?? '', id: body.factoryProjectId });\n if (!factory) return c.json({ error: 'unknown factory' }, 400);\n }\n\n const updated = await accountLinks.setDefaultFactory({\n userId: tenant.userId,\n platform: body.platform,\n externalTeamId: body.externalTeamId,\n externalUserId: body.externalUserId,\n factoryProjectId: body.factoryProjectId,\n });\n return c.json({ updated });\n },\n }),\n ];\n}\n"],"mappings":"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"}
|
|
@@ -22,7 +22,7 @@ var SlackFeedPublisher = class {
|
|
|
22
22
|
};
|
|
23
23
|
function fitSlackMarkdown(markdown) {
|
|
24
24
|
if (markdown.length <= MAX_SLACK_MARKDOWN_CHARS) return markdown;
|
|
25
|
-
return `${markdown.slice(0,
|
|
25
|
+
return `${markdown.slice(0, 11999).replace(/[\uD800-\uDBFF]$/, "")}…`;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* The one key both sync directions stamp on a mirrored message, so an ingested
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feed-publisher.js","names":[
|
|
1
|
+
{"version":3,"file":"feed-publisher.js","names":[],"sources":["../../../src/integrations/slack/feed-publisher.ts"],"sourcesContent":["/**\n * Mirrors work-item feed comments into the Slack thread the card was born from.\n * The bot cannot post as the commenter, so attribution is textual: `**Name**: body`.\n */\n\nimport type { MountedMastraCode } from '@mastra/code-sdk';\n\nimport type { WorkItemCommentRow } from '../../storage/domains/comments/base.js';\nimport type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';\nimport type { ExternalWorkItemSource, WorkItemRow } from '../../storage/domains/work-items/base.js';\n\n/** Slack rejects a `markdown_text` block over this; comments allow 16k. */\nconst MAX_SLACK_MARKDOWN_CHARS = 12_000;\n\nexport class SlackFeedPublisher implements WorkItemFeedPublisher {\n readonly id = 'slack';\n\n readonly #controller: MountedMastraCode['controller'] | undefined;\n\n constructor({ controller }: { controller?: MountedMastraCode['controller'] }) {\n this.#controller = controller;\n }\n\n async publish(\n comment: WorkItemCommentRow,\n workItem: WorkItemRow,\n ): Promise<{ source: ExternalWorkItemSource } | null> {\n const source = workItem.externalSource;\n if (source?.integrationId !== this.id || source.type !== 'slack-thread') return null;\n\n // Null until the channels' lazy `initialize()` resolves; no outbox retries.\n const sdk = this.#controller?.getChannels()?.sdk;\n if (!sdk) {\n console.warn('[slack] feed mirror skipped, channels not initialized yet', { commentId: comment.id });\n return null;\n }\n\n const author = comment.author.displayName ?? comment.author.id;\n const sent = await sdk.thread(source.externalId).post({\n markdown: fitSlackMarkdown(`**${author}**: ${comment.body}`),\n });\n return { source: slackCommentSource(source.externalId, sent.id, source.workspaceId) };\n }\n}\n\nfunction fitSlackMarkdown(markdown: string): string {\n if (markdown.length <= MAX_SLACK_MARKDOWN_CHARS) return markdown;\n // `slice` counts UTF-16 units, so a cut inside an emoji leaves half a pair.\n return `${markdown.slice(0, MAX_SLACK_MARKDOWN_CHARS - 1).replace(/[\\uD800-\\uDBFF]$/, '')}…`;\n}\n\n/**\n * The one key both sync directions stamp on a mirrored message, so an ingested\n * aside and a mirrored comment cannot diverge. Workspace-scoped like the card's\n * own key: a channel id and a `ts` only identify a message inside the team that\n * issued them, and one project can hold two Slack workspaces.\n */\nexport function slackCommentSource(threadId: string, messageTs: string, teamId?: string): ExternalWorkItemSource {\n return {\n integrationId: 'slack',\n type: 'message',\n ...(teamId ? { workspaceId: teamId } : {}),\n externalId: `${threadId.split(':')[1] ?? ''}:${messageTs}`,\n };\n}\n"],"mappings":";;AAYA,MAAM,2BAA2B;AAEjC,IAAa,qBAAb,MAAiE;CAC/D,KAAc;CAEd;CAEA,YAAY,EAAE,cAAgE;EAC5E,KAAK,cAAc;CACrB;CAEA,MAAM,QACJ,SACA,UACoD;EACpD,MAAM,SAAS,SAAS;EACxB,IAAI,QAAQ,kBAAkB,KAAK,MAAM,OAAO,SAAS,gBAAgB,OAAO;EAGhF,MAAM,MAAM,KAAK,aAAa,YAAY,CAAC,EAAE;EAC7C,IAAI,CAAC,KAAK;GACR,QAAQ,KAAK,6DAA6D,EAAE,WAAW,QAAQ,GAAG,CAAC;GACnG,OAAO;EACT;EAEA,MAAM,SAAS,QAAQ,OAAO,eAAe,QAAQ,OAAO;EAC5D,MAAM,OAAO,MAAM,IAAI,OAAO,OAAO,UAAU,CAAC,CAAC,KAAK,EACpD,UAAU,iBAAiB,KAAK,OAAO,MAAM,QAAQ,MAAM,EAC7D,CAAC;EACD,OAAO,EAAE,QAAQ,mBAAmB,OAAO,YAAY,KAAK,IAAI,OAAO,WAAW,EAAE;CACtF;AACF;AAEA,SAAS,iBAAiB,UAA0B;CAClD,IAAI,SAAS,UAAU,0BAA0B,OAAO;CAExD,OAAO,GAAG,SAAS,MAAM,GAAG,KAA4B,CAAC,CAAC,QAAQ,oBAAoB,EAAE,EAAE;AAC5F;;;;;;;AAQA,SAAgB,mBAAmB,UAAkB,WAAmB,QAAyC;CAC/G,OAAO;EACL,eAAe;EACf,MAAM;EACN,GAAI,SAAS,EAAE,aAAa,OAAO,IAAI,CAAC;EACxC,YAAY,GAAG,SAAS,MAAM,GAAG,CAAC,CAAC,MAAM,GAAG,GAAG;CACjD;AACF"}
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
* of the box). The integration interface remains open — third parties still add
|
|
18
18
|
* capabilities by implementing `FactoryIntegration` from outside the package.
|
|
19
19
|
*/
|
|
20
|
+
import type { AgentChannels } from '@mastra/core/channels';
|
|
21
|
+
import type { InputProcessor, OutputProcessor } from '@mastra/core/processors';
|
|
22
|
+
import type { RequestContext } from '@mastra/core/request-context';
|
|
20
23
|
import type { ApiRoute } from '@mastra/core/server';
|
|
21
24
|
import type { SlackAdapterChannelConfig } from '@mastra/slack';
|
|
22
25
|
import type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';
|
|
23
|
-
import type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext } from '../base.js';
|
|
26
|
+
import type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext, IntegrationTools } from '../base.js';
|
|
24
27
|
/**
|
|
25
28
|
* Slack app credentials, read from env ONCE by the deploy entry. `signingSecret`
|
|
26
29
|
* is required because the Slack adapter validates it at construction — an
|
|
@@ -71,7 +74,28 @@ export declare class SlackIntegration implements FactoryIntegration {
|
|
|
71
74
|
* replica signed.
|
|
72
75
|
*/
|
|
73
76
|
readonly requiresStableStateSigner = true;
|
|
77
|
+
/**
|
|
78
|
+
* With reply text hidden (the Factory default) the agent speaks through
|
|
79
|
+
* `reply_in_thread`, so the prompt, reminder and live streaming for that
|
|
80
|
+
* tool come with it. The setting is fixed for the deployment, so it is
|
|
81
|
+
* decided once here rather than on every run.
|
|
82
|
+
*/
|
|
83
|
+
readonly processors: {
|
|
84
|
+
input: InputProcessor[];
|
|
85
|
+
output: OutputProcessor[];
|
|
86
|
+
};
|
|
74
87
|
constructor(config: SlackIntegrationConfig);
|
|
88
|
+
initialize(args: {
|
|
89
|
+
channels?: () => AgentChannels | null | undefined;
|
|
90
|
+
}): void;
|
|
91
|
+
/**
|
|
92
|
+
* `attach_file`, and `reply_in_thread` when reply text is hidden, on runs
|
|
93
|
+
* whose session is linked to a Slack thread. The tools find the thread to
|
|
94
|
+
* post into when they are called.
|
|
95
|
+
*/
|
|
96
|
+
agentTools({ requestContext }: {
|
|
97
|
+
requestContext: RequestContext;
|
|
98
|
+
}): Promise<IntegrationTools>;
|
|
75
99
|
channels(ctx: IntegrationContext): FactoryChannelsConfig;
|
|
76
100
|
feedPublisher(ctx: IntegrationContext): WorkItemFeedPublisher;
|
|
77
101
|
routes(ctx: IntegrationContext): ApiRoute[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAE/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6CAA6C,CAAC;AAEzF,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAWlH;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,yBAAyB,CAAC;CAC5C;AAyED,qBAAa,gBAAiB,YAAW,kBAAkB;;IACzD,QAAQ,CAAC,EAAE,WAAW;IACtB;;;;OAIG;IACH,QAAQ,CAAC,yBAAyB,QAAQ;IAO1C;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE;QAAE,KAAK,EAAE,cAAc,EAAE,CAAC;QAAC,MAAM,EAAE,eAAe,EAAE,CAAA;KAAE,CAAC;gBAQhE,MAAM,EAAE,sBAAsB;IAyB1C,UAAU,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,aAAa,GAAG,IAAI,GAAG,SAAS,CAAA;KAAE,GAAG,IAAI;IAI7E;;;;OAIG;IACG,UAAU,CAAC,EAAE,cAAc,EAAE,EAAE;QAAE,cAAc,EAAE,cAAc,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAQnG,QAAQ,CAAC,GAAG,EAAE,kBAAkB,GAAG,qBAAqB;IAuBxD,aAAa,CAAC,GAAG,EAAE,kBAAkB,GAAG,qBAAqB;IAI7D,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,QAAQ,EAAE;IAc3C,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASvC"}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { FactoryChannelReplyProcessor } from "./channel-reply-processor.js";
|
|
2
|
+
import { FactoryChannelSessionLinkProcessor } from "./channel-session-link-processor.js";
|
|
1
3
|
import { createSlackConnectRoutes } from "./connect-route.js";
|
|
2
4
|
import { SlackFeedPublisher } from "./feed-publisher.js";
|
|
5
|
+
import { createShareFileTool, isSlackThread } from "./share-file-tool.js";
|
|
6
|
+
import { FactoryReplyStreamProcessor, createReplyTool } from "./reply-tool.js";
|
|
7
|
+
import { FactoryReplyReminderProcessor } from "./reply-reminder-processor.js";
|
|
3
8
|
import { createSlackChannelsConfig } from "./slack.js";
|
|
4
9
|
//#region src/integrations/slack/integration.ts
|
|
5
10
|
/** Slack renders the status inline next to the app name; keep it to one short line. */
|
|
@@ -15,14 +20,22 @@ function describeToolCall(args) {
|
|
|
15
20
|
if (typeof description !== "string") return void 0;
|
|
16
21
|
const line = description.split("\n").map((part) => part.replace(/\s+/g, " ").trim()).find(Boolean);
|
|
17
22
|
if (!line) return void 0;
|
|
18
|
-
const text = line.length > MAX_STATUS_LENGTH ? `${line.slice(0,
|
|
23
|
+
const text = line.length > MAX_STATUS_LENGTH ? `${line.slice(0, 79)}…` : line;
|
|
19
24
|
return `is ${/^[A-Z][a-z]/.test(text) ? text.charAt(0).toLowerCase() + text.slice(1) : text}`;
|
|
20
25
|
}
|
|
21
|
-
|
|
26
|
+
/**
|
|
27
|
+
* With reply text hidden, streamed text never reaches the thread, so it gets
|
|
28
|
+
* no status: an "is typing..." set after the reply tool's post would sit on
|
|
29
|
+
* the thread with nothing coming, and race the clear at the end of the run.
|
|
30
|
+
*/
|
|
31
|
+
const factoryTypingStatus = (textDisplay) => (chunk) => {
|
|
22
32
|
switch (chunk.type) {
|
|
23
33
|
case "reasoning-delta": return "is thinking...";
|
|
24
|
-
case "text-delta": return "is typing...";
|
|
25
|
-
case "tool-call": return
|
|
34
|
+
case "text-delta": return textDisplay === "hidden" ? void 0 : "is typing...";
|
|
35
|
+
case "tool-call-input-streaming-start": return chunk.payload.toolName === "reply_in_thread" ? "is typing..." : void 0;
|
|
36
|
+
case "tool-call":
|
|
37
|
+
if (chunk.payload.toolName === "reply_in_thread") return "is typing...";
|
|
38
|
+
return describeToolCall(chunk.payload.args) ?? "is working...";
|
|
26
39
|
default: return;
|
|
27
40
|
}
|
|
28
41
|
};
|
|
@@ -42,9 +55,11 @@ function adapterOverrides(options) {
|
|
|
42
55
|
* who want Slack's "Thinking Steps" widget.
|
|
43
56
|
*/
|
|
44
57
|
function factoryAdapterOptions(overrides) {
|
|
58
|
+
const textDisplay = overrides.textDisplay ?? "hidden";
|
|
45
59
|
return {
|
|
46
|
-
typingStatus: factoryTypingStatus,
|
|
60
|
+
typingStatus: factoryTypingStatus(textDisplay),
|
|
47
61
|
toolDisplay: "hidden",
|
|
62
|
+
textDisplay,
|
|
48
63
|
...overrides
|
|
49
64
|
};
|
|
50
65
|
}
|
|
@@ -57,6 +72,17 @@ var SlackIntegration = class {
|
|
|
57
72
|
*/
|
|
58
73
|
requiresStableStateSigner = true;
|
|
59
74
|
#config;
|
|
75
|
+
#adapterOptions;
|
|
76
|
+
/** Bound in `initialize()`; the controller mounts after integrations are built. */
|
|
77
|
+
#channels;
|
|
78
|
+
#deps = { channels: () => this.#channels?.() };
|
|
79
|
+
/**
|
|
80
|
+
* With reply text hidden (the Factory default) the agent speaks through
|
|
81
|
+
* `reply_in_thread`, so the prompt, reminder and live streaming for that
|
|
82
|
+
* tool come with it. The setting is fixed for the deployment, so it is
|
|
83
|
+
* decided once here rather than on every run.
|
|
84
|
+
*/
|
|
85
|
+
processors;
|
|
60
86
|
/**
|
|
61
87
|
* Whether `channels()` found a source-control owner on the context and wired
|
|
62
88
|
* repo-backed sessions. Set at the channels() attach path, which runs once at
|
|
@@ -66,6 +92,30 @@ var SlackIntegration = class {
|
|
|
66
92
|
constructor(config) {
|
|
67
93
|
if (!config.signingSecret) throw new Error("SlackIntegration: 'signingSecret' is required — Slack cannot verify inbound requests without it.");
|
|
68
94
|
this.#config = config;
|
|
95
|
+
this.#adapterOptions = factoryAdapterOptions(adapterOverrides(config.adapterOptions));
|
|
96
|
+
const textHidden = this.#textHidden;
|
|
97
|
+
this.processors = {
|
|
98
|
+
input: [new FactoryChannelReplyProcessor(), ...textHidden ? [new FactoryReplyReminderProcessor()] : []],
|
|
99
|
+
output: [new FactoryChannelSessionLinkProcessor(), ...textHidden ? [new FactoryReplyStreamProcessor(this.#deps)] : []]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
get #textHidden() {
|
|
103
|
+
return this.#adapterOptions.textDisplay === "hidden";
|
|
104
|
+
}
|
|
105
|
+
initialize(args) {
|
|
106
|
+
this.#channels = args.channels;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `attach_file`, and `reply_in_thread` when reply text is hidden, on runs
|
|
110
|
+
* whose session is linked to a Slack thread. The tools find the thread to
|
|
111
|
+
* post into when they are called.
|
|
112
|
+
*/
|
|
113
|
+
async agentTools({ requestContext }) {
|
|
114
|
+
if (!await isSlackThread(requestContext, this.#deps)) return {};
|
|
115
|
+
return {
|
|
116
|
+
...createShareFileTool(requestContext, this.#deps),
|
|
117
|
+
...this.#textHidden ? createReplyTool(requestContext, this.#deps) : {}
|
|
118
|
+
};
|
|
69
119
|
}
|
|
70
120
|
channels(ctx) {
|
|
71
121
|
const sourceControls = ctx.storage.sourceControls ?? (ctx.storage.sourceControlOwner ? [ctx.storage.sourceControlOwner] : []);
|
|
@@ -83,7 +133,7 @@ var SlackIntegration = class {
|
|
|
83
133
|
modelDefaults: ctx.storage.modelDefaults,
|
|
84
134
|
workItems: ctx.runtime?.workItems,
|
|
85
135
|
feed: ctx.feed,
|
|
86
|
-
adapterOptions:
|
|
136
|
+
adapterOptions: this.#adapterOptions
|
|
87
137
|
});
|
|
88
138
|
}
|
|
89
139
|
feedPublisher(ctx) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.js","names":["#config","#repoBackedSessions"],"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 { TypingStatusFn } from '@mastra/core/channels';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\n\nimport type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';\nimport type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext } from '../base.js';\n\nimport { createSlackConnectRoutes } from './connect-route.js';\nimport { SlackFeedPublisher } from './feed-publisher.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\nconst factoryTypingStatus: TypingStatusFn = chunk => {\n switch (chunk.type) {\n case 'reasoning-delta':\n return 'is thinking...';\n case 'text-delta':\n return 'is typing...';\n case 'tool-call':\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 return { typingStatus: factoryTypingStatus, toolDisplay: 'hidden', ...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 /**\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 }\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: factoryAdapterOptions(adapterOverrides(this.#config.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":";;;;;AA0EA,MAAM,oBAAoB;;;;;;AAO1B,SAAS,iBAAiB,MAAmC;CAC3D,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO,KAAA;CAC9C,MAAM,EAAE,gBAAgB;CACxB,IAAI,OAAO,gBAAgB,UAAU,OAAO,KAAA;CAC5C,MAAM,OAAO,YACV,MAAM,IAAI,CAAC,CACX,KAAI,SAAQ,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAC7C,KAAK,OAAO;CACf,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,MAAM,OAAO,KAAK,SAAS,oBAAoB,GAAG,KAAK,MAAM,GAAG,oBAAoB,CAAC,EAAE,KAAK;CAG5F,OAAO,MAAM,cAAc,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,IAAI;AACzF;AAEA,MAAM,uBAAsC,UAAS;CACnD,QAAQ,MAAM,MAAd;EACE,KAAK,mBACH,OAAO;EACT,KAAK,cACH,OAAO;EACT,KAAK,aACH,OAAO,iBAAiB,MAAM,QAAQ,IAAI,KAAK;EACjD,SACE;CACJ;AACF;;;;;AAMA,SAAS,iBAAiB,SAA2E;CACnG,OAAO,OAAO,YACZ,OAAO,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CACzE;AACF;;;;;;;;;AAUA,SAAS,sBAAsB,WAAiE;CAC9F,OAAO;EAAE,cAAc;EAAqB,aAAa;EAAU,GAAG;CAAU;AAClF;AAEA,IAAa,mBAAb,MAA4D;CAC1D,KAAc;;;;;;CAMd,4BAAqC;CAErC;;;;;;CAMA,sBAAsB;CAEtB,YAAY,QAAgC;EAC1C,IAAI,CAAC,OAAO,eACV,MAAM,IAAI,MACR,kGACF;EAEF,KAAKA,UAAU;CACjB;CAEA,SAAS,KAAgD;EAGvD,MAAM,iBACJ,IAAI,QAAQ,mBAAmB,IAAI,QAAQ,qBAAqB,CAAC,IAAI,QAAQ,kBAAkB,IAAI,CAAC;EACtG,KAAKC,sBAAsB,eAAe,SAAS;EACnD,OAAO,0BAA0B;GAC/B,OAAO;IACL,UAAU,KAAKD,QAAQ;IACvB,cAAc,KAAKA,QAAQ;IAC3B,eAAe,KAAKA,QAAQ;IAC5B,UAAU,KAAKA,QAAQ;GACzB;GACA,cAAc,IAAI,QAAQ;GAC1B,UAAU,IAAI,QAAQ;GACtB;GACA,eAAe,IAAI,QAAQ;GAC3B,WAAW,IAAI,SAAS;GACxB,MAAM,IAAI;GACV,gBAAgB,sBAAsB,iBAAiB,KAAKA,QAAQ,cAAc,CAAC;EACrF,CAAC;CACH;CAEA,cAAc,KAAgD;EAC5D,OAAO,IAAI,mBAAmB,EAAE,YAAY,IAAI,WAAW,CAAC;CAC9D;CAEA,OAAO,KAAqC;EAC1C,MAAM,EAAE,UAAU,cAAc,qBAAqB,aAAa,KAAKA;EACvE,OAAO,yBAAyB;GAC9B,MAAM,IAAI;GACV,cAAc,IAAI,QAAQ;GAC1B,mBAAmB,IAAI;GACvB,MACE,YAAY,gBAAgB,sBACxB;IAAE;IAAU;IAAc,iBAAiB;IAAqB;GAAS,IACzE,KAAA;GACN,UAAU,IAAI,QAAQ;EACxB,CAAC;CACH;CAEA,cAAuC;EACrC,MAAM,EAAE,UAAU,cAAc,UAAU,wBAAwB,KAAKA;EACvE,OAAO;GACL,YAAY;GACZ,oBAAoB,QAAQ,QAAQ;GACpC,gBAAgB,QAAQ,YAAY,gBAAgB,mBAAmB;GACvE,oBAAoB,KAAKC;EAC3B;CACF;AACF"}
|
|
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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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ProcessInputStepArgs, Processor } from '@mastra/core/processors';
|
|
2
|
+
/** How long a run may work without posting before it is nudged. */
|
|
3
|
+
export declare const REPLY_REMINDER_AFTER_MS: number;
|
|
4
|
+
export interface FactoryReplyReminderOptions {
|
|
5
|
+
afterMs?: number;
|
|
6
|
+
now?: () => number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* With reply text hidden, a long job can run silently for an hour while the
|
|
10
|
+
* person who asked has no idea whether anything is happening. Once a run has
|
|
11
|
+
* gone `afterMs` without calling `reply_in_thread`, this sends the agent one
|
|
12
|
+
* signal suggesting a status update, then restarts the clock. Ignoring it is
|
|
13
|
+
* how the agent stays quiet. A signal joins the end of the transcript, so the
|
|
14
|
+
* cached prompt prefix is untouched. Only runs that have the reply tool are
|
|
15
|
+
* touched.
|
|
16
|
+
*/
|
|
17
|
+
export declare class FactoryReplyReminderProcessor implements Processor {
|
|
18
|
+
#private;
|
|
19
|
+
readonly id = "factory-reply-reminder";
|
|
20
|
+
constructor({ afterMs, now }?: FactoryReplyReminderOptions);
|
|
21
|
+
processInputStep({ messageList, tools, state, sendSignal }: ProcessInputStepArgs): Promise<undefined>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=reply-reminder-processor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reply-reminder-processor.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/reply-reminder-processor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAM/E,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,QAAa,CAAC;AAQlD,MAAM,WAAW,2BAA2B;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,qBAAa,6BAA8B,YAAW,SAAS;;IAC7D,QAAQ,CAAC,EAAE,4BAAM;gBAIL,EAAE,OAAiC,EAAE,GAAc,EAAE,GAAE,2BAAgC;IAK7F,gBAAgB,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,oBAAoB;CAqBvF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { REPLY_TOOL_NAME } from "./reply-tool.js";
|
|
2
|
+
//#region src/integrations/slack/reply-reminder-processor.ts
|
|
3
|
+
const ID = "factory-reply-reminder";
|
|
4
|
+
/** How long a run may work without posting before it is nudged. */
|
|
5
|
+
const REPLY_REMINDER_AFTER_MS = 3e5;
|
|
6
|
+
/**
|
|
7
|
+
* With reply text hidden, a long job can run silently for an hour while the
|
|
8
|
+
* person who asked has no idea whether anything is happening. Once a run has
|
|
9
|
+
* gone `afterMs` without calling `reply_in_thread`, this sends the agent one
|
|
10
|
+
* signal suggesting a status update, then restarts the clock. Ignoring it is
|
|
11
|
+
* how the agent stays quiet. A signal joins the end of the transcript, so the
|
|
12
|
+
* cached prompt prefix is untouched. Only runs that have the reply tool are
|
|
13
|
+
* touched.
|
|
14
|
+
*/
|
|
15
|
+
var FactoryReplyReminderProcessor = class {
|
|
16
|
+
id = ID;
|
|
17
|
+
#afterMs;
|
|
18
|
+
#now;
|
|
19
|
+
constructor({ afterMs = REPLY_REMINDER_AFTER_MS, now = Date.now } = {}) {
|
|
20
|
+
this.#afterMs = afterMs;
|
|
21
|
+
this.#now = now;
|
|
22
|
+
}
|
|
23
|
+
async processInputStep({ messageList, tools, state, sendSignal }) {
|
|
24
|
+
if (!tools || !("reply_in_thread" in tools) || !sendSignal) return void 0;
|
|
25
|
+
const reminder = state;
|
|
26
|
+
const now = this.#now();
|
|
27
|
+
reminder.quietSince ??= now;
|
|
28
|
+
const seen = new Set(reminder.seenCalls ?? []);
|
|
29
|
+
const calls = replyCalls(messageList.get.response.db());
|
|
30
|
+
if (calls.some((id) => !seen.has(id))) reminder.quietSince = now;
|
|
31
|
+
reminder.seenCalls = calls;
|
|
32
|
+
const silentFor = now - reminder.quietSince;
|
|
33
|
+
if (silentFor < this.#afterMs) return void 0;
|
|
34
|
+
const minutes = Math.floor(silentFor / 6e4);
|
|
35
|
+
await sendSignal({
|
|
36
|
+
type: "reactive",
|
|
37
|
+
contents: `It has been ${minutes} minute${minutes === 1 ? "" : "s"} since you last posted in this thread. If the people there would want to know where things stand, post a short status with \`${REPLY_TOOL_NAME}\`. Otherwise keep working.`
|
|
38
|
+
});
|
|
39
|
+
reminder.quietSince = now;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/** Ids of the `reply_in_thread` calls this run has finished. */
|
|
43
|
+
function replyCalls(messages) {
|
|
44
|
+
const ids = [];
|
|
45
|
+
for (const message of messages) {
|
|
46
|
+
if (message.role !== "assistant") continue;
|
|
47
|
+
for (const part of message.content.parts ?? []) {
|
|
48
|
+
if (part.type !== "tool-invocation") continue;
|
|
49
|
+
const { toolName, toolCallId, state } = part.toolInvocation;
|
|
50
|
+
if (toolName === "reply_in_thread" && state === "result") ids.push(toolCallId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return ids;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { FactoryReplyReminderProcessor, REPLY_REMINDER_AFTER_MS };
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=reply-reminder-processor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reply-reminder-processor.js","names":[],"sources":["../../../src/integrations/slack/reply-reminder-processor.ts"],"sourcesContent":["import type { MastraDBMessage } from '@mastra/core/agent';\nimport type { ProcessInputStepArgs, Processor } from '@mastra/core/processors';\n\nimport { REPLY_TOOL_NAME } from './reply-tool.js';\n\nconst ID = 'factory-reply-reminder';\n\n/** How long a run may work without posting before it is nudged. */\nexport const REPLY_REMINDER_AFTER_MS = 5 * 60_000;\n\ninterface ReminderState {\n /** When the current silence started: run start, last post, or last nudge. */\n quietSince?: number;\n seenCalls?: string[];\n}\n\nexport interface FactoryReplyReminderOptions {\n afterMs?: number;\n now?: () => number;\n}\n\n/**\n * With reply text hidden, a long job can run silently for an hour while the\n * person who asked has no idea whether anything is happening. Once a run has\n * gone `afterMs` without calling `reply_in_thread`, this sends the agent one\n * signal suggesting a status update, then restarts the clock. Ignoring it is\n * how the agent stays quiet. A signal joins the end of the transcript, so the\n * cached prompt prefix is untouched. Only runs that have the reply tool are\n * touched.\n */\nexport class FactoryReplyReminderProcessor implements Processor {\n readonly id = ID;\n readonly #afterMs: number;\n readonly #now: () => number;\n\n constructor({ afterMs = REPLY_REMINDER_AFTER_MS, now = Date.now }: FactoryReplyReminderOptions = {}) {\n this.#afterMs = afterMs;\n this.#now = now;\n }\n\n async processInputStep({ messageList, tools, state, sendSignal }: ProcessInputStepArgs) {\n if (!tools || !(REPLY_TOOL_NAME in tools) || !sendSignal) return undefined;\n const reminder = state as ReminderState;\n const now = this.#now();\n reminder.quietSince ??= now;\n\n const seen = new Set(reminder.seenCalls ?? []);\n const calls = replyCalls(messageList.get.response.db());\n if (calls.some(id => !seen.has(id))) reminder.quietSince = now;\n reminder.seenCalls = calls;\n\n const silentFor = now - reminder.quietSince;\n if (silentFor < this.#afterMs) return undefined;\n const minutes = Math.floor(silentFor / 60_000);\n await sendSignal({\n type: 'reactive',\n contents: `It has been ${minutes} minute${minutes === 1 ? '' : 's'} since you last posted in this thread. If the people there would want to know where things stand, post a short status with \\`${REPLY_TOOL_NAME}\\`. Otherwise keep working.`,\n });\n reminder.quietSince = now;\n return undefined;\n }\n}\n\n/** Ids of the `reply_in_thread` calls this run has finished. */\nfunction replyCalls(messages: MastraDBMessage[]): string[] {\n const ids: string[] = [];\n for (const message of messages) {\n if (message.role !== 'assistant') continue;\n for (const part of message.content.parts ?? []) {\n if (part.type !== 'tool-invocation') continue;\n const { toolName, toolCallId, state } = part.toolInvocation;\n if (toolName === REPLY_TOOL_NAME && state === 'result') ids.push(toolCallId);\n }\n }\n return ids;\n}\n"],"mappings":";;AAKA,MAAM,KAAK;;AAGX,MAAa,0BAA0B;;;;;;;;;;AAsBvC,IAAa,gCAAb,MAAgE;CAC9D,KAAc;CACd;CACA;CAEA,YAAY,EAAE,UAAU,yBAAyB,MAAM,KAAK,QAAqC,CAAC,GAAG;EACnG,KAAK,WAAW;EAChB,KAAK,OAAO;CACd;CAEA,MAAM,iBAAiB,EAAE,aAAa,OAAO,OAAO,cAAoC;EACtF,IAAI,CAAC,SAAS,EAAA,qBAAqB,UAAU,CAAC,YAAY,OAAO,KAAA;EACjE,MAAM,WAAW;EACjB,MAAM,MAAM,KAAK,KAAK;EACtB,SAAS,eAAe;EAExB,MAAM,OAAO,IAAI,IAAI,SAAS,aAAa,CAAC,CAAC;EAC7C,MAAM,QAAQ,WAAW,YAAY,IAAI,SAAS,GAAG,CAAC;EACtD,IAAI,MAAM,MAAK,OAAM,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG,SAAS,aAAa;EAC3D,SAAS,YAAY;EAErB,MAAM,YAAY,MAAM,SAAS;EACjC,IAAI,YAAY,KAAK,UAAU,OAAO,KAAA;EACtC,MAAM,UAAU,KAAK,MAAM,YAAY,GAAM;EAC7C,MAAM,WAAW;GACf,MAAM;GACN,UAAU,eAAe,QAAQ,SAAS,YAAY,IAAI,KAAK,IAAI,+HAA+H,gBAAgB;EACpN,CAAC;EACD,SAAS,aAAa;CAExB;AACF;;AAGA,SAAS,WAAW,UAAuC;CACzD,MAAM,MAAgB,CAAC;CACvB,KAAK,MAAM,WAAW,UAAU;EAC9B,IAAI,QAAQ,SAAS,aAAa;EAClC,KAAK,MAAM,QAAQ,QAAQ,QAAQ,SAAS,CAAC,GAAG;GAC9C,IAAI,KAAK,SAAS,mBAAmB;GACrC,MAAM,EAAE,UAAU,YAAY,UAAU,KAAK;GAC7C,IAAI,aAAA,qBAAgC,UAAU,UAAU,IAAI,KAAK,UAAU;EAC7E;CACF;CACA,OAAO;AACT"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Processor, ProcessOutputStreamArgs } from '@mastra/core/processors';
|
|
2
|
+
import type { RequestContext } from '@mastra/core/request-context';
|
|
3
|
+
import type { ChunkType } from '@mastra/core/stream';
|
|
4
|
+
import type { IntegrationTools } from '../base.js';
|
|
5
|
+
import type { ShareFileToolDeps } from './share-file-tool.js';
|
|
6
|
+
export declare const REPLY_TOOL_NAME = "reply_in_thread";
|
|
7
|
+
/**
|
|
8
|
+
* The only way a channel run speaks when the adapter hides reply text
|
|
9
|
+
* (`textDisplay: 'hidden'`). Posting becomes a choice the agent makes, so it
|
|
10
|
+
* stops narrating each step into the thread and can say nothing on purpose.
|
|
11
|
+
* Offered only when the Slack adapter hides reply text and the run's thread is
|
|
12
|
+
* linked to Slack; with text shown the reply stream already posts and this
|
|
13
|
+
* tool would double every message.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createReplyTool(requestContext: RequestContext, deps?: ShareFileToolDeps): IntegrationTools;
|
|
16
|
+
/**
|
|
17
|
+
* Streams a `reply_in_thread` call's `text` into the thread while the model is
|
|
18
|
+
* still writing it, so a long reply appears as it is written instead of all at
|
|
19
|
+
* once when the call completes. The tool finishes the post with the validated
|
|
20
|
+
* text. Runs whose adapter doesn't stream, or that show reply text, are left
|
|
21
|
+
* alone.
|
|
22
|
+
*/
|
|
23
|
+
export declare class FactoryReplyStreamProcessor implements Processor {
|
|
24
|
+
private readonly deps;
|
|
25
|
+
readonly id = "factory-reply-stream";
|
|
26
|
+
constructor(deps?: ShareFileToolDeps);
|
|
27
|
+
processOutputStream({ part, state, requestContext }: ProcessOutputStreamArgs): Promise<ChunkType>;
|
|
28
|
+
}
|
|
29
|
+
/** The decoded value of a top-level string field in JSON that may still be cut off mid-string. */
|
|
30
|
+
export declare function partialStringField(json: string, field: string): string | undefined;
|
|
31
|
+
//# sourceMappingURL=reply-tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reply-tool.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/reply-tool.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAKrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAWjD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,cAAc,EAAE,IAAI,GAAE,iBAAsB,GAAG,gBAAgB,CAyB9G;AA8FD;;;;;;GAMG;AACH,qBAAa,2BAA4B,YAAW,SAAS;IAG/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,QAAQ,CAAC,EAAE,0BAA0B;gBAER,IAAI,GAAE,iBAAsB;IAEnD,mBAAmB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,uBAAuB,GAAG,OAAO,CAAC,SAAS,CAAC;CAyBxG;AAmBD,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAwBlF"}
|