@mastra/factory 0.16.0-alpha.9 → 0.16.1-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.
Files changed (216) hide show
  1. package/README.md +37 -0
  2. package/dist/auth.js +1 -1
  3. package/dist/auth.js.map +1 -1
  4. package/dist/boards/review.d.ts.map +1 -1
  5. package/dist/boards/review.js +19 -8
  6. package/dist/boards/review.js.map +1 -1
  7. package/dist/boards/work.d.ts.map +1 -1
  8. package/dist/boards/work.js +31 -3
  9. package/dist/boards/work.js.map +1 -1
  10. package/dist/capabilities/intake.d.ts +4 -0
  11. package/dist/capabilities/intake.d.ts.map +1 -1
  12. package/dist/capabilities/version-control.d.ts +15 -0
  13. package/dist/capabilities/version-control.d.ts.map +1 -1
  14. package/dist/factory.d.ts +7 -7
  15. package/dist/factory.d.ts.map +1 -1
  16. package/dist/factory.js +100 -40
  17. package/dist/factory.js.map +1 -1
  18. package/dist/integrations/base.d.ts +3 -0
  19. package/dist/integrations/base.d.ts.map +1 -1
  20. package/dist/integrations/github/integration.d.ts.map +1 -1
  21. package/dist/integrations/github/integration.js +21 -0
  22. package/dist/integrations/github/integration.js.map +1 -1
  23. package/dist/integrations/github/routes.js +2 -1
  24. package/dist/integrations/github/routes.js.map +1 -1
  25. package/dist/integrations/github/sandbox.d.ts +41 -42
  26. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  27. package/dist/integrations/github/sandbox.js +416 -227
  28. package/dist/integrations/github/sandbox.js.map +1 -1
  29. package/dist/integrations/github/webhook.d.ts +2 -5
  30. package/dist/integrations/github/webhook.d.ts.map +1 -1
  31. package/dist/integrations/github/webhook.js +8 -48
  32. package/dist/integrations/github/webhook.js.map +1 -1
  33. package/dist/integrations/gitlab/agent-tools.d.ts +14 -0
  34. package/dist/integrations/gitlab/agent-tools.d.ts.map +1 -0
  35. package/dist/integrations/gitlab/agent-tools.js +42 -0
  36. package/dist/integrations/gitlab/agent-tools.js.map +1 -0
  37. package/dist/integrations/gitlab/api.d.ts +222 -0
  38. package/dist/integrations/gitlab/api.d.ts.map +1 -0
  39. package/dist/integrations/gitlab/api.js +274 -0
  40. package/dist/integrations/gitlab/api.js.map +1 -0
  41. package/dist/integrations/gitlab/default-rules.d.ts +125 -0
  42. package/dist/integrations/gitlab/default-rules.d.ts.map +1 -0
  43. package/dist/integrations/gitlab/default-rules.js +172 -0
  44. package/dist/integrations/gitlab/default-rules.js.map +1 -0
  45. package/dist/integrations/gitlab/integration.d.ts +138 -0
  46. package/dist/integrations/gitlab/integration.d.ts.map +1 -0
  47. package/dist/integrations/gitlab/integration.js +705 -0
  48. package/dist/integrations/gitlab/integration.js.map +1 -0
  49. package/dist/integrations/gitlab/issue-reconciler.d.ts +6 -0
  50. package/dist/integrations/gitlab/issue-reconciler.d.ts.map +1 -0
  51. package/dist/integrations/gitlab/issue-reconciler.js +123 -0
  52. package/dist/integrations/gitlab/issue-reconciler.js.map +1 -0
  53. package/dist/integrations/gitlab/merge-request-reconciler.d.ts +6 -0
  54. package/dist/integrations/gitlab/merge-request-reconciler.d.ts.map +1 -0
  55. package/dist/integrations/gitlab/merge-request-reconciler.js +267 -0
  56. package/dist/integrations/gitlab/merge-request-reconciler.js.map +1 -0
  57. package/dist/integrations/gitlab/reconciler.d.ts +5 -0
  58. package/dist/integrations/gitlab/reconciler.d.ts.map +1 -0
  59. package/dist/integrations/gitlab/reconciler.js +31 -0
  60. package/dist/integrations/gitlab/reconciler.js.map +1 -0
  61. package/dist/integrations/gitlab/reconciliation-config.d.ts +3 -0
  62. package/dist/integrations/gitlab/reconciliation-config.d.ts.map +1 -0
  63. package/dist/integrations/gitlab/reconciliation-config.js +17 -0
  64. package/dist/integrations/gitlab/reconciliation-config.js.map +1 -0
  65. package/dist/integrations/gitlab/routes.d.ts +20 -0
  66. package/dist/integrations/gitlab/routes.d.ts.map +1 -0
  67. package/dist/integrations/gitlab/routes.js +473 -0
  68. package/dist/integrations/gitlab/routes.js.map +1 -0
  69. package/dist/integrations/gitlab/rules.d.ts +35 -0
  70. package/dist/integrations/gitlab/rules.d.ts.map +1 -0
  71. package/dist/integrations/gitlab/rules.js +391 -0
  72. package/dist/integrations/gitlab/rules.js.map +1 -0
  73. package/dist/integrations/gitlab/session-subscriptions.d.ts +32 -0
  74. package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -0
  75. package/dist/integrations/gitlab/session-subscriptions.js +192 -0
  76. package/dist/integrations/gitlab/session-subscriptions.js.map +1 -0
  77. package/dist/integrations/gitlab/subscriptions.d.ts +70 -0
  78. package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -0
  79. package/dist/integrations/gitlab/subscriptions.js +76 -0
  80. package/dist/integrations/gitlab/subscriptions.js.map +1 -0
  81. package/dist/integrations/gitlab/version-control.d.ts +16 -0
  82. package/dist/integrations/gitlab/version-control.d.ts.map +1 -0
  83. package/dist/integrations/gitlab/version-control.js +622 -0
  84. package/dist/integrations/gitlab/version-control.js.map +1 -0
  85. package/dist/integrations/gitlab/webhook-dispatch.d.ts +75 -0
  86. package/dist/integrations/gitlab/webhook-dispatch.d.ts.map +1 -0
  87. package/dist/integrations/gitlab/webhook-dispatch.js +235 -0
  88. package/dist/integrations/gitlab/webhook-dispatch.js.map +1 -0
  89. package/dist/integrations/gitlab/webhook.d.ts +72 -0
  90. package/dist/integrations/gitlab/webhook.d.ts.map +1 -0
  91. package/dist/integrations/gitlab/webhook.js +201 -0
  92. package/dist/integrations/gitlab/webhook.js.map +1 -0
  93. package/dist/integrations/incidentio/integration.js +1 -1
  94. package/dist/integrations/issue-reconciler.d.ts +3 -3
  95. package/dist/integrations/issue-reconciler.d.ts.map +1 -1
  96. package/dist/integrations/issue-reconciler.js +22 -2
  97. package/dist/integrations/issue-reconciler.js.map +1 -1
  98. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  99. package/dist/integrations/platform/github/integration.js +21 -0
  100. package/dist/integrations/platform/github/integration.js.map +1 -1
  101. package/dist/integrations/platform/gitlab/event-worker.d.ts +64 -0
  102. package/dist/integrations/platform/gitlab/event-worker.d.ts.map +1 -0
  103. package/dist/integrations/platform/gitlab/event-worker.js +306 -0
  104. package/dist/integrations/platform/gitlab/event-worker.js.map +1 -0
  105. package/dist/integrations/platform/gitlab/integration.d.ts +57 -0
  106. package/dist/integrations/platform/gitlab/integration.d.ts.map +1 -0
  107. package/dist/integrations/platform/gitlab/integration.js +125 -0
  108. package/dist/integrations/platform/gitlab/integration.js.map +1 -0
  109. package/dist/integrations/platform/incidentio/integration.js +1 -1
  110. package/dist/integrations/slack/integration.d.ts.map +1 -1
  111. package/dist/integrations/slack/integration.js +3 -3
  112. package/dist/integrations/slack/integration.js.map +1 -1
  113. package/dist/integrations/slack/slack.d.ts +2 -0
  114. package/dist/integrations/slack/slack.d.ts.map +1 -1
  115. package/dist/integrations/slack/slack.js +21 -7
  116. package/dist/integrations/slack/slack.js.map +1 -1
  117. package/dist/integrations/subscription-session.d.ts +37 -0
  118. package/dist/integrations/subscription-session.d.ts.map +1 -0
  119. package/dist/integrations/subscription-session.js +83 -0
  120. package/dist/integrations/subscription-session.js.map +1 -0
  121. package/dist/packages/_internals/workspace/dist/index.js +1 -11
  122. package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
  123. package/dist/routes/intake.d.ts.map +1 -1
  124. package/dist/routes/intake.js +4 -2
  125. package/dist/routes/intake.js.map +1 -1
  126. package/dist/routes/oauth.js +1 -1
  127. package/dist/routes/projects.d.ts +1 -0
  128. package/dist/routes/projects.d.ts.map +1 -1
  129. package/dist/routes/projects.js +1 -0
  130. package/dist/routes/projects.js.map +1 -1
  131. package/dist/routes/source-control-sessions.d.ts +20 -0
  132. package/dist/routes/source-control-sessions.d.ts.map +1 -0
  133. package/dist/routes/source-control-sessions.js +319 -0
  134. package/dist/routes/source-control-sessions.js.map +1 -0
  135. package/dist/routes/source-control-settings.d.ts +10 -0
  136. package/dist/routes/source-control-settings.d.ts.map +1 -0
  137. package/dist/routes/source-control-settings.js +87 -0
  138. package/dist/routes/source-control-settings.js.map +1 -0
  139. package/dist/routes/surface.d.ts +3 -3
  140. package/dist/routes/surface.d.ts.map +1 -1
  141. package/dist/routes/surface.js +55 -13
  142. package/dist/routes/surface.js.map +1 -1
  143. package/dist/routes/tenant-credentials.d.ts +8 -0
  144. package/dist/routes/tenant-credentials.d.ts.map +1 -1
  145. package/dist/routes/tenant-credentials.js +20 -3
  146. package/dist/routes/tenant-credentials.js.map +1 -1
  147. package/dist/routes/work-items.d.ts.map +1 -1
  148. package/dist/routes/work-items.js +1 -1
  149. package/dist/routes/work-items.js.map +1 -1
  150. package/dist/rules/dispatcher.d.ts.map +1 -1
  151. package/dist/rules/dispatcher.js +7 -1
  152. package/dist/rules/dispatcher.js.map +1 -1
  153. package/dist/rules/start-coordinator.d.ts +1 -1
  154. package/dist/rules/start-coordinator.d.ts.map +1 -1
  155. package/dist/rules/start-coordinator.js +3 -2
  156. package/dist/rules/start-coordinator.js.map +1 -1
  157. package/dist/rules/transition-service.d.ts.map +1 -1
  158. package/dist/rules/transition-service.js +6 -3
  159. package/dist/rules/transition-service.js.map +1 -1
  160. package/dist/rules/types.d.ts +67 -3
  161. package/dist/rules/types.d.ts.map +1 -1
  162. package/dist/rules/types.js +23 -2
  163. package/dist/rules/types.js.map +1 -1
  164. package/dist/rules/validation.d.ts.map +1 -1
  165. package/dist/rules/validation.js +2 -0
  166. package/dist/rules/validation.js.map +1 -1
  167. package/dist/sandbox/git-ref.d.ts +7 -0
  168. package/dist/sandbox/git-ref.d.ts.map +1 -0
  169. package/dist/sandbox/git-ref.js +15 -0
  170. package/dist/sandbox/git-ref.js.map +1 -0
  171. package/dist/sandbox/session-retirement.js +1 -1
  172. package/dist/sandbox/workdir.d.ts.map +1 -1
  173. package/dist/sandbox/workdir.js +6 -6
  174. package/dist/sandbox/workdir.js.map +1 -1
  175. package/dist/session/factory-session.d.ts +31 -1
  176. package/dist/session/factory-session.d.ts.map +1 -1
  177. package/dist/session/factory-session.js +60 -1
  178. package/dist/session/factory-session.js.map +1 -1
  179. package/dist/session/source-control-tools.d.ts +16 -0
  180. package/dist/session/source-control-tools.d.ts.map +1 -0
  181. package/dist/session/source-control-tools.js +472 -0
  182. package/dist/session/source-control-tools.js.map +1 -0
  183. package/dist/storage/domains/intake/base.d.ts +18 -0
  184. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  185. package/dist/storage/domains/intake/base.js +77 -0
  186. package/dist/storage/domains/intake/base.js.map +1 -1
  187. package/dist/work-item-branch.d.ts +3 -1
  188. package/dist/work-item-branch.d.ts.map +1 -1
  189. package/dist/work-item-branch.js +22 -4
  190. package/dist/work-item-branch.js.map +1 -1
  191. package/dist/workspace.d.ts +15 -1
  192. package/dist/workspace.d.ts.map +1 -1
  193. package/dist/workspace.js +57 -23
  194. package/dist/workspace.js.map +1 -1
  195. package/factory-skills/factory-gitlab-rereview/SKILL.md +19 -0
  196. package/factory-skills/factory-gitlab-review/SKILL.md +30 -0
  197. package/factory-skills/factory-rereview/SKILL.md +13 -6
  198. package/factory-skills/factory-review/SKILL.md +13 -6
  199. package/factory-skills/factory-review/references/archaeology.md +225 -0
  200. package/factory-skills/factory-review/references/categories/README.md +34 -0
  201. package/factory-skills/factory-review/references/categories/behavior-change.md +36 -0
  202. package/factory-skills/factory-review/references/categories/bug-fix.md +37 -0
  203. package/factory-skills/factory-review/references/categories/docs.md +32 -0
  204. package/factory-skills/factory-review/references/categories/experimental-flagged.md +34 -0
  205. package/factory-skills/factory-review/references/categories/infra-tooling.md +37 -0
  206. package/factory-skills/factory-review/references/categories/internal-capability.md +35 -0
  207. package/factory-skills/factory-review/references/categories/large-cross-cutting.md +35 -0
  208. package/factory-skills/factory-review/references/categories/mechanical.md +36 -0
  209. package/factory-skills/factory-review/references/categories/performance.md +33 -0
  210. package/factory-skills/factory-review/references/categories/public-api.md +38 -0
  211. package/factory-skills/factory-review/references/categories/refactor.md +35 -0
  212. package/factory-skills/factory-review/references/categories/revert.md +31 -0
  213. package/factory-skills/factory-review/references/categories/schema-storage.md +39 -0
  214. package/factory-skills/factory-review/references/categories/security.md +35 -0
  215. package/factory-skills/factory-review/references/categories/tests-only.md +38 -0
  216. package/package.json +9 -9
