@lobu/cli 6.0.1 → 6.1.1

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 (217) hide show
  1. package/README.md +20 -27
  2. package/dist/bundled-skills/lobu/SKILL.md +11 -11
  3. package/dist/commands/_lib/apply/apply-cmd.d.ts +2 -0
  4. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  5. package/dist/commands/_lib/apply/apply-cmd.js +26 -0
  6. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  7. package/dist/commands/_lib/apply/client.d.ts +1 -1
  8. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  9. package/dist/commands/_lib/apply/desired-state.js +4 -4
  10. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  11. package/dist/commands/agent.d.ts +7 -0
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +65 -1
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/chat.d.ts +12 -9
  16. package/dist/commands/chat.d.ts.map +1 -1
  17. package/dist/commands/chat.js +117 -56
  18. package/dist/commands/chat.js.map +1 -1
  19. package/dist/commands/dev.d.ts +15 -7
  20. package/dist/commands/dev.d.ts.map +1 -1
  21. package/dist/commands/dev.js +79 -44
  22. package/dist/commands/dev.js.map +1 -1
  23. package/dist/commands/doctor.d.ts +1 -0
  24. package/dist/commands/doctor.d.ts.map +1 -1
  25. package/dist/commands/doctor.js +136 -0
  26. package/dist/commands/doctor.js.map +1 -1
  27. package/dist/commands/eval.d.ts +8 -0
  28. package/dist/commands/eval.d.ts.map +1 -1
  29. package/dist/commands/eval.js +56 -1
  30. package/dist/commands/eval.js.map +1 -1
  31. package/dist/commands/init.d.ts +20 -5
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +332 -183
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/commands/link.d.ts +11 -0
  36. package/dist/commands/link.d.ts.map +1 -0
  37. package/dist/commands/link.js +28 -0
  38. package/dist/commands/link.js.map +1 -0
  39. package/dist/commands/login.d.ts.map +1 -1
  40. package/dist/commands/login.js +14 -2
  41. package/dist/commands/login.js.map +1 -1
  42. package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
  43. package/dist/commands/memory/_lib/browser-auth-cmd.js +3 -3
  44. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  45. package/dist/commands/memory/_lib/mcp.d.ts +2 -2
  46. package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
  47. package/dist/commands/memory/_lib/mcp.js +24 -12
  48. package/dist/commands/memory/_lib/mcp.js.map +1 -1
  49. package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
  50. package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
  51. package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
  52. package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
  53. package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
  54. package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
  55. package/dist/commands/memory/_lib/schema.d.ts +1 -1
  56. package/dist/commands/memory/_lib/schema.js +1 -1
  57. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  58. package/dist/commands/memory/_lib/seed-cmd.js +5 -6
  59. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  60. package/dist/commands/memory/run.d.ts.map +1 -1
  61. package/dist/commands/memory/run.js +2 -2
  62. package/dist/commands/memory/run.js.map +1 -1
  63. package/dist/commands/platforms/platform-prompts.d.ts +0 -1
  64. package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
  65. package/dist/commands/platforms/platform-prompts.js +54 -8
  66. package/dist/commands/platforms/platform-prompts.js.map +1 -1
  67. package/dist/commands/telemetry.d.ts +10 -0
  68. package/dist/commands/telemetry.d.ts.map +1 -0
  69. package/dist/commands/telemetry.js +68 -0
  70. package/dist/commands/telemetry.js.map +1 -0
  71. package/dist/commands/whoami.d.ts.map +1 -1
  72. package/dist/commands/whoami.js +1 -1
  73. package/dist/commands/whoami.js.map +1 -1
  74. package/dist/connectors/README.md +534 -0
  75. package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
  76. package/dist/connectors/browser-scraper-utils.ts +214 -0
  77. package/dist/connectors/capterra.ts +273 -0
  78. package/dist/connectors/g2.ts +286 -0
  79. package/dist/connectors/github.ts +1553 -0
  80. package/dist/connectors/glassdoor.ts +291 -0
  81. package/dist/connectors/gmaps.ts +197 -0
  82. package/dist/connectors/google_calendar.ts +631 -0
  83. package/dist/connectors/google_gmail.ts +751 -0
  84. package/dist/connectors/google_photos.ts +776 -0
  85. package/dist/connectors/google_play.ts +342 -0
  86. package/dist/connectors/hackernews.ts +471 -0
  87. package/dist/connectors/index.ts +23 -0
  88. package/dist/connectors/ios_appstore.ts +226 -0
  89. package/dist/connectors/linkedin.ts +471 -0
  90. package/dist/connectors/microsoft_outlook.ts +410 -0
  91. package/dist/connectors/producthunt.ts +471 -0
  92. package/dist/connectors/reddit.ts +600 -0
  93. package/dist/connectors/rss.ts +448 -0
  94. package/dist/connectors/spotify.ts +590 -0
  95. package/dist/connectors/trustpilot.ts +199 -0
  96. package/dist/connectors/website.ts +629 -0
  97. package/dist/connectors/whatsapp.ts +1073 -0
  98. package/dist/connectors/x.ts +526 -0
  99. package/dist/connectors/youtube.ts +666 -0
  100. package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
  101. package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
  102. package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
  103. package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
  104. package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
  105. package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
  106. package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
  107. package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
  108. package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
  109. package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
  110. package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
  111. package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
  112. package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
  113. package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
  114. package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
  115. package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
  116. package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
  117. package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
  118. package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
  119. package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
  120. package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
  121. package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
  122. package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
  123. package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
  124. package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
  125. package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
  126. package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
  127. package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
  128. package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
  129. package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
  130. package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
  131. package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
  132. package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
  133. package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
  134. package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
  135. package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
  136. package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
  137. package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
  138. package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
  139. package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
  140. package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
  141. package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
  142. package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
  143. package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
  144. package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
  145. package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +147 -23
  148. package/dist/index.js.map +1 -1
  149. package/dist/internal/api-client.d.ts +4 -8
  150. package/dist/internal/api-client.d.ts.map +1 -1
  151. package/dist/internal/api-client.js +1 -1
  152. package/dist/internal/api-client.js.map +1 -1
  153. package/dist/internal/context.js +2 -2
  154. package/dist/internal/context.js.map +1 -1
  155. package/dist/internal/credentials.d.ts.map +1 -1
  156. package/dist/internal/credentials.js +6 -1
  157. package/dist/internal/credentials.js.map +1 -1
  158. package/dist/internal/index.d.ts +2 -3
  159. package/dist/internal/index.d.ts.map +1 -1
  160. package/dist/internal/index.js +2 -2
  161. package/dist/internal/index.js.map +1 -1
  162. package/dist/internal/oauth.d.ts +6 -5
  163. package/dist/internal/oauth.d.ts.map +1 -1
  164. package/dist/internal/oauth.js +2 -2
  165. package/dist/internal/project-link.d.ts +10 -0
  166. package/dist/internal/project-link.d.ts.map +1 -0
  167. package/dist/internal/project-link.js +48 -0
  168. package/dist/internal/project-link.js.map +1 -0
  169. package/dist/providers.json +2 -2
  170. package/dist/server.bundle.mjs +3090 -4321
  171. package/dist/start-local.bundle.mjs +71481 -0
  172. package/dist/templates/README.md.tmpl +10 -11
  173. package/package.json +14 -12
  174. package/dist/__tests__/chat.integration.test.d.ts +0 -2
  175. package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
  176. package/dist/__tests__/chat.integration.test.js +0 -337
  177. package/dist/__tests__/chat.integration.test.js.map +0 -1
  178. package/dist/__tests__/dev.test.d.ts +0 -2
  179. package/dist/__tests__/dev.test.d.ts.map +0 -1
  180. package/dist/__tests__/dev.test.js +0 -25
  181. package/dist/__tests__/dev.test.js.map +0 -1
  182. package/dist/__tests__/init-memory.test.d.ts +0 -2
  183. package/dist/__tests__/init-memory.test.d.ts.map +0 -1
  184. package/dist/__tests__/init-memory.test.js +0 -45
  185. package/dist/__tests__/init-memory.test.js.map +0 -1
  186. package/dist/__tests__/token.test.d.ts +0 -2
  187. package/dist/__tests__/token.test.d.ts.map +0 -1
  188. package/dist/__tests__/token.test.js +0 -52
  189. package/dist/__tests__/token.test.js.map +0 -1
  190. package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
  191. package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
  192. package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
  193. package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
  194. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
  195. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
  196. package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
  197. package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
  198. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
  199. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
  200. package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
  201. package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
  202. package/dist/commands/apply.d.ts +0 -3
  203. package/dist/commands/apply.d.ts.map +0 -1
  204. package/dist/commands/apply.js +0 -5
  205. package/dist/commands/apply.js.map +0 -1
  206. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
  207. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
  208. package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
  209. package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
  210. package/dist/internal/__tests__/api-client.test.d.ts +0 -2
  211. package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
  212. package/dist/internal/__tests__/api-client.test.js +0 -95
  213. package/dist/internal/__tests__/api-client.test.js.map +0 -1
  214. package/dist/internal/__tests__/context.test.d.ts +0 -2
  215. package/dist/internal/__tests__/context.test.d.ts.map +0 -1
  216. package/dist/internal/__tests__/context.test.js +0 -77
  217. package/dist/internal/__tests__/context.test.js.map +0 -1
