@lobu/cli 6.0.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) 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 +38 -0
  4. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  5. package/dist/commands/_lib/apply/apply-cmd.js +574 -40
  6. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  7. package/dist/commands/_lib/apply/client.d.ts +180 -1
  8. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  9. package/dist/commands/_lib/apply/client.js +308 -28
  10. package/dist/commands/_lib/apply/client.js.map +1 -1
  11. package/dist/commands/_lib/apply/desired-state.d.ts +134 -3
  12. package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
  13. package/dist/commands/_lib/apply/desired-state.js +703 -89
  14. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  15. package/dist/commands/_lib/apply/diff.d.ts +61 -3
  16. package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
  17. package/dist/commands/_lib/apply/diff.js +382 -92
  18. package/dist/commands/_lib/apply/diff.js.map +1 -1
  19. package/dist/commands/_lib/apply/prompt.d.ts +6 -0
  20. package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
  21. package/dist/commands/_lib/apply/prompt.js +16 -0
  22. package/dist/commands/_lib/apply/prompt.js.map +1 -1
  23. package/dist/commands/_lib/apply/render.d.ts +9 -0
  24. package/dist/commands/_lib/apply/render.d.ts.map +1 -1
  25. package/dist/commands/_lib/apply/render.js +80 -3
  26. package/dist/commands/_lib/apply/render.js.map +1 -1
  27. package/dist/commands/agent.d.ts +7 -0
  28. package/dist/commands/agent.d.ts.map +1 -1
  29. package/dist/commands/agent.js +65 -1
  30. package/dist/commands/agent.js.map +1 -1
  31. package/dist/commands/chat.d.ts +12 -9
  32. package/dist/commands/chat.d.ts.map +1 -1
  33. package/dist/commands/chat.js +125 -57
  34. package/dist/commands/chat.js.map +1 -1
  35. package/dist/commands/dev.d.ts +23 -7
  36. package/dist/commands/dev.d.ts.map +1 -1
  37. package/dist/commands/dev.js +197 -49
  38. package/dist/commands/dev.js.map +1 -1
  39. package/dist/commands/doctor.d.ts +1 -0
  40. package/dist/commands/doctor.d.ts.map +1 -1
  41. package/dist/commands/doctor.js +136 -0
  42. package/dist/commands/doctor.js.map +1 -1
  43. package/dist/commands/eval.d.ts +8 -0
  44. package/dist/commands/eval.d.ts.map +1 -1
  45. package/dist/commands/eval.js +72 -6
  46. package/dist/commands/eval.js.map +1 -1
  47. package/dist/commands/init.d.ts +22 -5
  48. package/dist/commands/init.d.ts.map +1 -1
  49. package/dist/commands/init.js +355 -182
  50. package/dist/commands/init.js.map +1 -1
  51. package/dist/commands/link.d.ts +11 -0
  52. package/dist/commands/link.d.ts.map +1 -0
  53. package/dist/commands/link.js +28 -0
  54. package/dist/commands/link.js.map +1 -0
  55. package/dist/commands/login.d.ts.map +1 -1
  56. package/dist/commands/login.js +14 -2
  57. package/dist/commands/login.js.map +1 -1
  58. package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
  59. package/dist/commands/memory/_lib/browser-auth-cmd.js +3 -3
  60. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  61. package/dist/commands/memory/_lib/mcp.d.ts +2 -2
  62. package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
  63. package/dist/commands/memory/_lib/mcp.js +24 -12
  64. package/dist/commands/memory/_lib/mcp.js.map +1 -1
  65. package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
  66. package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
  67. package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
  68. package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
  69. package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
  70. package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
  71. package/dist/commands/memory/_lib/schema.d.ts +29 -2
  72. package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
  73. package/dist/commands/memory/_lib/schema.js +121 -5
  74. package/dist/commands/memory/_lib/schema.js.map +1 -1
  75. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  76. package/dist/commands/memory/_lib/seed-cmd.js +46 -24
  77. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  78. package/dist/commands/memory/run.d.ts.map +1 -1
  79. package/dist/commands/memory/run.js +2 -2
  80. package/dist/commands/memory/run.js.map +1 -1
  81. package/dist/commands/org.d.ts +4 -0
  82. package/dist/commands/org.d.ts.map +1 -1
  83. package/dist/commands/org.js +10 -0
  84. package/dist/commands/org.js.map +1 -1
  85. package/dist/commands/platforms/platform-prompts.d.ts +0 -1
  86. package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
  87. package/dist/commands/platforms/platform-prompts.js +54 -8
  88. package/dist/commands/platforms/platform-prompts.js.map +1 -1
  89. package/dist/commands/telemetry.d.ts +10 -0
  90. package/dist/commands/telemetry.d.ts.map +1 -0
  91. package/dist/commands/telemetry.js +68 -0
  92. package/dist/commands/telemetry.js.map +1 -0
  93. package/dist/commands/token.d.ts +9 -0
  94. package/dist/commands/token.d.ts.map +1 -1
  95. package/dist/commands/token.js +54 -0
  96. package/dist/commands/token.js.map +1 -1
  97. package/dist/commands/whoami.d.ts.map +1 -1
  98. package/dist/commands/whoami.js +1 -1
  99. package/dist/commands/whoami.js.map +1 -1
  100. package/dist/connectors/README.md +534 -0
  101. package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
  102. package/dist/connectors/apple_health.ts +138 -0
  103. package/dist/connectors/apple_screen_time.ts +82 -0
  104. package/dist/connectors/browser-scraper-utils.ts +246 -0
  105. package/dist/connectors/capterra.ts +277 -0
  106. package/dist/connectors/g2.ts +290 -0
  107. package/dist/connectors/github.ts +1530 -0
  108. package/dist/connectors/glassdoor.ts +295 -0
  109. package/dist/connectors/gmaps.ts +197 -0
  110. package/dist/connectors/google_calendar.ts +641 -0
  111. package/dist/connectors/google_gmail.ts +754 -0
  112. package/dist/connectors/google_photos.ts +776 -0
  113. package/dist/connectors/google_play.ts +349 -0
  114. package/dist/connectors/hackernews.ts +471 -0
  115. package/dist/connectors/index.ts +28 -0
  116. package/dist/connectors/ios_appstore.ts +226 -0
  117. package/dist/connectors/linkedin.ts +494 -0
  118. package/dist/connectors/local_directory.ts +91 -0
  119. package/dist/connectors/microsoft_outlook.ts +410 -0
  120. package/dist/connectors/producthunt.ts +471 -0
  121. package/dist/connectors/reddit.ts +600 -0
  122. package/dist/connectors/revolut.ts +572 -0
  123. package/dist/connectors/rss.ts +448 -0
  124. package/dist/connectors/spotify.ts +590 -0
  125. package/dist/connectors/trustpilot.ts +203 -0
  126. package/dist/connectors/website.ts +629 -0
  127. package/dist/connectors/whatsapp.ts +1081 -0
  128. package/dist/connectors/whatsapp_local.ts +125 -0
  129. package/dist/connectors/x.ts +536 -0
  130. package/dist/connectors/youtube.ts +666 -0
  131. package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
  132. package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
  133. package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
  134. package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
  135. package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
  136. package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
  137. package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
  138. package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
  139. package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
  140. package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
  141. package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
  142. package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
  143. package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
  144. package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
  145. package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
  146. package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
  147. package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
  148. package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
  149. package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
  150. package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
  151. package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
  152. package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
  153. package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
  154. package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
  155. package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
  156. package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
  157. package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
  158. package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
  159. package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
  160. package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
  161. package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
  162. package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
  163. package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
  164. package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
  165. package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
  166. package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
  167. package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
  168. package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
  169. package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
  170. package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
  171. package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
  172. package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
  173. package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
  174. package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
  175. package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
  176. package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
  177. package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
  178. package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
  179. package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
  180. package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
  181. package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
  182. package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
  183. package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
  184. package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
  185. package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
  186. package/dist/eval/types.d.ts +2 -0
  187. package/dist/eval/types.d.ts.map +1 -1
  188. package/dist/index.d.ts +11 -0
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +210 -132
  191. package/dist/index.js.map +1 -1
  192. package/dist/internal/api-client.d.ts +4 -8
  193. package/dist/internal/api-client.d.ts.map +1 -1
  194. package/dist/internal/api-client.js +1 -1
  195. package/dist/internal/api-client.js.map +1 -1
  196. package/dist/internal/context.js +2 -2
  197. package/dist/internal/context.js.map +1 -1
  198. package/dist/internal/credentials.d.ts.map +1 -1
  199. package/dist/internal/credentials.js +6 -1
  200. package/dist/internal/credentials.js.map +1 -1
  201. package/dist/internal/gateway-url.d.ts +14 -0
  202. package/dist/internal/gateway-url.d.ts.map +1 -1
  203. package/dist/internal/gateway-url.js +19 -0
  204. package/dist/internal/gateway-url.js.map +1 -1
  205. package/dist/internal/index.d.ts +3 -4
  206. package/dist/internal/index.d.ts.map +1 -1
  207. package/dist/internal/index.js +3 -3
  208. package/dist/internal/index.js.map +1 -1
  209. package/dist/internal/oauth.d.ts +6 -5
  210. package/dist/internal/oauth.d.ts.map +1 -1
  211. package/dist/internal/oauth.js +2 -2
  212. package/dist/internal/project-link.d.ts +10 -0
  213. package/dist/internal/project-link.d.ts.map +1 -0
  214. package/dist/internal/project-link.js +48 -0
  215. package/dist/internal/project-link.js.map +1 -0
  216. package/dist/providers.json +2 -2
  217. package/dist/server.bundle.mjs +31654 -30866
  218. package/dist/start-local.bundle.mjs +74409 -0
  219. package/dist/templates/README.md.tmpl +10 -11
  220. package/dist/templates/TESTING.md.tmpl +9 -9
  221. package/package.json +15 -13
  222. package/dist/__tests__/chat.integration.test.d.ts +0 -2
  223. package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
  224. package/dist/__tests__/chat.integration.test.js +0 -337
  225. package/dist/__tests__/chat.integration.test.js.map +0 -1
  226. package/dist/__tests__/dev.test.d.ts +0 -2
  227. package/dist/__tests__/dev.test.d.ts.map +0 -1
  228. package/dist/__tests__/dev.test.js +0 -25
  229. package/dist/__tests__/dev.test.js.map +0 -1
  230. package/dist/__tests__/init-memory.test.d.ts +0 -2
  231. package/dist/__tests__/init-memory.test.d.ts.map +0 -1
  232. package/dist/__tests__/init-memory.test.js +0 -45
  233. package/dist/__tests__/init-memory.test.js.map +0 -1
  234. package/dist/__tests__/token.test.d.ts +0 -2
  235. package/dist/__tests__/token.test.d.ts.map +0 -1
  236. package/dist/__tests__/token.test.js +0 -52
  237. package/dist/__tests__/token.test.js.map +0 -1
  238. package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
  239. package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
  240. package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
  241. package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
  242. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
  243. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
  244. package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
  245. package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
  246. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
  247. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
  248. package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
  249. package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
  250. package/dist/commands/apply.d.ts +0 -3
  251. package/dist/commands/apply.d.ts.map +0 -1
  252. package/dist/commands/apply.js +0 -5
  253. package/dist/commands/apply.js.map +0 -1
  254. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
  255. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
  256. package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
  257. package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
  258. package/dist/internal/__tests__/api-client.test.d.ts +0 -2
  259. package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
  260. package/dist/internal/__tests__/api-client.test.js +0 -95
  261. package/dist/internal/__tests__/api-client.test.js.map +0 -1
  262. package/dist/internal/__tests__/context.test.d.ts +0 -2
  263. package/dist/internal/__tests__/context.test.d.ts.map +0 -1
  264. package/dist/internal/__tests__/context.test.js +0 -77
  265. package/dist/internal/__tests__/context.test.js.map +0 -1
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Capterra Connector
3
+ * Scrapes software reviews from Capterra using browser rendering with stealth mode.
4
+ */
5
+
6
+ import {
7
+ type ActionContext,
8
+ type ActionResult,
9
+ type ConnectorDefinition,
10
+ ConnectorRuntime,
11
+ calculateEngagementScore,
12
+ type EventEnvelope,
13
+ type SyncContext,
14
+ type SyncResult,
15
+ } from '@lobu/connector-sdk';
16
+ import {
17
+ getBrowserCdpUrl,
18
+ getBrowserUserDataDir,
19
+ handleCookieConsent,
20
+ openStealthBrowser,
21
+ validateUrlDomain,
22
+ withBrowserErrorCapture,
23
+ } from './browser-scraper-utils.ts';
24
+
25
+ interface CapterraReview {
26
+ id: string;
27
+ rating: number;
28
+ title: string;
29
+ text: string;
30
+ date: string;
31
+ author: string;
32
+ helpfulCount: number;
33
+ }
34
+
35
+ export default class CapterraConnector extends ConnectorRuntime {
36
+ readonly definition: ConnectorDefinition = {
37
+ key: 'capterra',
38
+ name: 'Capterra',
39
+ version: '1.0.0',
40
+ faviconDomain: 'capterra.com',
41
+ description: 'Scrapes software reviews from Capterra.',
42
+ authSchema: {
43
+ methods: [{ type: 'none' }],
44
+ },
45
+ feeds: {
46
+ reviews: {
47
+ key: 'reviews',
48
+ name: 'Reviews',
49
+ description: 'Capterra software reviews',
50
+ configSchema: {
51
+ type: 'object',
52
+ required: ['product_id'],
53
+ properties: {
54
+ product_id: {
55
+ type: 'string',
56
+ description: 'Capterra product ID (e.g., "12345")',
57
+ minLength: 1,
58
+ },
59
+ product_name: {
60
+ type: 'string',
61
+ description:
62
+ 'Product name slug for URL (e.g., "spotify"). Optional - Capterra will redirect without it.',
63
+ minLength: 1,
64
+ },
65
+ vendor_name: {
66
+ type: 'string',
67
+ description:
68
+ 'Vendor/company name (e.g., "Spotify AB"). Optional but recommended for disambiguation.',
69
+ minLength: 1,
70
+ },
71
+ lookback_days: {
72
+ type: 'integer',
73
+ description:
74
+ 'Number of days to look back for historical data. Default: 365 (1 year). Maximum: 730 (2 years).',
75
+ minimum: 1,
76
+ maximum: 730,
77
+ default: 365,
78
+ },
79
+ },
80
+ },
81
+ eventKinds: {
82
+ review: {
83
+ description: 'A Capterra software review',
84
+ metadataSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ rating: { type: 'number', description: 'Star rating (0-5)' },
88
+ helpful_count: { type: 'number', description: 'Number of helpful votes' },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ },
94
+ },
95
+ optionsSchema: {
96
+ type: 'object',
97
+ required: ['product_id'],
98
+ properties: {
99
+ product_id: {
100
+ type: 'string',
101
+ description: 'Capterra product ID (e.g., "12345")',
102
+ minLength: 1,
103
+ },
104
+ product_name: {
105
+ type: 'string',
106
+ description:
107
+ 'Product name slug for URL (e.g., "spotify"). Optional - Capterra will redirect without it.',
108
+ minLength: 1,
109
+ },
110
+ vendor_name: {
111
+ type: 'string',
112
+ description:
113
+ 'Vendor/company name (e.g., "Spotify AB"). Optional but recommended for disambiguation.',
114
+ minLength: 1,
115
+ },
116
+ lookback_days: {
117
+ type: 'integer',
118
+ description:
119
+ 'Number of days to look back for historical data. Default: 365 (1 year). Maximum: 730 (2 years).',
120
+ minimum: 1,
121
+ maximum: 730,
122
+ default: 365,
123
+ },
124
+ },
125
+ },
126
+ };
127
+
128
+ async sync(ctx: SyncContext): Promise<SyncResult> {
129
+ const productId = ctx.config.product_id as string;
130
+ const productName = ctx.config.product_name as string | undefined;
131
+
132
+ const baseUrl = productName
133
+ ? `https://www.capterra.com/p/${productId}/${productName}/reviews`
134
+ : `https://www.capterra.com/p/${productId}/reviews`;
135
+ validateUrlDomain(baseUrl, 'capterra.com');
136
+
137
+ const userDataDir = getBrowserUserDataDir(ctx.sessionState);
138
+ const cdpUrl = getBrowserCdpUrl(ctx.sessionState) ?? 'auto';
139
+ const session = await openStealthBrowser({ cdpUrl, userDataDir });
140
+
141
+ return withBrowserErrorCapture(session, 'capterra-sync', async (page) => {
142
+ await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
143
+
144
+ await handleCookieConsent(page, '[data-test="cookie-accept"], #onetrust-accept-btn-handler');
145
+
146
+ // Wait for review cards
147
+ try {
148
+ await page.waitForSelector('[data-test="review-card"], .review-card', { timeout: 10000 });
149
+ } catch {
150
+ // Review selectors not found - page structure may have changed
151
+ }
152
+
153
+ // Extract reviews using page.evaluate
154
+ const rawReviews: CapterraReview[] = await page.evaluate(() => {
155
+ const reviewElements = Array.from(
156
+ document.querySelectorAll('[data-test="review-card"], .review-card')
157
+ );
158
+
159
+ return reviewElements.map((el: Element, index: number) => {
160
+ // Extract rating (usually shown as stars)
161
+ const ratingElement = el.querySelector(
162
+ '[data-test="rating"], .rating, [aria-label*="star"]'
163
+ );
164
+ let rating = 0;
165
+ if (ratingElement) {
166
+ const ariaLabel = ratingElement.getAttribute('aria-label');
167
+ const ratingMatch = ariaLabel?.match(/(\d+(?:\.\d+)?)/);
168
+ rating = ratingMatch ? parseFloat(ratingMatch[1]) : 0;
169
+ }
170
+
171
+ // Extract review title
172
+ const titleElement = el.querySelector(
173
+ '[data-test="review-title"], .review-title, h3, h4'
174
+ );
175
+ const title = titleElement?.textContent?.trim() || '';
176
+
177
+ // Extract review text
178
+ const textElement = el.querySelector(
179
+ '[data-test="review-body"], .review-body, .review-content, .review-text'
180
+ );
181
+ const text = textElement?.textContent?.trim() || '';
182
+
183
+ // Extract date
184
+ const dateElement = el.querySelector('[data-test="review-date"], .review-date, time');
185
+ const dateText =
186
+ dateElement?.textContent?.trim() || dateElement?.getAttribute('datetime') || '';
187
+
188
+ // Parse relative dates like "2 weeks ago"
189
+ let date = new Date();
190
+ if (dateText) {
191
+ const weeksMatch = dateText.match(/(\d+)\s+weeks?\s+ago/i);
192
+ const monthsMatch = dateText.match(/(\d+)\s+months?\s+ago/i);
193
+ const daysMatch = dateText.match(/(\d+)\s+days?\s+ago/i);
194
+
195
+ if (weeksMatch) {
196
+ date = new Date(Date.now() - parseInt(weeksMatch[1], 10) * 7 * 24 * 60 * 60 * 1000);
197
+ } else if (monthsMatch) {
198
+ date = new Date(Date.now() - parseInt(monthsMatch[1], 10) * 30 * 24 * 60 * 60 * 1000);
199
+ } else if (daysMatch) {
200
+ date = new Date(Date.now() - parseInt(daysMatch[1], 10) * 24 * 60 * 60 * 1000);
201
+ } else {
202
+ // Try parsing as date
203
+ const parsed = new Date(dateText);
204
+ if (!Number.isNaN(parsed.getTime())) {
205
+ date = parsed;
206
+ }
207
+ }
208
+ }
209
+
210
+ // Extract author
211
+ const authorElement = el.querySelector(
212
+ '[data-test="reviewer-name"], .reviewer-name, .author'
213
+ );
214
+ const author = authorElement?.textContent?.trim() || 'Anonymous';
215
+
216
+ // Extract review ID from data attributes or generate
217
+ const reviewId =
218
+ (el as HTMLElement).getAttribute('data-review-id') ||
219
+ (el as HTMLElement).id ||
220
+ `${date.toISOString()}_${index}`.replace(/[^a-zA-Z0-9]/g, '_');
221
+
222
+ // Extract helpful count
223
+ const helpfulElement = el.querySelector('[data-test="helpful-count"], .helpful-count');
224
+ const helpfulCount = helpfulElement
225
+ ? parseInt(helpfulElement.textContent?.replace(/\D/g, '') || '0', 10)
226
+ : 0;
227
+
228
+ return {
229
+ id: reviewId,
230
+ rating,
231
+ title,
232
+ text,
233
+ date: date.toISOString(),
234
+ author,
235
+ helpfulCount,
236
+ };
237
+ });
238
+ });
239
+
240
+ // Filter reviews with content
241
+ const reviews = rawReviews.filter((r) => r.text.length > 0);
242
+
243
+ // Transform to EventEnvelope
244
+ const events: EventEnvelope[] = reviews.map((review) => {
245
+ const engagementData = {
246
+ rating: review.rating,
247
+ helpful_count: review.helpfulCount,
248
+ };
249
+
250
+ return {
251
+ origin_id: review.id,
252
+ title: review.title,
253
+ payload_text: review.text,
254
+ author_name: review.author,
255
+ occurred_at: new Date(review.date),
256
+ origin_type: 'review',
257
+ score: calculateEngagementScore('capterra', engagementData),
258
+ source_url: baseUrl,
259
+ metadata: engagementData,
260
+ };
261
+ });
262
+
263
+ return {
264
+ events,
265
+ checkpoint: { last_sync_at: new Date().toISOString() },
266
+ metadata: {
267
+ items_found: rawReviews.length,
268
+ items_skipped: rawReviews.length - reviews.length,
269
+ },
270
+ };
271
+ });
272
+ }
273
+
274
+ async execute(_ctx: ActionContext): Promise<ActionResult> {
275
+ return { success: false, error: 'Actions not supported' };
276
+ }
277
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * G2 Connector (V1 runtime)
3
+ *
4
+ * Scrapes B2B software reviews from G2.com using browser rendering with stealth mode.
5
+ */
6
+
7
+ import {
8
+ type ActionContext,
9
+ type ActionResult,
10
+ type ConnectorDefinition,
11
+ ConnectorRuntime,
12
+ calculateEngagementScore,
13
+ type EventEnvelope,
14
+ type SyncContext,
15
+ type SyncResult,
16
+ } from '@lobu/connector-sdk';
17
+ import {
18
+ getBrowserCdpUrl,
19
+ getBrowserUserDataDir,
20
+ handleCookieConsent,
21
+ openStealthBrowser,
22
+ validateUrlDomain,
23
+ withBrowserErrorCapture,
24
+ } from './browser-scraper-utils.ts';
25
+
26
+ interface G2Review {
27
+ rating: number;
28
+ title: string;
29
+ text: string;
30
+ author: string;
31
+ jobTitle: string;
32
+ industry: string;
33
+ companySize: string;
34
+ date: string;
35
+ badges: string[];
36
+ reviewUrl: string;
37
+ helpfulCount: number;
38
+ }
39
+
40
+ interface G2Checkpoint {
41
+ last_sync_at?: string;
42
+ pages_crawled?: number;
43
+ }
44
+
45
+ const configSchema = {
46
+ type: 'object',
47
+ required: ['product_url'],
48
+ properties: {
49
+ product_url: {
50
+ type: 'string',
51
+ description: 'Full G2 product review URL e.g. https://www.g2.com/products/confluence/reviews',
52
+ },
53
+ lookback_days: {
54
+ type: 'integer',
55
+ minimum: 1,
56
+ maximum: 730,
57
+ default: 365,
58
+ description: 'Number of days to look back for reviews (default 365)',
59
+ },
60
+ },
61
+ };
62
+
63
+ export default class G2Connector extends ConnectorRuntime {
64
+ readonly definition: ConnectorDefinition = {
65
+ key: 'g2',
66
+ name: 'G2',
67
+ description: 'Scrapes B2B software reviews from G2.com.',
68
+ version: '1.0.0',
69
+ faviconDomain: 'g2.com',
70
+ authSchema: {
71
+ methods: [{ type: 'none' }],
72
+ },
73
+ feeds: {
74
+ reviews: {
75
+ key: 'reviews',
76
+ name: 'Product Reviews',
77
+ description: 'Scrape reviews for a G2 product listing.',
78
+ configSchema,
79
+ eventKinds: {
80
+ review: {
81
+ description: 'A G2 B2B software review',
82
+ metadataSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ rating: { type: 'number', description: 'Star rating (0-5)' },
86
+ helpful_count: { type: 'number' },
87
+ job_title: { type: 'string', description: 'Reviewer job title' },
88
+ industry: { type: 'string', description: 'Reviewer industry' },
89
+ company_size: { type: 'string', description: 'Reviewer company size' },
90
+ badges: { type: 'array', items: { type: 'string' }, description: 'Review badges' },
91
+ },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ optionsSchema: configSchema,
98
+ };
99
+
100
+ async sync(ctx: SyncContext): Promise<SyncResult> {
101
+ const productUrl = ctx.config.product_url as string;
102
+
103
+ if (!productUrl?.match(/^https:\/\/www\.g2\.com\/products\/[^/]+\/reviews/)) {
104
+ return {
105
+ events: [],
106
+ checkpoint: ctx.checkpoint,
107
+ metadata: { items_found: 0, error: 'Invalid product_url' },
108
+ };
109
+ }
110
+ validateUrlDomain(productUrl, 'g2.com');
111
+
112
+ // Extract product key from URL for origin_id generation
113
+ const productMatch = productUrl.match(/\/products\/([^/]+)/);
114
+ const productKey = productMatch ? productMatch[1] : 'unknown';
115
+
116
+ const baseUrl = productUrl;
117
+ const allEvents: EventEnvelope[] = [];
118
+
119
+ const userDataDir = getBrowserUserDataDir(ctx.sessionState);
120
+ const cdpUrl = getBrowserCdpUrl(ctx.sessionState) ?? 'auto';
121
+ const session = await openStealthBrowser({ cdpUrl, userDataDir });
122
+
123
+ return withBrowserErrorCapture(session, 'g2-sync', async (page) => {
124
+ const maxPages = 5;
125
+
126
+ for (let pageNum = 1; pageNum <= maxPages; pageNum++) {
127
+ const pageUrl = pageNum === 1 ? baseUrl : `${baseUrl}?page=${pageNum}`;
128
+
129
+ await page.goto(pageUrl, { waitUntil: 'domcontentloaded' });
130
+
131
+ if (pageNum === 1) {
132
+ await handleCookieConsent(page, '#onetrust-accept-btn-handler');
133
+ }
134
+
135
+ // Wait for review cards to load
136
+ try {
137
+ await page.waitForSelector('[itemprop="review"]', { timeout: 10000 });
138
+ } catch {
139
+ // No reviews found on this page — stop paginating
140
+ break;
141
+ }
142
+
143
+ // Extract reviews from the page
144
+ const reviews: G2Review[] = await page.evaluate(() => {
145
+ const results: G2Review[] = [];
146
+ const reviewCards = document.querySelectorAll('[itemprop="review"]');
147
+
148
+ reviewCards.forEach((card) => {
149
+ try {
150
+ // Extract author name from meta tag
151
+ const authorMeta = card.querySelector('[itemprop="author"] meta[itemprop="name"]');
152
+ const author = authorMeta?.getAttribute('content') || 'Anonymous';
153
+
154
+ // Extract author details from sibling divs with elv-text-subtle class
155
+ const authorContainer = card.querySelector('[itemprop="author"]');
156
+ const parentDiv = authorContainer?.closest('.elv-gap-2')?.parentElement;
157
+ const detailDivs = parentDiv
158
+ ? Array.from(parentDiv.querySelectorAll('.elv-text-subtle'))
159
+ : [];
160
+
161
+ // Parse author details (job title, industry, company size)
162
+ let jobTitle = '';
163
+ let industry = '';
164
+ let companySize = '';
165
+
166
+ if (detailDivs.length >= 3) {
167
+ jobTitle = detailDivs[0]?.textContent?.trim() || '';
168
+ industry = detailDivs[1]?.textContent?.trim() || '';
169
+ companySize = detailDivs[2]?.textContent?.trim() || '';
170
+ } else if (detailDivs.length === 2) {
171
+ jobTitle = detailDivs[0]?.textContent?.trim() || '';
172
+ companySize = detailDivs[1]?.textContent?.trim() || '';
173
+ } else if (detailDivs.length === 1) {
174
+ companySize = detailDivs[0]?.textContent?.trim() || '';
175
+ }
176
+
177
+ // Extract date from meta tag
178
+ const dateMeta = card.querySelector('meta[itemprop="datePublished"]');
179
+ const dateStr = dateMeta?.getAttribute('content') || '';
180
+
181
+ // Extract rating
182
+ const ratingMeta = card.querySelector('[itemprop="ratingValue"]');
183
+ const rating = ratingMeta ? parseFloat(ratingMeta.getAttribute('content') || '0') : 0;
184
+
185
+ // Extract review title
186
+ const titleDiv = card.querySelector('[itemprop="name"] .elv-font-bold');
187
+ const title = titleDiv?.textContent?.trim().replace(/^"|"$/g, '') || '';
188
+
189
+ // Extract review body - use innerText to preserve visual spacing/newlines
190
+ const reviewBodyEl = card.querySelector('[itemprop="reviewBody"]');
191
+ const reviewBody = (reviewBodyEl as HTMLElement)?.innerText?.trim() || '';
192
+
193
+ // Extract badges
194
+ const badgeEls = card.querySelectorAll(
195
+ '[class*="badge"], [class*="tag"], .elv-rounded-sm.elv-border'
196
+ );
197
+ const badges = Array.from(badgeEls)
198
+ .map((el) => el.textContent?.trim())
199
+ .filter((text): text is string => !!text && text.length < 50 && text.length > 3);
200
+
201
+ // Extract review URL
202
+ const linkEl = card.querySelector('a[href*="survey_responses"]');
203
+ const href = linkEl?.getAttribute('href') || '';
204
+ const reviewUrl = href
205
+ ? href.startsWith('http')
206
+ ? href
207
+ : `https://www.g2.com${href}`
208
+ : '';
209
+
210
+ // Skip reviews with minimal content
211
+ if ((reviewBody || '').length < 50) return;
212
+
213
+ results.push({
214
+ rating,
215
+ title,
216
+ text: reviewBody,
217
+ author,
218
+ jobTitle,
219
+ industry,
220
+ companySize,
221
+ date: dateStr,
222
+ badges: badges.slice(0, 10),
223
+ reviewUrl,
224
+ helpfulCount: 0,
225
+ });
226
+ } catch (e) {
227
+ console.error('[G2Connector] Error parsing review card:', e);
228
+ }
229
+ });
230
+
231
+ return results;
232
+ });
233
+
234
+ // Transform reviews to EventEnvelope format
235
+ for (const review of reviews) {
236
+ const event: EventEnvelope = {
237
+ origin_id: `g2-${productKey}-${review.date || 'nodate'}-${review.author.replace(/\s+/g, '-')}`,
238
+ title: review.title,
239
+ payload_text: review.text,
240
+ author_name: review.author,
241
+ occurred_at: review.date ? new Date(review.date) : new Date(),
242
+ origin_type: 'review',
243
+ score: calculateEngagementScore('g2', {
244
+ rating: review.rating,
245
+ helpful_count: 0,
246
+ }),
247
+ source_url: review.reviewUrl || baseUrl,
248
+ metadata: {
249
+ rating: review.rating,
250
+ helpful_count: review.helpfulCount,
251
+ job_title: review.jobTitle,
252
+ industry: review.industry,
253
+ company_size: review.companySize,
254
+ badges: review.badges,
255
+ },
256
+ };
257
+ allEvents.push(event);
258
+ }
259
+
260
+ // If this page had no reviews, stop paginating
261
+ if (reviews.length === 0) break;
262
+
263
+ // Delay between pages
264
+ if (pageNum < maxPages) {
265
+ await new Promise((resolve) => setTimeout(resolve, 2000));
266
+ }
267
+
268
+ // Rate limit
269
+ await new Promise((resolve) => setTimeout(resolve, 6000));
270
+ }
271
+
272
+ const newCheckpoint: G2Checkpoint = {
273
+ last_sync_at: new Date().toISOString(),
274
+ pages_crawled: Math.min(5, allEvents.length > 0 ? Math.ceil(allEvents.length / 10) : 0),
275
+ };
276
+
277
+ return {
278
+ events: allEvents,
279
+ checkpoint: newCheckpoint as Record<string, unknown>,
280
+ metadata: {
281
+ items_found: allEvents.length,
282
+ },
283
+ };
284
+ });
285
+ }
286
+
287
+ async execute(_ctx: ActionContext): Promise<ActionResult> {
288
+ return { success: false, error: 'Actions not supported' };
289
+ }
290
+ }