@lobu/cli 6.0.0 → 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 (222) hide show
  1. package/README.md +20 -27
  2. package/dist/bundled-skills/lobu/SKILL.md +12 -12
  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 +6 -6
  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 +4 -4
  44. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  45. package/dist/commands/memory/_lib/install-targets.d.ts.map +1 -1
  46. package/dist/commands/memory/_lib/install-targets.js +1 -5
  47. package/dist/commands/memory/_lib/install-targets.js.map +1 -1
  48. package/dist/commands/memory/_lib/mcp.d.ts +2 -2
  49. package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
  50. package/dist/commands/memory/_lib/mcp.js +24 -12
  51. package/dist/commands/memory/_lib/mcp.js.map +1 -1
  52. package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
  53. package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
  54. package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
  55. package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
  56. package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
  57. package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
  58. package/dist/commands/memory/_lib/schema.d.ts +2 -2
  59. package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
  60. package/dist/commands/memory/_lib/schema.js +3 -3
  61. package/dist/commands/memory/_lib/schema.js.map +1 -1
  62. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  63. package/dist/commands/memory/_lib/seed-cmd.js +5 -6
  64. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  65. package/dist/commands/memory/run.d.ts.map +1 -1
  66. package/dist/commands/memory/run.js +2 -2
  67. package/dist/commands/memory/run.js.map +1 -1
  68. package/dist/commands/platforms/platform-prompts.d.ts +0 -1
  69. package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
  70. package/dist/commands/platforms/platform-prompts.js +54 -8
  71. package/dist/commands/platforms/platform-prompts.js.map +1 -1
  72. package/dist/commands/telemetry.d.ts +10 -0
  73. package/dist/commands/telemetry.d.ts.map +1 -0
  74. package/dist/commands/telemetry.js +68 -0
  75. package/dist/commands/telemetry.js.map +1 -0
  76. package/dist/commands/whoami.d.ts.map +1 -1
  77. package/dist/commands/whoami.js +1 -1
  78. package/dist/commands/whoami.js.map +1 -1
  79. package/dist/connectors/README.md +534 -0
  80. package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
  81. package/dist/connectors/browser-scraper-utils.ts +214 -0
  82. package/dist/connectors/capterra.ts +273 -0
  83. package/dist/connectors/g2.ts +286 -0
  84. package/dist/connectors/github.ts +1553 -0
  85. package/dist/connectors/glassdoor.ts +291 -0
  86. package/dist/connectors/gmaps.ts +197 -0
  87. package/dist/connectors/google_calendar.ts +631 -0
  88. package/dist/connectors/google_gmail.ts +751 -0
  89. package/dist/connectors/google_photos.ts +776 -0
  90. package/dist/connectors/google_play.ts +342 -0
  91. package/dist/connectors/hackernews.ts +471 -0
  92. package/dist/connectors/index.ts +23 -0
  93. package/dist/connectors/ios_appstore.ts +226 -0
  94. package/dist/connectors/linkedin.ts +471 -0
  95. package/dist/connectors/microsoft_outlook.ts +410 -0
  96. package/dist/connectors/producthunt.ts +471 -0
  97. package/dist/connectors/reddit.ts +600 -0
  98. package/dist/connectors/rss.ts +448 -0
  99. package/dist/connectors/spotify.ts +590 -0
  100. package/dist/connectors/trustpilot.ts +199 -0
  101. package/dist/connectors/website.ts +629 -0
  102. package/dist/connectors/whatsapp.ts +1073 -0
  103. package/dist/connectors/x.ts +526 -0
  104. package/dist/connectors/youtube.ts +666 -0
  105. package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
  106. package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
  107. package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
  108. package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
  109. package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
  110. package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
  111. package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
  112. package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
  113. package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
  114. package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
  115. package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
  116. package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
  117. package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
  118. package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
  119. package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
  120. package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
  121. package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
  122. package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
  123. package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
  124. package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
  125. package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
  126. package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
  127. package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
  128. package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
  129. package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
  130. package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
  131. package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
  132. package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
  133. package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
  134. package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
  135. package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
  136. package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
  137. package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
  138. package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
  139. package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
  140. package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
  141. package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
  142. package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
  143. package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
  144. package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
  145. package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
  146. package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
  147. package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
  148. package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
  149. package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
  150. package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.js +147 -23
  153. package/dist/index.js.map +1 -1
  154. package/dist/internal/api-client.d.ts +4 -8
  155. package/dist/internal/api-client.d.ts.map +1 -1
  156. package/dist/internal/api-client.js +1 -1
  157. package/dist/internal/api-client.js.map +1 -1
  158. package/dist/internal/context.js +2 -2
  159. package/dist/internal/context.js.map +1 -1
  160. package/dist/internal/credentials.d.ts.map +1 -1
  161. package/dist/internal/credentials.js +6 -1
  162. package/dist/internal/credentials.js.map +1 -1
  163. package/dist/internal/index.d.ts +2 -3
  164. package/dist/internal/index.d.ts.map +1 -1
  165. package/dist/internal/index.js +2 -2
  166. package/dist/internal/index.js.map +1 -1
  167. package/dist/internal/oauth.d.ts +7 -6
  168. package/dist/internal/oauth.d.ts.map +1 -1
  169. package/dist/internal/oauth.js +3 -3
  170. package/dist/internal/project-link.d.ts +10 -0
  171. package/dist/internal/project-link.d.ts.map +1 -0
  172. package/dist/internal/project-link.js +48 -0
  173. package/dist/internal/project-link.js.map +1 -0
  174. package/dist/providers.json +2 -2
  175. package/dist/server.bundle.mjs +3173 -4404
  176. package/dist/start-local.bundle.mjs +71481 -0
  177. package/dist/templates/README.md.tmpl +10 -11
  178. package/package.json +14 -12
  179. package/dist/__tests__/chat.integration.test.d.ts +0 -2
  180. package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
  181. package/dist/__tests__/chat.integration.test.js +0 -337
  182. package/dist/__tests__/chat.integration.test.js.map +0 -1
  183. package/dist/__tests__/dev.test.d.ts +0 -2
  184. package/dist/__tests__/dev.test.d.ts.map +0 -1
  185. package/dist/__tests__/dev.test.js +0 -25
  186. package/dist/__tests__/dev.test.js.map +0 -1
  187. package/dist/__tests__/init-memory.test.d.ts +0 -2
  188. package/dist/__tests__/init-memory.test.d.ts.map +0 -1
  189. package/dist/__tests__/init-memory.test.js +0 -45
  190. package/dist/__tests__/init-memory.test.js.map +0 -1
  191. package/dist/__tests__/token.test.d.ts +0 -2
  192. package/dist/__tests__/token.test.d.ts.map +0 -1
  193. package/dist/__tests__/token.test.js +0 -52
  194. package/dist/__tests__/token.test.js.map +0 -1
  195. package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
  196. package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
  197. package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
  198. package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
  199. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
  200. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
  201. package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
  202. package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
  203. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
  204. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
  205. package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
  206. package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
  207. package/dist/commands/apply.d.ts +0 -3
  208. package/dist/commands/apply.d.ts.map +0 -1
  209. package/dist/commands/apply.js +0 -5
  210. package/dist/commands/apply.js.map +0 -1
  211. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
  212. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
  213. package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
  214. package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
  215. package/dist/internal/__tests__/api-client.test.d.ts +0 -2
  216. package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
  217. package/dist/internal/__tests__/api-client.test.js +0 -95
  218. package/dist/internal/__tests__/api-client.test.js.map +0 -1
  219. package/dist/internal/__tests__/context.test.d.ts +0 -2
  220. package/dist/internal/__tests__/context.test.d.ts.map +0 -1
  221. package/dist/internal/__tests__/context.test.js +0 -77
  222. package/dist/internal/__tests__/context.test.js.map +0 -1
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Product Hunt Connector (V1 runtime)
3
+ *
4
+ * Searches Product Hunt posts and comments via the GraphQL API.
5
+ * Supports both authenticated (Developer Token) and unauthenticated modes.
6
+ */
7
+
8
+ import {
9
+ type ActionContext,
10
+ type ActionResult,
11
+ type ConnectorDefinition,
12
+ ConnectorRuntime,
13
+ calculateEngagementScore,
14
+ type EventEnvelope,
15
+ type SyncContext,
16
+ type SyncResult,
17
+ } from '@lobu/connector-sdk';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Product Hunt GraphQL API types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ interface ProductHuntMaker {
24
+ name: string;
25
+ }
26
+
27
+ interface ProductHuntTopicEdge {
28
+ node: {
29
+ name: string;
30
+ };
31
+ }
32
+
33
+ interface ProductHuntComment {
34
+ id: string;
35
+ body: string;
36
+ createdAt: string;
37
+ votesCount: number;
38
+ user: {
39
+ name: string;
40
+ };
41
+ }
42
+
43
+ interface ProductHuntCommentEdge {
44
+ node: ProductHuntComment;
45
+ }
46
+
47
+ interface ProductHuntPost {
48
+ id: string;
49
+ name: string;
50
+ tagline: string;
51
+ description: string;
52
+ url: string;
53
+ votesCount: number;
54
+ commentsCount: number;
55
+ createdAt: string;
56
+ makers: ProductHuntMaker[];
57
+ topics: {
58
+ edges: ProductHuntTopicEdge[];
59
+ };
60
+ comments: {
61
+ edges: ProductHuntCommentEdge[];
62
+ };
63
+ }
64
+
65
+ interface ProductHuntPostEdge {
66
+ node: ProductHuntPost;
67
+ cursor: string;
68
+ }
69
+
70
+ interface ProductHuntPageInfo {
71
+ hasNextPage: boolean;
72
+ endCursor: string | null;
73
+ }
74
+
75
+ interface ProductHuntPostsResponse {
76
+ data: {
77
+ posts: {
78
+ edges: ProductHuntPostEdge[];
79
+ pageInfo: ProductHuntPageInfo;
80
+ };
81
+ };
82
+ errors?: Array<{ message: string }>;
83
+ }
84
+
85
+ interface ProductHuntCheckpoint {
86
+ last_cursor?: string;
87
+ last_sync_at?: string;
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // GraphQL queries
92
+ // ---------------------------------------------------------------------------
93
+
94
+ const LIST_POSTS_QUERY = `
95
+ query ListPosts($topic: String, $postedAfter: DateTime, $after: String) {
96
+ posts(topic: $topic, postedAfter: $postedAfter, after: $after, first: 10, order: NEWEST) {
97
+ edges {
98
+ node {
99
+ id
100
+ name
101
+ tagline
102
+ description
103
+ url
104
+ votesCount
105
+ commentsCount
106
+ createdAt
107
+ makers { name }
108
+ topics { edges { node { name } } }
109
+ comments(first: 10) {
110
+ edges {
111
+ node {
112
+ id
113
+ body
114
+ createdAt
115
+ votesCount
116
+ user { name }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ cursor
122
+ }
123
+ pageInfo {
124
+ hasNextPage
125
+ endCursor
126
+ }
127
+ }
128
+ }
129
+ `;
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Connector
133
+ // ---------------------------------------------------------------------------
134
+
135
+ export default class ProductHuntConnector extends ConnectorRuntime {
136
+ readonly definition: ConnectorDefinition = {
137
+ key: 'producthunt',
138
+ name: 'Product Hunt',
139
+ description: 'Searches Product Hunt posts and comments for a given query.',
140
+ version: '1.0.0',
141
+ faviconDomain: 'producthunt.com',
142
+ authSchema: {
143
+ methods: [
144
+ {
145
+ type: 'env_keys',
146
+ required: false,
147
+ fields: [
148
+ {
149
+ key: 'PRODUCTHUNT_TOKEN',
150
+ label: 'Product Hunt Developer Token',
151
+ description:
152
+ 'Create at producthunt.com/v2/oauth/applications — add an app, then copy the Developer Token.',
153
+ secret: true,
154
+ },
155
+ ],
156
+ },
157
+ ],
158
+ },
159
+ feeds: {
160
+ posts: {
161
+ key: 'posts',
162
+ name: 'Posts & Comments',
163
+ description: 'Search Product Hunt for posts and their comments.',
164
+ configSchema: {
165
+ type: 'object',
166
+ required: ['search_query'],
167
+ properties: {
168
+ search_query: {
169
+ type: 'string',
170
+ minLength: 1,
171
+ description: 'Search term to find posts on Product Hunt.',
172
+ },
173
+ lookback_days: {
174
+ type: 'integer',
175
+ minimum: 1,
176
+ maximum: 730,
177
+ default: 365,
178
+ description: 'Number of days to look back for historical data.',
179
+ },
180
+ max_pages: {
181
+ type: 'integer',
182
+ minimum: 1,
183
+ maximum: 50,
184
+ default: 10,
185
+ description: 'Maximum number of pages to fetch.',
186
+ },
187
+ },
188
+ },
189
+ eventKinds: {
190
+ post: {
191
+ description: 'A Product Hunt launch/post',
192
+ metadataSchema: {
193
+ type: 'object',
194
+ properties: {
195
+ tagline: { type: 'string', description: 'Short tagline for the product' },
196
+ votes_count: { type: 'number', description: 'Number of upvotes' },
197
+ comments_count: { type: 'number', description: 'Number of comments' },
198
+ makers: {
199
+ type: 'array',
200
+ items: { type: 'string' },
201
+ description: 'Names of the product makers',
202
+ },
203
+ topics: {
204
+ type: 'array',
205
+ items: { type: 'string' },
206
+ description: 'Topic tags',
207
+ },
208
+ },
209
+ },
210
+ },
211
+ comment: {
212
+ description: 'A comment on a Product Hunt post',
213
+ metadataSchema: {
214
+ type: 'object',
215
+ properties: {
216
+ votes_count: { type: 'number', description: 'Number of upvotes on the comment' },
217
+ post_id: { type: 'string', description: 'ID of the parent post' },
218
+ post_name: { type: 'string', description: 'Name of the parent post' },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ },
225
+ optionsSchema: {
226
+ type: 'object',
227
+ required: ['search_query'],
228
+ properties: {
229
+ search_query: {
230
+ type: 'string',
231
+ minLength: 1,
232
+ description: 'Search term to find posts on Product Hunt.',
233
+ },
234
+ lookback_days: {
235
+ type: 'integer',
236
+ minimum: 1,
237
+ maximum: 730,
238
+ default: 365,
239
+ description: 'Number of days to look back for historical data.',
240
+ },
241
+ max_pages: {
242
+ type: 'integer',
243
+ minimum: 1,
244
+ maximum: 50,
245
+ default: 10,
246
+ description: 'Maximum number of pages to fetch.',
247
+ },
248
+ },
249
+ },
250
+ };
251
+
252
+ private readonly API_URL = 'https://api.producthunt.com/v2/api/graphql';
253
+ private readonly RATE_LIMIT_MS = 1000;
254
+
255
+ // -------------------------------------------------------------------------
256
+ // sync
257
+ // -------------------------------------------------------------------------
258
+
259
+ async sync(ctx: SyncContext): Promise<SyncResult> {
260
+ const searchQuery = ctx.config.search_query as string;
261
+ const lookbackDays = (ctx.config.lookback_days as number) ?? 365;
262
+ const maxPages = (ctx.config.max_pages as number) ?? 10;
263
+ const token = ctx.config.PRODUCTHUNT_TOKEN as string | undefined;
264
+
265
+ const cutoffDate = new Date();
266
+ cutoffDate.setDate(cutoffDate.getDate() - lookbackDays);
267
+
268
+ const previousCheckpoint = ctx.checkpoint as ProductHuntCheckpoint | null;
269
+
270
+ const events: EventEnvelope[] = [];
271
+ const seenIds = new Set<string>();
272
+ let cursor: string | null = previousCheckpoint?.last_cursor ?? null;
273
+ let page = 0;
274
+ let reachedCutoff = false;
275
+
276
+ while (page < maxPages && !reachedCutoff) {
277
+ const variables: Record<string, string | null> = {
278
+ postedAfter: cutoffDate.toISOString(),
279
+ after: cursor,
280
+ };
281
+
282
+ const headers: Record<string, string> = {
283
+ 'Content-Type': 'application/json',
284
+ Accept: 'application/json',
285
+ };
286
+ if (token) {
287
+ headers.Authorization = `Bearer ${token}`;
288
+ }
289
+
290
+ const response = await fetch(this.API_URL, {
291
+ method: 'POST',
292
+ headers,
293
+ body: JSON.stringify({
294
+ query: LIST_POSTS_QUERY,
295
+ variables,
296
+ }),
297
+ });
298
+
299
+ if (!response.ok) {
300
+ const status = response.status;
301
+ if (status === 429) {
302
+ throw new Error('Product Hunt rate limit exceeded. Please wait before retrying.');
303
+ }
304
+ if (status === 401) {
305
+ if (!token) {
306
+ // PH v2 API requires auth; return empty results when no token configured
307
+ console.warn(
308
+ 'Product Hunt API requires a Developer Token. Configure PRODUCTHUNT_TOKEN for results.'
309
+ );
310
+ break;
311
+ }
312
+ throw new Error('Product Hunt authentication failed. Check your Developer Token.');
313
+ }
314
+ throw new Error(`Product Hunt API error (${status}): ${await response.text()}`);
315
+ }
316
+
317
+ const result = (await response.json()) as ProductHuntPostsResponse;
318
+
319
+ if (result.errors && result.errors.length > 0) {
320
+ throw new Error(
321
+ `Product Hunt GraphQL error: ${result.errors.map((e) => e.message).join(', ')}`
322
+ );
323
+ }
324
+
325
+ const edges = result.data.posts.edges;
326
+ if (edges.length === 0) break;
327
+
328
+ const queryLower = searchQuery.toLowerCase();
329
+
330
+ for (const edge of edges) {
331
+ const post = edge.node;
332
+ const postDate = new Date(post.createdAt);
333
+
334
+ if (postDate < cutoffDate) {
335
+ reachedCutoff = true;
336
+ break;
337
+ }
338
+
339
+ // Client-side filter: match search query in name, tagline, or description
340
+ const matchesQuery =
341
+ post.name.toLowerCase().includes(queryLower) ||
342
+ post.tagline.toLowerCase().includes(queryLower) ||
343
+ (post.description ?? '').toLowerCase().includes(queryLower) ||
344
+ post.topics?.edges?.some((t) => t.node.name.toLowerCase().includes(queryLower));
345
+
346
+ if (!matchesQuery) continue;
347
+
348
+ // Add post event
349
+ const postExternalId = `producthunt_post_${post.id}`;
350
+ if (!seenIds.has(postExternalId)) {
351
+ seenIds.add(postExternalId);
352
+ events.push(this.transformPost(post));
353
+ }
354
+
355
+ // Add comment events
356
+ if (post.comments?.edges) {
357
+ for (const commentEdge of post.comments.edges) {
358
+ const comment = commentEdge.node;
359
+ const commentExternalId = `producthunt_comment_${comment.id}`;
360
+ if (!seenIds.has(commentExternalId)) {
361
+ seenIds.add(commentExternalId);
362
+ events.push(this.transformComment(comment, post));
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ const pageInfo = result.data.posts.pageInfo;
369
+ if (!pageInfo.hasNextPage) break;
370
+
371
+ cursor = pageInfo.endCursor;
372
+ page++;
373
+
374
+ if (page < maxPages && !reachedCutoff) {
375
+ await this.sleep(this.RATE_LIMIT_MS);
376
+ }
377
+ }
378
+
379
+ // Sort events by occurred_at descending
380
+ events.sort((a, b) => b.occurred_at.getTime() - a.occurred_at.getTime());
381
+
382
+ const checkpoint: ProductHuntCheckpoint = {
383
+ last_cursor: cursor ?? undefined,
384
+ last_sync_at: new Date().toISOString(),
385
+ };
386
+
387
+ return {
388
+ events,
389
+ checkpoint: checkpoint as Record<string, unknown>,
390
+ metadata: {
391
+ items_found: events.length,
392
+ },
393
+ };
394
+ }
395
+
396
+ // -------------------------------------------------------------------------
397
+ // execute
398
+ // -------------------------------------------------------------------------
399
+
400
+ async execute(_ctx: ActionContext): Promise<ActionResult> {
401
+ return { success: false, error: 'Actions not supported' };
402
+ }
403
+
404
+ // -------------------------------------------------------------------------
405
+ // Transform helpers
406
+ // -------------------------------------------------------------------------
407
+
408
+ private transformPost(post: ProductHuntPost): EventEnvelope {
409
+ const makers = post.makers.map((m) => m.name);
410
+ const topics = post.topics.edges.map((e) => e.node.name);
411
+
412
+ const engagementScore = calculateEngagementScore('producthunt', {
413
+ upvotes: post.votesCount,
414
+ reply_count: post.commentsCount,
415
+ });
416
+
417
+ const description = post.description ?? '';
418
+ const content = description.trim() || post.tagline;
419
+
420
+ return {
421
+ origin_id: `producthunt_post_${post.id}`,
422
+ title: post.name,
423
+ payload_text: content,
424
+ author_name: makers.join(', ') || undefined,
425
+ source_url: post.url,
426
+ occurred_at: new Date(post.createdAt),
427
+ origin_type: 'post',
428
+ score: engagementScore,
429
+ metadata: {
430
+ tagline: post.tagline,
431
+ votes_count: post.votesCount,
432
+ comments_count: post.commentsCount,
433
+ makers,
434
+ topics,
435
+ },
436
+ };
437
+ }
438
+
439
+ private transformComment(
440
+ comment: ProductHuntComment,
441
+ parentPost: ProductHuntPost
442
+ ): EventEnvelope {
443
+ const engagementScore = calculateEngagementScore('producthunt', {
444
+ upvotes: comment.votesCount,
445
+ });
446
+
447
+ return {
448
+ origin_id: `producthunt_comment_${comment.id}`,
449
+ payload_text: comment.body ?? '',
450
+ author_name: comment.user.name,
451
+ source_url: parentPost.url,
452
+ occurred_at: new Date(comment.createdAt),
453
+ origin_type: 'comment',
454
+ score: engagementScore,
455
+ origin_parent_id: `producthunt_post_${parentPost.id}`,
456
+ metadata: {
457
+ votes_count: comment.votesCount,
458
+ post_id: parentPost.id,
459
+ post_name: parentPost.name,
460
+ },
461
+ };
462
+ }
463
+
464
+ // -------------------------------------------------------------------------
465
+ // Utilities
466
+ // -------------------------------------------------------------------------
467
+
468
+ private sleep(ms: number): Promise<void> {
469
+ return new Promise((resolve) => setTimeout(resolve, ms));
470
+ }
471
+ }