@mastra/factory 0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150

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 (313) hide show
  1. package/CHANGELOG.md +695 -0
  2. package/LICENSE.md +30 -0
  3. package/README.md +27 -0
  4. package/dist/auth.d.ts +181 -0
  5. package/dist/auth.d.ts.map +1 -0
  6. package/dist/auth.js +505 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/capabilities/connection.d.ts +9 -0
  9. package/dist/capabilities/connection.d.ts.map +1 -0
  10. package/dist/capabilities/connection.js +1 -0
  11. package/dist/capabilities/intake.d.ts +140 -0
  12. package/dist/capabilities/intake.d.ts.map +1 -0
  13. package/dist/capabilities/intake.js +1 -0
  14. package/dist/capabilities/version-control.d.ts +245 -0
  15. package/dist/capabilities/version-control.d.ts.map +1 -0
  16. package/dist/capabilities/version-control.js +1 -0
  17. package/dist/factory.d.ts +156 -0
  18. package/dist/factory.d.ts.map +1 -0
  19. package/dist/factory.js +483 -0
  20. package/dist/factory.js.map +1 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +7 -0
  24. package/dist/integrations/base.d.ts +210 -0
  25. package/dist/integrations/base.d.ts.map +1 -0
  26. package/dist/integrations/base.js +1 -0
  27. package/dist/integrations/github/config.d.ts +60 -0
  28. package/dist/integrations/github/config.d.ts.map +1 -0
  29. package/dist/integrations/github/config.js +42 -0
  30. package/dist/integrations/github/config.js.map +1 -0
  31. package/dist/integrations/github/factory-session.d.ts +12 -0
  32. package/dist/integrations/github/factory-session.d.ts.map +1 -0
  33. package/dist/integrations/github/factory-session.js +37 -0
  34. package/dist/integrations/github/factory-session.js.map +1 -0
  35. package/dist/integrations/github/integration.d.ts +191 -0
  36. package/dist/integrations/github/integration.d.ts.map +1 -0
  37. package/dist/integrations/github/integration.js +1077 -0
  38. package/dist/integrations/github/integration.js.map +1 -0
  39. package/dist/integrations/github/issue-triage.d.ts +27 -0
  40. package/dist/integrations/github/issue-triage.d.ts.map +1 -0
  41. package/dist/integrations/github/issue-triage.js +82 -0
  42. package/dist/integrations/github/issue-triage.js.map +1 -0
  43. package/dist/integrations/github/pat.d.ts +38 -0
  44. package/dist/integrations/github/pat.d.ts.map +1 -0
  45. package/dist/integrations/github/pat.js +59 -0
  46. package/dist/integrations/github/pat.js.map +1 -0
  47. package/dist/integrations/github/provenance.d.ts +27 -0
  48. package/dist/integrations/github/provenance.d.ts.map +1 -0
  49. package/dist/integrations/github/provenance.js +75 -0
  50. package/dist/integrations/github/provenance.js.map +1 -0
  51. package/dist/integrations/github/routes.d.ts +69 -0
  52. package/dist/integrations/github/routes.d.ts.map +1 -0
  53. package/dist/integrations/github/routes.js +1324 -0
  54. package/dist/integrations/github/routes.js.map +1 -0
  55. package/dist/integrations/github/rules.d.ts +78 -0
  56. package/dist/integrations/github/rules.d.ts.map +1 -0
  57. package/dist/integrations/github/rules.js +362 -0
  58. package/dist/integrations/github/rules.js.map +1 -0
  59. package/dist/integrations/github/sandbox-release.d.ts +38 -0
  60. package/dist/integrations/github/sandbox-release.d.ts.map +1 -0
  61. package/dist/integrations/github/sandbox-release.js +74 -0
  62. package/dist/integrations/github/sandbox-release.js.map +1 -0
  63. package/dist/integrations/github/sandbox.d.ts +281 -0
  64. package/dist/integrations/github/sandbox.d.ts.map +1 -0
  65. package/dist/integrations/github/sandbox.js +547 -0
  66. package/dist/integrations/github/sandbox.js.map +1 -0
  67. package/dist/integrations/github/session-subscriptions.d.ts +26 -0
  68. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
  69. package/dist/integrations/github/session-subscriptions.js +192 -0
  70. package/dist/integrations/github/session-subscriptions.js.map +1 -0
  71. package/dist/integrations/github/subscriptions.d.ts +54 -0
  72. package/dist/integrations/github/subscriptions.d.ts.map +1 -0
  73. package/dist/integrations/github/subscriptions.js +62 -0
  74. package/dist/integrations/github/subscriptions.js.map +1 -0
  75. package/dist/integrations/github/token-refresh.d.ts +12 -0
  76. package/dist/integrations/github/token-refresh.d.ts.map +1 -0
  77. package/dist/integrations/github/token-refresh.js +24 -0
  78. package/dist/integrations/github/token-refresh.js.map +1 -0
  79. package/dist/integrations/github/webhook.d.ts +87 -0
  80. package/dist/integrations/github/webhook.d.ts.map +1 -0
  81. package/dist/integrations/github/webhook.js +397 -0
  82. package/dist/integrations/github/webhook.js.map +1 -0
  83. package/dist/integrations/linear/agent-tools.d.ts +34 -0
  84. package/dist/integrations/linear/agent-tools.d.ts.map +1 -0
  85. package/dist/integrations/linear/agent-tools.js +85 -0
  86. package/dist/integrations/linear/agent-tools.js.map +1 -0
  87. package/dist/integrations/linear/integration.d.ts +233 -0
  88. package/dist/integrations/linear/integration.d.ts.map +1 -0
  89. package/dist/integrations/linear/integration.js +672 -0
  90. package/dist/integrations/linear/integration.js.map +1 -0
  91. package/dist/integrations/linear/routes.d.ts +61 -0
  92. package/dist/integrations/linear/routes.d.ts.map +1 -0
  93. package/dist/integrations/linear/routes.js +239 -0
  94. package/dist/integrations/linear/routes.js.map +1 -0
  95. package/dist/integrations/linear/rules.d.ts +42 -0
  96. package/dist/integrations/linear/rules.d.ts.map +1 -0
  97. package/dist/integrations/linear/rules.js +133 -0
  98. package/dist/integrations/linear/rules.js.map +1 -0
  99. package/dist/integrations/linear/storage.d.ts +65 -0
  100. package/dist/integrations/linear/storage.d.ts.map +1 -0
  101. package/dist/integrations/linear/storage.js +1 -0
  102. package/dist/integrations/platform/api-client.d.ts +26 -0
  103. package/dist/integrations/platform/api-client.d.ts.map +1 -0
  104. package/dist/integrations/platform/api-client.js +156 -0
  105. package/dist/integrations/platform/api-client.js.map +1 -0
  106. package/dist/integrations/platform/github/event-worker.d.ts +49 -0
  107. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
  108. package/dist/integrations/platform/github/event-worker.js +348 -0
  109. package/dist/integrations/platform/github/event-worker.js.map +1 -0
  110. package/dist/integrations/platform/github/integration.d.ts +50 -0
  111. package/dist/integrations/platform/github/integration.d.ts.map +1 -0
  112. package/dist/integrations/platform/github/integration.js +961 -0
  113. package/dist/integrations/platform/github/integration.js.map +1 -0
  114. package/dist/integrations/platform/linear/integration.d.ts +46 -0
  115. package/dist/integrations/platform/linear/integration.d.ts.map +1 -0
  116. package/dist/integrations/platform/linear/integration.js +483 -0
  117. package/dist/integrations/platform/linear/integration.js.map +1 -0
  118. package/dist/integrations/workos/integration.d.ts +39 -0
  119. package/dist/integrations/workos/integration.d.ts.map +1 -0
  120. package/dist/integrations/workos/integration.js +91 -0
  121. package/dist/integrations/workos/integration.js.map +1 -0
  122. package/dist/routes/config.d.ts +235 -0
  123. package/dist/routes/config.d.ts.map +1 -0
  124. package/dist/routes/config.js +972 -0
  125. package/dist/routes/config.js.map +1 -0
  126. package/dist/routes/custom-provider-source.d.ts +52 -0
  127. package/dist/routes/custom-provider-source.d.ts.map +1 -0
  128. package/dist/routes/custom-provider-source.js +124 -0
  129. package/dist/routes/custom-provider-source.js.map +1 -0
  130. package/dist/routes/fs.d.ts +151 -0
  131. package/dist/routes/fs.d.ts.map +1 -0
  132. package/dist/routes/fs.js +708 -0
  133. package/dist/routes/fs.js.map +1 -0
  134. package/dist/routes/intake.d.ts +23 -0
  135. package/dist/routes/intake.d.ts.map +1 -0
  136. package/dist/routes/intake.js +179 -0
  137. package/dist/routes/intake.js.map +1 -0
  138. package/dist/routes/oauth.d.ts +43 -0
  139. package/dist/routes/oauth.d.ts.map +1 -0
  140. package/dist/routes/oauth.js +463 -0
  141. package/dist/routes/oauth.js.map +1 -0
  142. package/dist/routes/projects.d.ts +19 -0
  143. package/dist/routes/projects.d.ts.map +1 -0
  144. package/dist/routes/projects.js +461 -0
  145. package/dist/routes/projects.js.map +1 -0
  146. package/dist/routes/provider-credentials.d.ts +81 -0
  147. package/dist/routes/provider-credentials.d.ts.map +1 -0
  148. package/dist/routes/provider-credentials.js +86 -0
  149. package/dist/routes/provider-credentials.js.map +1 -0
  150. package/dist/routes/route.d.ts +48 -0
  151. package/dist/routes/route.d.ts.map +1 -0
  152. package/dist/routes/route.js +15 -0
  153. package/dist/routes/route.js.map +1 -0
  154. package/dist/routes/skills.d.ts +30 -0
  155. package/dist/routes/skills.d.ts.map +1 -0
  156. package/dist/routes/skills.js +168 -0
  157. package/dist/routes/skills.js.map +1 -0
  158. package/dist/routes/surface.d.ts +91 -0
  159. package/dist/routes/surface.d.ts.map +1 -0
  160. package/dist/routes/surface.js +260 -0
  161. package/dist/routes/surface.js.map +1 -0
  162. package/dist/routes/tenant-credentials.d.ts +74 -0
  163. package/dist/routes/tenant-credentials.d.ts.map +1 -0
  164. package/dist/routes/tenant-credentials.js +156 -0
  165. package/dist/routes/tenant-credentials.js.map +1 -0
  166. package/dist/routes/work-items.d.ts +40 -0
  167. package/dist/routes/work-items.d.ts.map +1 -0
  168. package/dist/routes/work-items.js +695 -0
  169. package/dist/routes/work-items.js.map +1 -0
  170. package/dist/rules/binding-context.d.ts +5 -0
  171. package/dist/rules/binding-context.d.ts.map +1 -0
  172. package/dist/rules/binding-context.js +28 -0
  173. package/dist/rules/binding-context.js.map +1 -0
  174. package/dist/rules/defaults.d.ts +9 -0
  175. package/dist/rules/defaults.d.ts.map +1 -0
  176. package/dist/rules/defaults.js +260 -0
  177. package/dist/rules/defaults.js.map +1 -0
  178. package/dist/rules/dispatcher.d.ts +42 -0
  179. package/dist/rules/dispatcher.d.ts.map +1 -0
  180. package/dist/rules/dispatcher.js +518 -0
  181. package/dist/rules/dispatcher.js.map +1 -0
  182. package/dist/rules/index.d.ts +7 -0
  183. package/dist/rules/index.d.ts.map +1 -0
  184. package/dist/rules/index.js +5 -0
  185. package/dist/rules/processor.d.ts +54 -0
  186. package/dist/rules/processor.d.ts.map +1 -0
  187. package/dist/rules/processor.js +378 -0
  188. package/dist/rules/processor.js.map +1 -0
  189. package/dist/rules/resolve.d.ts +17 -0
  190. package/dist/rules/resolve.d.ts.map +1 -0
  191. package/dist/rules/resolve.js +30 -0
  192. package/dist/rules/resolve.js.map +1 -0
  193. package/dist/rules/start-coordinator.d.ts +60 -0
  194. package/dist/rules/start-coordinator.d.ts.map +1 -0
  195. package/dist/rules/start-coordinator.js +194 -0
  196. package/dist/rules/start-coordinator.js.map +1 -0
  197. package/dist/rules/tools.d.ts +10 -0
  198. package/dist/rules/tools.d.ts.map +1 -0
  199. package/dist/rules/tools.js +58 -0
  200. package/dist/rules/tools.js.map +1 -0
  201. package/dist/rules/transition-service.d.ts +49 -0
  202. package/dist/rules/transition-service.d.ts.map +1 -0
  203. package/dist/rules/transition-service.js +232 -0
  204. package/dist/rules/transition-service.js.map +1 -0
  205. package/dist/rules/types.d.ts +234 -0
  206. package/dist/rules/types.d.ts.map +1 -0
  207. package/dist/rules/types.js +38 -0
  208. package/dist/rules/types.js.map +1 -0
  209. package/dist/rules/validation.d.ts +11 -0
  210. package/dist/rules/validation.d.ts.map +1 -0
  211. package/dist/rules/validation.js +308 -0
  212. package/dist/rules/validation.js.map +1 -0
  213. package/dist/sandbox/fleet.d.ts +222 -0
  214. package/dist/sandbox/fleet.d.ts.map +1 -0
  215. package/dist/sandbox/fleet.js +322 -0
  216. package/dist/sandbox/fleet.js.map +1 -0
  217. package/dist/sandbox/reattach.d.ts +3 -0
  218. package/dist/sandbox/reattach.d.ts.map +1 -0
  219. package/dist/sandbox/reattach.js +16 -0
  220. package/dist/sandbox/reattach.js.map +1 -0
  221. package/dist/server-error.d.ts +12 -0
  222. package/dist/server-error.d.ts.map +1 -0
  223. package/dist/server-error.js +16 -0
  224. package/dist/server-error.js.map +1 -0
  225. package/dist/skills/service.d.ts +48 -0
  226. package/dist/skills/service.d.ts.map +1 -0
  227. package/dist/skills/service.js +40 -0
  228. package/dist/skills/service.js.map +1 -0
  229. package/dist/spa-static.d.ts +33 -0
  230. package/dist/spa-static.d.ts.map +1 -0
  231. package/dist/spa-static.js +104 -0
  232. package/dist/spa-static.js.map +1 -0
  233. package/dist/state-signing.d.ts +59 -0
  234. package/dist/state-signing.d.ts.map +1 -0
  235. package/dist/state-signing.js +83 -0
  236. package/dist/state-signing.js.map +1 -0
  237. package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
  238. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
  239. package/dist/storage/domains/audit/agent-audit.js +66 -0
  240. package/dist/storage/domains/audit/agent-audit.js.map +1 -0
  241. package/dist/storage/domains/audit/base.d.ts +140 -0
  242. package/dist/storage/domains/audit/base.d.ts.map +1 -0
  243. package/dist/storage/domains/audit/base.js +184 -0
  244. package/dist/storage/domains/audit/base.js.map +1 -0
  245. package/dist/storage/domains/audit/domain.d.ts +69 -0
  246. package/dist/storage/domains/audit/domain.d.ts.map +1 -0
  247. package/dist/storage/domains/audit/domain.js +167 -0
  248. package/dist/storage/domains/audit/domain.js.map +1 -0
  249. package/dist/storage/domains/channel-identity/base.d.ts +96 -0
  250. package/dist/storage/domains/channel-identity/base.d.ts.map +1 -0
  251. package/dist/storage/domains/channel-identity/base.js +149 -0
  252. package/dist/storage/domains/channel-identity/base.js.map +1 -0
  253. package/dist/storage/domains/credentials/base.d.ts +134 -0
  254. package/dist/storage/domains/credentials/base.d.ts.map +1 -0
  255. package/dist/storage/domains/credentials/base.js +273 -0
  256. package/dist/storage/domains/credentials/base.js.map +1 -0
  257. package/dist/storage/domains/custom-providers/base.d.ts +57 -0
  258. package/dist/storage/domains/custom-providers/base.d.ts.map +1 -0
  259. package/dist/storage/domains/custom-providers/base.js +145 -0
  260. package/dist/storage/domains/custom-providers/base.js.map +1 -0
  261. package/dist/storage/domains/intake/base.d.ts +38 -0
  262. package/dist/storage/domains/intake/base.d.ts.map +1 -0
  263. package/dist/storage/domains/intake/base.js +80 -0
  264. package/dist/storage/domains/intake/base.js.map +1 -0
  265. package/dist/storage/domains/integrations/base.d.ts +126 -0
  266. package/dist/storage/domains/integrations/base.d.ts.map +1 -0
  267. package/dist/storage/domains/integrations/base.js +317 -0
  268. package/dist/storage/domains/integrations/base.js.map +1 -0
  269. package/dist/storage/domains/memory-settings/base.d.ts +62 -0
  270. package/dist/storage/domains/memory-settings/base.d.ts.map +1 -0
  271. package/dist/storage/domains/memory-settings/base.js +126 -0
  272. package/dist/storage/domains/memory-settings/base.js.map +1 -0
  273. package/dist/storage/domains/model-packs/base.d.ts +49 -0
  274. package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
  275. package/dist/storage/domains/model-packs/base.js +96 -0
  276. package/dist/storage/domains/model-packs/base.js.map +1 -0
  277. package/dist/storage/domains/projects/base.d.ts +58 -0
  278. package/dist/storage/domains/projects/base.d.ts.map +1 -0
  279. package/dist/storage/domains/projects/base.js +111 -0
  280. package/dist/storage/domains/projects/base.js.map +1 -0
  281. package/dist/storage/domains/queue-health/base.d.ts +52 -0
  282. package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
  283. package/dist/storage/domains/queue-health/base.js +122 -0
  284. package/dist/storage/domains/queue-health/base.js.map +1 -0
  285. package/dist/storage/domains/source-control/base.d.ts +358 -0
  286. package/dist/storage/domains/source-control/base.d.ts.map +1 -0
  287. package/dist/storage/domains/source-control/base.js +908 -0
  288. package/dist/storage/domains/source-control/base.js.map +1 -0
  289. package/dist/storage/domains/source-control/inmemory.d.ts +175 -0
  290. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
  291. package/dist/storage/domains/source-control/inmemory.js +377 -0
  292. package/dist/storage/domains/source-control/inmemory.js.map +1 -0
  293. package/dist/storage/domains/work-items/base.d.ts +412 -0
  294. package/dist/storage/domains/work-items/base.d.ts.map +1 -0
  295. package/dist/storage/domains/work-items/base.js +1309 -0
  296. package/dist/storage/domains/work-items/base.js.map +1 -0
  297. package/dist/storage/domains/work-items/metrics.d.ts +101 -0
  298. package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
  299. package/dist/storage/domains/work-items/metrics.js +221 -0
  300. package/dist/storage/domains/work-items/metrics.js.map +1 -0
  301. package/dist/timing.d.ts +15 -0
  302. package/dist/timing.d.ts.map +1 -0
  303. package/dist/timing.js +34 -0
  304. package/dist/timing.js.map +1 -0
  305. package/dist/workspace.d.ts +24 -0
  306. package/dist/workspace.d.ts.map +1 -0
  307. package/dist/workspace.js +286 -0
  308. package/dist/workspace.js.map +1 -0
  309. package/factory-skills/configure-factory-rules/SKILL.md +67 -0
  310. package/factory-skills/factory-plan/SKILL.md +61 -0
  311. package/factory-skills/factory-review/SKILL.md +155 -0
  312. package/factory-skills/factory-triage/SKILL.md +77 -0
  313. package/package.json +81 -0