@@ -0,0 +1,1553 @@
1
+ /**
2
+ * GitHub Connector (V1 runtime)
3
+ *
4
+ * Syncs GitHub repository content and executes write actions.
5
+ */
6
+
7
+ import {
8
+ type ActionContext,
9
+ type ActionResult,
10
+ type ConnectorDefinition,
11
+ ConnectorRuntime,
12
+ type EventEnvelope,
13
+ IDENTITY,
14
+ type SyncContext,
15
+ type SyncResult,
16
+ } from '@lobu/connector-sdk';
17
+
18
+ type GitHubContentType =
19
+ | 'issues'
20
+ | 'pull_requests'
21
+ | 'issue_comments'
22
+ | 'pr_comments'
23
+ | 'discussions'
24
+ | 'discussion_comments'
25
+ | 'stargazers';
26
+
27
+ interface GitHubConfig {
28
+ repo_owner?: string;
29
+ repo_name?: string;
30
+ content_type?: GitHubContentType;
31
+ lookback_days?: number;
32
+ labels_filter?: string[];
33
+ env_overrides?: Record<string, unknown>;
34
+ }
35
+
36
+ interface GitHubCheckpoint {
37
+ last_sync_at?: string;
38
+ stargazers?: GitHubStargazerCheckpoint[];
39
+ }
40
+
41
+ interface GitHubStargazerCheckpoint {
42
+ key: string;
43
+ login: string;
44
+ starred_at?: string;
45
+ user_id?: number | null;
46
+ user_type?: string | null;
47
+ html_url?: string | null;
48
+ profile_fetched_at?: string | null;
49
+ }
50
+
51
+ interface RepoRef {
52
+ owner: string;
53
+ repo: string;
54
+ }
55
+
56
+ interface GitHubRepositoryLike {
57
+ id?: number;
58
+ full_name?: string;
59
+ html_url?: string;
60
+ }
61
+
62
+ interface GitHubIssueLike {
63
+ id: number;
64
+ number: number;
65
+ title: string;
66
+ body: string | null;
67
+ user?: { login?: string };
68
+ html_url: string;
69
+ created_at: string;
70
+ updated_at: string;
71
+ state: string;
72
+ labels?: Array<{ name?: string }>;
73
+ comments?: number;
74
+ reactions?: { '+1'?: number; '-1'?: number; total_count?: number };
75
+ pull_request?: Record<string, unknown>;
76
+ }
77
+
78
+ interface GitHubCommentLike {
79
+ id: number;
80
+ body: string;
81
+ user?: { login?: string };
82
+ html_url: string;
83
+ issue_url?: string;
84
+ pull_request_url?: string;
85
+ created_at: string;
86
+ updated_at: string;
87
+ reactions?: { '+1'?: number; '-1'?: number; total_count?: number };
88
+ }
89
+
90
+ interface GraphQLDiscussionNode {
91
+ id: string;
92
+ number: number;
93
+ title: string;
94
+ body: string;
95
+ author?: { login?: string };
96
+ url: string;
97
+ createdAt: string;
98
+ updatedAt: string;
99
+ category?: { name?: string };
100
+ comments?: { totalCount?: number };
101
+ reactions?: { totalCount?: number };
102
+ }
103
+
104
+ interface GraphQLDiscussionCommentNode {
105
+ id: string;
106
+ body: string;
107
+ author?: { login?: string };
108
+ url: string;
109
+ createdAt: string;
110
+ updatedAt: string;
111
+ reactions?: { totalCount?: number };
112
+ discussion?: { number?: number };
113
+ }
114
+
115
+ interface GitHubStargazerLike {
116
+ starred_at?: string;
117
+ user?: {
118
+ id?: number;
119
+ login?: string;
120
+ html_url?: string;
121
+ type?: string;
122
+ site_admin?: boolean;
123
+ avatar_url?: string;
124
+ };
125
+ login?: string;
126
+ html_url?: string;
127
+ id?: number;
128
+ type?: string;
129
+ site_admin?: boolean;
130
+ avatar_url?: string;
131
+ }
132
+
133
+ interface GitHubUserProfile {
134
+ id?: number;
135
+ login?: string;
136
+ name?: string | null;
137
+ company?: string | null;
138
+ blog?: string | null;
139
+ location?: string | null;
140
+ email?: string | null;
141
+ bio?: string | null;
142
+ twitter_username?: string | null;
143
+ public_repos?: number;
144
+ public_gists?: number;
145
+ followers?: number;
146
+ following?: number;
147
+ html_url?: string;
148
+ avatar_url?: string;
149
+ type?: string;
150
+ site_admin?: boolean;
151
+ created_at?: string;
152
+ updated_at?: string;
153
+ }
154
+
155
+ function toInt(value: unknown, fallback: number): number {
156
+ const n = typeof value === 'number' ? value : Number(value);
157
+ return Number.isFinite(n) ? n : fallback;
158
+ }
159
+
160
+ function asString(value: unknown): string | undefined {
161
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
162
+ }
163
+
164
+ function toIsoOrUndefined(value: unknown): string | undefined {
165
+ const str = asString(value);
166
+ if (!str) return undefined;
167
+ const parsed = new Date(str);
168
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();
169
+ }
170
+
171
+ function stripMarkdown(code: string): string {
172
+ return code
173
+ .replace(/```[a-zA-Z]*\n?/g, '')
174
+ .replace(/```/g, '')
175
+ .trim();
176
+ }
177
+
178
+ const REPO_PROPS = {
179
+ repo_owner: { type: 'string', minLength: 1, description: 'Repository owner' },
180
+ repo_name: { type: 'string', minLength: 1, description: 'Repository name' },
181
+ } as const;
182
+
183
+ const LOOKBACK_PROP = {
184
+ lookback_days: {
185
+ type: 'integer',
186
+ minimum: 1,
187
+ maximum: 730,
188
+ default: 365,
189
+ description: 'Initial sync lookback window',
190
+ },
191
+ } as const;
192
+
193
+ const STARGAZER_PROFILE_REFRESH_MS = 30 * 24 * 60 * 60 * 1000;
194
+ const STARGAZER_PROFILE_FETCH_LIMIT = 25;
195
+
196
+ const LABELS_PROP = {
197
+ labels_filter: {
198
+ type: 'array',
199
+ items: { type: 'string' },
200
+ description: 'Optional label filter',
201
+ },
202
+ } as const;
203
+
204
+ export default class GitHubConnector extends ConnectorRuntime {
205
+ readonly definition: ConnectorDefinition = {
206
+ key: 'github',
207
+ name: 'GitHub',
208
+ description: 'Collects GitHub issues/discussions and executes repo actions.',
209
+ version: '1.2.0',
210
+ authSchema: {
211
+ methods: [
212
+ {
213
+ type: 'oauth',
214
+ provider: 'github',
215
+ requiredScopes: ['read:user'],
216
+ optionalScopes: ['repo'],
217
+ loginScopes: ['read:user', 'user:email'],
218
+ clientIdKey: 'GITHUB_CLIENT_ID',
219
+ clientSecretKey: 'GITHUB_CLIENT_SECRET',
220
+ tokenUrl: 'https://github.com/login/oauth/access_token',
221
+ tokenEndpointAuthMethod: 'client_secret_post',
222
+ required: false,
223
+ description:
224
+ 'GitHub OAuth enables repo access for this connection. Upgrade to the optional repo scope for private repositories and write actions.',
225
+ loginProvisioning: {
226
+ autoCreateConnection: true,
227
+ },
228
+ setupInstructions:
229
+ 'Create a GitHub OAuth App in GitHub Settings > Developer settings > OAuth Apps. Set the authorization callback URL to {{redirect_uri}}, then copy the client ID and client secret below.',
230
+ },
231
+ {
232
+ type: 'env_keys',
233
+ required: false,
234
+ description: 'Optional fallback token for sync/action calls.',
235
+ fields: [
236
+ {
237
+ key: 'GITHUB_TOKEN',
238
+ label: 'GitHub Token',
239
+ description: 'Personal access token used as fallback auth for API requests.',
240
+ secret: true,
241
+ },
242
+ ],
243
+ },
244
+ ],
245
+ },
246
+ feeds: {
247
+ issues: {
248
+ key: 'issues',
249
+ name: 'Issues',
250
+ requiredScopes: [],
251
+ description: 'Sync GitHub issues from a repository.',
252
+ displayNameTemplate: '{repo_owner}/{repo_name} issues',
253
+ configSchema: {
254
+ type: 'object',
255
+ required: ['repo_owner', 'repo_name'],
256
+ properties: { ...REPO_PROPS, ...LABELS_PROP, ...LOOKBACK_PROP },
257
+ },
258
+ eventKinds: {
259
+ issue: {
260
+ description: 'A GitHub issue',
261
+ metadataSchema: {
262
+ type: 'object',
263
+ properties: {
264
+ number: { type: 'number' },
265
+ state: { type: 'string' },
266
+ labels: { type: 'array', items: { type: 'string' } },
267
+ updated_at: { type: 'string' },
268
+ reactions: { type: 'object' },
269
+ comments: { type: 'number' },
270
+ },
271
+ },
272
+ },
273
+ },
274
+ },
275
+ pull_requests: {
276
+ key: 'pull_requests',
277
+ name: 'Pull Requests',
278
+ requiredScopes: [],
279
+ description: 'Sync GitHub pull requests from a repository.',
280
+ displayNameTemplate: '{repo_owner}/{repo_name} PRs',
281
+ configSchema: {
282
+ type: 'object',
283
+ required: ['repo_owner', 'repo_name'],
284
+ properties: { ...REPO_PROPS, ...LABELS_PROP, ...LOOKBACK_PROP },
285
+ },
286
+ eventKinds: {
287
+ pull_request: {
288
+ description: 'A GitHub pull request',
289
+ metadataSchema: {
290
+ type: 'object',
291
+ properties: {
292
+ number: { type: 'number' },
293
+ state: { type: 'string' },
294
+ labels: { type: 'array', items: { type: 'string' } },
295
+ updated_at: { type: 'string' },
296
+ reactions: { type: 'object' },
297
+ comments: { type: 'number' },
298
+ },
299
+ },
300
+ },
301
+ },
302
+ },
303
+ issue_comments: {
304
+ key: 'issue_comments',
305
+ name: 'Issue Comments',
306
+ requiredScopes: [],
307
+ description: 'Sync comments on GitHub issues.',
308
+ displayNameTemplate: '{repo_owner}/{repo_name} issue comments',
309
+ configSchema: {
310
+ type: 'object',
311
+ required: ['repo_owner', 'repo_name'],
312
+ properties: { ...REPO_PROPS, ...LOOKBACK_PROP },
313
+ },
314
+ eventKinds: {
315
+ issue_comment: {
316
+ description: 'A comment on a GitHub issue',
317
+ metadataSchema: {
318
+ type: 'object',
319
+ properties: {
320
+ updated_at: { type: 'string' },
321
+ reactions: { type: 'object' },
322
+ },
323
+ },
324
+ },
325
+ },
326
+ },
327
+ pr_comments: {
328
+ key: 'pr_comments',
329
+ name: 'PR Comments',
330
+ requiredScopes: [],
331
+ description: 'Sync comments on GitHub pull requests.',
332
+ displayNameTemplate: '{repo_owner}/{repo_name} PR comments',
333
+ configSchema: {
334
+ type: 'object',
335
+ required: ['repo_owner', 'repo_name'],
336
+ properties: { ...REPO_PROPS, ...LOOKBACK_PROP },
337
+ },
338
+ eventKinds: {
339
+ pr_comment: {
340
+ description: 'A comment on a GitHub pull request',
341
+ metadataSchema: {
342
+ type: 'object',
343
+ properties: {
344
+ updated_at: { type: 'string' },
345
+ reactions: { type: 'object' },
346
+ },
347
+ },
348
+ },
349
+ },
350
+ },
351
+ discussions: {
352
+ key: 'discussions',
353
+ name: 'Discussions',
354
+ requiredScopes: [],
355
+ description: 'Sync GitHub discussions from a repository.',
356
+ displayNameTemplate: '{repo_owner}/{repo_name} discussions',
357
+ configSchema: {
358
+ type: 'object',
359
+ required: ['repo_owner', 'repo_name'],
360
+ properties: { ...REPO_PROPS, ...LOOKBACK_PROP },
361
+ },
362
+ eventKinds: {
363
+ discussion: {
364
+ description: 'A GitHub discussion',
365
+ metadataSchema: {
366
+ type: 'object',
367
+ properties: {
368
+ number: { type: 'number' },
369
+ category: { type: 'string' },
370
+ updated_at: { type: 'string' },
371
+ comments: { type: 'number' },
372
+ reactions: { type: 'number' },
373
+ },
374
+ },
375
+ },
376
+ },
377
+ },
378
+ discussion_comments: {
379
+ key: 'discussion_comments',
380
+ name: 'Discussion Comments',
381
+ requiredScopes: [],
382
+ description: 'Sync comments on GitHub discussions.',
383
+ displayNameTemplate: '{repo_owner}/{repo_name} discussion comments',
384
+ configSchema: {
385
+ type: 'object',
386
+ required: ['repo_owner', 'repo_name'],
387
+ properties: { ...REPO_PROPS, ...LOOKBACK_PROP },
388
+ },
389
+ eventKinds: {
390
+ discussion_comment: {
391
+ description: 'A comment on a GitHub discussion',
392
+ metadataSchema: {
393
+ type: 'object',
394
+ properties: {
395
+ discussion_number: { type: 'number' },
396
+ updated_at: { type: 'string' },
397
+ reactions: { type: 'number' },
398
+ },
399
+ },
400
+ },
401
+ },
402
+ },
403
+ stargazers: {
404
+ key: 'stargazers',
405
+ name: 'Stargazers',
406
+ requiredScopes: [],
407
+ description: 'Sync GitHub users who starred a repository.',
408
+ displayNameTemplate: '{repo_owner}/{repo_name} stargazers',
409
+ configSchema: {
410
+ type: 'object',
411
+ required: ['repo_owner', 'repo_name'],
412
+ properties: { ...REPO_PROPS, ...LOOKBACK_PROP },
413
+ },
414
+ eventKinds: {
415
+ stargazer: {
416
+ description: 'A GitHub user starred the repository',
417
+ metadataSchema: {
418
+ type: 'object',
419
+ properties: {
420
+ actor: { type: 'object' },
421
+ target: { type: 'object' },
422
+ action: { type: 'string' },
423
+ starred_at: { type: 'string' },
424
+ source: { type: 'string' },
425
+ },
426
+ },
427
+ },
428
+ stargazer_unstarred: {
429
+ description: 'A GitHub user unstarred the repository',
430
+ metadataSchema: {
431
+ type: 'object',
432
+ properties: {
433
+ actor: { type: 'object' },
434
+ target: { type: 'object' },
435
+ action: { type: 'string' },
436
+ starred_at: { type: 'string' },
437
+ unstarred_at: { type: 'string' },
438
+ unstarred_at_is_inferred: { type: 'boolean' },
439
+ source: { type: 'string' },
440
+ },
441
+ },
442
+ },
443
+ stargazer_profile: {
444
+ description: 'A public GitHub profile observation for a stargazer',
445
+ metadataSchema: {
446
+ type: 'object',
447
+ properties: {
448
+ public_identity_profile: { type: 'boolean' },
449
+ provider: { type: 'string' },
450
+ identities: { type: 'array' },
451
+ account: { type: 'object' },
452
+ },
453
+ },
454
+ },
455
+ },
456
+ },
457
+ },
458
+ actions: {
459
+ create_issue: {
460
+ key: 'create_issue',
461
+ name: 'Create Issue',
462
+ description: 'Create a new issue in the configured repository.',
463
+ requiresApproval: true,
464
+ inputSchema: {
465
+ type: 'object',
466
+ required: ['title'],
467
+ properties: {
468
+ title: { type: 'string' },
469
+ body: { type: 'string' },
470
+ labels: { type: 'array', items: { type: 'string' } },
471
+ assignees: { type: 'array', items: { type: 'string' } },
472
+ repo_owner: { type: 'string' },
473
+ repo_name: { type: 'string' },
474
+ },
475
+ },
476
+ },
477
+ add_issue_comment: {
478
+ key: 'add_issue_comment',
479
+ name: 'Add Issue Comment',
480
+ description: 'Add a comment to an issue or pull request.',
481
+ requiresApproval: true,
482
+ inputSchema: {
483
+ type: 'object',
484
+ required: ['issue_number', 'body'],
485
+ properties: {
486
+ issue_number: { type: 'integer' },
487
+ body: { type: 'string' },
488
+ repo_owner: { type: 'string' },
489
+ repo_name: { type: 'string' },
490
+ },
491
+ },
492
+ },
493
+ close_issue: {
494
+ key: 'close_issue',
495
+ name: 'Close Issue',
496
+ description: 'Close an issue by number.',
497
+ requiresApproval: true,
498
+ inputSchema: {
499
+ type: 'object',
500
+ required: ['issue_number'],
501
+ properties: {
502
+ issue_number: { type: 'integer' },
503
+ repo_owner: { type: 'string' },
504
+ repo_name: { type: 'string' },
505
+ },
506
+ },
507
+ },
508
+ reopen_issue: {
509
+ key: 'reopen_issue',
510
+ name: 'Reopen Issue',
511
+ description: 'Reopen an issue by number.',
512
+ requiresApproval: true,
513
+ inputSchema: {
514
+ type: 'object',
515
+ required: ['issue_number'],
516
+ properties: {
517
+ issue_number: { type: 'integer' },
518
+ repo_owner: { type: 'string' },
519
+ repo_name: { type: 'string' },
520
+ },
521
+ },
522
+ },
523
+ create_pull_request: {
524
+ key: 'create_pull_request',
525
+ name: 'Create Pull Request',
526
+ description: 'Create a pull request from head to base branch.',
527
+ requiresApproval: true,
528
+ inputSchema: {
529
+ type: 'object',
530
+ required: ['title', 'head', 'base'],
531
+ properties: {
532
+ title: { type: 'string' },
533
+ head: { type: 'string' },
534
+ base: { type: 'string' },
535
+ body: { type: 'string' },
536
+ draft: { type: 'boolean' },
537
+ repo_owner: { type: 'string' },
538
+ repo_name: { type: 'string' },
539
+ },
540
+ },
541
+ },
542
+ merge_pull_request: {
543
+ key: 'merge_pull_request',
544
+ name: 'Merge Pull Request',
545
+ description: 'Merge a pull request by number.',
546
+ requiresApproval: true,
547
+ inputSchema: {
548
+ type: 'object',
549
+ required: ['pull_number'],
550
+ properties: {
551
+ pull_number: { type: 'integer' },
552
+ merge_method: {
553
+ type: 'string',
554
+ enum: ['merge', 'squash', 'rebase'],
555
+ },
556
+ commit_title: { type: 'string' },
557
+ commit_message: { type: 'string' },
558
+ repo_owner: { type: 'string' },
559
+ repo_name: { type: 'string' },
560
+ },
561
+ },
562
+ },
563
+ },
564
+ optionsSchema: {
565
+ type: 'object',
566
+ required: ['repo_owner', 'repo_name'],
567
+ properties: { ...REPO_PROPS, ...LABELS_PROP, ...LOOKBACK_PROP },
568
+ },
569
+ };
570
+
571
+ async sync(ctx: SyncContext): Promise<SyncResult> {
572
+ const config = this.parseConfig(ctx.config);
573
+ const repo = this.resolveRepo(config, {});
574
+ const token = this.resolveToken(ctx.credentials?.accessToken, config);
575
+ const contentType = (ctx.feedKey ?? 'issues') as GitHubContentType;
576
+ const sinceIso = this.resolveSince(ctx.checkpoint, config.lookback_days ?? 365);
577
+
578
+ if (contentType === 'stargazers') {
579
+ const result = await this.syncStargazers(repo, ctx.checkpoint, token);
580
+ return {
581
+ events: result.events,
582
+ checkpoint: {
583
+ last_sync_at: new Date().toISOString(),
584
+ stargazers: result.currentStargazers,
585
+ } as Record<string, unknown>,
586
+ metadata: {
587
+ items_found: result.events.length,
588
+ current_stargazers: result.currentStargazers.length,
589
+ },
590
+ };
591
+ }
592
+
593
+ const events = await this.syncContent({
594
+ repo,
595
+ contentType,
596
+ sinceIso,
597
+ labelsFilter: config.labels_filter ?? [],
598
+ token,
599
+ });
600
+
601
+ return {
602
+ events,
603
+ checkpoint: {
604
+ last_sync_at: new Date().toISOString(),
605
+ } as Record<string, unknown>,
606
+ metadata: {
607
+ items_found: events.length,
608
+ },
609
+ };
610
+ }
611
+
612
+ async execute(ctx: ActionContext): Promise<ActionResult> {
613
+ try {
614
+ const config = this.parseConfig(ctx.config);
615
+ const repo = this.resolveRepo(config, ctx.input);
616
+ const token = this.resolveToken(ctx.credentials?.accessToken, config);
617
+
618
+ if (!token) {
619
+ return { success: false, error: 'GitHub action requires OAuth or GITHUB_TOKEN.' };
620
+ }
621
+
622
+ switch (ctx.actionKey) {
623
+ case 'create_issue':
624
+ return await this.createIssue(repo, token, ctx.input);
625
+ case 'add_issue_comment':
626
+ return await this.addIssueComment(repo, token, ctx.input);
627
+ case 'close_issue':
628
+ return await this.updateIssueState(repo, token, ctx.input, 'closed');
629
+ case 'reopen_issue':
630
+ return await this.updateIssueState(repo, token, ctx.input, 'open');
631
+ case 'create_pull_request':
632
+ return await this.createPullRequest(repo, token, ctx.input);
633
+ case 'merge_pull_request':
634
+ return await this.mergePullRequest(repo, token, ctx.input);
635
+ default:
636
+ return { success: false, error: `Unknown action: ${ctx.actionKey}` };
637
+ }
638
+ } catch (error) {
639
+ return {
640
+ success: false,
641
+ error: error instanceof Error ? error.message : String(error),
642
+ };
643
+ }
644
+ }
645
+
646
+ private parseConfig(raw: Record<string, unknown>): GitHubConfig {
647
+ return raw as GitHubConfig;
648
+ }
649
+
650
+ private resolveRepo(config: GitHubConfig, input: Record<string, unknown>): RepoRef {
651
+ const owner = asString(input.repo_owner) ?? config.repo_owner;
652
+ const repo = asString(input.repo_name) ?? config.repo_name;
653
+
654
+ if (!owner || !repo) {
655
+ throw new Error(
656
+ 'Repository is not configured. Provide repo_owner/repo_name in connection config or action input.'
657
+ );
658
+ }
659
+
660
+ return { owner, repo };
661
+ }
662
+
663
+ private resolveToken(oauthToken: string | undefined, config: GitHubConfig): string | null {
664
+ if (oauthToken && oauthToken.trim().length > 0) {
665
+ return oauthToken;
666
+ }
667
+
668
+ const envOverrides = config.env_overrides ?? {};
669
+ const configuredToken =
670
+ asString(envOverrides.GITHUB_TOKEN) ??
671
+ asString((config as Record<string, unknown>).GITHUB_TOKEN) ??
672
+ asString((config as Record<string, unknown>).github_token);
673
+
674
+ return configuredToken ?? null;
675
+ }
676
+
677
+ private resolveSince(checkpoint: Record<string, unknown> | null, lookbackDays: number): string {
678
+ const cp = (checkpoint ?? {}) as GitHubCheckpoint;
679
+ const fromCheckpoint = toIsoOrUndefined(cp.last_sync_at);
680
+ if (fromCheckpoint) return fromCheckpoint;
681
+
682
+ const fallback = new Date();
683
+ fallback.setDate(fallback.getDate() - lookbackDays);
684
+ return fallback.toISOString();
685
+ }
686
+
687
+ private async syncContent(params: {
688
+ repo: RepoRef;
689
+ contentType: GitHubContentType;
690
+ sinceIso: string;
691
+ labelsFilter: string[];
692
+ token: string | null;
693
+ }): Promise<EventEnvelope[]> {
694
+ const { repo, contentType, sinceIso, labelsFilter, token } = params;
695
+
696
+ switch (contentType) {
697
+ case 'issues':
698
+ case 'pull_requests':
699
+ return await this.syncIssuesAndPulls(repo, contentType, sinceIso, labelsFilter, token);
700
+ case 'issue_comments':
701
+ return await this.syncIssueComments(repo, sinceIso, token);
702
+ case 'pr_comments':
703
+ return await this.syncPullRequestComments(repo, sinceIso, token);
704
+ case 'discussions':
705
+ return await this.syncDiscussions(repo, sinceIso, token);
706
+ case 'discussion_comments':
707
+ return await this.syncDiscussionComments(repo, sinceIso, token);
708
+ case 'stargazers':
709
+ return [];
710
+ default:
711
+ return [];
712
+ }
713
+ }
714
+
715
+ private async syncIssuesAndPulls(
716
+ repo: RepoRef,
717
+ contentType: 'issues' | 'pull_requests',
718
+ sinceIso: string,
719
+ labelsFilter: string[],
720
+ token: string | null
721
+ ): Promise<EventEnvelope[]> {
722
+ const query = new URLSearchParams({
723
+ state: 'all',
724
+ per_page: '100',
725
+ sort: 'updated',
726
+ direction: 'desc',
727
+ since: sinceIso,
728
+ });
729
+ if (labelsFilter.length > 0) {
730
+ query.set('labels', labelsFilter.join(','));
731
+ }
732
+
733
+ const url = `https://api.github.com/repos/${repo.owner}/${repo.repo}/issues?${query.toString()}`;
734
+ const items = await this.requestJson<GitHubIssueLike[]>({ url, token });
735
+ const events: EventEnvelope[] = [];
736
+
737
+ for (const item of items) {
738
+ const isPR = !!item.pull_request;
739
+ if (contentType === 'issues' && isPR) continue;
740
+ if (contentType === 'pull_requests' && !isPR) continue;
741
+
742
+ const createdAt = new Date(item.created_at);
743
+ if (Number.isNaN(createdAt.getTime())) continue;
744
+
745
+ const score = toInt(item.reactions?.total_count, 0) + toInt(item.comments, 0);
746
+ const labels = Array.isArray(item.labels)
747
+ ? item.labels.map((label) => label.name).filter((v): v is string => !!v)
748
+ : [];
749
+
750
+ events.push({
751
+ origin_id: `${isPR ? 'pr' : 'issue'}_${repo.owner}_${repo.repo}_${item.number}`,
752
+ title: item.title,
753
+ payload_text: (item.body ?? '').trim(),
754
+ author_name: item.user?.login,
755
+ source_url: item.html_url,
756
+ occurred_at: createdAt,
757
+ origin_type: isPR ? 'pull_request' : 'issue',
758
+ score,
759
+ metadata: {
760
+ number: item.number,
761
+ state: item.state,
762
+ labels,
763
+ updated_at: item.updated_at,
764
+ reactions: item.reactions ?? {},
765
+ comments: item.comments ?? 0,
766
+ },
767
+ });
768
+ }
769
+
770
+ return events;
771
+ }
772
+
773
+ private async syncIssueComments(
774
+ repo: RepoRef,
775
+ sinceIso: string,
776
+ token: string | null
777
+ ): Promise<EventEnvelope[]> {
778
+ const query = new URLSearchParams({
779
+ per_page: '100',
780
+ sort: 'updated',
781
+ direction: 'desc',
782
+ since: sinceIso,
783
+ });
784
+ const url = `https://api.github.com/repos/${repo.owner}/${repo.repo}/issues/comments?${query.toString()}`;
785
+ const comments = await this.requestJson<GitHubCommentLike[]>({ url, token });
786
+
787
+ return comments
788
+ .map((comment): EventEnvelope | null => {
789
+ const createdAt = new Date(comment.created_at);
790
+ if (Number.isNaN(createdAt.getTime())) return null;
791
+ if (!comment.body) return null;
792
+
793
+ const issueNumber = comment.issue_url?.match(/\/issues\/(\d+)$/)?.[1];
794
+ return {
795
+ origin_id: `issue_comment_${repo.owner}_${repo.repo}_${comment.id}`,
796
+ payload_text: comment.body,
797
+ author_name: comment.user?.login,
798
+ source_url: comment.html_url,
799
+ occurred_at: createdAt,
800
+ origin_type: 'issue_comment',
801
+ score: toInt(comment.reactions?.total_count, 0),
802
+ origin_parent_id: issueNumber
803
+ ? `issue_${repo.owner}_${repo.repo}_${issueNumber}`
804
+ : undefined,
805
+ metadata: {
806
+ updated_at: comment.updated_at,
807
+ reactions: comment.reactions ?? {},
808
+ },
809
+ };
810
+ })
811
+ .filter((value): value is EventEnvelope => value !== null);
812
+ }
813
+
814
+ private async syncPullRequestComments(
815
+ repo: RepoRef,
816
+ sinceIso: string,
817
+ token: string | null
818
+ ): Promise<EventEnvelope[]> {
819
+ const query = new URLSearchParams({
820
+ per_page: '100',
821
+ sort: 'updated',
822
+ direction: 'desc',
823
+ since: sinceIso,
824
+ });
825
+ const url = `https://api.github.com/repos/${repo.owner}/${repo.repo}/pulls/comments?${query.toString()}`;
826
+ const comments = await this.requestJson<GitHubCommentLike[]>({ url, token });
827
+
828
+ return comments
829
+ .map((comment): EventEnvelope | null => {
830
+ const createdAt = new Date(comment.created_at);
831
+ if (Number.isNaN(createdAt.getTime())) return null;
832
+ if (!comment.body) return null;
833
+
834
+ const prNumber = comment.pull_request_url?.match(/\/pulls\/(\d+)$/)?.[1];
835
+ return {
836
+ origin_id: `pr_comment_${repo.owner}_${repo.repo}_${comment.id}`,
837
+ payload_text: comment.body,
838
+ author_name: comment.user?.login,
839
+ source_url: comment.html_url,
840
+ occurred_at: createdAt,
841
+ origin_type: 'pr_comment',
842
+ score: toInt(comment.reactions?.total_count, 0),
843
+ origin_parent_id: prNumber ? `pr_${repo.owner}_${repo.repo}_${prNumber}` : undefined,
844
+ metadata: {
845
+ updated_at: comment.updated_at,
846
+ reactions: comment.reactions ?? {},
847
+ },
848
+ };
849
+ })
850
+ .filter((value): value is EventEnvelope => value !== null);
851
+ }
852
+
853
+ private async syncDiscussions(
854
+ repo: RepoRef,
855
+ sinceIso: string,
856
+ token: string | null
857
+ ): Promise<EventEnvelope[]> {
858
+ const query = `
859
+ query($owner: String!, $repo: String!) {
860
+ repository(owner: $owner, name: $repo) {
861
+ discussions(first: 100, orderBy: {field: UPDATED_AT, direction: DESC}) {
862
+ nodes {
863
+ id
864
+ number
865
+ title
866
+ body
867
+ author { login }
868
+ url
869
+ createdAt
870
+ updatedAt
871
+ category { name }
872
+ comments { totalCount }
873
+ reactions { totalCount }
874
+ }
875
+ }
876
+ }
877
+ }
878
+ `;
879
+
880
+ const response = await this.requestGraphQL<{
881
+ data?: {
882
+ repository?: {
883
+ discussions?: { nodes?: GraphQLDiscussionNode[] };
884
+ };
885
+ };
886
+ }>({
887
+ token,
888
+ query,
889
+ variables: { owner: repo.owner, repo: repo.repo },
890
+ });
891
+
892
+ const discussions = response.data?.repository?.discussions?.nodes ?? [];
893
+ const since = new Date(sinceIso).getTime();
894
+
895
+ return discussions
896
+ .map((discussion): EventEnvelope | null => {
897
+ const createdAt = new Date(discussion.createdAt);
898
+ const updatedAt = new Date(discussion.updatedAt).getTime();
899
+ if (Number.isNaN(createdAt.getTime())) return null;
900
+ if (!Number.isNaN(since) && updatedAt < since) return null;
901
+
902
+ return {
903
+ origin_id: `discussion_${repo.owner}_${repo.repo}_${discussion.number}`,
904
+ title: discussion.title,
905
+ payload_text: (discussion.body ?? '').trim(),
906
+ author_name: discussion.author?.login,
907
+ source_url: discussion.url,
908
+ occurred_at: createdAt,
909
+ origin_type: 'discussion',
910
+ score:
911
+ toInt(discussion.reactions?.totalCount, 0) + toInt(discussion.comments?.totalCount, 0),
912
+ metadata: {
913
+ number: discussion.number,
914
+ category: discussion.category?.name ?? null,
915
+ updated_at: discussion.updatedAt,
916
+ comments: discussion.comments?.totalCount ?? 0,
917
+ reactions: discussion.reactions?.totalCount ?? 0,
918
+ },
919
+ };
920
+ })
921
+ .filter((value): value is EventEnvelope => value !== null);
922
+ }
923
+
924
+ private async syncDiscussionComments(
925
+ repo: RepoRef,
926
+ sinceIso: string,
927
+ token: string | null
928
+ ): Promise<EventEnvelope[]> {
929
+ const query = `
930
+ query($owner: String!, $repo: String!) {
931
+ repository(owner: $owner, name: $repo) {
932
+ discussions(first: 50, orderBy: {field: UPDATED_AT, direction: DESC}) {
933
+ nodes {
934
+ number
935
+ comments(first: 50) {
936
+ nodes {
937
+ id
938
+ body
939
+ url
940
+ createdAt
941
+ updatedAt
942
+ author { login }
943
+ reactions { totalCount }
944
+ }
945
+ }
946
+ }
947
+ }
948
+ }
949
+ }
950
+ `;
951
+
952
+ const response = await this.requestGraphQL<{
953
+ data?: {
954
+ repository?: {
955
+ discussions?: {
956
+ nodes?: Array<{
957
+ number: number;
958
+ comments?: { nodes?: Array<Omit<GraphQLDiscussionCommentNode, 'discussion'>> };
959
+ }>;
960
+ };
961
+ };
962
+ };
963
+ }>({
964
+ token,
965
+ query,
966
+ variables: { owner: repo.owner, repo: repo.repo },
967
+ });
968
+
969
+ const discussions = response.data?.repository?.discussions?.nodes ?? [];
970
+ const since = new Date(sinceIso).getTime();
971
+ const result: EventEnvelope[] = [];
972
+
973
+ for (const discussion of discussions) {
974
+ const comments = discussion.comments?.nodes ?? [];
975
+ for (const comment of comments) {
976
+ const createdAt = new Date(comment.createdAt);
977
+ const updatedAt = new Date(comment.updatedAt).getTime();
978
+ if (Number.isNaN(createdAt.getTime())) continue;
979
+ if (!Number.isNaN(since) && updatedAt < since) continue;
980
+ if (!comment.body?.trim()) continue;
981
+
982
+ result.push({
983
+ origin_id: `discussion_comment_${repo.owner}_${repo.repo}_${comment.id}`,
984
+ payload_text: comment.body.trim(),
985
+ author_name: comment.author?.login,
986
+ source_url: comment.url,
987
+ occurred_at: createdAt,
988
+ origin_type: 'discussion_comment',
989
+ score: toInt(comment.reactions?.totalCount, 0),
990
+ origin_parent_id: `discussion_${repo.owner}_${repo.repo}_${discussion.number}`,
991
+ metadata: {
992
+ discussion_number: discussion.number,
993
+ updated_at: comment.updatedAt,
994
+ reactions: comment.reactions?.totalCount ?? 0,
995
+ },
996
+ });
997
+ }
998
+ }
999
+
1000
+ return result;
1001
+ }
1002
+
1003
+ private async syncStargazers(
1004
+ repo: RepoRef,
1005
+ checkpoint: Record<string, unknown> | null,
1006
+ token: string | null
1007
+ ): Promise<{ events: EventEnvelope[]; currentStargazers: GitHubStargazerCheckpoint[] }> {
1008
+ const previous = this.parseStargazerCheckpoint(checkpoint);
1009
+ const previousByKey = new Map(previous.map((stargazer) => [stargazer.key, stargazer]));
1010
+ const currentStargazers: GitHubStargazerCheckpoint[] = [];
1011
+ const events: EventEnvelope[] = [];
1012
+ const now = new Date();
1013
+ const repoInfo = await this.fetchRepository(repo, token);
1014
+ const target = this.buildRepoTarget(repo, repoInfo);
1015
+ let remainingProfileFetches = STARGAZER_PROFILE_FETCH_LIMIT;
1016
+
1017
+ for (let page = 1; ; page += 1) {
1018
+ const query = new URLSearchParams({
1019
+ per_page: '100',
1020
+ page: String(page),
1021
+ });
1022
+ const url = `https://api.github.com/repos/${repo.owner}/${repo.repo}/stargazers?${query.toString()}`;
1023
+ const stargazers = await this.requestJson<GitHubStargazerLike[]>({
1024
+ url,
1025
+ token,
1026
+ accept: 'application/vnd.github.star+json',
1027
+ });
1028
+
1029
+ if (stargazers.length === 0) break;
1030
+
1031
+ for (const stargazer of stargazers) {
1032
+ const user = stargazer.user ?? stargazer;
1033
+ const login = user.login;
1034
+ if (!login) continue;
1035
+
1036
+ const key = this.githubUserKey(user, login);
1037
+ const starredAtIso = toIsoOrUndefined(stargazer.starred_at) ?? now.toISOString();
1038
+ const starredAt = new Date(starredAtIso);
1039
+ if (Number.isNaN(starredAt.getTime())) continue;
1040
+
1041
+ const previousStargazer = previousByKey.get(key);
1042
+ const shouldRefreshProfile = this.shouldRefreshStargazerProfile(previousStargazer, now);
1043
+ const profileFetchedAt = shouldRefreshProfile && remainingProfileFetches > 0
1044
+ ? await this.enqueueStargazerProfileEvent({
1045
+ events,
1046
+ login,
1047
+ token,
1048
+ fallback: user,
1049
+ fetchedAt: now,
1050
+ })
1051
+ : (previousStargazer?.profile_fetched_at ?? null);
1052
+ if (shouldRefreshProfile && remainingProfileFetches > 0) remainingProfileFetches -= 1;
1053
+
1054
+ currentStargazers.push({
1055
+ key,
1056
+ login,
1057
+ starred_at: starredAt.toISOString(),
1058
+ user_id: user.id ?? previousStargazer?.user_id ?? null,
1059
+ user_type: user.type ?? previousStargazer?.user_type ?? null,
1060
+ html_url: user.html_url ?? previousStargazer?.html_url ?? `https://github.com/${login}`,
1061
+ profile_fetched_at: profileFetchedAt,
1062
+ });
1063
+
1064
+ if (!previousStargazer) {
1065
+ events.push({
1066
+ origin_id: `stargazer_${repo.owner}_${repo.repo}_${this.keyForOriginId(key)}`,
1067
+ title: `${login} starred ${repo.owner}/${repo.repo}`,
1068
+ payload_text: `${login} starred ${repo.owner}/${repo.repo}.`,
1069
+ author_name: login,
1070
+ source_url: user.html_url ?? `https://github.com/${login}`,
1071
+ occurred_at: starredAt,
1072
+ origin_type: 'stargazer',
1073
+ score: 1,
1074
+ metadata: {
1075
+ actor: this.buildActor(user, login),
1076
+ target,
1077
+ action: 'starred',
1078
+ starred_at: starredAt.toISOString(),
1079
+ source: 'github_stargazers_snapshot',
1080
+ },
1081
+ });
1082
+ }
1083
+ }
1084
+
1085
+ if (stargazers.length < 100) break;
1086
+ }
1087
+
1088
+ const currentKeys = new Set(currentStargazers.map((stargazer) => stargazer.key));
1089
+ for (const previousStargazer of previous) {
1090
+ if (currentKeys.has(previousStargazer.key)) continue;
1091
+
1092
+ events.push({
1093
+ origin_id: `stargazer_unstarred_${repo.owner}_${repo.repo}_${this.keyForOriginId(previousStargazer.key)}_${now.getTime()}`,
1094
+ title: `${previousStargazer.login} unstarred ${repo.owner}/${repo.repo}`,
1095
+ payload_text: `${previousStargazer.login} unstarred ${repo.owner}/${repo.repo}.`,
1096
+ author_name: previousStargazer.login,
1097
+ source_url: previousStargazer.html_url ?? `https://github.com/${previousStargazer.login}`,
1098
+ occurred_at: now,
1099
+ origin_type: 'stargazer_unstarred',
1100
+ score: 1,
1101
+ metadata: {
1102
+ actor: this.buildActorFromCheckpoint(previousStargazer),
1103
+ target,
1104
+ action: 'unstarred',
1105
+ starred_at: previousStargazer.starred_at ?? null,
1106
+ unstarred_at: now.toISOString(),
1107
+ unstarred_at_is_inferred: true,
1108
+ source: 'github_stargazers_snapshot_diff',
1109
+ },
1110
+ });
1111
+ }
1112
+
1113
+ return { events, currentStargazers };
1114
+ }
1115
+
1116
+ private shouldRefreshStargazerProfile(
1117
+ previous: GitHubStargazerCheckpoint | undefined,
1118
+ now: Date
1119
+ ): boolean {
1120
+ const fetchedAt = toIsoOrUndefined(previous?.profile_fetched_at);
1121
+ if (!fetchedAt) return true;
1122
+ return now.getTime() - new Date(fetchedAt).getTime() >= STARGAZER_PROFILE_REFRESH_MS;
1123
+ }
1124
+
1125
+ private async enqueueStargazerProfileEvent(params: {
1126
+ events: EventEnvelope[];
1127
+ login: string;
1128
+ token: string | null;
1129
+ fallback: GitHubStargazerLike['user'] | GitHubStargazerLike;
1130
+ fetchedAt: Date;
1131
+ }): Promise<string> {
1132
+ const profile = await this.fetchUserProfile(params.login, params.token, params.fallback);
1133
+ const profileUrl = profile.html_url ?? `https://github.com/${params.login}`;
1134
+ const occurredAt = new Date(
1135
+ toIsoOrUndefined(profile.updated_at) ?? params.fetchedAt.toISOString()
1136
+ );
1137
+ const identities = this.githubUserIdentities(profile, params.login);
1138
+
1139
+ params.events.push({
1140
+ origin_id: `stargazer_profile_${this.keyForOriginId(this.githubUserKey(profile, params.login))}`,
1141
+ title: profile.name || params.login,
1142
+ payload_text: profile.bio || `GitHub profile for ${params.login}.`,
1143
+ author_name: params.login,
1144
+ source_url: profileUrl,
1145
+ occurred_at: Number.isNaN(occurredAt.getTime()) ? params.fetchedAt : occurredAt,
1146
+ origin_type: 'stargazer_profile',
1147
+ score: toInt(profile.followers, 0),
1148
+ metadata: {
1149
+ public_identity_profile: true,
1150
+ provider: 'github',
1151
+ identities,
1152
+ account: {
1153
+ provider: 'github',
1154
+ provider_user_id: profile.id ? String(profile.id) : null,
1155
+ handle: params.login,
1156
+ url: profileUrl,
1157
+ avatar_url: profile.avatar_url ?? null,
1158
+ user_type: profile.type ?? null,
1159
+ site_admin: profile.site_admin ?? false,
1160
+ profile: {
1161
+ name: profile.name ?? null,
1162
+ company: profile.company ?? null,
1163
+ blog: profile.blog ?? null,
1164
+ location: profile.location ?? null,
1165
+ email: profile.email ?? null,
1166
+ bio: profile.bio ?? null,
1167
+ twitter_username: profile.twitter_username ?? null,
1168
+ public_repos: profile.public_repos ?? null,
1169
+ public_gists: profile.public_gists ?? null,
1170
+ followers: profile.followers ?? null,
1171
+ following: profile.following ?? null,
1172
+ github_created_at: profile.created_at ?? null,
1173
+ github_updated_at: profile.updated_at ?? null,
1174
+ },
1175
+ fetched_at: params.fetchedAt.toISOString(),
1176
+ },
1177
+ },
1178
+ });
1179
+
1180
+ return params.fetchedAt.toISOString();
1181
+ }
1182
+
1183
+ private async fetchUserProfile(
1184
+ login: string,
1185
+ token: string | null,
1186
+ fallback: GitHubStargazerLike['user'] | GitHubStargazerLike
1187
+ ): Promise<GitHubUserProfile> {
1188
+ try {
1189
+ return await this.requestJson<GitHubUserProfile>({
1190
+ url: `https://api.github.com/users/${encodeURIComponent(login)}`,
1191
+ token,
1192
+ });
1193
+ } catch {
1194
+ return {
1195
+ id: fallback?.id,
1196
+ login,
1197
+ html_url: fallback?.html_url ?? `https://github.com/${login}`,
1198
+ avatar_url: fallback?.avatar_url,
1199
+ type: fallback?.type,
1200
+ site_admin: fallback?.site_admin,
1201
+ };
1202
+ }
1203
+ }
1204
+
1205
+ private async fetchRepository(
1206
+ repo: RepoRef,
1207
+ token: string | null
1208
+ ): Promise<GitHubRepositoryLike> {
1209
+ try {
1210
+ return await this.requestJson<GitHubRepositoryLike>({
1211
+ url: `https://api.github.com/repos/${repo.owner}/${repo.repo}`,
1212
+ token,
1213
+ });
1214
+ } catch {
1215
+ return {
1216
+ full_name: `${repo.owner}/${repo.repo}`,
1217
+ html_url: `https://github.com/${repo.owner}/${repo.repo}`,
1218
+ };
1219
+ }
1220
+ }
1221
+
1222
+ private githubUserKey(user: Pick<GitHubUserProfile, 'id'>, login: string): string {
1223
+ return user.id ? `${IDENTITY.GITHUB_USER_ID}:${user.id}` : `${IDENTITY.GITHUB_LOGIN}:${login.toLowerCase()}`;
1224
+ }
1225
+
1226
+ private keyForOriginId(key: string): string {
1227
+ return key.replace(/[^a-z0-9]+/gi, '_');
1228
+ }
1229
+
1230
+ private githubUserIdentities(
1231
+ user: Pick<GitHubUserProfile, 'id'>,
1232
+ login: string
1233
+ ): Array<{ namespace: string; identifier: string; verification_status: string }> {
1234
+ const identities: Array<{ namespace: string; identifier: string; verification_status: string }> = [];
1235
+ if (user.id) {
1236
+ identities.push({
1237
+ namespace: IDENTITY.GITHUB_USER_ID,
1238
+ identifier: String(user.id),
1239
+ verification_status: 'observed',
1240
+ });
1241
+ }
1242
+ identities.push({
1243
+ namespace: IDENTITY.GITHUB_LOGIN,
1244
+ identifier: login.toLowerCase(),
1245
+ verification_status: 'observed',
1246
+ });
1247
+ return identities;
1248
+ }
1249
+
1250
+ private buildActor(user: GitHubStargazerLike['user'] | GitHubStargazerLike, login: string) {
1251
+ return {
1252
+ provider: 'github',
1253
+ identity: user?.id
1254
+ ? { namespace: IDENTITY.GITHUB_USER_ID, identifier: String(user.id) }
1255
+ : { namespace: IDENTITY.GITHUB_LOGIN, identifier: login.toLowerCase() },
1256
+ handle: { namespace: IDENTITY.GITHUB_LOGIN, identifier: login.toLowerCase() },
1257
+ profile_url: user?.html_url ?? `https://github.com/${login}`,
1258
+ user_type: user?.type ?? null,
1259
+ };
1260
+ }
1261
+
1262
+ private buildActorFromCheckpoint(stargazer: GitHubStargazerCheckpoint) {
1263
+ const [namespace, ...identifierParts] = stargazer.key.split(':');
1264
+ const identifier = identifierParts.join(':');
1265
+ return {
1266
+ provider: 'github',
1267
+ identity: { namespace, identifier },
1268
+ handle: { namespace: IDENTITY.GITHUB_LOGIN, identifier: stargazer.login.toLowerCase() },
1269
+ profile_url: stargazer.html_url ?? `https://github.com/${stargazer.login}`,
1270
+ user_type: stargazer.user_type ?? null,
1271
+ };
1272
+ }
1273
+
1274
+ private buildRepoTarget(repo: RepoRef, repoInfo: GitHubRepositoryLike) {
1275
+ const fullName = (repoInfo.full_name ?? `${repo.owner}/${repo.repo}`).toLowerCase();
1276
+ return {
1277
+ provider: 'github',
1278
+ identity: repoInfo.id
1279
+ ? { namespace: IDENTITY.GITHUB_REPO_ID, identifier: String(repoInfo.id) }
1280
+ : { namespace: IDENTITY.GITHUB_REPO_FULL_NAME, identifier: fullName },
1281
+ handle: { namespace: IDENTITY.GITHUB_REPO_FULL_NAME, identifier: fullName },
1282
+ url: repoInfo.html_url ?? `https://github.com/${repo.owner}/${repo.repo}`,
1283
+ };
1284
+ }
1285
+
1286
+ private parseStargazerCheckpoint(
1287
+ checkpoint: Record<string, unknown> | null
1288
+ ): GitHubStargazerCheckpoint[] {
1289
+ const raw = (checkpoint as GitHubCheckpoint | null)?.stargazers;
1290
+ if (!Array.isArray(raw)) return [];
1291
+
1292
+ return raw
1293
+ .map((item): GitHubStargazerCheckpoint | null => {
1294
+ if (!item || typeof item !== 'object') return null;
1295
+ const value = item as Record<string, unknown>;
1296
+ const login = asString(value.login);
1297
+ if (!login) return null;
1298
+ const userId = typeof value.user_id === 'number' ? value.user_id : null;
1299
+ const key = asString(value.key) ?? (userId ? `${IDENTITY.GITHUB_USER_ID}:${userId}` : `${IDENTITY.GITHUB_LOGIN}:${login.toLowerCase()}`);
1300
+
1301
+ return {
1302
+ key,
1303
+ login,
1304
+ starred_at: asString(value.starred_at),
1305
+ user_id: userId,
1306
+ user_type: asString(value.user_type) ?? null,
1307
+ html_url: asString(value.html_url) ?? null,
1308
+ profile_fetched_at: asString(value.profile_fetched_at) ?? null,
1309
+ };
1310
+ })
1311
+ .filter((value): value is GitHubStargazerCheckpoint => value !== null);
1312
+ }
1313
+
1314
+ private async createIssue(
1315
+ repo: RepoRef,
1316
+ token: string,
1317
+ input: Record<string, unknown>
1318
+ ): Promise<ActionResult> {
1319
+ const title = asString(input.title);
1320
+ if (!title) return { success: false, error: 'title is required' };
1321
+
1322
+ const body = asString(input.body);
1323
+ const labels = Array.isArray(input.labels)
1324
+ ? input.labels.filter((value): value is string => typeof value === 'string')
1325
+ : undefined;
1326
+ const assignees = Array.isArray(input.assignees)
1327
+ ? input.assignees.filter((value): value is string => typeof value === 'string')
1328
+ : undefined;
1329
+
1330
+ const issue = await this.requestJson<{
1331
+ id: number;
1332
+ number: number;
1333
+ html_url: string;
1334
+ state: string;
1335
+ }>({
1336
+ method: 'POST',
1337
+ url: `https://api.github.com/repos/${repo.owner}/${repo.repo}/issues`,
1338
+ token,
1339
+ body: {
1340
+ title,
1341
+ body,
1342
+ labels,
1343
+ assignees,
1344
+ },
1345
+ });
1346
+
1347
+ return {
1348
+ success: true,
1349
+ output: {
1350
+ issue_id: issue.id,
1351
+ issue_number: issue.number,
1352
+ url: issue.html_url,
1353
+ state: issue.state,
1354
+ },
1355
+ };
1356
+ }
1357
+
1358
+ private async addIssueComment(
1359
+ repo: RepoRef,
1360
+ token: string,
1361
+ input: Record<string, unknown>
1362
+ ): Promise<ActionResult> {
1363
+ const issueNumber = toInt(input.issue_number, 0);
1364
+ const body = asString(input.body);
1365
+ if (!issueNumber) return { success: false, error: 'issue_number is required' };
1366
+ if (!body) return { success: false, error: 'body is required' };
1367
+
1368
+ const comment = await this.requestJson<{
1369
+ id: number;
1370
+ html_url: string;
1371
+ }>({
1372
+ method: 'POST',
1373
+ url: `https://api.github.com/repos/${repo.owner}/${repo.repo}/issues/${issueNumber}/comments`,
1374
+ token,
1375
+ body: { body },
1376
+ });
1377
+
1378
+ return {
1379
+ success: true,
1380
+ output: {
1381
+ comment_id: comment.id,
1382
+ issue_number: issueNumber,
1383
+ url: comment.html_url,
1384
+ },
1385
+ };
1386
+ }
1387
+
1388
+ private async updateIssueState(
1389
+ repo: RepoRef,
1390
+ token: string,
1391
+ input: Record<string, unknown>,
1392
+ state: 'open' | 'closed'
1393
+ ): Promise<ActionResult> {
1394
+ const issueNumber = toInt(input.issue_number, 0);
1395
+ if (!issueNumber) return { success: false, error: 'issue_number is required' };
1396
+
1397
+ const issue = await this.requestJson<{
1398
+ id: number;
1399
+ number: number;
1400
+ html_url: string;
1401
+ state: string;
1402
+ }>({
1403
+ method: 'PATCH',
1404
+ url: `https://api.github.com/repos/${repo.owner}/${repo.repo}/issues/${issueNumber}`,
1405
+ token,
1406
+ body: { state },
1407
+ });
1408
+
1409
+ return {
1410
+ success: true,
1411
+ output: {
1412
+ issue_id: issue.id,
1413
+ issue_number: issue.number,
1414
+ url: issue.html_url,
1415
+ state: issue.state,
1416
+ },
1417
+ };
1418
+ }
1419
+
1420
+ private async createPullRequest(
1421
+ repo: RepoRef,
1422
+ token: string,
1423
+ input: Record<string, unknown>
1424
+ ): Promise<ActionResult> {
1425
+ const title = asString(input.title);
1426
+ const head = asString(input.head);
1427
+ const base = asString(input.base);
1428
+ if (!title) return { success: false, error: 'title is required' };
1429
+ if (!head) return { success: false, error: 'head is required' };
1430
+ if (!base) return { success: false, error: 'base is required' };
1431
+
1432
+ const body = asString(input.body);
1433
+ const draft = typeof input.draft === 'boolean' ? input.draft : undefined;
1434
+
1435
+ const pr = await this.requestJson<{
1436
+ id: number;
1437
+ number: number;
1438
+ html_url: string;
1439
+ state: string;
1440
+ draft?: boolean;
1441
+ }>({
1442
+ method: 'POST',
1443
+ url: `https://api.github.com/repos/${repo.owner}/${repo.repo}/pulls`,
1444
+ token,
1445
+ body: {
1446
+ title,
1447
+ head,
1448
+ base,
1449
+ body,
1450
+ draft,
1451
+ },
1452
+ });
1453
+
1454
+ return {
1455
+ success: true,
1456
+ output: {
1457
+ pull_request_id: pr.id,
1458
+ pull_number: pr.number,
1459
+ url: pr.html_url,
1460
+ state: pr.state,
1461
+ draft: pr.draft ?? false,
1462
+ },
1463
+ };
1464
+ }
1465
+
1466
+ private async mergePullRequest(
1467
+ repo: RepoRef,
1468
+ token: string,
1469
+ input: Record<string, unknown>
1470
+ ): Promise<ActionResult> {
1471
+ const pullNumber = toInt(input.pull_number, 0);
1472
+ if (!pullNumber) return { success: false, error: 'pull_number is required' };
1473
+
1474
+ const mergeMethod = asString(input.merge_method);
1475
+ const commitTitle = asString(input.commit_title);
1476
+ const commitMessage = asString(input.commit_message);
1477
+
1478
+ const merged = await this.requestJson<{
1479
+ sha: string;
1480
+ merged: boolean;
1481
+ message: string;
1482
+ }>({
1483
+ method: 'PUT',
1484
+ url: `https://api.github.com/repos/${repo.owner}/${repo.repo}/pulls/${pullNumber}/merge`,
1485
+ token,
1486
+ body: {
1487
+ merge_method:
1488
+ mergeMethod === 'merge' || mergeMethod === 'squash' || mergeMethod === 'rebase'
1489
+ ? mergeMethod
1490
+ : undefined,
1491
+ commit_title: commitTitle,
1492
+ commit_message: commitMessage ? stripMarkdown(commitMessage) : undefined,
1493
+ },
1494
+ });
1495
+
1496
+ return {
1497
+ success: true,
1498
+ output: {
1499
+ pull_number: pullNumber,
1500
+ merged: !!merged.merged,
1501
+ message: merged.message,
1502
+ sha: merged.sha,
1503
+ },
1504
+ };
1505
+ }
1506
+
1507
+ private async requestGraphQL<T>(params: {
1508
+ token: string | null;
1509
+ query: string;
1510
+ variables?: Record<string, unknown>;
1511
+ }): Promise<T> {
1512
+ return await this.requestJson<T>({
1513
+ method: 'POST',
1514
+ url: 'https://api.github.com/graphql',
1515
+ token: params.token,
1516
+ body: {
1517
+ query: params.query,
1518
+ variables: params.variables ?? {},
1519
+ },
1520
+ });
1521
+ }
1522
+
1523
+ private async requestJson<T>(params: {
1524
+ url: string;
1525
+ method?: string;
1526
+ token: string | null;
1527
+ body?: Record<string, unknown>;
1528
+ accept?: string;
1529
+ }): Promise<T> {
1530
+ const method = params.method ?? 'GET';
1531
+ const headers: Record<string, string> = {
1532
+ Accept: params.accept ?? 'application/vnd.github+json',
1533
+ 'X-GitHub-Api-Version': '2022-11-28',
1534
+ 'Content-Type': 'application/json',
1535
+ };
1536
+ if (params.token) {
1537
+ headers.Authorization = `Bearer ${params.token}`;
1538
+ }
1539
+
1540
+ const response = await fetch(params.url, {
1541
+ method,
1542
+ headers,
1543
+ body: params.body ? JSON.stringify(params.body) : undefined,
1544
+ });
1545
+
1546
+ if (!response.ok) {
1547
+ const text = await response.text();
1548
+ throw new Error(`GitHub API ${method} ${params.url} failed (${response.status}): ${text}`);
1549
+ }
1550
+
1551
+ return (await response.json()) as T;
1552
+ }
1553
+ }