@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
+ * HackerNews Connector (V1 runtime)
3
+ *
4
+ * Searches Hacker News stories and comments via the Algolia HN Search API.
5
+ * No authentication required.
6
+ */
7
+
8
+ import TurndownService from 'turndown';
9
+ import {
10
+ type ActionContext,
11
+ type ActionResult,
12
+ type ConnectorDefinition,
13
+ ConnectorRuntime,
14
+ calculateEngagementScore,
15
+ type EventEnvelope,
16
+ type SyncContext,
17
+ type SyncResult,
18
+ } from '@lobu/connector-sdk';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Algolia HN API types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ interface AlgoliaHit {
25
+ objectID: string;
26
+ created_at: string;
27
+ created_at_i: number;
28
+ author: string;
29
+ title?: string;
30
+ story_text?: string;
31
+ comment_text?: string;
32
+ url?: string;
33
+ points?: number;
34
+ num_comments?: number;
35
+ story_id?: number;
36
+ parent_id?: number;
37
+ _tags: string[];
38
+ }
39
+
40
+ interface AlgoliaResponse {
41
+ hits: AlgoliaHit[];
42
+ nbHits: number;
43
+ page: number;
44
+ nbPages: number;
45
+ hitsPerPage: number;
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Content-type tag mapping
50
+ // ---------------------------------------------------------------------------
51
+
52
+ const CONTENT_TYPE_TAG: Record<string, string> = {
53
+ story: 'story',
54
+ comment: 'comment',
55
+ ask_hn: 'ask_hn',
56
+ show_hn: 'show_hn',
57
+ };
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Connector
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export default class HackerNewsConnector extends ConnectorRuntime {
64
+ readonly definition: ConnectorDefinition = {
65
+ key: 'hackernews',
66
+ name: 'Hacker News',
67
+ description: 'Searches Hacker News stories and comments via Algolia API.',
68
+ version: '1.0.0',
69
+ faviconDomain: 'news.ycombinator.com',
70
+ authSchema: {
71
+ methods: [{ type: 'none' }],
72
+ },
73
+ feeds: {
74
+ stories: {
75
+ key: 'stories',
76
+ name: 'Stories',
77
+ description: 'Search HN for stories, Ask HN, and Show HN posts.',
78
+ configSchema: {
79
+ type: 'object',
80
+ required: ['search_query'],
81
+ properties: {
82
+ search_query: {
83
+ type: 'string',
84
+ minLength: 1,
85
+ description: 'Search term',
86
+ },
87
+ story_type: {
88
+ type: 'string',
89
+ enum: ['story', 'ask_hn', 'show_hn'],
90
+ default: 'story',
91
+ description: 'Story type filter',
92
+ },
93
+ lookback_days: {
94
+ type: 'integer',
95
+ minimum: 1,
96
+ maximum: 730,
97
+ default: 365,
98
+ description: 'Lookback window in days',
99
+ },
100
+ search_fields: {
101
+ type: 'array',
102
+ items: {
103
+ type: 'string',
104
+ enum: ['title', 'url', 'story_text'],
105
+ },
106
+ default: ['title'],
107
+ description:
108
+ 'Algolia fields to search in. Defaults to title only. Add url and/or story_text for broader matching (may increase noise for common words like "notion" or "linear").',
109
+ },
110
+ },
111
+ },
112
+ eventKinds: {
113
+ story: {
114
+ description: 'A Hacker News story',
115
+ metadataSchema: {
116
+ type: 'object',
117
+ properties: {
118
+ story_type: { type: 'string', description: 'story, ask_hn, or show_hn' },
119
+ tags: { type: 'array', items: { type: 'string' } },
120
+ external_url: { type: 'string', format: 'uri' },
121
+ score: { type: 'number', description: 'HN points' },
122
+ reply_count: { type: 'number' },
123
+ },
124
+ },
125
+ },
126
+ ask_hn: {
127
+ description: 'An Ask HN post',
128
+ metadataSchema: {
129
+ type: 'object',
130
+ properties: {
131
+ story_type: { type: 'string' },
132
+ tags: { type: 'array', items: { type: 'string' } },
133
+ score: { type: 'number' },
134
+ reply_count: { type: 'number' },
135
+ },
136
+ },
137
+ },
138
+ show_hn: {
139
+ description: 'A Show HN post',
140
+ metadataSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ story_type: { type: 'string' },
144
+ tags: { type: 'array', items: { type: 'string' } },
145
+ external_url: { type: 'string', format: 'uri' },
146
+ score: { type: 'number' },
147
+ reply_count: { type: 'number' },
148
+ },
149
+ },
150
+ },
151
+ },
152
+ },
153
+ comments: {
154
+ key: 'comments',
155
+ name: 'Comments',
156
+ description: 'Search HN for comments.',
157
+ configSchema: {
158
+ type: 'object',
159
+ required: ['search_query'],
160
+ properties: {
161
+ search_query: {
162
+ type: 'string',
163
+ minLength: 1,
164
+ description: 'Search term',
165
+ },
166
+ lookback_days: {
167
+ type: 'integer',
168
+ minimum: 1,
169
+ maximum: 730,
170
+ default: 365,
171
+ description: 'Lookback window in days',
172
+ },
173
+ search_fields: {
174
+ type: 'array',
175
+ items: {
176
+ type: 'string',
177
+ enum: ['comment_text', 'author'],
178
+ },
179
+ default: ['comment_text'],
180
+ description: 'Algolia fields to search in. Defaults to comment_text.',
181
+ },
182
+ },
183
+ },
184
+ eventKinds: {
185
+ comment: {
186
+ description: 'A Hacker News comment',
187
+ metadataSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ story_id: { type: 'number' },
191
+ parent_id: { type: 'number' },
192
+ tags: { type: 'array', items: { type: 'string' } },
193
+ },
194
+ },
195
+ },
196
+ },
197
+ },
198
+ },
199
+ optionsSchema: {
200
+ type: 'object',
201
+ required: ['search_query'],
202
+ properties: {
203
+ search_query: {
204
+ type: 'string',
205
+ minLength: 1,
206
+ description: 'Search term',
207
+ },
208
+ lookback_days: {
209
+ type: 'integer',
210
+ minimum: 1,
211
+ maximum: 730,
212
+ default: 365,
213
+ description: 'Lookback window in days',
214
+ },
215
+ },
216
+ },
217
+ };
218
+
219
+ private readonly BASE_URL = 'https://hn.algolia.com/api/v1';
220
+ private readonly ENGAGEMENT_THRESHOLD = 50;
221
+ private readonly CONTENT_FETCH_TIMEOUT = 5000;
222
+ private readonly MAX_PAGES = 50;
223
+ private readonly PAGE_DELAY_MS = 1000;
224
+ private readonly FETCH_DELAY_MS = 2000;
225
+ private turndownService: TurndownService;
226
+
227
+ constructor() {
228
+ super();
229
+ this.turndownService = new TurndownService({
230
+ headingStyle: 'atx',
231
+ codeBlockStyle: 'fenced',
232
+ });
233
+ }
234
+
235
+ // -------------------------------------------------------------------------
236
+ // sync
237
+ // -------------------------------------------------------------------------
238
+
239
+ async sync(ctx: SyncContext): Promise<SyncResult> {
240
+ const searchQuery = ctx.config.search_query as string;
241
+ const contentType =
242
+ ctx.feedKey === 'comments' ? 'comment' : ((ctx.config.story_type as string) ?? 'story');
243
+ const lookbackDays = (ctx.config.lookback_days as number) ?? 365;
244
+ const searchFields =
245
+ (ctx.config.search_fields as string[] | undefined) ??
246
+ (contentType === 'comment' ? ['comment_text'] : ['title']);
247
+
248
+ const lookbackTimestamp = Math.floor((Date.now() - lookbackDays * 86400000) / 1000);
249
+ const tag = CONTENT_TYPE_TAG[contentType] ?? 'story';
250
+
251
+ const events: EventEnvelope[] = [];
252
+ let page = 0;
253
+ let hasMore = true;
254
+
255
+ while (hasMore && page < this.MAX_PAGES) {
256
+ const url =
257
+ `${this.BASE_URL}/search?query=${encodeURIComponent(searchQuery)}` +
258
+ `&tags=${tag}&hitsPerPage=100&page=${page}` +
259
+ '&typoTolerance=false' +
260
+ `&restrictSearchableAttributes=${encodeURIComponent(searchFields.join(','))}` +
261
+ `&numericFilters=${encodeURIComponent(`created_at_i>${lookbackTimestamp}`)}`;
262
+
263
+ const response = await fetch(url);
264
+ if (!response.ok) {
265
+ throw new Error(`Algolia API error (${response.status}): ${await response.text()}`);
266
+ }
267
+
268
+ const data = (await response.json()) as AlgoliaResponse;
269
+
270
+ for (const hit of data.hits) {
271
+ if (contentType === 'comment') {
272
+ const event = this.transformComment(hit);
273
+ if (event) events.push(event);
274
+ } else {
275
+ events.push(this.transformStory(hit));
276
+ }
277
+ }
278
+
279
+ hasMore = data.page < data.nbPages - 1 && data.hits.length > 0;
280
+ page++;
281
+
282
+ if (hasMore) {
283
+ await this.sleep(this.PAGE_DELAY_MS);
284
+ }
285
+ }
286
+
287
+ // Enrich high-engagement stories with external content
288
+ if (contentType !== 'comment') {
289
+ await this.enrichStoriesWithExternalContent(events);
290
+ }
291
+
292
+ return {
293
+ events,
294
+ checkpoint: { last_sync_at: new Date().toISOString() },
295
+ metadata: { items_found: events.length },
296
+ };
297
+ }
298
+
299
+ // -------------------------------------------------------------------------
300
+ // execute
301
+ // -------------------------------------------------------------------------
302
+
303
+ async execute(_ctx: ActionContext): Promise<ActionResult> {
304
+ return { success: false, error: 'Actions not supported' };
305
+ }
306
+
307
+ // -------------------------------------------------------------------------
308
+ // Transform helpers
309
+ // -------------------------------------------------------------------------
310
+
311
+ private transformStory(hit: AlgoliaHit): EventEnvelope {
312
+ const isAskHN = hit._tags.includes('ask_hn');
313
+ const isShowHN = hit._tags.includes('show_hn');
314
+
315
+ let storyType = 'story';
316
+ let originType = 'story';
317
+ if (isAskHN) {
318
+ storyType = 'ask_hn';
319
+ originType = 'ask_hn';
320
+ } else if (isShowHN) {
321
+ storyType = 'show_hn';
322
+ originType = 'show_hn';
323
+ }
324
+
325
+ const engagementData = {
326
+ score: hit.points ?? 0,
327
+ reply_count: hit.num_comments ?? 0,
328
+ };
329
+
330
+ return {
331
+ origin_id: `hn_story_${hit.objectID}`,
332
+ title: hit.title ?? '',
333
+ payload_text: (hit.story_text ?? '').trim(),
334
+ author_name: hit.author,
335
+ source_url: `https://news.ycombinator.com/item?id=${hit.objectID}`,
336
+ occurred_at: new Date(hit.created_at_i * 1000),
337
+ origin_type: originType,
338
+ score: calculateEngagementScore('hackernews', engagementData),
339
+ metadata: {
340
+ type: 'story',
341
+ story_type: storyType,
342
+ tags: hit._tags,
343
+ external_url: hit.url,
344
+ created_at_i: hit.created_at_i,
345
+ score: hit.points ?? 0,
346
+ reply_count: hit.num_comments ?? 0,
347
+ },
348
+ };
349
+ }
350
+
351
+ private transformComment(hit: AlgoliaHit): EventEnvelope | null {
352
+ let parentExternalId: string | undefined;
353
+ if (hit.parent_id != null && hit.story_id != null && hit.parent_id !== hit.story_id) {
354
+ parentExternalId = `hn_comment_${hit.parent_id}`;
355
+ } else if (hit.story_id != null) {
356
+ parentExternalId = `hn_story_${hit.story_id}`;
357
+ }
358
+
359
+ if (!hit.comment_text) return null;
360
+
361
+ return {
362
+ origin_id: `hn_comment_${hit.objectID}`,
363
+ payload_text: hit.comment_text,
364
+ author_name: hit.author,
365
+ source_url: `https://news.ycombinator.com/item?id=${hit.objectID}`,
366
+ occurred_at: new Date(hit.created_at_i * 1000),
367
+ origin_type: 'comment',
368
+ score: calculateEngagementScore('hackernews', { score: 0 }),
369
+ origin_parent_id: parentExternalId,
370
+ metadata: {
371
+ type: 'comment',
372
+ story_id: hit.story_id,
373
+ parent_id: hit.parent_id,
374
+ created_at_i: hit.created_at_i,
375
+ tags: hit._tags,
376
+ },
377
+ };
378
+ }
379
+
380
+ // -------------------------------------------------------------------------
381
+ // External content enrichment
382
+ // -------------------------------------------------------------------------
383
+
384
+ private async enrichStoriesWithExternalContent(events: EventEnvelope[]): Promise<void> {
385
+ for (const event of events) {
386
+ const externalUrl = event.metadata?.external_url as string | undefined;
387
+ const points = event.metadata?.score as number | undefined;
388
+
389
+ if (!event.content && externalUrl && points != null && points >= this.ENGAGEMENT_THRESHOLD) {
390
+ const fetched = await this.fetchExternalContent(externalUrl);
391
+ if (fetched) {
392
+ event.content = fetched;
393
+ event.metadata = {
394
+ ...event.metadata,
395
+ fetched_content: true,
396
+ original_url: externalUrl,
397
+ };
398
+ }
399
+
400
+ await this.sleep(this.FETCH_DELAY_MS);
401
+ }
402
+ }
403
+ }
404
+
405
+ private async fetchExternalContent(url: string): Promise<string | null> {
406
+ try {
407
+ const controller = new AbortController();
408
+ const timeoutId = setTimeout(() => controller.abort(), this.CONTENT_FETCH_TIMEOUT);
409
+
410
+ const response = await fetch(url, {
411
+ signal: controller.signal,
412
+ headers: {
413
+ 'User-Agent': 'Mozilla/5.0 (compatible; HNBot/1.0)',
414
+ Accept: 'text/html,application/xhtml+xml',
415
+ },
416
+ });
417
+
418
+ clearTimeout(timeoutId);
419
+
420
+ if (!response.ok) return null;
421
+
422
+ const contentType = response.headers.get('content-type') ?? '';
423
+ if (!contentType.includes('text/html')) return null;
424
+
425
+ const html = await response.text();
426
+
427
+ // Strip non-article elements
428
+ const stripTags = [
429
+ 'script',
430
+ 'style',
431
+ 'noscript',
432
+ 'nav',
433
+ 'header',
434
+ 'footer',
435
+ 'aside',
436
+ 'iframe',
437
+ 'svg',
438
+ 'canvas',
439
+ 'video',
440
+ 'audio',
441
+ 'menu',
442
+ 'dialog',
443
+ 'embed',
444
+ 'object',
445
+ ];
446
+ let cleanHtml = html;
447
+ for (const tag of stripTags) {
448
+ cleanHtml = cleanHtml.replace(
449
+ new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, 'gi'),
450
+ ''
451
+ );
452
+ }
453
+ cleanHtml = cleanHtml.replace(/<(link|meta|input)\b[^>]*\/?>/gi, '');
454
+
455
+ const markdown = this.turndownService.turndown(cleanHtml);
456
+ const trimmed = markdown.trim().substring(0, 2000);
457
+
458
+ return trimmed.length >= 100 ? trimmed : null;
459
+ } catch {
460
+ return null;
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
+ }
@@ -0,0 +1,23 @@
1
+ export * from './browser-scraper-utils.ts';
2
+ export * from './capterra.ts';
3
+ export * from './g2.ts';
4
+ export * from './github.ts';
5
+ export * from './glassdoor.ts';
6
+ export * from './gmaps.ts';
7
+ export * from './google_calendar.ts';
8
+ export * from './google_gmail.ts';
9
+ export * from './google_photos.ts';
10
+ export * from './google_play.ts';
11
+ export * from './hackernews.ts';
12
+ export * from './ios_appstore.ts';
13
+ export * from './linkedin.ts';
14
+ export * from './microsoft_outlook.ts';
15
+ export * from './producthunt.ts';
16
+ export * from './reddit.ts';
17
+ export * from './rss.ts';
18
+ export * from './spotify.ts';
19
+ export * from './trustpilot.ts';
20
+ export * from './website.ts';
21
+ export * from './whatsapp.ts';
22
+ export * from './x.ts';
23
+ export * from './youtube.ts';