@mastra/factory 0.24.0-alpha.9 → 0.24.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 +4 -4
- 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.map +1 -1
- package/dist/factory.js +111 -19
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +1 -1
- 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/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.js +1 -1
- package/dist/integrations/slack/integration.js.map +1 -1
- 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 +87 -34
- 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/routes.d.ts.map +1 -1
- package/dist/mcp/routes.js +1 -1
- 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/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 +6 -7
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +20 -19
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.js +1 -1
- 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 +55 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +165 -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 +19 -65
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +6 -1
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +95 -12
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +5 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -4
- 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 +10 -10
- package/dist/rules/dispatcher.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.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/channel-reply-processor.d.ts +7 -6
- package/dist/session/channel-reply-processor.d.ts.map +1 -1
- package/dist/session/channel-reply-processor.js +9 -9
- package/dist/session/channel-reply-processor.js.map +1 -1
- package/dist/session/channel-session-link-processor.js +1 -0
- package/dist/session/channel-session-link-processor.js.map +1 -1
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js +7 -6
- 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/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/session-threads.d.ts +58 -0
- package/dist/session/session-threads.d.ts.map +1 -0
- package/dist/session/session-threads.js +90 -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.map +1 -1
- package/dist/session/thinking-default.js +3 -4
- 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/skills/service.d.ts +6 -8
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +3 -3
- 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/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.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.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 +19 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +42 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +5 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +7 -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 +1 -1
- 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/scope.d.ts +2 -1
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +3 -2
- 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 +33 -16
- 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/workspace.d.ts +5 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +28 -15
- package/dist/workspace.js.map +1 -1
- package/package.json +13 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.js","names":["#config","#orgIdByResourceId","#rules","#projects","#auth","#resolveIntakeDispatch","#listSources","#listItems","#listIssues","#getIssue","#createComment","#updateIssue","#toIntakeIssue","#listAllComments"],"sources":["../../../src/integrations/jira/integration.ts"],"sourcesContent":["/**\n * `JiraIntegration` — the self-contained Jira Cloud integration.\n *\n * Implements the system-wide `FactoryIntegration` contract\n * (`../base.ts`): the deploy entry reads the `JIRA_*` env vars ONCE,\n * constructs an instance with explicit credentials, and passes it to\n * `MastraFactory`. Everything Jira-flavored the system does — project/issue\n * reads for Intake and the agent's issue tools — flows through this\n * instance. No other module reads `JIRA_*` env vars.\n *\n * Unlike Linear, credentials are **deployment-global** (Basic auth with\n * `email:apiToken` from the environment): there is no OAuth flow, no state\n * signer, and no per-org connection storage. The `IntegrationConnection`\n * arguments required by the `Intake` contract are accepted but not consulted\n * — the instance's own credentials authenticate every call.\n */\n\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { MastraWorker } from '@mastra/core/worker';\n\nimport type { IntegrationConnection } from '../../capabilities/connection.js';\nimport type {\n CreateIntakeCommentInput,\n CreatedIntakeComment,\n GetIntakeIssueInput,\n Intake,\n IntakeIssue,\n IntakeIssueDetail,\n IntakeItemPage,\n IntakeSource,\n ListIntakeIssuesInput,\n ListIntakeItemsInput,\n ResolveIntakeDispatchInput,\n ResolvedIntakeDispatch,\n UpdateIntakeIssueInput,\n} from '../../capabilities/intake.js';\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../base.js';\nimport { IssueReconcileWorker } from '../issue-reconcile-worker.js';\nimport { adfToText } from './adf.js';\nimport { buildJiraAgentTools } from './agent-tools.js';\nimport type { JiraComment, JiraIssue, JiraTransition } from './api.js';\nimport { JiraApiClient, JiraApiError } from './api.js';\nimport type { JiraEventRules, JiraRuleOverrides } from './default-rules.js';\nimport { resolveJiraRules } from './default-rules.js';\nimport { attachJiraIssueReconciler } from './issue-reconciler.js';\nimport { jiraReconciliationEnabled, jiraReconciliationInterval } from './reconciliation-config.js';\nimport { buildJiraRoutes } from './routes.js';\nimport { attachJiraRules } from './rules.js';\n\n/** Deployment-global Jira Cloud credentials. All credential fields are required. */\nexport interface JiraIntegrationConfig {\n /** Site base URL, e.g. `https://acme.atlassian.net`. */\n baseUrl: string;\n /** Atlassian account email the API token belongs to. */\n email: string;\n /** API token from id.atlassian.com → Security → API tokens. */\n apiToken: string;\n /** Per-event replacements; omitted events retain defaults, null disables. */\n rules?: JiraRuleOverrides;\n}\n\n/** Hard stop for comment pagination so a misbehaving `total` can't loop forever. */\nconst ISSUE_COMMENTS_MAX_PAGES = 20;\n\n/** Hard stop for project pagination so a misbehaving `isLast` can't loop forever. */\nconst PROJECT_SEARCH_MAX_PAGES = 50;\n\n/**\n * The `Intake` contract requires a connection argument, but Jira credentials\n * live on the integration instance — this inert placeholder is accepted and\n * ignored everywhere, and keeps the real API token out of resolved dispatches\n * and agent-tool calls.\n */\nexport const DEPLOYMENT_CONNECTION: IntegrationConnection = { type: 'oauth', accessToken: 'deployment-global' };\n\n/** Jira issue keys look like `ENG-42`. */\nconst ISSUE_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]*-\\d+$/;\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Capability state types (`capabilities/intake.ts`) → Jira status-category\n * keys. `canceled` has no Jira category — it is resolved separately against\n * `done`-category transitions whose status name contains \"cancel\".\n */\nconst STATE_TYPE_TO_CATEGORY: Record<'unstarted' | 'started' | 'completed', string> = {\n unstarted: 'new',\n started: 'indeterminate',\n completed: 'done',\n};\n\n/** Jira status-category keys → capability state types. */\nfunction stateTypeFromCategory(key: string | undefined): string | null {\n switch (key) {\n case 'new':\n return 'unstarted';\n case 'indeterminate':\n return 'started';\n case 'done':\n return 'completed';\n default:\n return null;\n }\n}\n\nexport class JiraIntegration implements FactoryIntegration {\n /** Stable integration identifier (see `../base.ts`). */\n readonly id = 'jira';\n\n readonly #config: JiraIntegrationConfig;\n /** Typed REST client bound to the deployment credentials. */\n readonly api: JiraApiClient;\n\n /** Bound once by the factory via `initialize()` before any surface is used. */\n #projects: FactoryProjectsStorage | undefined;\n #auth: RouteAuth | undefined;\n readonly #orgIdByResourceId = new Map<string, string | null>();\n\n readonly #rules: JiraEventRules;\n\n get rules(): JiraEventRules {\n return this.#rules;\n }\n\n constructor(config: JiraIntegrationConfig) {\n this.#rules = resolveJiraRules(config.rules);\n // JiraApiClient validates the credential group and normalizes the URL.\n this.api = new JiraApiClient(config);\n this.#config = config;\n }\n\n /**\n * Bind the projects domain and the host auth seam. Jira has no per-org\n * connection rows, so the generic storage handle is unused — credentials\n * are deployment-global constructor config.\n */\n initialize({ projects, auth }: { projects: FactoryProjectsStorage; auth: RouteAuth }): void {\n this.#projects = projects;\n this.#auth = auth;\n }\n\n /** Factory projects domain — maps a session's resourceId to its owning org. */\n get projects(): FactoryProjectsStorage {\n if (!this.#projects) {\n throw new Error('JiraIntegration is not initialized — the factory binds storage during prepare().');\n }\n return this.#projects;\n }\n\n /**\n * Whether the host runs with web auth enabled. Intake selections are\n * org-owned, so every Jira surface is inert without a tenant auth seam.\n */\n get authEnabled(): boolean {\n return this.#auth?.enabled() ?? false;\n }\n\n /**\n * Map a session's resourceId (the factory project id) to its owning org.\n * Same caching semantics as the Linear integration: definitive misses are\n * cached, transient database failures are not.\n */\n async resolveOrgId(resourceId: string): Promise<string | null> {\n const cached = this.#orgIdByResourceId.get(resourceId);\n if (cached !== undefined) return cached;\n // Non-UUID resource ids (local/dev resources) would make the uuid column\n // comparison throw — they're definitively \"not a project\", so cache that.\n if (!UUID_PATTERN.test(resourceId)) {\n this.#orgIdByResourceId.set(resourceId, null);\n return null;\n }\n let orgId: string | null;\n try {\n await this.projects.ensureReady();\n const project = await this.projects.getById({ id: resourceId });\n orgId = project?.orgId ?? null;\n } catch {\n // Transient database failure: skip the tools for this request but don't\n // cache the miss, so the next request retries the lookup.\n return null;\n }\n this.#orgIdByResourceId.set(resourceId, orgId);\n return orgId;\n }\n\n /** Test hook: clear the org cache between specs. */\n clearCaches(): void {\n this.#orgIdByResourceId.clear();\n }\n\n /** Normalized site base URL (no trailing slash) — `/browse/<key>` links hang off this. */\n get baseUrl(): string {\n return this.api.baseUrl;\n }\n\n readonly intake: Intake = {\n resolveIntakeDispatch: input => this.#resolveIntakeDispatch(input),\n listSources: () => this.#listSources(),\n listItems: input => this.#listItems(input),\n listIssues: input => this.#listIssues(input),\n getIssue: input => this.#getIssue(input),\n createComment: input => this.#createComment(input),\n updateIssue: input => this.#updateIssue(input),\n };\n\n /**\n * Background-dispatch context. Credentials are deployment-global, so the\n * returned connection is a formality the Jira intake methods ignore; the\n * issue key is stored directly as the work item's `externalId` (mirroring\n * Linear, which stores the issue UUID).\n */\n async #resolveIntakeDispatch({ externalSource }: ResolveIntakeDispatchInput): Promise<ResolvedIntakeDispatch | null> {\n if (externalSource.type !== 'issue') return null;\n return {\n // Inert placeholder, never the real token: the Jira intake methods read\n // credentials from the client, and the resolved dispatch travels through\n // generic reconciler/dispatcher code that must stay secret-free.\n connection: DEPLOYMENT_CONNECTION,\n issueId: externalSource.externalId,\n };\n }\n\n /** All the site's projects (Settings intake-source picker), following `isLast` paging. */\n async #listSources(): Promise<IntakeSource[]> {\n const sources: IntakeSource[] = [];\n let startAt = 0;\n for (let pageCount = 0; pageCount < PROJECT_SEARCH_MAX_PAGES; pageCount++) {\n const page = await this.api.listProjects({ startAt });\n for (const project of page.values) {\n sources.push({\n id: project.id,\n name: project.name,\n type: 'project',\n metadata: { key: project.key },\n });\n }\n if (page.isLast || page.values.length === 0) break;\n startAt += page.values.length;\n }\n return sources;\n }\n\n async #listItems({ sourceIds, cursor }: ListIntakeItemsInput): Promise<IntakeItemPage> {\n if (sourceIds.length === 0) return { items: [], nextCursor: null };\n const page = await this.listActiveIssues(cursor, sourceIds);\n return {\n items: page.issues.map(issue => ({\n source: { type: 'issue', externalId: issue.identifier, url: issue.url },\n sourceId: issue.sourceId,\n title: `${issue.identifier}: ${issue.title}`,\n status: issue.state ?? undefined,\n labels: issue.labels,\n assignee: issue.assignee,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n metadata: {\n identifier: issue.identifier,\n issueRef: issue.identifier,\n state: issue.state,\n stateType: issue.stateType,\n priority: issue.priority,\n project: issue.source,\n assignee: issue.assignee,\n assignees: issue.assignees ?? [],\n creator: issue.author,\n author: issue.author,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n },\n })),\n nextCursor: page.nextCursor,\n };\n }\n\n async #listIssues(input: ListIntakeIssuesInput): Promise<{ issues: IntakeIssue[]; nextCursor: string | null }> {\n const page = await this.listActiveIssues(input.cursor, input.sourceIds, input.labels);\n return { issues: page.issues, nextCursor: page.nextCursor };\n }\n\n /**\n * One page of active issues (status category not Done), most recently\n * updated first, optionally filtered to projects and labels. The\n * capability cursor is Jira's opaque `nextPageToken` passed through.\n */\n async listActiveIssues(\n cursor?: string,\n projectIds?: string[],\n labels?: string[],\n ): Promise<{ issues: Array<IntakeIssue & { sourceId: string }>; nextCursor: string | null }> {\n const page = await this.api.searchIssues({\n jql: buildIntakeJql(projectIds, labels),\n ...(cursor ? { nextPageToken: cursor } : {}),\n });\n return {\n issues: page.issues.map(issue => ({\n ...this.#toIntakeIssue(issue),\n sourceId: issue.fields.project?.id ?? '',\n })),\n nextCursor: page.nextPageToken ?? null,\n };\n }\n\n async #getIssue({ issueId }: GetIntakeIssueInput): Promise<IntakeIssueDetail | null> {\n let issue: JiraIssue;\n try {\n issue = await this.api.getIssue(issueId);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n const { comments, total } = await this.#listAllComments(issue.key);\n return {\n ...this.#toIntakeIssue(issue),\n commentCount: total,\n description: issue.fields.description ? adfToText(issue.fields.description) || null : null,\n comments: comments.map(comment => ({\n author: comment.author?.displayName ?? null,\n body: adfToText(comment.body),\n createdAt: comment.created,\n })),\n };\n }\n\n /** Every comment on the issue, oldest first, bounded by {@link ISSUE_COMMENTS_MAX_PAGES}. */\n async #listAllComments(keyOrId: string): Promise<{ comments: JiraComment[]; total: number }> {\n const comments: JiraComment[] = [];\n let total = 0;\n for (let page = 0; page < ISSUE_COMMENTS_MAX_PAGES; page++) {\n const result = await this.api.listComments(keyOrId, { startAt: comments.length });\n comments.push(...result.comments);\n total = result.total;\n if (result.comments.length === 0 || comments.length >= total) break;\n }\n return { comments, total };\n }\n\n async #createComment({ issueId, body }: CreateIntakeCommentInput): Promise<CreatedIntakeComment | null> {\n // The browse URL needs the issue key; resolve it when the caller passed a\n // numeric id instead.\n let key = issueId;\n if (!ISSUE_KEY_PATTERN.test(issueId)) {\n try {\n key = (await this.api.getIssue(issueId)).key;\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n }\n let comment;\n try {\n comment = await this.api.createComment(key, body);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n return {\n id: comment.id,\n url: `${this.baseUrl}/browse/${key}?focusedCommentId=${comment.id}`,\n };\n }\n\n /**\n * Move an issue to a target state by applying a workflow transition. Jira\n * only exposes transitions that are currently legal, so a policy miss (no\n * transition reaches the target) returns `null` per the `Intake` contract —\n * infrastructure errors (network, auth) still throw.\n */\n async #updateIssue(input: UpdateIntakeIssueInput): Promise<IntakeIssue | null> {\n let issue: JiraIssue;\n try {\n issue = await this.api.getIssue(input.issueId);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n if (currentStatusMatches(issue, input.state)) {\n return this.#toIntakeIssue(issue);\n }\n const transitions = await this.api.listTransitions(issue.key);\n const transition = resolveTransition(transitions, input.state);\n if (!transition) return null;\n await this.api.applyTransition(issue.key, transition.id);\n const fresh = await this.api.getIssue(issue.key);\n return this.#toIntakeIssue(fresh);\n }\n\n #toIntakeIssue(issue: JiraIssue): IntakeIssue {\n const { fields } = issue;\n return {\n id: issue.id,\n identifier: issue.key,\n title: fields.summary ?? issue.key,\n url: `${this.baseUrl}/browse/${issue.key}`,\n author: fields.reporter?.displayName ?? null,\n state: fields.status?.name ?? null,\n stateType: stateTypeFromCategory(fields.status?.statusCategory?.key),\n priority: fields.priority?.name ?? null,\n assignee: fields.assignee?.displayName ?? null,\n source: fields.project?.key ?? null,\n sourceId: fields.project?.id ?? null,\n labels: fields.labels ?? [],\n commentCount: null,\n createdAt: fields.created ?? '',\n updatedAt: fields.updated ?? '',\n };\n }\n\n // ── FactoryIntegration surface ───────────────────────────────────────────\n\n /**\n * The integration's HTTP surface: `/web/jira/*` Mastra `apiRoutes` (status,\n * projects + issues for Intake). No OAuth routes — credentials are\n * deployment-global. Handlers operate on this instance.\n */\n routes(ctx: IntegrationContext): ApiRoute[] {\n return buildJiraRoutes({\n jira: this,\n auth: ctx.auth,\n intake: ctx.storage.intake,\n ingestFactoryIssues: attachJiraRules(this, ctx),\n });\n }\n\n /**\n * Background reconciliation, matching the Platform integration: keeps filed\n * Jira cards' metadata fresh and replays close events through the rules\n * ingress. Registered only when reconciliation is enabled.\n */\n workers(ctx: IntegrationContext): MastraWorker[] {\n if (!jiraReconciliationEnabled()) return [];\n const reconcile = attachJiraIssueReconciler(this, ctx);\n if (!reconcile) return [];\n const intervalMs = jiraReconciliationInterval();\n return [\n new IssueReconcileWorker({\n integrationId: this.id,\n reconcile,\n ...(intervalMs ? { intervalMs } : {}),\n }),\n ];\n }\n\n /**\n * Org-scoped agent tools: the read-only issue-detail tool for sessions whose\n * resource is a factory project (Jira credentials are deployment-global,\n * so no per-org connection gates apply). v1 exposes no mutating Jira tools.\n */\n async agentTools(args: { requestContext: RequestContext }): Promise<IntegrationTools> {\n return buildJiraAgentTools({ requestContext: args.requestContext, jira: this });\n }\n\n /** Non-secret config snapshot for system diagnostics/startup logs. */\n diagnostics(): Record<string, unknown> {\n return {\n configured: true,\n site: new URL(this.baseUrl).host,\n email: maskEmail(this.#config.email),\n };\n }\n}\n\n/** `ops@acme.test` → `o***@acme.test` — enough to identify, never the value. */\nfunction maskEmail(email: string): string {\n const at = email.indexOf('@');\n if (at <= 0) return '***';\n return `${email[0]}***${email.slice(at)}`;\n}\n\n/**\n * Build the intake JQL. Source ids and labels come from stored org config /\n * request input, so both are sanitized before interpolation: numeric project\n * ids pass through bare, project keys are restricted to a safe charset, and\n * label quotes/backslashes are stripped.\n */\nfunction buildIntakeJql(projectIds?: string[], labels?: string[]): string {\n const clauses: string[] = [];\n const safeProjects = (projectIds ?? [])\n .map(id => id.trim())\n .filter(id => /^[A-Za-z0-9_]+$/.test(id))\n .map(id => (/^\\d+$/.test(id) ? id : `\"${id}\"`));\n if (safeProjects.length > 0) {\n clauses.push(`project IN (${safeProjects.join(', ')})`);\n }\n clauses.push('statusCategory != Done');\n const safeLabels = [...new Set((labels ?? []).map(label => label.trim().replace(/[\"\\\\]/g, '')).filter(Boolean))];\n if (safeLabels.length > 0) {\n clauses.push(`labels IN (${safeLabels.map(label => `\"${label}\"`).join(', ')})`);\n }\n return `${clauses.join(' AND ')} ORDER BY updated DESC`;\n}\n\n/** Whether the issue is already in the requested target state. */\nfunction currentStatusMatches(issue: JiraIssue, target: UpdateIntakeIssueInput['state']): boolean {\n const status = issue.fields.status;\n if (!status) return false;\n if (target.kind === 'byName') {\n return status.name.toLowerCase() === target.name.toLowerCase();\n }\n if (target.stateType === 'canceled') {\n return status.statusCategory?.key === 'done' && /cancel/i.test(status.name);\n }\n return status.statusCategory?.key === STATE_TYPE_TO_CATEGORY[target.stateType];\n}\n\n/** Resolve the target state against the issue's currently-legal transitions. */\nfunction resolveTransition(\n transitions: JiraTransition[],\n target: UpdateIntakeIssueInput['state'],\n): JiraTransition | null {\n if (target.kind === 'byName') {\n const wanted = target.name.toLowerCase();\n return transitions.find(transition => transition.to?.name.toLowerCase() === wanted) ?? null;\n }\n if (target.stateType === 'canceled') {\n return (\n transitions.find(\n transition => transition.to?.statusCategory?.key === 'done' && /cancel/i.test(transition.to.name),\n ) ?? null\n );\n }\n const category = STATE_TYPE_TO_CATEGORY[target.stateType];\n return transitions.find(transition => transition.to?.statusCategory?.key === category) ?? null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"integration.js","names":[],"sources":["../../../src/integrations/jira/integration.ts"],"sourcesContent":["/**\n * `JiraIntegration` — the self-contained Jira Cloud integration.\n *\n * Implements the system-wide `FactoryIntegration` contract\n * (`../base.ts`): the deploy entry reads the `JIRA_*` env vars ONCE,\n * constructs an instance with explicit credentials, and passes it to\n * `MastraFactory`. Everything Jira-flavored the system does — project/issue\n * reads for Intake and the agent's issue tools — flows through this\n * instance. No other module reads `JIRA_*` env vars.\n *\n * Unlike Linear, credentials are **deployment-global** (Basic auth with\n * `email:apiToken` from the environment): there is no OAuth flow, no state\n * signer, and no per-org connection storage. The `IntegrationConnection`\n * arguments required by the `Intake` contract are accepted but not consulted\n * — the instance's own credentials authenticate every call.\n */\n\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { MastraWorker } from '@mastra/core/worker';\n\nimport type { IntegrationConnection } from '../../capabilities/connection.js';\nimport type {\n CreateIntakeCommentInput,\n CreatedIntakeComment,\n GetIntakeIssueInput,\n Intake,\n IntakeIssue,\n IntakeIssueDetail,\n IntakeItemPage,\n IntakeSource,\n ListIntakeIssuesInput,\n ListIntakeItemsInput,\n ResolveIntakeDispatchInput,\n ResolvedIntakeDispatch,\n UpdateIntakeIssueInput,\n} from '../../capabilities/intake.js';\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../base.js';\nimport { IssueReconcileWorker } from '../issue-reconcile-worker.js';\nimport { adfToText } from './adf.js';\nimport { buildJiraAgentTools } from './agent-tools.js';\nimport type { JiraComment, JiraIssue, JiraTransition } from './api.js';\nimport { JiraApiClient, JiraApiError } from './api.js';\nimport type { JiraEventRules, JiraRuleOverrides } from './default-rules.js';\nimport { resolveJiraRules } from './default-rules.js';\nimport { attachJiraIssueReconciler } from './issue-reconciler.js';\nimport { jiraReconciliationEnabled, jiraReconciliationInterval } from './reconciliation-config.js';\nimport { buildJiraRoutes } from './routes.js';\nimport { attachJiraRules } from './rules.js';\n\n/** Deployment-global Jira Cloud credentials. All credential fields are required. */\nexport interface JiraIntegrationConfig {\n /** Site base URL, e.g. `https://acme.atlassian.net`. */\n baseUrl: string;\n /** Atlassian account email the API token belongs to. */\n email: string;\n /** API token from id.atlassian.com → Security → API tokens. */\n apiToken: string;\n /** Per-event replacements; omitted events retain defaults, null disables. */\n rules?: JiraRuleOverrides;\n}\n\n/** Hard stop for comment pagination so a misbehaving `total` can't loop forever. */\nconst ISSUE_COMMENTS_MAX_PAGES = 20;\n\n/** Hard stop for project pagination so a misbehaving `isLast` can't loop forever. */\nconst PROJECT_SEARCH_MAX_PAGES = 50;\n\n/**\n * The `Intake` contract requires a connection argument, but Jira credentials\n * live on the integration instance — this inert placeholder is accepted and\n * ignored everywhere, and keeps the real API token out of resolved dispatches\n * and agent-tool calls.\n */\nexport const DEPLOYMENT_CONNECTION: IntegrationConnection = { type: 'oauth', accessToken: 'deployment-global' };\n\n/** Jira issue keys look like `ENG-42`. */\nconst ISSUE_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]*-\\d+$/;\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Capability state types (`capabilities/intake.ts`) → Jira status-category\n * keys. `canceled` has no Jira category — it is resolved separately against\n * `done`-category transitions whose status name contains \"cancel\".\n */\nconst STATE_TYPE_TO_CATEGORY: Record<'unstarted' | 'started' | 'completed', string> = {\n unstarted: 'new',\n started: 'indeterminate',\n completed: 'done',\n};\n\n/** Jira status-category keys → capability state types. */\nfunction stateTypeFromCategory(key: string | undefined): string | null {\n switch (key) {\n case 'new':\n return 'unstarted';\n case 'indeterminate':\n return 'started';\n case 'done':\n return 'completed';\n default:\n return null;\n }\n}\n\nexport class JiraIntegration implements FactoryIntegration {\n /** Stable integration identifier (see `../base.ts`). */\n readonly id = 'jira';\n\n readonly #config: JiraIntegrationConfig;\n /** Typed REST client bound to the deployment credentials. */\n readonly api: JiraApiClient;\n\n /** Bound once by the factory via `initialize()` before any surface is used. */\n #projects: FactoryProjectsStorage | undefined;\n #auth: RouteAuth | undefined;\n readonly #orgIdByResourceId = new Map<string, string | null>();\n\n readonly #rules: JiraEventRules;\n\n get rules(): JiraEventRules {\n return this.#rules;\n }\n\n constructor(config: JiraIntegrationConfig) {\n this.#rules = resolveJiraRules(config.rules);\n // JiraApiClient validates the credential group and normalizes the URL.\n this.api = new JiraApiClient(config);\n this.#config = config;\n }\n\n /**\n * Bind the projects domain and the host auth seam. Jira has no per-org\n * connection rows, so the generic storage handle is unused — credentials\n * are deployment-global constructor config.\n */\n initialize({ projects, auth }: { projects: FactoryProjectsStorage; auth: RouteAuth }): void {\n this.#projects = projects;\n this.#auth = auth;\n }\n\n /** Factory projects domain — maps a session's resourceId to its owning org. */\n get projects(): FactoryProjectsStorage {\n if (!this.#projects) {\n throw new Error('JiraIntegration is not initialized — the factory binds storage during prepare().');\n }\n return this.#projects;\n }\n\n /**\n * Whether the host runs with web auth enabled. Intake selections are\n * org-owned, so every Jira surface is inert without a tenant auth seam.\n */\n get authEnabled(): boolean {\n return this.#auth?.enabled() ?? false;\n }\n\n /**\n * Map a session's resourceId (the factory project id) to its owning org.\n * Same caching semantics as the Linear integration: definitive misses are\n * cached, transient database failures are not.\n */\n async resolveOrgId(resourceId: string): Promise<string | null> {\n const cached = this.#orgIdByResourceId.get(resourceId);\n if (cached !== undefined) return cached;\n // Non-UUID resource ids (local/dev resources) would make the uuid column\n // comparison throw — they're definitively \"not a project\", so cache that.\n if (!UUID_PATTERN.test(resourceId)) {\n this.#orgIdByResourceId.set(resourceId, null);\n return null;\n }\n let orgId: string | null;\n try {\n await this.projects.ensureReady();\n const project = await this.projects.getById({ id: resourceId });\n orgId = project?.orgId ?? null;\n } catch {\n // Transient database failure: skip the tools for this request but don't\n // cache the miss, so the next request retries the lookup.\n return null;\n }\n this.#orgIdByResourceId.set(resourceId, orgId);\n return orgId;\n }\n\n /** Test hook: clear the org cache between specs. */\n clearCaches(): void {\n this.#orgIdByResourceId.clear();\n }\n\n /** Normalized site base URL (no trailing slash) — `/browse/<key>` links hang off this. */\n get baseUrl(): string {\n return this.api.baseUrl;\n }\n\n readonly intake: Intake = {\n resolveIntakeDispatch: input => this.#resolveIntakeDispatch(input),\n listSources: () => this.#listSources(),\n listItems: input => this.#listItems(input),\n listIssues: input => this.#listIssues(input),\n getIssue: input => this.#getIssue(input),\n createComment: input => this.#createComment(input),\n updateIssue: input => this.#updateIssue(input),\n };\n\n /**\n * Background-dispatch context. Credentials are deployment-global, so the\n * returned connection is a formality the Jira intake methods ignore; the\n * issue key is stored directly as the work item's `externalId` (mirroring\n * Linear, which stores the issue UUID).\n */\n async #resolveIntakeDispatch({ externalSource }: ResolveIntakeDispatchInput): Promise<ResolvedIntakeDispatch | null> {\n if (externalSource.type !== 'issue') return null;\n return {\n // Inert placeholder, never the real token: the Jira intake methods read\n // credentials from the client, and the resolved dispatch travels through\n // generic reconciler/dispatcher code that must stay secret-free.\n connection: DEPLOYMENT_CONNECTION,\n issueId: externalSource.externalId,\n };\n }\n\n /** All the site's projects (Settings intake-source picker), following `isLast` paging. */\n async #listSources(): Promise<IntakeSource[]> {\n const sources: IntakeSource[] = [];\n let startAt = 0;\n for (let pageCount = 0; pageCount < PROJECT_SEARCH_MAX_PAGES; pageCount++) {\n const page = await this.api.listProjects({ startAt });\n for (const project of page.values) {\n sources.push({\n id: project.id,\n name: project.name,\n type: 'project',\n metadata: { key: project.key },\n });\n }\n if (page.isLast || page.values.length === 0) break;\n startAt += page.values.length;\n }\n return sources;\n }\n\n async #listItems({ sourceIds, cursor }: ListIntakeItemsInput): Promise<IntakeItemPage> {\n if (sourceIds.length === 0) return { items: [], nextCursor: null };\n const page = await this.listActiveIssues(cursor, sourceIds);\n return {\n items: page.issues.map(issue => ({\n source: { type: 'issue', externalId: issue.identifier, url: issue.url },\n sourceId: issue.sourceId,\n title: `${issue.identifier}: ${issue.title}`,\n status: issue.state ?? undefined,\n labels: issue.labels,\n assignee: issue.assignee,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n metadata: {\n identifier: issue.identifier,\n issueRef: issue.identifier,\n state: issue.state,\n stateType: issue.stateType,\n priority: issue.priority,\n project: issue.source,\n assignee: issue.assignee,\n assignees: issue.assignees ?? [],\n creator: issue.author,\n author: issue.author,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n },\n })),\n nextCursor: page.nextCursor,\n };\n }\n\n async #listIssues(input: ListIntakeIssuesInput): Promise<{ issues: IntakeIssue[]; nextCursor: string | null }> {\n const page = await this.listActiveIssues(input.cursor, input.sourceIds, input.labels);\n return { issues: page.issues, nextCursor: page.nextCursor };\n }\n\n /**\n * One page of active issues (status category not Done), most recently\n * updated first, optionally filtered to projects and labels. The\n * capability cursor is Jira's opaque `nextPageToken` passed through.\n */\n async listActiveIssues(\n cursor?: string,\n projectIds?: string[],\n labels?: string[],\n ): Promise<{ issues: Array<IntakeIssue & { sourceId: string }>; nextCursor: string | null }> {\n const page = await this.api.searchIssues({\n jql: buildIntakeJql(projectIds, labels),\n ...(cursor ? { nextPageToken: cursor } : {}),\n });\n return {\n issues: page.issues.map(issue => ({\n ...this.#toIntakeIssue(issue),\n sourceId: issue.fields.project?.id ?? '',\n })),\n nextCursor: page.nextPageToken ?? null,\n };\n }\n\n async #getIssue({ issueId }: GetIntakeIssueInput): Promise<IntakeIssueDetail | null> {\n let issue: JiraIssue;\n try {\n issue = await this.api.getIssue(issueId);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n const { comments, total } = await this.#listAllComments(issue.key);\n return {\n ...this.#toIntakeIssue(issue),\n commentCount: total,\n description: issue.fields.description ? adfToText(issue.fields.description) || null : null,\n comments: comments.map(comment => ({\n author: comment.author?.displayName ?? null,\n body: adfToText(comment.body),\n createdAt: comment.created,\n })),\n };\n }\n\n /** Every comment on the issue, oldest first, bounded by {@link ISSUE_COMMENTS_MAX_PAGES}. */\n async #listAllComments(keyOrId: string): Promise<{ comments: JiraComment[]; total: number }> {\n const comments: JiraComment[] = [];\n let total = 0;\n for (let page = 0; page < ISSUE_COMMENTS_MAX_PAGES; page++) {\n const result = await this.api.listComments(keyOrId, { startAt: comments.length });\n comments.push(...result.comments);\n total = result.total;\n if (result.comments.length === 0 || comments.length >= total) break;\n }\n return { comments, total };\n }\n\n async #createComment({ issueId, body }: CreateIntakeCommentInput): Promise<CreatedIntakeComment | null> {\n // The browse URL needs the issue key; resolve it when the caller passed a\n // numeric id instead.\n let key = issueId;\n if (!ISSUE_KEY_PATTERN.test(issueId)) {\n try {\n key = (await this.api.getIssue(issueId)).key;\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n }\n let comment;\n try {\n comment = await this.api.createComment(key, body);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n return {\n id: comment.id,\n url: `${this.baseUrl}/browse/${key}?focusedCommentId=${comment.id}`,\n };\n }\n\n /**\n * Move an issue to a target state by applying a workflow transition. Jira\n * only exposes transitions that are currently legal, so a policy miss (no\n * transition reaches the target) returns `null` per the `Intake` contract —\n * infrastructure errors (network, auth) still throw.\n */\n async #updateIssue(input: UpdateIntakeIssueInput): Promise<IntakeIssue | null> {\n let issue: JiraIssue;\n try {\n issue = await this.api.getIssue(input.issueId);\n } catch (err) {\n if (err instanceof JiraApiError && err.status === 404) return null;\n throw err;\n }\n if (currentStatusMatches(issue, input.state)) {\n return this.#toIntakeIssue(issue);\n }\n const transitions = await this.api.listTransitions(issue.key);\n const transition = resolveTransition(transitions, input.state);\n if (!transition) return null;\n await this.api.applyTransition(issue.key, transition.id);\n const fresh = await this.api.getIssue(issue.key);\n return this.#toIntakeIssue(fresh);\n }\n\n #toIntakeIssue(issue: JiraIssue): IntakeIssue {\n const { fields } = issue;\n return {\n id: issue.id,\n identifier: issue.key,\n title: fields.summary ?? issue.key,\n url: `${this.baseUrl}/browse/${issue.key}`,\n author: fields.reporter?.displayName ?? null,\n state: fields.status?.name ?? null,\n stateType: stateTypeFromCategory(fields.status?.statusCategory?.key),\n priority: fields.priority?.name ?? null,\n assignee: fields.assignee?.displayName ?? null,\n source: fields.project?.key ?? null,\n sourceId: fields.project?.id ?? null,\n labels: fields.labels ?? [],\n commentCount: null,\n createdAt: fields.created ?? '',\n updatedAt: fields.updated ?? '',\n };\n }\n\n // ── FactoryIntegration surface ───────────────────────────────────────────\n\n /**\n * The integration's HTTP surface: `/web/jira/*` Mastra `apiRoutes` (status,\n * projects + issues for Intake). No OAuth routes — credentials are\n * deployment-global. Handlers operate on this instance.\n */\n routes(ctx: IntegrationContext): ApiRoute[] {\n return buildJiraRoutes({\n jira: this,\n auth: ctx.auth,\n intake: ctx.storage.intake,\n ingestFactoryIssues: attachJiraRules(this, ctx),\n });\n }\n\n /**\n * Background reconciliation, matching the Platform integration: keeps filed\n * Jira cards' metadata fresh and replays close events through the rules\n * ingress. Registered only when reconciliation is enabled.\n */\n workers(ctx: IntegrationContext): MastraWorker[] {\n if (!jiraReconciliationEnabled()) return [];\n const reconcile = attachJiraIssueReconciler(this, ctx);\n if (!reconcile) return [];\n const intervalMs = jiraReconciliationInterval();\n return [\n new IssueReconcileWorker({\n integrationId: this.id,\n reconcile,\n ...(intervalMs ? { intervalMs } : {}),\n }),\n ];\n }\n\n /**\n * Org-scoped agent tools: the read-only issue-detail tool for sessions whose\n * resource is a factory project (Jira credentials are deployment-global,\n * so no per-org connection gates apply). v1 exposes no mutating Jira tools.\n */\n async agentTools(args: { requestContext: RequestContext }): Promise<IntegrationTools> {\n return buildJiraAgentTools({ requestContext: args.requestContext, jira: this });\n }\n\n /** Non-secret config snapshot for system diagnostics/startup logs. */\n diagnostics(): Record<string, unknown> {\n return {\n configured: true,\n site: new URL(this.baseUrl).host,\n email: maskEmail(this.#config.email),\n };\n }\n}\n\n/** `ops@acme.test` → `o***@acme.test` — enough to identify, never the value. */\nfunction maskEmail(email: string): string {\n const at = email.indexOf('@');\n if (at <= 0) return '***';\n return `${email[0]}***${email.slice(at)}`;\n}\n\n/**\n * Build the intake JQL. Source ids and labels come from stored org config /\n * request input, so both are sanitized before interpolation: numeric project\n * ids pass through bare, project keys are restricted to a safe charset, and\n * label quotes/backslashes are stripped.\n */\nfunction buildIntakeJql(projectIds?: string[], labels?: string[]): string {\n const clauses: string[] = [];\n const safeProjects = (projectIds ?? [])\n .map(id => id.trim())\n .filter(id => /^[A-Za-z0-9_]+$/.test(id))\n .map(id => (/^\\d+$/.test(id) ? id : `\"${id}\"`));\n if (safeProjects.length > 0) {\n clauses.push(`project IN (${safeProjects.join(', ')})`);\n }\n clauses.push('statusCategory != Done');\n const safeLabels = [...new Set((labels ?? []).map(label => label.trim().replace(/[\"\\\\]/g, '')).filter(Boolean))];\n if (safeLabels.length > 0) {\n clauses.push(`labels IN (${safeLabels.map(label => `\"${label}\"`).join(', ')})`);\n }\n return `${clauses.join(' AND ')} ORDER BY updated DESC`;\n}\n\n/** Whether the issue is already in the requested target state. */\nfunction currentStatusMatches(issue: JiraIssue, target: UpdateIntakeIssueInput['state']): boolean {\n const status = issue.fields.status;\n if (!status) return false;\n if (target.kind === 'byName') {\n return status.name.toLowerCase() === target.name.toLowerCase();\n }\n if (target.stateType === 'canceled') {\n return status.statusCategory?.key === 'done' && /cancel/i.test(status.name);\n }\n return status.statusCategory?.key === STATE_TYPE_TO_CATEGORY[target.stateType];\n}\n\n/** Resolve the target state against the issue's currently-legal transitions. */\nfunction resolveTransition(\n transitions: JiraTransition[],\n target: UpdateIntakeIssueInput['state'],\n): JiraTransition | null {\n if (target.kind === 'byName') {\n const wanted = target.name.toLowerCase();\n return transitions.find(transition => transition.to?.name.toLowerCase() === wanted) ?? null;\n }\n if (target.stateType === 'canceled') {\n return (\n transitions.find(\n transition => transition.to?.statusCategory?.key === 'done' && /cancel/i.test(transition.to.name),\n ) ?? null\n );\n }\n const category = STATE_TYPE_TO_CATEGORY[target.stateType];\n return transitions.find(transition => transition.to?.statusCategory?.key === category) ?? null;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.js","names":["#heldElsewhere","#ingestIssue"],"sources":["../../../src/integrations/jira/rules.ts"],"sourcesContent":["import {\n boardEventHandlers,\n boardForWorkItem,\n eventBoardId,\n isTerminalWorkItem,\n rulePhaseField,\n withBoardClosure,\n} from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.js';\nimport { selectEventRule } from '../../rules/event-rules.js';\nimport type { FactoryJiraRuleContext, FactoryRuleDecision } from '../../rules/types.js';\nimport { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport { jiraClaimKey } from './claim.js';\nimport type { JiraEventRules } from './default-rules.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nconst MAX_STALE_RULE_ATTEMPTS = 3;\n\nexport interface JiraIssueIngress {\n /** Stable issue reference — direct Jira id or the Platform-encoded issue reference. */\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n /** Display name of the Jira reporter, when available. */\n author: string | null;\n /** Jira project key the issue belongs to, when available. */\n project: string | null;\n /** Jira site host the issue was read from, when available. */\n site: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n /** Jira source the issue was read from; resolves the bound board via `intakeBoards`. */\n sourceId?: string | null;\n}\n\nexport interface JiraRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n configVersion: string;\n boards: BoardRegistry;\n jiraRules: JiraEventRules;\n}\n\nexport interface JiraRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: JiraIssueIngress[];\n /** Board id per bound Jira source id, from the project's intake bindings. */\n intakeBoards?: Readonly<Record<string, string>>;\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class JiraRules {\n constructor(private readonly options: JiraRulesOptions) {}\n\n async ingest(input: JiraRulesIngress): Promise<{ status: IngressStatus; ingested: number }> {\n const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });\n if (!project) return { status: 'missing', ingested: 0 };\n\n const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });\n const itemsBySourceKey = new Map(items.map(item => [item.externalSource?.externalId, item]));\n const itemsByClaimKey = new Map(items.filter(item => item.claimKey).map(item => [item.claimKey, item]));\n const statuses: IngressStatus[] = [];\n for (const issue of input.issues) {\n // The claim finds the card even after a metadata drift; the raw issue\n // reference covers cards filed before claims existed.\n const relatedItem = itemsByClaimKey.get(jiraClaimKey(issue.id)) ?? itemsBySourceKey.get(issue.id);\n // One live card per Jira issue per org. The store's claim index is the\n // guarantee; this check spares the dispatcher a refused upsert.\n if (!relatedItem && (await this.#heldElsewhere(input, issue))) {\n statuses.push('missing');\n continue;\n }\n statuses.push(await this.#ingestIssue(input, issue, relatedItem));\n }\n if (statuses.some(status => status === 'committed')) return { status: 'committed', ingested: statuses.length };\n if (statuses.some(status => status === 'replayed')) return { status: 'replayed', ingested: statuses.length };\n return { status: 'missing', ingested: statuses.length };\n }\n\n async #heldElsewhere(input: JiraRulesIngress, issue: JiraIssueIngress): Promise<boolean> {\n const heldLive = (row: WorkItemRow) =>\n row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row);\n const claimant = await this.options.storage.getByClaimKey({\n orgId: input.orgId,\n claimKey: jiraClaimKey(issue.id),\n });\n if (claimant && heldLive(claimant)) return true;\n // Cards filed before claims existed carry only the issue reference.\n const legacy = await this.options.storage.listBySource({\n orgId: input.orgId,\n source: { integrationId: 'jira', type: 'issue', externalId: issue.id },\n });\n return legacy.some(heldLive);\n }\n\n async #ingestIssue(\n input: JiraRulesIngress,\n issue: JiraIssueIngress,\n relatedItem: WorkItemRow | undefined,\n attempt = 1,\n ): Promise<IngressStatus> {\n const ingressId = `jira:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n\n // Closed issues without existing work items should not create new ones.\n const isClosed = issue.stateType === 'completed' || issue.stateType === 'canceled';\n if (isClosed && !relatedItem) {\n return 'missing';\n }\n\n const event = isClosed ? 'issueClosed' : 'issueObserved';\n const boundBoardId = issue.sourceId ? input.intakeBoards?.[issue.sourceId] : undefined;\n const boundBoard = boundBoardId ? this.options.boards.get(boundBoardId) : undefined;\n const context: FactoryJiraRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'jira', id: ingressId },\n cause: `jira.${event}`,\n causalChain: [],\n configVersion: this.options.configVersion,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'jira-issue',\n sourceKey: relatedItem.externalSource?.externalId ?? null,\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n acceptedAt: relatedItem.acceptedAt,\n metadata: relatedItem.metadata,\n },\n board: boardForWorkItem(relatedItem),\n itemRevision: relatedItem.revision,\n ...withBoardClosure(this.options.boards, relatedItem),\n ...rulePhaseField(this.options.boards, relatedItem),\n }\n : {}),\n ...(boundBoard ? { intake: { board: boundBoard.id, initialPhase: boundBoard.initialPhase } } : {}),\n event,\n issue,\n };\n\n const rule = selectEventRule(\n this.options.jiraRules,\n boardEventHandlers(this.options.boards, eventBoardId(context), 'jira'),\n context.event,\n );\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => {\n assertFactoryDecisionTarget(\n entry,\n this.options.boards,\n relatedItem ? boardForWorkItem(relatedItem) : undefined,\n );\n return { ...entry };\n });\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory Jira rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: input.orgId,\n factoryProjectId: input.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: ingressId, triggerType: 'jira.issueObserved' },\n configVersion: this.options.configVersion,\n expectedRevision: relatedItem?.revision ?? null,\n actor,\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n if (committed.status === 'stale') {\n // Nothing was persisted: re-read the item and evaluate again at its fresh revision.\n if (attempt >= MAX_STALE_RULE_ATTEMPTS) {\n throw new Error(`Factory Jira rule evaluation for ${ingressId} kept losing revision races.`);\n }\n const fresh = relatedItem ? await this.options.storage.get({ orgId: input.orgId, id: relatedItem.id }) : null;\n if (relatedItem && !fresh) return 'missing';\n return this.#ingestIssue(input, issue, fresh ?? undefined, attempt + 1);\n }\n return committed.status;\n }\n}\n\nexport function attachJiraRules(\n jira: { readonly rules: JiraEventRules },\n context: IntegrationContext,\n): ((input: JiraRulesIngress) => Promise<unknown>) | undefined {\n if (!context.runtime) return undefined;\n const rules = new JiraRules({\n projects: context.storage.projects,\n storage: context.runtime.workItems,\n configVersion: context.runtime.configVersion,\n boards: context.runtime.boards,\n jiraRules: jira.rules,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rules.js","names":[],"sources":["../../../src/integrations/jira/rules.ts"],"sourcesContent":["import {\n boardEventHandlers,\n boardForWorkItem,\n eventBoardId,\n isTerminalWorkItem,\n rulePhaseField,\n withBoardClosure,\n} from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.js';\nimport { selectEventRule } from '../../rules/event-rules.js';\nimport type { FactoryJiraRuleContext, FactoryRuleDecision } from '../../rules/types.js';\nimport { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport { jiraClaimKey } from './claim.js';\nimport type { JiraEventRules } from './default-rules.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nconst MAX_STALE_RULE_ATTEMPTS = 3;\n\nexport interface JiraIssueIngress {\n /** Stable issue reference — direct Jira id or the Platform-encoded issue reference. */\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n /** Display name of the Jira reporter, when available. */\n author: string | null;\n /** Jira project key the issue belongs to, when available. */\n project: string | null;\n /** Jira site host the issue was read from, when available. */\n site: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n /** Jira source the issue was read from; resolves the bound board via `intakeBoards`. */\n sourceId?: string | null;\n}\n\nexport interface JiraRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n configVersion: string;\n boards: BoardRegistry;\n jiraRules: JiraEventRules;\n}\n\nexport interface JiraRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: JiraIssueIngress[];\n /** Board id per bound Jira source id, from the project's intake bindings. */\n intakeBoards?: Readonly<Record<string, string>>;\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class JiraRules {\n constructor(private readonly options: JiraRulesOptions) {}\n\n async ingest(input: JiraRulesIngress): Promise<{ status: IngressStatus; ingested: number }> {\n const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });\n if (!project) return { status: 'missing', ingested: 0 };\n\n const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });\n const itemsBySourceKey = new Map(items.map(item => [item.externalSource?.externalId, item]));\n const itemsByClaimKey = new Map(items.filter(item => item.claimKey).map(item => [item.claimKey, item]));\n const statuses: IngressStatus[] = [];\n for (const issue of input.issues) {\n // The claim finds the card even after a metadata drift; the raw issue\n // reference covers cards filed before claims existed.\n const relatedItem = itemsByClaimKey.get(jiraClaimKey(issue.id)) ?? itemsBySourceKey.get(issue.id);\n // One live card per Jira issue per org. The store's claim index is the\n // guarantee; this check spares the dispatcher a refused upsert.\n if (!relatedItem && (await this.#heldElsewhere(input, issue))) {\n statuses.push('missing');\n continue;\n }\n statuses.push(await this.#ingestIssue(input, issue, relatedItem));\n }\n if (statuses.some(status => status === 'committed')) return { status: 'committed', ingested: statuses.length };\n if (statuses.some(status => status === 'replayed')) return { status: 'replayed', ingested: statuses.length };\n return { status: 'missing', ingested: statuses.length };\n }\n\n async #heldElsewhere(input: JiraRulesIngress, issue: JiraIssueIngress): Promise<boolean> {\n const heldLive = (row: WorkItemRow) =>\n row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row);\n const claimant = await this.options.storage.getByClaimKey({\n orgId: input.orgId,\n claimKey: jiraClaimKey(issue.id),\n });\n if (claimant && heldLive(claimant)) return true;\n // Cards filed before claims existed carry only the issue reference.\n const legacy = await this.options.storage.listBySource({\n orgId: input.orgId,\n source: { integrationId: 'jira', type: 'issue', externalId: issue.id },\n });\n return legacy.some(heldLive);\n }\n\n async #ingestIssue(\n input: JiraRulesIngress,\n issue: JiraIssueIngress,\n relatedItem: WorkItemRow | undefined,\n attempt = 1,\n ): Promise<IngressStatus> {\n const ingressId = `jira:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n\n // Closed issues without existing work items should not create new ones.\n const isClosed = issue.stateType === 'completed' || issue.stateType === 'canceled';\n if (isClosed && !relatedItem) {\n return 'missing';\n }\n\n const event = isClosed ? 'issueClosed' : 'issueObserved';\n const boundBoardId = issue.sourceId ? input.intakeBoards?.[issue.sourceId] : undefined;\n const boundBoard = boundBoardId ? this.options.boards.get(boundBoardId) : undefined;\n const context: FactoryJiraRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'jira', id: ingressId },\n cause: `jira.${event}`,\n causalChain: [],\n configVersion: this.options.configVersion,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'jira-issue',\n sourceKey: relatedItem.externalSource?.externalId ?? null,\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n acceptedAt: relatedItem.acceptedAt,\n metadata: relatedItem.metadata,\n },\n board: boardForWorkItem(relatedItem),\n itemRevision: relatedItem.revision,\n ...withBoardClosure(this.options.boards, relatedItem),\n ...rulePhaseField(this.options.boards, relatedItem),\n }\n : {}),\n ...(boundBoard ? { intake: { board: boundBoard.id, initialPhase: boundBoard.initialPhase } } : {}),\n event,\n issue,\n };\n\n const rule = selectEventRule(\n this.options.jiraRules,\n boardEventHandlers(this.options.boards, eventBoardId(context), 'jira'),\n context.event,\n );\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => {\n assertFactoryDecisionTarget(\n entry,\n this.options.boards,\n relatedItem ? boardForWorkItem(relatedItem) : undefined,\n );\n return { ...entry };\n });\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory Jira rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: input.orgId,\n factoryProjectId: input.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: ingressId, triggerType: 'jira.issueObserved' },\n configVersion: this.options.configVersion,\n expectedRevision: relatedItem?.revision ?? null,\n actor,\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n if (committed.status === 'stale') {\n // Nothing was persisted: re-read the item and evaluate again at its fresh revision.\n if (attempt >= MAX_STALE_RULE_ATTEMPTS) {\n throw new Error(`Factory Jira rule evaluation for ${ingressId} kept losing revision races.`);\n }\n const fresh = relatedItem ? await this.options.storage.get({ orgId: input.orgId, id: relatedItem.id }) : null;\n if (relatedItem && !fresh) return 'missing';\n return this.#ingestIssue(input, issue, fresh ?? undefined, attempt + 1);\n }\n return committed.status;\n }\n}\n\nexport function attachJiraRules(\n jira: { readonly rules: JiraEventRules },\n context: IntegrationContext,\n): ((input: JiraRulesIngress) => Promise<unknown>) | undefined {\n if (!context.runtime) return undefined;\n const rules = new JiraRules({\n projects: context.storage.projects,\n storage: context.runtime.workItems,\n configVersion: context.runtime.configVersion,\n boards: context.runtime.boards,\n jiraRules: jira.rules,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-tools.d.ts","sourceRoot":"","sources":["../../../src/integrations/linear/agent-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAInE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,iBAAS,wBAAwB,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM;;yIA+BzE;AAED,iBAAS,uBAAuB,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM;;;8IAsCxE;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,cAAc,EACd,MAAM,GACP,EAAE;IACD,cAAc,EAAE,cAAc,CAAC;IAC/B,gEAAgE;IAChE,MAAM,EAAE,iBAAiB,CAAC;CAC3B,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"agent-tools.d.ts","sourceRoot":"","sources":["../../../src/integrations/linear/agent-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAInE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,iBAAS,wBAAwB,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM;;yIA+BzE;AAED,iBAAS,uBAAuB,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM;;;8IAsCxE;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,cAAc,EACd,MAAM,GACP,EAAE;IACD,cAAc,EAAE,cAAc,CAAC;IAC/B,gEAAgE;IAChE,MAAM,EAAE,iBAAiB,CAAC;CAC3B,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC,CAAC,CAyBpH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-tools.js","names":[],"sources":["../../../src/integrations/linear/agent-tools.ts"],"sourcesContent":["/**\n * Linear tools exposed to the coding agent.\n *\n * Wired into the agent through the SDK's async `extraTools` provider: on each\n * tool-set resolution we map the session's factory project to its owning org\n * and only expose the Linear tools when that org has a Linear connection.\n * Projects whose org never connected Linear (or when the\n * feature is disabled) see no Linear tools at all — the model is never shown\n * tools it can't use.\n *\n * Tenancy mirrors the Linear API routes: everything is scoped by the org that\n * owns the project, and tokens are refreshed through the integration's shared\n * connection lifecycle.\n */\n\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport type { LinearIntegration } from './integration.js';\nimport { LinearReauthRequiredError } from './integration.js';\n\nfunction createLinearGetIssueTool(linear: LinearIntegration, orgId: string) {\n return createTool({\n id: 'linear_get_issue',\n description:\n \"Fetch a Linear issue's full details — title, description, state, assignee, labels, priority, and discussion comments. Use this whenever you're working on a Linear issue (e.g. ENG-123) to get its complete context.\",\n inputSchema: z.object({\n issue: z.string().min(1).describe('The Linear issue identifier (e.g. \"ENG-123\") or issue UUID.'),\n }),\n execute: async ({ issue }: { issue: string }) => {\n const connection = await linear.loadConnection(orgId);\n if (!connection) {\n return { error: 'Linear is not connected for this repository. Connect Linear in Settings to fetch issues.' };\n }\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const detail = await linear.intake.getIssue({\n connection: { type: 'oauth', accessToken },\n issueId: issue.trim(),\n });\n if (!detail) {\n return { error: `Linear issue \"${issue}\" was not found in this workspace.` };\n }\n return detail;\n } catch (err) {\n if (err instanceof LinearReauthRequiredError) {\n return { error: err.message };\n }\n return { error: `Failed to fetch Linear issue: ${err instanceof Error ? err.message : String(err)}` };\n }\n },\n });\n}\n\nfunction createLinearCommentTool(linear: LinearIntegration, orgId: string) {\n return createTool({\n id: 'linear_create_comment',\n description:\n 'Post a comment on a Linear issue (e.g. to report investigation findings, link a PR, or ask a clarifying question). The comment is posted as the connected Linear integration, so make clear it comes from the agent.',\n inputSchema: z.object({\n issue: z.string().min(1).describe('The Linear issue identifier (e.g. \"ENG-123\") or issue UUID.'),\n body: z.string().min(1).describe('The comment body, as Linear-flavored markdown.'),\n }),\n execute: async ({ issue, body }: { issue: string; body: string }) => {\n const connection = await linear.loadConnection(orgId);\n if (!connection) {\n return { error: 'Linear is not connected for this repository. Connect Linear in Settings to post comments.' };\n }\n if (!linear.canPostComments(connection)) {\n return {\n error: 'The Linear connection does not have comment permissions. Reconnect Linear in Settings to grant them.',\n };\n }\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const comment = await linear.intake.createComment({\n connection: { type: 'oauth', accessToken },\n issueId: issue.trim(),\n body,\n });\n if (!comment) {\n return { error: `Linear issue \"${issue}\" was not found in this workspace.` };\n }\n return { posted: true, url: comment.url };\n } catch (err) {\n if (err instanceof LinearReauthRequiredError) {\n return { error: err.message };\n }\n return { error: `Failed to post Linear comment: ${err instanceof Error ? err.message : String(err)}` };\n }\n },\n });\n}\n\n/**\n * Async `extraTools` provider: expose Linear tools only when the session's\n * project belongs to an org with an active Linear connection.\n */\nexport async function buildLinearAgentTools({\n requestContext,\n linear,\n}: {\n requestContext: RequestContext;\n /** The integration instance providing the Linear API client. */\n linear: LinearIntegration;\n}): Promise<Record<string, ReturnType<typeof createLinearGetIssueTool> | ReturnType<typeof createLinearCommentTool>>> {\n if (!linear.authEnabled) return {};\n\n const ctx = requestContext.get('controller') as\n AgentControllerRequestContext<{ factoryProjectId?: string }> | undefined;\n if (!ctx) return {};\n\n //
|
|
1
|
+
{"version":3,"file":"agent-tools.js","names":[],"sources":["../../../src/integrations/linear/agent-tools.ts"],"sourcesContent":["/**\n * Linear tools exposed to the coding agent.\n *\n * Wired into the agent through the SDK's async `extraTools` provider: on each\n * tool-set resolution we map the session's factory project to its owning org\n * and only expose the Linear tools when that org has a Linear connection.\n * Projects whose org never connected Linear (or when the\n * feature is disabled) see no Linear tools at all — the model is never shown\n * tools it can't use.\n *\n * Tenancy mirrors the Linear API routes: everything is scoped by the org that\n * owns the project, and tokens are refreshed through the integration's shared\n * connection lifecycle.\n */\n\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport type { LinearIntegration } from './integration.js';\nimport { LinearReauthRequiredError } from './integration.js';\n\nfunction createLinearGetIssueTool(linear: LinearIntegration, orgId: string) {\n return createTool({\n id: 'linear_get_issue',\n description:\n \"Fetch a Linear issue's full details — title, description, state, assignee, labels, priority, and discussion comments. Use this whenever you're working on a Linear issue (e.g. ENG-123) to get its complete context.\",\n inputSchema: z.object({\n issue: z.string().min(1).describe('The Linear issue identifier (e.g. \"ENG-123\") or issue UUID.'),\n }),\n execute: async ({ issue }: { issue: string }) => {\n const connection = await linear.loadConnection(orgId);\n if (!connection) {\n return { error: 'Linear is not connected for this repository. Connect Linear in Settings to fetch issues.' };\n }\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const detail = await linear.intake.getIssue({\n connection: { type: 'oauth', accessToken },\n issueId: issue.trim(),\n });\n if (!detail) {\n return { error: `Linear issue \"${issue}\" was not found in this workspace.` };\n }\n return detail;\n } catch (err) {\n if (err instanceof LinearReauthRequiredError) {\n return { error: err.message };\n }\n return { error: `Failed to fetch Linear issue: ${err instanceof Error ? err.message : String(err)}` };\n }\n },\n });\n}\n\nfunction createLinearCommentTool(linear: LinearIntegration, orgId: string) {\n return createTool({\n id: 'linear_create_comment',\n description:\n 'Post a comment on a Linear issue (e.g. to report investigation findings, link a PR, or ask a clarifying question). The comment is posted as the connected Linear integration, so make clear it comes from the agent.',\n inputSchema: z.object({\n issue: z.string().min(1).describe('The Linear issue identifier (e.g. \"ENG-123\") or issue UUID.'),\n body: z.string().min(1).describe('The comment body, as Linear-flavored markdown.'),\n }),\n execute: async ({ issue, body }: { issue: string; body: string }) => {\n const connection = await linear.loadConnection(orgId);\n if (!connection) {\n return { error: 'Linear is not connected for this repository. Connect Linear in Settings to post comments.' };\n }\n if (!linear.canPostComments(connection)) {\n return {\n error: 'The Linear connection does not have comment permissions. Reconnect Linear in Settings to grant them.',\n };\n }\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const comment = await linear.intake.createComment({\n connection: { type: 'oauth', accessToken },\n issueId: issue.trim(),\n body,\n });\n if (!comment) {\n return { error: `Linear issue \"${issue}\" was not found in this workspace.` };\n }\n return { posted: true, url: comment.url };\n } catch (err) {\n if (err instanceof LinearReauthRequiredError) {\n return { error: err.message };\n }\n return { error: `Failed to post Linear comment: ${err instanceof Error ? err.message : String(err)}` };\n }\n },\n });\n}\n\n/**\n * Async `extraTools` provider: expose Linear tools only when the session's\n * project belongs to an org with an active Linear connection.\n */\nexport async function buildLinearAgentTools({\n requestContext,\n linear,\n}: {\n requestContext: RequestContext;\n /** The integration instance providing the Linear API client. */\n linear: LinearIntegration;\n}): Promise<Record<string, ReturnType<typeof createLinearGetIssueTool> | ReturnType<typeof createLinearCommentTool>>> {\n if (!linear.authEnabled) return {};\n\n const ctx = requestContext.get('controller') as\n AgentControllerRequestContext<{ factoryProjectId?: string }> | undefined;\n if (!ctx) return {};\n\n // Prefer the Factory recorded on the session. Factory sessions run under the\n // Factory as their resource, so the resource is the fallback; a legacy\n // session whose resource is its own id resolves to no project.\n const projectId = ctx.getState().factoryProjectId ?? ctx.resourceId;\n if (!projectId) return {};\n\n const orgId = await linear.resolveOrgId(projectId);\n if (!orgId) return {};\n const check = await linear.checkConnection(orgId);\n if (!check.connected) return {};\n\n return {\n linear_get_issue: createLinearGetIssueTool(linear, orgId),\n // Only offered when the granted OAuth scope allows posting comments —\n // connections made before `comments:create` was requested are read-only\n // until the org reconnects Linear.\n ...(check.canComment ? { linear_create_comment: createLinearCommentTool(linear, orgId) } : {}),\n };\n}\n"],"mappings":";;;;AAuBA,SAAS,yBAAyB,QAA2B,OAAe;CAC1E,OAAO,WAAW;EAChB,IAAI;EACJ,aACE;EACF,aAAa,EAAE,OAAO,EACpB,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,+DAA6D,EACjG,CAAC;EACD,SAAS,OAAO,EAAE,YAA+B;GAC/C,MAAM,aAAa,MAAM,OAAO,eAAe,KAAK;GACpD,IAAI,CAAC,YACH,OAAO,EAAE,OAAO,2FAA2F;GAE7G,IAAI;IACF,MAAM,cAAc,MAAM,OAAO,oBAAoB,UAAU;IAC/D,MAAM,SAAS,MAAM,OAAO,OAAO,SAAS;KAC1C,YAAY;MAAE,MAAM;MAAS;KAAY;KACzC,SAAS,MAAM,KAAK;IACtB,CAAC;IACD,IAAI,CAAC,QACH,OAAO,EAAE,OAAO,iBAAiB,MAAM,oCAAoC;IAE7E,OAAO;GACT,SAAS,KAAK;IACZ,IAAI,eAAe,2BACjB,OAAO,EAAE,OAAO,IAAI,QAAQ;IAE9B,OAAO,EAAE,OAAO,iCAAiC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,IAAI;GACtG;EACF;CACF,CAAC;AACH;AAEA,SAAS,wBAAwB,QAA2B,OAAe;CACzE,OAAO,WAAW;EAChB,IAAI;EACJ,aACE;EACF,aAAa,EAAE,OAAO;GACpB,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,+DAA6D;GAC/F,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,gDAAgD;EACnF,CAAC;EACD,SAAS,OAAO,EAAE,OAAO,WAA4C;GACnE,MAAM,aAAa,MAAM,OAAO,eAAe,KAAK;GACpD,IAAI,CAAC,YACH,OAAO,EAAE,OAAO,4FAA4F;GAE9G,IAAI,CAAC,OAAO,gBAAgB,UAAU,GACpC,OAAO,EACL,OAAO,uGACT;GAEF,IAAI;IACF,MAAM,cAAc,MAAM,OAAO,oBAAoB,UAAU;IAC/D,MAAM,UAAU,MAAM,OAAO,OAAO,cAAc;KAChD,YAAY;MAAE,MAAM;MAAS;KAAY;KACzC,SAAS,MAAM,KAAK;KACpB;IACF,CAAC;IACD,IAAI,CAAC,SACH,OAAO,EAAE,OAAO,iBAAiB,MAAM,oCAAoC;IAE7E,OAAO;KAAE,QAAQ;KAAM,KAAK,QAAQ;IAAI;GAC1C,SAAS,KAAK;IACZ,IAAI,eAAe,2BACjB,OAAO,EAAE,OAAO,IAAI,QAAQ;IAE9B,OAAO,EAAE,OAAO,kCAAkC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,IAAI;GACvG;EACF;CACF,CAAC;AACH;;;;;AAMA,eAAsB,sBAAsB,EAC1C,gBACA,UAKoH;CACpH,IAAI,CAAC,OAAO,aAAa,OAAO,CAAC;CAEjC,MAAM,MAAM,eAAe,IAAI,YAAY;CAE3C,IAAI,CAAC,KAAK,OAAO,CAAC;CAKlB,MAAM,YAAY,IAAI,SAAS,CAAC,CAAC,oBAAoB,IAAI;CACzD,IAAI,CAAC,WAAW,OAAO,CAAC;CAExB,MAAM,QAAQ,MAAM,OAAO,aAAa,SAAS;CACjD,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,MAAM,QAAQ,MAAM,OAAO,gBAAgB,KAAK;CAChD,IAAI,CAAC,MAAM,WAAW,OAAO,CAAC;CAE9B,OAAO;EACL,kBAAkB,yBAAyB,QAAQ,KAAK;EAIxD,GAAI,MAAM,aAAa,EAAE,uBAAuB,wBAAwB,QAAQ,KAAK,EAAE,IAAI,CAAC;CAC9F;AACF"}
|