@@ -0,0 +1,705 @@
1
+ import { GitLabApiClient, GitLabApiError } from "./api.js";
2
+ import { IssueReconcileWorker } from "../issue-reconcile-worker.js";
3
+ import { buildGitLabAgentTools } from "./agent-tools.js";
4
+ import { resolveGitLabRules } from "./default-rules.js";
5
+ import { attachGitLabRules } from "./rules.js";
6
+ import { attachGitLabReconciler } from "./reconciler.js";
7
+ import { gitlabReconciliationEnabled, gitlabReconciliationInterval } from "./reconciliation-config.js";
8
+ import { buildGitLabRoutes } from "./routes.js";
9
+ import { createGitLabSubscriptionTools, parseCreatedMergeRequest, subscribeCurrentSessionToMergeRequest } from "./session-subscriptions.js";
10
+ import { buildGitLabVersionControl } from "./version-control.js";
11
+ //#region src/integrations/gitlab/integration.ts
12
+ const DIRECT_CONNECTION_ID = "direct";
13
+ const GITLAB_CONNECTION_TOKEN_PREFIX = "gitlab-connection:";
14
+ const GITLAB_SOURCE_PREFIX = "gitlab-project:";
15
+ const GITLAB_ISSUE_PREFIX = "gitlab-issue:";
16
+ const GITLAB_MERGE_REQUEST_PREFIX = "gitlab-pr:";
17
+ const GITLAB_TRUSTED_ACCESS_LEVEL = 30;
18
+ const MAX_NOTES_PAGES = 20;
19
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20
+ var GitLabIntegrationBase = class {
21
+ id = "gitlab";
22
+ #projects;
23
+ #sourceControl;
24
+ #integrationStorage;
25
+ #auth;
26
+ #orgIdByResourceId = /* @__PURE__ */ new Map();
27
+ rules;
28
+ constructor(rules) {
29
+ this.rules = resolveGitLabRules(rules);
30
+ }
31
+ intake = {
32
+ resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
33
+ listSources: () => this.#listSources(),
34
+ listItems: (input) => this.#listItems(input),
35
+ listIssues: (input) => this.#listIssues(input),
36
+ getIssue: (input) => this.#getIssue(input),
37
+ createComment: (input) => this.#createComment(input),
38
+ updateIssue: (input) => this.#updateIssue(input)
39
+ };
40
+ versionControl = buildGitLabVersionControl({
41
+ contextForConnection: (connection) => this.#versionControlContext(connection),
42
+ contextForStoredInstallation: (connection, host) => this.#versionControlContextForInstallation(connection, host)
43
+ });
44
+ initialize({ storage, projects, auth, sourceControl }) {
45
+ this.#projects = projects;
46
+ this.#auth = auth;
47
+ this.#sourceControl = sourceControl;
48
+ this.#integrationStorage = storage;
49
+ }
50
+ /** Generic integration persistence (merge-request subscriptions). */
51
+ get integrationStorage() {
52
+ if (!this.#integrationStorage) throw new Error(`${this.constructor.name} is not initialized — the factory binds storage during prepare().`);
53
+ return this.#integrationStorage;
54
+ }
55
+ /** Source-control rows for this integration; absent when the host runs without them. */
56
+ get sourceControlStorage() {
57
+ return this.#sourceControl;
58
+ }
59
+ get authEnabled() {
60
+ return this.#auth?.enabled() ?? false;
61
+ }
62
+ get projects() {
63
+ if (!this.#projects) throw new Error(`${this.constructor.name} is not initialized — the factory binds storage during prepare().`);
64
+ return this.#projects;
65
+ }
66
+ async resolveOrgId(resourceId) {
67
+ const cached = this.#orgIdByResourceId.get(resourceId);
68
+ if (cached !== void 0) return cached;
69
+ if (!UUID_PATTERN.test(resourceId)) {
70
+ this.#orgIdByResourceId.set(resourceId, null);
71
+ return null;
72
+ }
73
+ try {
74
+ await this.projects.ensureReady();
75
+ const orgId = (await this.projects.getById({ id: resourceId }))?.orgId ?? null;
76
+ this.#orgIdByResourceId.set(resourceId, orgId);
77
+ return orgId;
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+ clearCaches() {
83
+ this.#orgIdByResourceId.clear();
84
+ }
85
+ /** Resolve only a GitLab repository linked to this Factory in this organization. */
86
+ async getLinkedRepository(input) {
87
+ if (!this.#sourceControl) return null;
88
+ const connections = await this.#sourceControl.connections.list({
89
+ orgId: input.orgId,
90
+ factoryProjectId: input.factoryProjectId
91
+ });
92
+ for (const connection of connections) {
93
+ if (connection.integrationId !== "gitlab") continue;
94
+ const link = (await this.#sourceControl.projectRepositories.list({
95
+ orgId: input.orgId,
96
+ connectionId: connection.id
97
+ })).find((candidate) => candidate.id === input.projectRepositoryId);
98
+ if (!link) continue;
99
+ const repository = await this.#sourceControl.repositories.get({
100
+ orgId: input.orgId,
101
+ id: link.repositoryId
102
+ });
103
+ const installation = await this.#sourceControl.installations.get({
104
+ orgId: input.orgId,
105
+ id: connection.installationId
106
+ });
107
+ if (!repository || !installation) return null;
108
+ const host = installation.providerMetadata.host;
109
+ if (typeof host !== "string" || !host) return null;
110
+ return {
111
+ repository,
112
+ host: normalizeGitLabHost(host)
113
+ };
114
+ }
115
+ return null;
116
+ }
117
+ async getProjectMemberAccessLevel(connectionId, projectId, username) {
118
+ const normalized = username.trim().toLowerCase();
119
+ if (!normalized) return void 0;
120
+ return (await (await this.contextById(connectionId)).api.listProjectMembers(projectId, { query: username })).find((candidate) => candidate.username.toLowerCase() === normalized && candidate.state !== "blocked")?.access_level;
121
+ }
122
+ async getWorkItemAuthorUsername(connectionId, projectId, kind, iid) {
123
+ const api = (await this.contextById(connectionId)).api;
124
+ return (kind === "issue" ? await api.getIssue(projectId, iid) : await api.getMergeRequest(projectId, iid)).author?.username;
125
+ }
126
+ async isProjectMemberTrustedForSource(sourceId, username) {
127
+ const reference = decodeSourceId(sourceId);
128
+ if (!reference) throw new GitLabApiError("GitLab source identity is invalid.", 400);
129
+ const { context, source } = await this.#resolveSourceReference(reference);
130
+ return (await this.getProjectMemberAccessLevel(context.id, source.projectId, username) ?? 0) >= 30;
131
+ }
132
+ async getIssueForFactoryProject(input) {
133
+ if (!this.#sourceControl) throw new GitLabApiError("GitLab source control is unavailable.", 503);
134
+ const reference = decodeIssueReference(input.issueId);
135
+ const locator = parseIssueLocator(input.issueId);
136
+ if (!reference && !locator) throw new GitLabApiError("GitLab issue must include a project and issue IID.", 400);
137
+ const normalizePath = (value) => value.replace(/^\/+|\/+$/g, "").toLowerCase();
138
+ const matches = [];
139
+ const connections = await this.#sourceControl.connections.list({
140
+ orgId: input.orgId,
141
+ factoryProjectId: input.factoryProjectId
142
+ });
143
+ for (const connection of connections) {
144
+ const installation = await this.#sourceControl.installations.get({
145
+ orgId: input.orgId,
146
+ id: connection.installationId
147
+ });
148
+ if (!installation) continue;
149
+ for (const projectRepository of await this.#sourceControl.projectRepositories.list({
150
+ orgId: input.orgId,
151
+ connectionId: connection.id
152
+ })) {
153
+ const repository = await this.#sourceControl.repositories.get({
154
+ orgId: input.orgId,
155
+ id: projectRepository.repositoryId
156
+ });
157
+ if (!repository) continue;
158
+ if (reference?.projectId && repository.externalId !== reference.projectId) continue;
159
+ if (reference?.projectPath && normalizePath(repository.slug) !== normalizePath(reference.projectPath)) continue;
160
+ if (locator?.projectPath && normalizePath(repository.slug) !== normalizePath(locator.projectPath)) continue;
161
+ if (reference?.connectionId && installation.externalId !== reference.connectionId) continue;
162
+ const installationHost = typeof installation.providerMetadata.host === "string" ? normalizeGitLabHost(installation.providerMetadata.host) : null;
163
+ if (reference?.host && installationHost !== normalizeGitLabHost(reference.host)) continue;
164
+ if (locator?.host && installationHost !== normalizeGitLabHost(locator.host)) continue;
165
+ matches.push({
166
+ connectionId: installation.externalId,
167
+ projectId: repository.externalId,
168
+ projectPath: repository.slug
169
+ });
170
+ }
171
+ }
172
+ const match = matches.sort((left, right) => left.connectionId.localeCompare(right.connectionId))[0];
173
+ if (!match) throw new GitLabApiError("GitLab issue is outside the active Factory project.", 404);
174
+ return this.intake.getIssue({
175
+ connection: gitlabConnection(match.connectionId),
176
+ sourceId: encodeSourceId(match),
177
+ issueId: input.issueId
178
+ });
179
+ }
180
+ /** Platform mode exposes org connections; direct mode returns undefined. */
181
+ async statusConnections() {}
182
+ /** Direct mode verifies its configured credential; Platform mode uses connection status. */
183
+ async verifyStatus() {}
184
+ get webhookSecret() {}
185
+ workers(ctx) {
186
+ if (!gitlabReconciliationEnabled()) return [];
187
+ const reconcile = attachGitLabReconciler(this, ctx);
188
+ if (!reconcile) return [];
189
+ const intervalMs = gitlabReconciliationInterval();
190
+ return [new IssueReconcileWorker({
191
+ integrationId: this.id,
192
+ reconcile,
193
+ ...intervalMs ? { intervalMs } : {}
194
+ })];
195
+ }
196
+ routes(ctx) {
197
+ const ingestFactoryEvent = attachGitLabRules(this, ctx);
198
+ return buildGitLabRoutes({
199
+ gitlab: this,
200
+ auth: ctx.auth,
201
+ intake: ctx.storage?.intake,
202
+ emitAudit: ctx.hooks?.emitAudit,
203
+ sandbox: ctx.sandbox,
204
+ webhookSecret: this.webhookSecret,
205
+ ingestFactoryEvent,
206
+ ...ctx.controller ? { controller: ctx.controller } : {}
207
+ });
208
+ }
209
+ async agentTools(args) {
210
+ return buildGitLabAgentTools({
211
+ requestContext: args.requestContext,
212
+ gitlab: this
213
+ });
214
+ }
215
+ sessionTools({ requestContext }) {
216
+ return createGitLabSubscriptionTools(requestContext, this);
217
+ }
218
+ async postToolObserver({ toolContext, requestContext }) {
219
+ const mergeRequestUrl = parseCreatedMergeRequest(toolContext);
220
+ if (!mergeRequestUrl || !requestContext) return;
221
+ await subscribeCurrentSessionToMergeRequest(requestContext, mergeRequestUrl, "auto-create-change-request", this);
222
+ }
223
+ /** Prefer the configured direct token over an older Platform installation on the same host. */
224
+ async resolveActiveConnectionForHost(storedConnectionId, host) {
225
+ if (this.diagnostics().mode !== "direct") return storedConnectionId;
226
+ const [direct] = await this.activeContexts();
227
+ return direct && normalizeGitLabHost(direct.host) === normalizeGitLabHost(host) ? direct.id : storedConnectionId;
228
+ }
229
+ async #versionControlContext(connection) {
230
+ const connectionId = connectionIdFromConnection(connection);
231
+ if (connectionId) return this.contextById(connectionId);
232
+ const contexts = await this.activeContexts();
233
+ if (contexts.length !== 1) throw new GitLabApiError("GitLab version-control requests must identify a connection when multiple GitLab accounts are connected.", 400);
234
+ return contexts[0];
235
+ }
236
+ async #versionControlContextForInstallation(connection, host) {
237
+ if (this.diagnostics().mode === "direct" && host) {
238
+ const [direct] = await this.activeContexts();
239
+ if (direct && normalizeGitLabHost(host) === normalizeGitLabHost(direct.host)) return direct;
240
+ }
241
+ return this.#versionControlContext(connection);
242
+ }
243
+ async #contextForReference(reference) {
244
+ if (reference.connectionId) return this.contextById(reference.connectionId);
245
+ const contexts = (await this.activeContexts()).filter((context) => !reference.host || normalizeGitLabHost(context.host) === normalizeGitLabHost(reference.host));
246
+ if (contexts.length === 0) throw new GitLabApiError("No active GitLab connection can access this source.", 401);
247
+ if (contexts.length === 1) return contexts[0];
248
+ if (this.#sourceControl) {
249
+ const linkedConnectionIds = new Set((await this.#sourceControl.projectRepositories.listConfiguredExternalKeys()).filter((key) => key.repositoryExternalId === reference.projectId).map((key) => key.installationExternalId));
250
+ const linked = contexts.filter((context) => linkedConnectionIds.has(context.id));
251
+ if (linked.length > 0) return linked.sort((left, right) => left.id.localeCompare(right.id))[0];
252
+ }
253
+ for (const context of contexts.sort((left, right) => left.id.localeCompare(right.id))) try {
254
+ await context.api.getProject(reference.projectId);
255
+ return context;
256
+ } catch (error) {
257
+ if (!(error instanceof GitLabApiError) || ![
258
+ 401,
259
+ 403,
260
+ 404
261
+ ].includes(error.status ?? 0)) throw error;
262
+ }
263
+ throw new GitLabApiError("No active GitLab connection can access this source.", 404);
264
+ }
265
+ async #resolveSourceReference(reference) {
266
+ const context = await this.#contextForReference(reference);
267
+ const projectPath = reference.projectPath ?? (await context.api.getProject(reference.projectId)).path_with_namespace;
268
+ return {
269
+ context,
270
+ source: {
271
+ connectionId: context.id,
272
+ host: normalizeGitLabHost(reference.host ?? context.host),
273
+ projectId: reference.projectId,
274
+ projectPath
275
+ }
276
+ };
277
+ }
278
+ async #resolveIntakeDispatch({ externalSource }) {
279
+ if (externalSource.type !== "issue") return null;
280
+ const reference = decodeIssueReference(externalSource.externalId);
281
+ if (!reference) return null;
282
+ const { context, source } = await this.#resolveSourceReference(reference);
283
+ return {
284
+ connection: context.connection,
285
+ sourceId: encodeSourceId(source),
286
+ issueId: String(reference.issueIid)
287
+ };
288
+ }
289
+ async #listSources() {
290
+ const sources = /* @__PURE__ */ new Map();
291
+ for (const context of await this.activeContexts()) for (let page = 1;; page++) {
292
+ const projects = await context.api.listProjects({ page });
293
+ for (const project of projects) {
294
+ const source = this.#toIntakeSource(context, project);
295
+ if (!sources.has(source.id)) sources.set(source.id, source);
296
+ }
297
+ if (projects.length < 100) break;
298
+ }
299
+ return [...sources.values()];
300
+ }
301
+ async #listItems(input) {
302
+ const result = await this.#listIssuePage(input.sourceIds, input.cursor);
303
+ return {
304
+ items: result.issues.map(({ issue, source, context }) => ({
305
+ source: {
306
+ type: "issue",
307
+ externalId: encodeIssueReference({
308
+ ...source,
309
+ issueIid: issue.iid
310
+ }),
311
+ url: issue.web_url
312
+ },
313
+ sourceId: encodeSourceId(source),
314
+ title: `${source.projectPath}#${issue.iid}: ${issue.title}`,
315
+ status: issue.state,
316
+ labels: issue.labels ?? [],
317
+ assignee: displayName(issue.assignee),
318
+ createdAt: issue.created_at,
319
+ updatedAt: issue.updated_at,
320
+ metadata: {
321
+ identifier: `${source.projectPath}#${issue.iid}`,
322
+ projectId: source.projectId,
323
+ projectPath: source.projectPath,
324
+ connectionId: context.id,
325
+ accountLabel: context.label,
326
+ labelColors: Object.fromEntries((issue.labelDetails ?? []).map((label) => [label.name, label.color]))
327
+ }
328
+ })),
329
+ nextCursor: result.nextCursor
330
+ };
331
+ }
332
+ async #listIssues(input) {
333
+ const result = await this.#listIssuePage(input.sourceIds, input.cursor, input.labels);
334
+ return {
335
+ issues: result.issues.map(({ issue, source }) => ({
336
+ ...this.#toIntakeIssue(issue, source.projectPath),
337
+ sourceId: encodeSourceId(source)
338
+ })),
339
+ nextCursor: result.nextCursor
340
+ };
341
+ }
342
+ async #listIssuePage(sourceIds, cursor, labels) {
343
+ const sources = sourceIds.map(decodeSourceId).filter((source) => source !== null);
344
+ if (sources.length === 0) return {
345
+ issues: [],
346
+ nextCursor: null
347
+ };
348
+ const current = decodePageCursor(cursor);
349
+ if (!current || current.source >= sources.length) return {
350
+ issues: [],
351
+ nextCursor: null
352
+ };
353
+ for (let sourceIndex = current.source; sourceIndex < sources.length; sourceIndex++) {
354
+ const { source, context } = await this.#resolveSourceReference(sources[sourceIndex]);
355
+ const page = sourceIndex === current.source ? current.page : 1;
356
+ const issues = await context.api.listIssues(source.projectId, {
357
+ page,
358
+ labels
359
+ });
360
+ const nextCursor = issues.length === 30 ? encodePageCursor({
361
+ source: sourceIndex,
362
+ page: page + 1
363
+ }) : sourceIndex + 1 < sources.length ? encodePageCursor({
364
+ source: sourceIndex + 1,
365
+ page: 1
366
+ }) : null;
367
+ return {
368
+ issues: issues.map((issue) => ({
369
+ issue,
370
+ source,
371
+ context
372
+ })),
373
+ nextCursor
374
+ };
375
+ }
376
+ return {
377
+ issues: [],
378
+ nextCursor: null
379
+ };
380
+ }
381
+ async #getIssue(input) {
382
+ const resolved = await this.#resolveRequest(input);
383
+ let issue;
384
+ try {
385
+ issue = await resolved.context.api.getIssue(resolved.projectId, resolved.issueIid);
386
+ } catch (error) {
387
+ if (error instanceof GitLabApiError && error.status === 404) return null;
388
+ throw error;
389
+ }
390
+ const notes = await this.#listAllNotes(resolved.context.api, resolved.projectId, resolved.issueIid);
391
+ return {
392
+ ...this.#toIntakeIssue(issue, resolved.projectPath),
393
+ description: issue.description?.trim() || null,
394
+ comments: notes.filter((note) => !note.system).map((note) => ({
395
+ author: displayName(note.author),
396
+ body: note.body,
397
+ createdAt: note.created_at
398
+ }))
399
+ };
400
+ }
401
+ async #listAllNotes(api, projectId, issueIid) {
402
+ const notes = [];
403
+ for (let page = 1; page <= MAX_NOTES_PAGES; page++) {
404
+ const result = await api.listNotes(projectId, issueIid, { page });
405
+ notes.push(...result);
406
+ if (result.length < 100) break;
407
+ }
408
+ return notes;
409
+ }
410
+ async #createComment(input) {
411
+ const resolved = await this.#resolveRequest(input);
412
+ try {
413
+ const issue = await resolved.context.api.getIssue(resolved.projectId, resolved.issueIid);
414
+ const note = await resolved.context.api.createNote(resolved.projectId, resolved.issueIid, input.body);
415
+ return {
416
+ id: String(note.id),
417
+ url: `${issue.web_url}#note_${note.id}`
418
+ };
419
+ } catch (error) {
420
+ if (error instanceof GitLabApiError && error.status === 404) return null;
421
+ throw error;
422
+ }
423
+ }
424
+ async #updateIssue(input) {
425
+ const resolved = await this.#resolveRequest(input);
426
+ let issue;
427
+ try {
428
+ issue = await resolved.context.api.getIssue(resolved.projectId, resolved.issueIid);
429
+ } catch (error) {
430
+ if (error instanceof GitLabApiError && error.status === 404) return null;
431
+ throw error;
432
+ }
433
+ const stateEvent = targetStateEvent(input);
434
+ if (!stateEvent) return null;
435
+ if (stateEvent === "close" && issue.state === "closed" || stateEvent === "reopen" && issue.state === "opened") return this.#toIntakeIssue(issue, resolved.projectPath);
436
+ const updated = await resolved.context.api.updateIssueState(resolved.projectId, resolved.issueIid, stateEvent);
437
+ return this.#toIntakeIssue(updated, resolved.projectPath);
438
+ }
439
+ async #resolveRequest(input) {
440
+ const issueReference = decodeIssueReference(input.issueId);
441
+ const source = input.sourceId ? decodeSourceId(input.sourceId) : null;
442
+ const locator = parseIssueLocator(input.issueId);
443
+ const reference = issueReference ?? source;
444
+ let context;
445
+ if (reference) context = await this.#contextForReference(reference);
446
+ else {
447
+ const connectionId = connectionIdFromConnection(input.connection);
448
+ const contexts = connectionId ? [await this.contextById(connectionId)] : await this.activeContexts();
449
+ if (contexts.length !== 1) throw new GitLabApiError("GitLab issue reference must resolve to exactly one active connection.", 400);
450
+ context = contexts[0];
451
+ }
452
+ const projectId = issueReference?.projectId ?? source?.projectId ?? locator?.projectPath;
453
+ const issueIid = issueReference?.issueIid ?? locator?.issueIid ?? parsePositiveInteger(input.issueId);
454
+ if (!projectId || !issueIid) throw new GitLabApiError("GitLab issue must include a project and issue IID (for example, group/project#42).", 400);
455
+ const projectPath = issueReference?.projectPath ?? source?.projectPath ?? locator?.projectPath ?? (await context.api.getProject(projectId)).path_with_namespace;
456
+ return {
457
+ context,
458
+ projectId,
459
+ projectPath,
460
+ issueIid
461
+ };
462
+ }
463
+ #toIntakeSource(context, project) {
464
+ return {
465
+ id: encodeSourceId({
466
+ host: normalizeGitLabHost(context.host),
467
+ projectId: String(project.id),
468
+ projectPath: project.path_with_namespace
469
+ }),
470
+ name: project.path_with_namespace,
471
+ type: "project",
472
+ metadata: {
473
+ projectId: String(project.id),
474
+ projectPath: project.path_with_namespace,
475
+ defaultBranch: project.default_branch ?? null,
476
+ connectionId: context.id,
477
+ accountLabel: context.label,
478
+ url: project.web_url
479
+ }
480
+ };
481
+ }
482
+ #toIntakeIssue(issue, projectPath) {
483
+ const assignees = issue.assignees?.map(displayName).filter((name) => name !== null) ?? [];
484
+ return {
485
+ id: String(issue.iid),
486
+ identifier: `${projectPath}#${issue.iid}`,
487
+ title: issue.title,
488
+ url: issue.web_url,
489
+ author: displayName(issue.author),
490
+ authorUsername: issue.author?.username ?? null,
491
+ state: issue.state,
492
+ stateType: issue.state === "closed" ? "completed" : "unstarted",
493
+ priority: typeof issue.weight === "number" ? String(issue.weight) : null,
494
+ assignee: displayName(issue.assignee) ?? assignees[0] ?? null,
495
+ assignees,
496
+ source: projectPath,
497
+ labels: issue.labels ?? [],
498
+ labelColors: Object.fromEntries((issue.labelDetails ?? []).map((label) => [label.name, label.color])),
499
+ commentCount: issue.user_notes_count ?? null,
500
+ createdAt: issue.created_at,
501
+ updatedAt: issue.updated_at
502
+ };
503
+ }
504
+ };
505
+ const DIRECT_CONNECTION_TOKEN = "gitlab-direct-access-token";
506
+ var GitLabIntegration = class extends GitLabIntegrationBase {
507
+ #accessToken;
508
+ #accessTokenType;
509
+ #baseUrl;
510
+ #webhookSecret;
511
+ #context;
512
+ constructor(config = {}) {
513
+ super(config.rules);
514
+ const accessToken = config.accessToken?.trim() || process.env.GITLAB_ACCESS_TOKEN?.trim();
515
+ if (!accessToken) throw new Error("GitLabIntegration: missing required GITLAB_ACCESS_TOKEN.");
516
+ this.#accessToken = accessToken;
517
+ this.#accessTokenType = parseAccessTokenType(config.accessTokenType ?? process.env.GITLAB_ACCESS_TOKEN_TYPE?.trim());
518
+ this.#baseUrl = (config.baseUrl ?? process.env.GITLAB_BASE_URL?.trim() ?? "https://gitlab.com").replace(/\/+$/, "");
519
+ this.#webhookSecret = config.webhookSecret?.trim() || process.env.GITLAB_WEBHOOK_SECRET?.trim() || void 0;
520
+ const api = new GitLabApiClient({
521
+ baseUrl: this.#baseUrl,
522
+ accessToken: this.#accessToken,
523
+ ...config.fetchImpl ? { fetchImpl: config.fetchImpl } : {}
524
+ });
525
+ this.#context = {
526
+ id: DIRECT_CONNECTION_ID,
527
+ label: new URL(this.#baseUrl).host,
528
+ api,
529
+ connection: {
530
+ type: "oauth",
531
+ accessToken: DIRECT_CONNECTION_TOKEN
532
+ },
533
+ host: new URL(this.#baseUrl).host,
534
+ webBaseUrl: this.#baseUrl,
535
+ repositoryAccessToken: async () => this.#accessToken
536
+ };
537
+ }
538
+ get webhookSecret() {
539
+ return this.#webhookSecret;
540
+ }
541
+ async hasActiveConnections() {
542
+ return true;
543
+ }
544
+ async verifyStatus() {
545
+ await this.#context.api.getCurrentUser();
546
+ }
547
+ authFailureMessage() {
548
+ return `GitLab rejected the configured ${this.#accessTokenType} access token. Check the token and its scopes.`;
549
+ }
550
+ async activeContexts() {
551
+ return [this.#context];
552
+ }
553
+ async contextById(connectionId) {
554
+ if (connectionId !== DIRECT_CONNECTION_ID) throw new GitLabApiError("GitLab connection is unavailable.", 401);
555
+ return this.#context;
556
+ }
557
+ diagnostics() {
558
+ return {
559
+ configured: true,
560
+ mode: "direct",
561
+ accessTokenType: this.#accessTokenType,
562
+ endpointHost: new URL(this.#baseUrl).host,
563
+ webhookConfigured: Boolean(this.#webhookSecret)
564
+ };
565
+ }
566
+ };
567
+ function parseAccessTokenType(value) {
568
+ const normalized = value?.trim().toLowerCase() || "personal";
569
+ if (normalized === "personal" || normalized === "group") return normalized;
570
+ throw new Error("GitLabIntegration: GITLAB_ACCESS_TOKEN_TYPE must be 'personal' or 'group'.");
571
+ }
572
+ function encodeSourceId(reference) {
573
+ if (reference.host) return `${GITLAB_SOURCE_PREFIX}${encodeOpaque({
574
+ version: 2,
575
+ host: normalizeGitLabHost(reference.host),
576
+ projectId: reference.projectId
577
+ })}`;
578
+ if (!reference.connectionId || !reference.projectPath) throw new Error("GitLab source reference is missing its instance host.");
579
+ return `${GITLAB_SOURCE_PREFIX}${encodeOpaque(reference)}`;
580
+ }
581
+ function decodeSourceId(value) {
582
+ return decodeOpaque(value, GITLAB_SOURCE_PREFIX, isSourceReference);
583
+ }
584
+ function encodeIssueReference(reference) {
585
+ if (reference.host) return `${GITLAB_ISSUE_PREFIX}${encodeOpaque({
586
+ version: 2,
587
+ host: normalizeGitLabHost(reference.host),
588
+ projectId: reference.projectId,
589
+ issueIid: reference.issueIid
590
+ })}`;
591
+ if (!reference.connectionId || !reference.projectPath) throw new Error("GitLab issue reference is missing its instance host.");
592
+ return `${GITLAB_ISSUE_PREFIX}${encodeOpaque(reference)}`;
593
+ }
594
+ function decodeIssueReference(value) {
595
+ return decodeOpaque(value, GITLAB_ISSUE_PREFIX, isIssueReference);
596
+ }
597
+ function decodeMergeRequestReference(value) {
598
+ return decodeOpaque(value, GITLAB_MERGE_REQUEST_PREFIX, isMergeRequestReference);
599
+ }
600
+ function gitlabConnection(connectionId) {
601
+ return {
602
+ type: "oauth",
603
+ accessToken: `${GITLAB_CONNECTION_TOKEN_PREFIX}${connectionId}`
604
+ };
605
+ }
606
+ function connectionIdFromConnection(connection) {
607
+ if (connection.type !== "oauth" || !connection.accessToken.startsWith(GITLAB_CONNECTION_TOKEN_PREFIX)) return null;
608
+ return connection.accessToken.slice(18) || null;
609
+ }
610
+ function encodeOpaque(value) {
611
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
612
+ }
613
+ function decodeOpaque(value, prefix, guard) {
614
+ if (!value.startsWith(prefix)) return null;
615
+ try {
616
+ const decoded = JSON.parse(Buffer.from(value.slice(prefix.length), "base64url").toString("utf8"));
617
+ return guard(decoded) ? decoded : null;
618
+ } catch {
619
+ return null;
620
+ }
621
+ }
622
+ function normalizeGitLabHost(host) {
623
+ return host.trim().toLowerCase().replace(/\.$/, "");
624
+ }
625
+ function isSourceReference(value) {
626
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
627
+ const ref = value;
628
+ if (typeof ref.projectId !== "string" || ref.projectId.length === 0) return false;
629
+ const canonical = ref.version === 2 && typeof ref.host === "string" && normalizeGitLabHost(ref.host).length > 0;
630
+ const legacy = typeof ref.connectionId === "string" && ref.connectionId.length > 0 && typeof ref.projectPath === "string" && ref.projectPath.length > 0;
631
+ return canonical || legacy;
632
+ }
633
+ function isIssueReference(value) {
634
+ return isSourceReference(value) && Number.isSafeInteger(value.issueIid) && value.issueIid > 0;
635
+ }
636
+ function isMergeRequestReference(value) {
637
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
638
+ const ref = value;
639
+ return ref.version === 1 && typeof ref.host === "string" && normalizeGitLabHost(ref.host).length > 0 && Number.isSafeInteger(ref.projectId) && ref.projectId > 0 && Number.isSafeInteger(ref.mergeRequestIid) && ref.mergeRequestIid > 0;
640
+ }
641
+ function encodePageCursor(cursor) {
642
+ return encodeOpaque(cursor);
643
+ }
644
+ function decodePageCursor(value) {
645
+ if (!value) return {
646
+ source: 0,
647
+ page: 1
648
+ };
649
+ try {
650
+ const decoded = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
651
+ if (!Number.isSafeInteger(decoded.source) || Number(decoded.source) < 0) return null;
652
+ if (!Number.isSafeInteger(decoded.page) || Number(decoded.page) < 1) return null;
653
+ return {
654
+ source: Number(decoded.source),
655
+ page: Number(decoded.page)
656
+ };
657
+ } catch {
658
+ return null;
659
+ }
660
+ }
661
+ function displayName(user) {
662
+ return user?.name?.trim() || user?.username || null;
663
+ }
664
+ function parsePositiveInteger(value) {
665
+ if (!/^\d+$/.test(value)) return null;
666
+ const parsed = Number(value);
667
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
668
+ }
669
+ function parseIssueLocator(value) {
670
+ const shorthand = value.match(/^(.+?)#(\d+)$/);
671
+ if (shorthand) {
672
+ const issueIid = parsePositiveInteger(shorthand[2]);
673
+ const projectPath = shorthand[1].replace(/^\/+|\/+$/g, "");
674
+ return issueIid && projectPath ? {
675
+ projectPath,
676
+ issueIid
677
+ } : null;
678
+ }
679
+ try {
680
+ const url = new URL(value);
681
+ const match = url.pathname.match(/^\/(.+)\/-\/issues\/(\d+)\/?$/);
682
+ if (!match) return null;
683
+ const issueIid = parsePositiveInteger(match[2]);
684
+ if (!issueIid) return null;
685
+ const projectPath = decodeURIComponent(match[1]);
686
+ return {
687
+ host: url.host,
688
+ projectPath,
689
+ issueIid
690
+ };
691
+ } catch {
692
+ return null;
693
+ }
694
+ }
695
+ function targetStateEvent(input) {
696
+ if (input.state.kind === "byType") return input.state.stateType === "completed" || input.state.stateType === "canceled" ? "close" : "reopen";
697
+ const name = input.state.name.trim().toLowerCase();
698
+ if (name === "closed" || name === "close") return "close";
699
+ if (name === "opened" || name === "open" || name === "reopen") return "reopen";
700
+ return null;
701
+ }
702
+ //#endregion
703
+ export { GITLAB_TRUSTED_ACCESS_LEVEL, GitLabIntegration, GitLabIntegrationBase, decodeIssueReference, decodeMergeRequestReference, decodeSourceId, encodeIssueReference, encodeSourceId, gitlabConnection, normalizeGitLabHost };
704
+
705
+ //# sourceMappingURL=integration.js.map