@@ -0,0 +1,961 @@
1
+ import { createGithubSubscriptionTools, parseCreatedPullRequest, subscribeCurrentSessionToPullRequest } from "../../github/session-subscriptions.js";
2
+ import { runGithubIssueTriage } from "../../github/issue-triage.js";
3
+ import { buildGithubRoutes } from "../../github/routes.js";
4
+ import { attachGithubReconciler, attachGithubRules } from "../../github/rules.js";
5
+ import { PlatformApiClient, PlatformApiError, logPlatformInfo, logPlatformWarn, platformApiClientConfigFromEnv } from "../api-client.js";
6
+ import { PlatformGithubEventWorker } from "./event-worker.js";
7
+ import { registerApiRoute } from "@mastra/core/server";
8
+ //#region src/integrations/platform/github/integration.ts
9
+ const PAGE_SIZE = 30;
10
+ const API_PREFIX = "/v1/server";
11
+ /**
12
+ * How long an installation's repository listing may be reused. The repos
13
+ * route and token minting both call it — often several times within one UI
14
+ * interaction — and each call is a full Platform round trip.
15
+ */
16
+ const INSTALLATION_REPOS_CACHE_TTL_MS = 3e4;
17
+ /**
18
+ * How long a minted repository-scoped token may be reused. GitHub
19
+ * installation tokens live ~60 minutes; 5 minutes keeps a wide validity
20
+ * margin while collapsing the per-session-materialization mint round trip.
21
+ */
22
+ const REPOSITORY_ACCESS_CACHE_TTL_MS = 5 * 6e4;
23
+ /**
24
+ * Upper bound for both TTL caches. Entries expire lazily on re-access, so
25
+ * without a hard cap keys that stop being queried would accumulate for the
26
+ * integration's (long) lifetime. Insertion-order eviction — matches the
27
+ * bounded verification cache in `@mastra/auth-studio`.
28
+ */
29
+ const MAX_CACHE_ENTRIES = 1e3;
30
+ function setBounded(cache, key, value) {
31
+ cache.delete(key);
32
+ cache.set(key, value);
33
+ if (cache.size > MAX_CACHE_ENTRIES) {
34
+ const oldest = cache.keys().next().value;
35
+ if (oldest !== void 0) cache.delete(oldest);
36
+ }
37
+ }
38
+ const REPOSITORY_TOKEN_PERMISSIONS = {
39
+ contents: "write",
40
+ issues: "write",
41
+ pull_requests: "write"
42
+ };
43
+ function loose(c) {
44
+ return c;
45
+ }
46
+ function routeBaseUrl(ctx, requestUrl) {
47
+ return (ctx.baseUrl || new URL(requestUrl).origin).replace(/\/+$/, "");
48
+ }
49
+ var PlatformGithubIntegration = class {
50
+ id = "github";
51
+ #client;
52
+ #endpointHost;
53
+ #pollingEnabled;
54
+ #pollingIntervalMs;
55
+ #reconcileEnabled;
56
+ #storage;
57
+ #integrationStorage;
58
+ /** installationId → cached repository listing (TTL-bounded). */
59
+ #installationReposCache = /* @__PURE__ */ new Map();
60
+ /** `orgId:repositoryId` → cached repository access (TTL-bounded). */
61
+ #repositoryAccessCache = /* @__PURE__ */ new Map();
62
+ #runIssueTriage;
63
+ intake = {
64
+ resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
65
+ listSources: async ({ orgId, userId }) => {
66
+ const usable = (await this.#client.request("GET", `${API_PREFIX}/github-app/installations`)).installations.filter((installation) => installation.usable && !installation.suspendedAt);
67
+ return (await Promise.all(usable.map(async (installation) => {
68
+ const storedInstallation = await this.versionControl.registerInstallation({
69
+ orgId,
70
+ userId,
71
+ installation: {
72
+ externalId: String(installation.installationId),
73
+ accountName: installation.accountLogin,
74
+ accountType: installation.accountType
75
+ }
76
+ });
77
+ const result = await this.#client.request("GET", `${API_PREFIX}/github-app/installations/${installation.installationId}/repositories`);
78
+ await this.versionControl.registerRepositories({
79
+ orgId,
80
+ installationId: storedInstallation.id,
81
+ repositories: result.repositories.map((repository) => ({
82
+ externalId: String(repository.id),
83
+ slug: repository.fullName,
84
+ defaultBranch: repository.defaultBranch,
85
+ metadata: {
86
+ private: repository.private,
87
+ url: repository.htmlUrl
88
+ }
89
+ }))
90
+ });
91
+ return result.repositories.map((repository) => ({
92
+ repository,
93
+ installation
94
+ }));
95
+ }))).flat().map(({ repository, installation }) => ({
96
+ id: repository.fullName,
97
+ name: repository.fullName,
98
+ type: "repository",
99
+ metadata: {
100
+ installationId: installation.installationId,
101
+ accountLogin: installation.accountLogin,
102
+ accountType: installation.accountType,
103
+ repositoryId: repository.id,
104
+ defaultBranch: repository.defaultBranch,
105
+ private: repository.private,
106
+ url: repository.htmlUrl
107
+ }
108
+ }));
109
+ },
110
+ listItems: async ({ sourceIds, cursor }) => {
111
+ const page = parsePositiveCursor(cursor);
112
+ const pages = await Promise.all(sourceIds.map(async (sourceId) => {
113
+ const connection = {
114
+ type: "app-installation",
115
+ installationId: 1
116
+ };
117
+ const [issues, pullRequests] = await Promise.all([this.#listIssues(connection, sourceId, page), this.#listPullRequests({
118
+ connection,
119
+ sourceId,
120
+ includeDrafts: false,
121
+ cursor: String(page)
122
+ })]);
123
+ return {
124
+ items: [...issues.issues.map((issue) => ({
125
+ source: {
126
+ type: "issue",
127
+ externalId: `${sourceId}:${issue.id}`,
128
+ url: issue.url
129
+ },
130
+ sourceId,
131
+ title: issue.title,
132
+ status: issue.state ?? void 0,
133
+ labels: issue.labels,
134
+ assignee: issue.assignee,
135
+ createdAt: issue.createdAt,
136
+ updatedAt: issue.updatedAt,
137
+ metadata: {
138
+ repository: sourceId,
139
+ number: Number(issue.id),
140
+ author: issue.author
141
+ }
142
+ })), ...pullRequests.pullRequests.map((pullRequest) => ({
143
+ source: {
144
+ type: "pull-request",
145
+ externalId: `${sourceId}:${pullRequest.id}`,
146
+ url: pullRequest.url
147
+ },
148
+ sourceId,
149
+ title: pullRequest.title,
150
+ status: pullRequest.state,
151
+ createdAt: pullRequest.createdAt,
152
+ updatedAt: pullRequest.updatedAt,
153
+ metadata: {
154
+ repository: sourceId,
155
+ number: Number(pullRequest.id),
156
+ author: pullRequest.author,
157
+ baseBranch: pullRequest.baseBranch,
158
+ headBranch: pullRequest.headBranch
159
+ }
160
+ }))],
161
+ hasNextPage: issues.nextCursor !== null || pullRequests.nextCursor !== null
162
+ };
163
+ }));
164
+ return {
165
+ items: pages.flatMap((result) => result.items),
166
+ nextCursor: pages.some((result) => result.hasNextPage) ? String(page + 1) : null
167
+ };
168
+ },
169
+ listIssues: async (input) => {
170
+ requireGithubConnection(input.connection);
171
+ const sourceId = requireSingleSource(input.sourceIds, "GitHub Intake requires exactly one repository source.");
172
+ return this.#listIssues(input.connection, sourceId, parsePositiveCursor(input.cursor), input.labels);
173
+ },
174
+ getIssue: (input) => this.#getIssue(input.connection, input.sourceId, input.issueId),
175
+ createComment: (input) => this.#createIssueComment(input),
176
+ updateIssue: (input) => this.#updateIntakeIssue(input)
177
+ };
178
+ versionControl = {
179
+ initialize: ({ storage }) => {
180
+ this.#storage = storage;
181
+ },
182
+ registerInstallation: ({ orgId, userId, installation }) => this.storage.installations.upsert({
183
+ orgId,
184
+ connectedByUserId: userId,
185
+ externalId: installation.externalId,
186
+ accountName: installation.accountName,
187
+ accountType: installation.accountType,
188
+ providerMetadata: installation.metadata
189
+ }),
190
+ registerRepositories: ({ orgId, installationId, repositories }) => Promise.all(repositories.map((repository) => this.storage.repositories.upsert({
191
+ orgId,
192
+ input: {
193
+ installationId,
194
+ externalId: repository.externalId,
195
+ slug: repository.slug,
196
+ defaultBranch: repository.defaultBranch,
197
+ providerMetadata: repository.metadata
198
+ }
199
+ }))),
200
+ getRepositoryAccess: async ({ orgId, repositoryId }) => {
201
+ const cacheKey = `${orgId}:${repositoryId}`;
202
+ const cached = this.#repositoryAccessCache.get(cacheKey);
203
+ if (cached && cached.expiresAt > Date.now()) return cached.access;
204
+ this.#repositoryAccessCache.delete(cacheKey);
205
+ const repository = await this.storage.repositories.get({
206
+ orgId,
207
+ id: repositoryId
208
+ });
209
+ if (!repository) throw new Error("Version-control repository not found.");
210
+ const cloneUrl = `https://github.com/${repository.slug}.git`;
211
+ const installation = await this.storage.installations.get({
212
+ orgId,
213
+ id: repository.installationId
214
+ });
215
+ if (!installation) throw new Error("Version-control installation not found.");
216
+ const installationId = parsePositiveInteger(installation.externalId);
217
+ if (installationId === null) throw new Error("GitHub installation id is invalid.");
218
+ const repositoryName = splitRepository(repository.slug).repo;
219
+ try {
220
+ const access = {
221
+ cloneUrl,
222
+ authorization: {
223
+ scheme: "bearer",
224
+ token: (await this.#client.request("POST", `${API_PREFIX}/github-app/installations/${installationId}/token`, {
225
+ repositories: [repositoryName],
226
+ permissions: REPOSITORY_TOKEN_PERMISSIONS
227
+ })).token
228
+ }
229
+ };
230
+ setBounded(this.#repositoryAccessCache, cacheKey, {
231
+ access,
232
+ expiresAt: Date.now() + REPOSITORY_ACCESS_CACHE_TTL_MS
233
+ });
234
+ return access;
235
+ } catch (err) {
236
+ if (!isNotFound(err) || !installation.accountName) throw err;
237
+ const newInstallation = (await this.storage.installations.list({ orgId })).find((inst) => inst.accountName === installation.accountName && inst.id !== installation.id);
238
+ if (!newInstallation) throw err;
239
+ const newInstallationId = parsePositiveInteger(newInstallation.externalId);
240
+ if (newInstallationId === null) throw err;
241
+ await this.storage.repositories.migrateInstallation({
242
+ orgId,
243
+ id: repositoryId,
244
+ newInstallationId: newInstallation.id
245
+ });
246
+ const access = {
247
+ cloneUrl,
248
+ authorization: {
249
+ scheme: "bearer",
250
+ token: (await this.#client.request("POST", `${API_PREFIX}/github-app/installations/${newInstallationId}/token`, {
251
+ repositories: [repositoryName],
252
+ permissions: REPOSITORY_TOKEN_PERMISSIONS
253
+ })).token
254
+ }
255
+ };
256
+ setBounded(this.#repositoryAccessCache, cacheKey, {
257
+ access,
258
+ expiresAt: Date.now() + REPOSITORY_ACCESS_CACHE_TTL_MS
259
+ });
260
+ return access;
261
+ }
262
+ },
263
+ listPullRequests: (input) => this.#listPullRequests(input),
264
+ getPullRequest: (input) => this.#getPullRequest(input),
265
+ createPullRequest: (input) => this.#createPullRequest(input),
266
+ updatePullRequest: (input) => this.#updatePullRequest(input),
267
+ closePullRequest: (input) => this.#updatePullRequest({
268
+ ...input,
269
+ state: "closed"
270
+ }),
271
+ mergePullRequest: (input) => this.#mergePullRequest(input),
272
+ listComments: (input) => this.#listComments(input),
273
+ createComment: (input) => this.#createComment(input),
274
+ updateComment: (input) => this.#updateComment(input),
275
+ deleteComment: (input) => this.#deleteComment(input),
276
+ listReviews: (input) => this.#listReviews(input),
277
+ getReview: (input) => this.#getReview(input),
278
+ createReview: (input) => this.#createReview(input),
279
+ updateReview: (input) => this.#updateReview(input),
280
+ submitReview: (input) => this.#submitReview(input),
281
+ dismissReview: (input) => this.#dismissReview(input),
282
+ deletePendingReview: (input) => this.#deletePendingReview(input),
283
+ listReviewComments: (input) => this.#listReviewComments(input),
284
+ createReviewComment: (input) => this.#createReviewComment(input),
285
+ updateReviewComment: (input) => this.#updateReviewComment(input),
286
+ deleteReviewComment: (input) => this.#deleteReviewComment(input),
287
+ listRequestedReviewers: (input) => this.#requestedReviewers("GET", input),
288
+ requestReviewers: (input) => this.#requestedReviewers("POST", input),
289
+ removeRequestedReviewers: (input) => this.#requestedReviewers("DELETE", input)
290
+ };
291
+ constructor(options = {}) {
292
+ const config = platformApiClientConfigFromEnv();
293
+ this.#client = new PlatformApiClient(config);
294
+ this.#endpointHost = new URL(config.baseUrl).host;
295
+ this.#pollingEnabled = process.env.MASTRA_PLATFORM_GITHUB_POLLING_ENABLED?.trim().toLowerCase() !== "false";
296
+ this.#pollingIntervalMs = optionalPositiveIntegerEnv("MASTRA_PLATFORM_GITHUB_POLLING_INTERVAL_MS");
297
+ this.#reconcileEnabled = process.env.MASTRA_PLATFORM_GITHUB_RECONCILE_ENABLED?.trim().toLowerCase() !== "false";
298
+ this.#runIssueTriage = options.runIssueTriage;
299
+ }
300
+ get storage() {
301
+ if (!this.#storage) throw new Error("PlatformGithubIntegration source-control storage has not been initialized.");
302
+ return this.#storage;
303
+ }
304
+ get sourceControlStorage() {
305
+ return this.storage;
306
+ }
307
+ get integrationStorage() {
308
+ if (!this.#integrationStorage) throw new Error("PlatformGithubIntegration generic storage has not been initialized.");
309
+ return this.#integrationStorage;
310
+ }
311
+ /** Resolve a stored GitHub locator without scanning installations or repositories. */
312
+ async #resolveIntakeDispatch({ orgId, externalSource }) {
313
+ const target = parseGithubExternalTarget(externalSource.externalId);
314
+ if (!target) return null;
315
+ const repository = target.repository.includes("/") ? target.repository : (await this.storage.repositories.findByExternalId({
316
+ orgId,
317
+ externalId: target.repository
318
+ }))?.slug;
319
+ if (!repository) return null;
320
+ return {
321
+ connection: {
322
+ type: "app-installation",
323
+ installationId: 1
324
+ },
325
+ sourceId: repository,
326
+ issueId: target.issueId
327
+ };
328
+ }
329
+ initialize({ storage }) {
330
+ this.#integrationStorage = storage;
331
+ logPlatformInfo("Platform GitHub integration initialized", {
332
+ endpointHost: this.#endpointHost,
333
+ pollingEnabled: this.#pollingEnabled,
334
+ pollingIntervalMs: this.#pollingIntervalMs,
335
+ reconcileEnabled: this.#reconcileEnabled
336
+ });
337
+ }
338
+ routes(ctx) {
339
+ const ingestFactoryEvent = attachGithubRules(this, ctx);
340
+ return [
341
+ this.#statusRoute(ctx),
342
+ this.#connectRoute(ctx),
343
+ this.#connectUserRoute(ctx),
344
+ ...buildGithubRoutes({
345
+ auth: ctx.auth,
346
+ fleet: ctx.fleet,
347
+ storage: ctx.factoryStorage,
348
+ github: this,
349
+ stateSigner: ctx.stateSigner,
350
+ baseUrl: ctx.baseUrl,
351
+ controller: ctx.controller,
352
+ projects: ctx.storage.projects,
353
+ emitAudit: ctx.hooks?.emitAudit,
354
+ ingestFactoryEvent,
355
+ runIssueTriage: this.#runIssueTriage ?? (ctx.controller ? (input) => runGithubIssueTriage({
356
+ controller: ctx.controller,
357
+ input
358
+ }) : void 0)
359
+ }).filter((route) => route.path !== "/web/github/status" && route.path !== "/web/github/webhook" && !route.path.startsWith("/auth/github/"))
360
+ ];
361
+ }
362
+ #statusRoute(ctx) {
363
+ return registerApiRoute("/web/github/status", {
364
+ method: "GET",
365
+ requiresAuth: false,
366
+ handler: async (c) => {
367
+ await ctx.auth.ensureUser(loose(c));
368
+ const tenant = ctx.auth.tenant(loose(c));
369
+ if (!tenant) return c.json({
370
+ error: "unauthorized",
371
+ reason: "auth_required"
372
+ }, 401);
373
+ if (!tenant.orgId) return c.json({
374
+ enabled: true,
375
+ sandboxEnabled: ctx.fleet.enabled,
376
+ organizationRequired: true,
377
+ connected: false,
378
+ installations: [],
379
+ userConnected: false,
380
+ userGithubUsername: null,
381
+ reason: "organization_required",
382
+ diagnostics: this.diagnostics()
383
+ });
384
+ const [installations, userConnection] = await Promise.all([this.#syncInstallations(tenant.orgId, tenant.userId), this.#fetchUserConnection(tenant.userId)]);
385
+ return c.json({
386
+ enabled: true,
387
+ sandboxEnabled: ctx.fleet.enabled,
388
+ connected: installations.length > 0,
389
+ installations: installations.map((installation) => ({
390
+ installationId: Number(installation.externalId),
391
+ accountLogin: installation.accountName,
392
+ accountType: installation.accountType
393
+ })),
394
+ userConnected: userConnection.connected,
395
+ userGithubUsername: userConnection.githubUsername,
396
+ reason: installations.length > 0 ? "ready" : "not_connected",
397
+ diagnostics: this.diagnostics()
398
+ });
399
+ }
400
+ });
401
+ }
402
+ #connectRoute(ctx) {
403
+ return registerApiRoute("/auth/github/connect", {
404
+ method: "GET",
405
+ requiresAuth: false,
406
+ handler: async (c) => {
407
+ await ctx.auth.ensureUser(loose(c));
408
+ const tenant = ctx.auth.tenant(loose(c));
409
+ if (!tenant?.orgId) return c.json({ error: "unauthorized" }, 401);
410
+ const redirectTo = c.req.query("redirectTo") || c.req.query("return_to") || "/";
411
+ const originator = routeBaseUrl(ctx, c.req.url);
412
+ logPlatformInfo("Starting Platform GitHub connect flow", {
413
+ orgId: tenant.orgId,
414
+ redirectTo,
415
+ originator
416
+ });
417
+ const query = new URLSearchParams({
418
+ action: "install",
419
+ redirectTo,
420
+ originator
421
+ });
422
+ const { url } = await this.#client.request("GET", `${API_PREFIX}/github-app/install-url?${query}`);
423
+ return c.redirect(url);
424
+ }
425
+ });
426
+ }
427
+ #connectUserRoute(ctx) {
428
+ return registerApiRoute("/auth/github/connect-user", {
429
+ method: "GET",
430
+ requiresAuth: false,
431
+ handler: async (c) => {
432
+ await ctx.auth.ensureUser(loose(c));
433
+ const tenant = ctx.auth.tenant(loose(c));
434
+ if (!tenant?.orgId) return c.json({ error: "unauthorized" }, 401);
435
+ const redirectTo = c.req.query("redirectTo") || c.req.query("return_to") || "/";
436
+ const originator = routeBaseUrl(ctx, c.req.url);
437
+ logPlatformInfo("Starting Platform GitHub user authorization flow", {
438
+ orgId: tenant.orgId,
439
+ redirectTo,
440
+ originator
441
+ });
442
+ const query = new URLSearchParams({
443
+ userId: tenant.userId,
444
+ redirectTo,
445
+ originator
446
+ });
447
+ const { url } = await this.#client.request("GET", `${API_PREFIX}/github-app/authenticate?${query}`);
448
+ return c.redirect(url);
449
+ }
450
+ });
451
+ }
452
+ /**
453
+ * Personal GitHub connection status for the acting user. Returns
454
+ * not-connected when the platform predates the user-connection endpoint.
455
+ */
456
+ async #fetchUserConnection(userId) {
457
+ try {
458
+ const connection = await this.#client.request("GET", `${API_PREFIX}/github-app/user-connection?${new URLSearchParams({ userId })}`);
459
+ if (!connection.connected && connection.reason) logPlatformWarn("Platform GitHub user connection verification failed", {
460
+ userId,
461
+ reason: connection.reason
462
+ });
463
+ return connection;
464
+ } catch {
465
+ return {
466
+ connected: false,
467
+ githubUsername: null
468
+ };
469
+ }
470
+ }
471
+ async #syncInstallations(orgId, userId) {
472
+ const usableInstallations = (await this.#client.request("GET", `${API_PREFIX}/github-app/installations`)).installations.filter((installation) => installation.usable && !installation.suspendedAt);
473
+ return Promise.all(usableInstallations.map((installation) => this.versionControl.registerInstallation({
474
+ orgId,
475
+ userId,
476
+ installation: {
477
+ externalId: String(installation.installationId),
478
+ accountName: installation.accountLogin,
479
+ accountType: installation.accountType
480
+ }
481
+ })));
482
+ }
483
+ workers(ctx) {
484
+ if (!this.#pollingEnabled && !this.#reconcileEnabled) return [];
485
+ if (!ctx.controller) throw new Error("Platform GitHub event polling requires the mounted Mastra Code controller.");
486
+ return [new PlatformGithubEventWorker({
487
+ client: this.#client,
488
+ controller: ctx.controller,
489
+ github: this,
490
+ storage: ctx.storage.generic,
491
+ ingestFactoryEvent: attachGithubRules(this, ctx),
492
+ reconcileFactoryState: this.#reconcileEnabled ? attachGithubReconciler(this, ctx, (input) => this.fetchPullRequestState(input)) : void 0,
493
+ pollEventsEnabled: this.#pollingEnabled,
494
+ intervalMs: this.#pollingIntervalMs
495
+ })];
496
+ }
497
+ /**
498
+ * Reads live PR state through the Platform GitHub proxy for the merge
499
+ * reconciler. Returns undefined when the PR cannot be resolved (missing,
500
+ * proxy error) so a sweep never fabricates a merge.
501
+ */
502
+ async fetchPullRequestState(input) {
503
+ let repository;
504
+ try {
505
+ repository = splitRepository(input.repository);
506
+ } catch {
507
+ return;
508
+ }
509
+ try {
510
+ const result = await this.#client.request("GET", `${API_PREFIX}/github/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/pulls/${input.number}`);
511
+ return {
512
+ title: result.title ?? `PR ${input.number}`,
513
+ url: result.html_url ?? `https://github.com/${input.repository}/pull/${input.number}`,
514
+ state: result.state === "closed" ? "closed" : "open",
515
+ merged: result.merged === true,
516
+ headBranch: result.head?.ref ?? "",
517
+ baseBranch: result.base?.ref ?? "",
518
+ ...result.created_at ? { createdAt: result.created_at } : {},
519
+ ...result.merged_by?.login ? { mergedBy: result.merged_by.login } : {}
520
+ };
521
+ } catch {
522
+ return;
523
+ }
524
+ }
525
+ sessionTools({ requestContext }) {
526
+ return createGithubSubscriptionTools(requestContext, this);
527
+ }
528
+ async postToolObserver({ toolContext, requestContext }) {
529
+ const pullRequestUrl = parseCreatedPullRequest(toolContext);
530
+ if (!pullRequestUrl || !requestContext) return;
531
+ await subscribeCurrentSessionToPullRequest(requestContext, pullRequestUrl, "auto-gh-pr-create", this);
532
+ }
533
+ diagnostics() {
534
+ return {
535
+ mode: "platform",
536
+ endpointHost: this.#endpointHost,
537
+ polling: {
538
+ enabled: this.#pollingEnabled,
539
+ ...this.#pollingIntervalMs === void 0 ? {} : { intervalMs: this.#pollingIntervalMs }
540
+ },
541
+ reconcile: { enabled: this.#reconcileEnabled }
542
+ };
543
+ }
544
+ async getRepositoryCollaboratorPermission(_installationId, repoFullName, username, signal) {
545
+ let repository;
546
+ try {
547
+ repository = splitRepository(repoFullName);
548
+ } catch {
549
+ return;
550
+ }
551
+ try {
552
+ return (await this.#client.request("GET", `${API_PREFIX}/github/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/collaborators/${encodeURIComponent(username)}/permission`, void 0, { signal })).permission;
553
+ } catch {
554
+ return;
555
+ }
556
+ }
557
+ async listInstallationRepos(installationId) {
558
+ const cached = this.#installationReposCache.get(installationId);
559
+ if (cached && cached.expiresAt > Date.now()) return cached.repos;
560
+ this.#installationReposCache.delete(installationId);
561
+ const repos = (await this.#client.request("GET", `${API_PREFIX}/github-app/installations/${installationId}/repositories`)).repositories.map((repository) => ({
562
+ ...repository,
563
+ installationId
564
+ }));
565
+ setBounded(this.#installationReposCache, installationId, {
566
+ repos,
567
+ expiresAt: Date.now() + INSTALLATION_REPOS_CACHE_TTL_MS
568
+ });
569
+ return repos;
570
+ }
571
+ async mintInstallationToken(installationId) {
572
+ const repositories = await this.listInstallationRepos(installationId);
573
+ if (repositories.length === 0 || repositories.length > 10) throw new Error("Platform GitHub token minting requires between one and ten installation repositories.");
574
+ return (await this.#client.request("POST", `${API_PREFIX}/github-app/installations/${installationId}/token`, {
575
+ repositories: repositories.map((repository) => repository.name),
576
+ permissions: REPOSITORY_TOKEN_PERMISSIONS
577
+ })).token;
578
+ }
579
+ async addIssueLabels(_installationId, sourceId, issueNumber, labels) {
580
+ return (await this.#client.request("POST", repositoryPath(sourceId, `issues/${issueNumber}/labels`), { labels })).labels;
581
+ }
582
+ getInstallationOctokit(_installationId) {
583
+ return { pulls: { get: async ({ owner, repo, pull_number }) => {
584
+ return { data: { base: { repo: { id: (await this.#client.request("GET", repositoryPath(`${owner}/${repo}`, `pulls/${pull_number}`))).base.repo.id } } } };
585
+ } } };
586
+ }
587
+ async #listIssues(connection, sourceId, page, labels) {
588
+ requireGithubConnection(connection);
589
+ const path = repositoryPath(sourceId, "issues");
590
+ const query = new URLSearchParams({
591
+ state: "open",
592
+ page: String(page),
593
+ per_page: String(PAGE_SIZE)
594
+ });
595
+ const normalizedLabels = normalizeLabels(labels);
596
+ if (normalizedLabels.length > 0) query.set("label", normalizedLabels.join(","));
597
+ const result = await this.#client.request("GET", `${path}?${query}`);
598
+ return {
599
+ issues: result.issues.map((issue) => parseIntakeIssue(sourceId, issue)),
600
+ nextCursor: result.issues.length === PAGE_SIZE ? String(page + 1) : null
601
+ };
602
+ }
603
+ async #getIssue(connection, sourceId, issueId) {
604
+ requireGithubConnection(connection);
605
+ const repository = requireSource(sourceId, "GitHub Intake requires a repository source.");
606
+ const issueNumber = requirePositiveId(issueId, "issue");
607
+ try {
608
+ const [issue, comments] = await Promise.all([this.#client.request("GET", repositoryPath(repository, `issues/${issueNumber}`)), this.#client.request("GET", `${repositoryPath(repository, `issues/${issueNumber}/comments`)}?per_page=100`)]);
609
+ return parseIntakeIssueDetail(repository, issue, comments.comments);
610
+ } catch (error) {
611
+ if (isNotFound(error)) return null;
612
+ throw error;
613
+ }
614
+ }
615
+ async #updateIntakeIssue(input) {
616
+ requireGithubConnection(input.connection);
617
+ const repository = requireSource(input.sourceId, "GitHub Intake requires a repository source.");
618
+ const issueNumber = requirePositiveId(input.issueId, "issue");
619
+ if (input.state.kind === "byName") {
620
+ logPlatformWarn(`Platform GitHub: updateIssue byName is not supported (name=${input.state.name}); ignoring.`);
621
+ return null;
622
+ }
623
+ const targetState = input.state.stateType === "unstarted" || input.state.stateType === "started" ? "open" : "closed";
624
+ const stateReason = targetState === "closed" ? input.state.stateType === "canceled" ? "not_planned" : "completed" : null;
625
+ try {
626
+ await this.#client.request("GET", repositoryPath(repository, `pulls/${issueNumber}`));
627
+ logPlatformWarn(`Platform GitHub: updateIssue rejected — target ${repository}#${issueNumber} is a pull request.`);
628
+ return null;
629
+ } catch (error) {
630
+ if (!isNotFound(error)) throw error;
631
+ }
632
+ try {
633
+ return parseIntakeIssue(repository, await this.#client.request("PATCH", repositoryPath(repository, `issues/${issueNumber}`), {
634
+ state: targetState,
635
+ ...stateReason ? { state_reason: stateReason } : {}
636
+ }, { actingUserId: input.actingUserId }));
637
+ } catch (error) {
638
+ if (isNotFound(error)) return null;
639
+ throw error;
640
+ }
641
+ }
642
+ async #createIssueComment(input) {
643
+ requireGithubConnection(input.connection);
644
+ const repository = requireSource(input.sourceId, "GitHub Intake requires a repository source.");
645
+ const issueNumber = requirePositiveId(input.issueId, "issue");
646
+ try {
647
+ const comment = await this.#client.request("POST", repositoryPath(repository, `issues/${issueNumber}/comments`), { body: input.body }, { actingUserId: input.actingUserId });
648
+ return {
649
+ id: String(comment.id),
650
+ url: comment.htmlUrl
651
+ };
652
+ } catch (error) {
653
+ if (isNotFound(error)) return null;
654
+ throw error;
655
+ }
656
+ }
657
+ async #listPullRequests(input) {
658
+ requireGithubConnection(input.connection);
659
+ const page = parsePositiveCursor(input.cursor);
660
+ const query = new URLSearchParams({
661
+ state: input.state ?? "open",
662
+ page: String(page),
663
+ per_page: String(PAGE_SIZE)
664
+ });
665
+ const result = await this.#client.request("GET", `${repositoryPath(input.sourceId, "pulls")}?${query}`);
666
+ return {
667
+ pullRequests: result.pullRequests.filter((pullRequest) => input.includeDrafts !== false || !pullRequest.draft).map(parsePullRequest),
668
+ nextCursor: result.pullRequests.length === PAGE_SIZE ? String(page + 1) : null
669
+ };
670
+ }
671
+ async #getPullRequest(input) {
672
+ try {
673
+ return parsePullRequest(await this.#client.request("GET", pullRequestPath(input, input.pullRequestId)));
674
+ } catch (error) {
675
+ if (isNotFound(error)) return null;
676
+ throw error;
677
+ }
678
+ }
679
+ async #createPullRequest(input) {
680
+ requireGithubConnection(input.connection);
681
+ return parsePullRequest(await this.#client.request("POST", repositoryPath(input.sourceId, "pulls"), {
682
+ head: input.headBranch,
683
+ base: input.baseBranch,
684
+ title: input.title,
685
+ body: input.body,
686
+ draft: input.draft
687
+ }, { actingUserId: input.actingUserId }));
688
+ }
689
+ async #updatePullRequest(input) {
690
+ return parsePullRequest(await this.#client.request("PATCH", pullRequestPath(input, input.pullRequestId), {
691
+ title: input.title,
692
+ body: input.body === null ? "" : input.body,
693
+ base: input.baseBranch,
694
+ state: input.state
695
+ }, { actingUserId: input.actingUserId }));
696
+ }
697
+ #mergePullRequest(input) {
698
+ return this.#client.request("PUT", `${pullRequestPath(input, input.pullRequestId)}/merge`, {
699
+ commitTitle: input.commitTitle,
700
+ commitMessage: input.commitMessage,
701
+ method: input.method
702
+ }, { actingUserId: input.actingUserId });
703
+ }
704
+ async #listComments(input) {
705
+ const page = parsePositiveCursor(input.cursor);
706
+ const result = await this.#client.request("GET", `${repositoryPath(input.sourceId, `issues/${requirePositiveId(input.pullRequestId, "pull request")}/comments`)}?page=${page}&per_page=${PAGE_SIZE}`);
707
+ return {
708
+ comments: result.comments.map(parseComment),
709
+ nextCursor: result.comments.length === PAGE_SIZE ? String(page + 1) : null
710
+ };
711
+ }
712
+ async #createComment(input) {
713
+ return parseComment(await this.#client.request("POST", repositoryPath(input.sourceId, `issues/${requirePositiveId(input.pullRequestId, "pull request")}/comments`), { body: input.body }, { actingUserId: input.actingUserId }));
714
+ }
715
+ async #updateComment(input) {
716
+ requireGithubConnection(input.connection);
717
+ return parseComment(await this.#client.request("PATCH", repositoryPath(input.sourceId, `issues/comments/${requirePositiveId(input.commentId, "comment")}`), { body: input.body }, { actingUserId: input.actingUserId }));
718
+ }
719
+ async #deleteComment(input) {
720
+ requireGithubConnection(input.connection);
721
+ await this.#client.request("DELETE", repositoryPath(input.sourceId, `issues/comments/${requirePositiveId(input.commentId, "comment")}`), void 0, { actingUserId: input.actingUserId });
722
+ }
723
+ async #listReviews(input) {
724
+ const page = parsePositiveCursor(input.cursor);
725
+ const result = await this.#client.request("GET", `${pullRequestPath(input, input.pullRequestId)}/reviews?page=${page}&per_page=${PAGE_SIZE}`);
726
+ return {
727
+ reviews: result.reviews.map(parseReview),
728
+ nextCursor: result.reviews.length === PAGE_SIZE ? String(page + 1) : null
729
+ };
730
+ }
731
+ async #getReview(input) {
732
+ try {
733
+ return parseReview(await this.#client.request("GET", `${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}`));
734
+ } catch (error) {
735
+ if (isNotFound(error)) return null;
736
+ throw error;
737
+ }
738
+ }
739
+ async #createReview(input) {
740
+ return parseReview(await this.#client.request("POST", `${pullRequestPath(input, input.pullRequestId)}/reviews`, {
741
+ body: input.body,
742
+ commitId: input.commitId,
743
+ event: input.event ? reviewEvent(input.event) : void 0
744
+ }, { actingUserId: input.actingUserId }));
745
+ }
746
+ async #updateReview(input) {
747
+ return parseReview(await this.#client.request("PUT", `${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}`, { body: input.body }, { actingUserId: input.actingUserId }));
748
+ }
749
+ async #submitReview(input) {
750
+ return parseReview(await this.#client.request("POST", `${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}/events`, {
751
+ body: input.body,
752
+ event: reviewEvent(input.event)
753
+ }, { actingUserId: input.actingUserId }));
754
+ }
755
+ async #dismissReview(input) {
756
+ return parseReview(await this.#client.request("PUT", `${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}/dismissals`, { message: input.message }, { actingUserId: input.actingUserId }));
757
+ }
758
+ async #deletePendingReview(input) {
759
+ await this.#client.request("DELETE", `${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}`, void 0, { actingUserId: input.actingUserId });
760
+ }
761
+ async #listReviewComments(input) {
762
+ const page = parsePositiveCursor(input.cursor);
763
+ const result = await this.#client.request("GET", `${pullRequestPath(input, input.pullRequestId)}/comments?page=${page}&per_page=${PAGE_SIZE}`);
764
+ return {
765
+ comments: result.comments.map(parseReviewComment),
766
+ nextCursor: result.comments.length === PAGE_SIZE ? String(page + 1) : null
767
+ };
768
+ }
769
+ async #createReviewComment(input) {
770
+ let body;
771
+ if (input.replyToId !== void 0) body = {
772
+ body: input.body,
773
+ replyToId: requirePositiveId(input.replyToId, "review comment")
774
+ };
775
+ else {
776
+ if (!input.commitId || !input.path || input.line === void 0 || !input.side) throw new Error("A review comment requires commitId, path, line, and side unless it is a reply.");
777
+ body = {
778
+ body: input.body,
779
+ commitId: input.commitId,
780
+ path: input.path,
781
+ line: input.line,
782
+ side: input.side.toUpperCase(),
783
+ startLine: input.startLine,
784
+ startSide: input.startSide?.toUpperCase()
785
+ };
786
+ }
787
+ return parseReviewComment(await this.#client.request("POST", `${pullRequestPath(input, input.pullRequestId)}/comments`, body, { actingUserId: input.actingUserId }));
788
+ }
789
+ async #updateReviewComment(input) {
790
+ requireGithubConnection(input.connection);
791
+ return parseReviewComment(await this.#client.request("PATCH", repositoryPath(input.sourceId, `pulls/comments/${requirePositiveId(input.commentId, "review comment")}`), { body: input.body }, { actingUserId: input.actingUserId }));
792
+ }
793
+ async #deleteReviewComment(input) {
794
+ requireGithubConnection(input.connection);
795
+ await this.#client.request("DELETE", repositoryPath(input.sourceId, `pulls/comments/${requirePositiveId(input.commentId, "review comment")}`), void 0, { actingUserId: input.actingUserId });
796
+ }
797
+ #requestedReviewers(method, input) {
798
+ requireGithubConnection(input.connection);
799
+ return this.#client.request(method, `${pullRequestPath(input, input.pullRequestId)}/requested-reviewers`, method === "GET" ? void 0 : {
800
+ users: input.users,
801
+ teams: input.teams
802
+ }, method === "GET" ? void 0 : { actingUserId: input.actingUserId });
803
+ }
804
+ };
805
+ function repositoryPath(sourceId, suffix) {
806
+ const { owner, repo } = splitRepository(sourceId);
807
+ return `${API_PREFIX}/github/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${suffix}`;
808
+ }
809
+ function pullRequestPath(input, pullRequestId) {
810
+ requireGithubConnection(input.connection);
811
+ return repositoryPath(input.sourceId, `pulls/${requirePositiveId(pullRequestId, "pull request")}`);
812
+ }
813
+ function splitRepository(sourceId) {
814
+ const slash = sourceId.indexOf("/");
815
+ if (slash <= 0 || slash === sourceId.length - 1) throw new Error("GitHub capabilities require an owner/repository source.");
816
+ return {
817
+ owner: sourceId.slice(0, slash),
818
+ repo: sourceId.slice(slash + 1)
819
+ };
820
+ }
821
+ function parseIntakeIssue(sourceId, issue) {
822
+ return {
823
+ id: String(issue.number),
824
+ identifier: `#${issue.number}`,
825
+ title: issue.title,
826
+ url: issue.htmlUrl,
827
+ author: issue.user?.login ?? null,
828
+ state: issue.state,
829
+ stateType: issue.state,
830
+ priority: null,
831
+ assignee: issue.assignees[0] ?? null,
832
+ source: sourceId,
833
+ labels: issue.labels,
834
+ commentCount: issue.commentCount,
835
+ createdAt: issue.createdAt,
836
+ updatedAt: issue.updatedAt
837
+ };
838
+ }
839
+ function parseIntakeIssueDetail(sourceId, issue, comments) {
840
+ return {
841
+ ...parseIntakeIssue(sourceId, issue),
842
+ description: issue.body?.trim() ? issue.body : null,
843
+ comments: comments.map((comment) => ({
844
+ author: comment.user?.login ?? null,
845
+ body: comment.body,
846
+ createdAt: comment.createdAt
847
+ }))
848
+ };
849
+ }
850
+ function parsePullRequest(pullRequest) {
851
+ return {
852
+ id: String(pullRequest.number),
853
+ title: pullRequest.title,
854
+ url: pullRequest.htmlUrl,
855
+ author: pullRequest.user?.login ?? null,
856
+ body: pullRequest.body?.trim() ? pullRequest.body : null,
857
+ state: pullRequest.state,
858
+ draft: pullRequest.draft,
859
+ merged: pullRequest.merged,
860
+ mergeable: pullRequest.mergeable,
861
+ baseBranch: pullRequest.base.ref,
862
+ headBranch: pullRequest.head.ref,
863
+ headSha: pullRequest.head.sha,
864
+ createdAt: pullRequest.createdAt,
865
+ updatedAt: pullRequest.updatedAt
866
+ };
867
+ }
868
+ function parseComment(comment) {
869
+ return {
870
+ id: String(comment.id),
871
+ url: comment.htmlUrl,
872
+ author: comment.user?.login ?? null,
873
+ body: comment.body,
874
+ createdAt: comment.createdAt,
875
+ updatedAt: comment.updatedAt
876
+ };
877
+ }
878
+ function parseReview(review) {
879
+ return {
880
+ id: String(review.id),
881
+ url: review.htmlUrl,
882
+ author: review.user?.login ?? null,
883
+ body: review.body?.trim() ? review.body : null,
884
+ state: {
885
+ PENDING: "pending",
886
+ COMMENTED: "commented",
887
+ APPROVED: "approved",
888
+ CHANGES_REQUESTED: "changes-requested",
889
+ DISMISSED: "dismissed"
890
+ }[review.state],
891
+ commitId: review.commitId,
892
+ submittedAt: review.submittedAt
893
+ };
894
+ }
895
+ function parseReviewComment(comment) {
896
+ return {
897
+ ...parseComment(comment),
898
+ path: comment.path,
899
+ line: comment.line,
900
+ side: comment.side?.toLowerCase(),
901
+ commitId: comment.commitId,
902
+ replyToId: comment.replyToId === null ? null : String(comment.replyToId)
903
+ };
904
+ }
905
+ function requireGithubConnection(connection) {
906
+ if (connection.type !== "app-installation" && connection.type !== "oauth") throw new Error("GitHub capabilities require a GitHub connection.");
907
+ }
908
+ function requireSingleSource(sourceIds, message) {
909
+ if (sourceIds.length !== 1) throw new Error(message);
910
+ return sourceIds[0];
911
+ }
912
+ function requireSource(sourceId, message) {
913
+ if (!sourceId) throw new Error(message);
914
+ return sourceId;
915
+ }
916
+ function normalizeLabels(labels) {
917
+ return [...new Set((labels ?? []).map((label) => label.trim()).filter(Boolean))];
918
+ }
919
+ function parsePositiveCursor(cursor) {
920
+ if (cursor === void 0) return 1;
921
+ const parsed = parsePositiveInteger(cursor);
922
+ if (parsed === null) throw new Error("GitHub cursor must be a positive page number.");
923
+ return parsed;
924
+ }
925
+ function parsePositiveInteger(value) {
926
+ if (!/^\d+$/.test(value)) return null;
927
+ const parsed = Number(value);
928
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
929
+ }
930
+ function parseGithubExternalTarget(externalId) {
931
+ const match = externalId.match(/^(.+\/.+):(\d+)$/) ?? externalId.match(/^github:(\d+):(?:issue|pull-request):(\d+)$/) ?? externalId.match(/^(\d+):(\d+)$/);
932
+ if (!match?.[1] || !match[2] || parsePositiveInteger(match[2]) === null) return null;
933
+ return {
934
+ repository: match[1],
935
+ issueId: match[2]
936
+ };
937
+ }
938
+ function optionalPositiveIntegerEnv(name) {
939
+ const value = process.env[name]?.trim();
940
+ if (!value) return void 0;
941
+ const parsed = parsePositiveInteger(value);
942
+ if (parsed === null) throw new Error(`${name} must be a positive integer.`);
943
+ return parsed;
944
+ }
945
+ function requirePositiveId(value, resource) {
946
+ const parsed = parsePositiveInteger(value);
947
+ if (parsed === null) throw new Error(`GitHub ${resource} id must be a positive integer.`);
948
+ return parsed;
949
+ }
950
+ function reviewEvent(event) {
951
+ if (event === "approve") return "APPROVE";
952
+ if (event === "request-changes") return "REQUEST_CHANGES";
953
+ return "COMMENT";
954
+ }
955
+ function isNotFound(error) {
956
+ return error instanceof PlatformApiError && error.status === 404;
957
+ }
958
+ //#endregion
959
+ export { PlatformGithubIntegration };
960
+
961
+ //# sourceMappingURL=integration.js.map