@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,349 @@
1
+ /**
2
+ * Google Play Store Connector (V1 runtime)
3
+ *
4
+ * Syncs app reviews from the Google Play Store.
5
+ * Directly calls the Play Store batchexecute API instead of using an npm package.
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
+ // ── Google Play batchexecute API helpers ────────────────────────────
20
+
21
+ const BASE_URL = 'https://play.google.com';
22
+
23
+ const SORT = {
24
+ HELPFULNESS: 1,
25
+ NEWEST: 2,
26
+ RATING: 3,
27
+ } as const;
28
+
29
+ /**
30
+ * Build the URL-encoded `f.req` form body for the batchexecute reviews RPC.
31
+ *
32
+ * The body encodes a nested JSON structure:
33
+ * [["UsvDTd","[null,null,[2,<sort>,[<num>,null,<token>],null,[]],[\"<appId>\",7]]",null,"generic"]]
34
+ *
35
+ * For the initial request the token slot is `null`; for paginated requests it
36
+ * is the string token returned by the previous response.
37
+ */
38
+ function buildRequestBody(
39
+ appId: string,
40
+ sort: number,
41
+ numReviews: number,
42
+ token: string | null
43
+ ): string {
44
+ const tokenPart =
45
+ token === null
46
+ ? `%5B${numReviews}%2Cnull%2Cnull%5D`
47
+ : `%5B${numReviews}%2Cnull%2C%5C%22${token}%5C%22%5D`;
48
+
49
+ return `f.req=%5B%5B%5B%22UsvDTd%22%2C%22%5Bnull%2Cnull%2C%5B2%2C${sort}%2C${tokenPart}%2Cnull%2C%5B%5D%5D%2C%5B%5C%22${appId}%5C%22%2C7%5D%5D%22%2Cnull%2C%22generic%22%5D%5D%5D`;
50
+ }
51
+
52
+ function buildBatchUrl(lang: string, country: string): string {
53
+ return `${BASE_URL}/_/PlayStoreUi/data/batchexecute?rpcids=qnKhOb&bl=boq_playuiserver_20190903.08_p0&hl=${lang}&gl=${country}&authuser&soc-app=121&soc-platform=1&soc-device=1&_reqid=1065213`;
54
+ }
55
+
56
+ interface RawReview {
57
+ id: string;
58
+ userName: string;
59
+ userImage: string | null;
60
+ date: string | null;
61
+ score: number;
62
+ text: string;
63
+ replyDate: string | null;
64
+ replyText: string | null;
65
+ version: string | null;
66
+ thumbsUp: number;
67
+ url: string;
68
+ }
69
+
70
+ /**
71
+ * Convert Google's epoch-seconds + partial-millis array into an ISO date string.
72
+ * The date field comes as `[seconds, partialMillis]`.
73
+ */
74
+ function parseDate(dateArray: unknown): string | null {
75
+ if (!Array.isArray(dateArray)) return null;
76
+ // Compute numerically: seconds*1000 + millis. The previous string-concat
77
+ // approach (`${seconds}${millis}`) only worked when millis was a 3-digit
78
+ // zero-padded string; Google sends a plain integer, so e.g. `[s, 5]` produced
79
+ // a date in 1970 and `[s, 50]` a date in year ~7340.
80
+ const seconds = Number(dateArray[0]);
81
+ const millis = Number(dateArray[1] ?? 0);
82
+ if (!Number.isFinite(seconds) || !Number.isFinite(millis)) return null;
83
+ const d = new Date(seconds * 1000 + millis);
84
+ if (Number.isNaN(d.getTime())) return null;
85
+ return d.toJSON();
86
+ }
87
+
88
+ /**
89
+ * Extract structured review objects from the raw nested-array response data.
90
+ */
91
+ function extractReviews(data: any[], appId: string): RawReview[] {
92
+ const reviewsList: any[] | undefined = data?.[0];
93
+ if (!Array.isArray(reviewsList)) return [];
94
+
95
+ return reviewsList.map((r: any) => ({
96
+ id: r[0] ?? '',
97
+ userName: r[1]?.[0] ?? '',
98
+ userImage: r[1]?.[1]?.[3]?.[2] ?? null,
99
+ date: parseDate(r[5]),
100
+ score: r[2] ?? 0,
101
+ text: r[4] ?? '',
102
+ replyDate: parseDate(r[7]?.[2]),
103
+ replyText: r[7]?.[1] ?? null,
104
+ version: r[10] ?? null,
105
+ thumbsUp: r[6] ?? 0,
106
+ url: `${BASE_URL}/store/apps/details?id=${appId}&reviewId=${r[0] ?? ''}`,
107
+ }));
108
+ }
109
+
110
+ function extractPaginationToken(data: any[]): string | null {
111
+ return data?.[1]?.[1] ?? null;
112
+ }
113
+
114
+ /**
115
+ * Fetch a single page of reviews from the Play Store batchexecute endpoint.
116
+ * Returns the parsed review objects and the next pagination token (if any).
117
+ */
118
+ async function fetchReviewsPage(
119
+ appId: string,
120
+ sort: number,
121
+ lang: string,
122
+ country: string,
123
+ token: string | null,
124
+ numPerPage = 150
125
+ ): Promise<{ reviews: RawReview[]; nextToken: string | null }> {
126
+ const url = buildBatchUrl(lang, country);
127
+ const body = buildRequestBody(appId, sort, numPerPage, token);
128
+
129
+ const res = await fetch(url, {
130
+ method: 'POST',
131
+ headers: {
132
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
133
+ },
134
+ body,
135
+ });
136
+
137
+ if (!res.ok) {
138
+ if (res.status === 404) throw new Error('App not found (404)');
139
+ throw new Error(`Google Play request failed: ${res.status} ${res.statusText}`);
140
+ }
141
+
142
+ const text = await res.text();
143
+
144
+ // Response starts with ")]}'" (security prefix), then a newline, then JSON.
145
+ // The library skips the first 5 characters.
146
+ const outer = JSON.parse(text.substring(5));
147
+ const innerJson: string | null = outer?.[0]?.[2];
148
+
149
+ if (innerJson === null || innerJson === undefined) {
150
+ return { reviews: [], nextToken: null };
151
+ }
152
+
153
+ const data = JSON.parse(innerJson);
154
+ return {
155
+ reviews: extractReviews(data, appId),
156
+ nextToken: extractPaginationToken(data),
157
+ };
158
+ }
159
+
160
+ // ── Connector implementation ────────────────────────────────────────
161
+
162
+ interface GooglePlayCheckpoint {
163
+ last_timestamp?: string;
164
+ pagination_token?: string;
165
+ }
166
+
167
+ export default class GooglePlayConnector extends ConnectorRuntime {
168
+ readonly definition: ConnectorDefinition = {
169
+ key: 'google_play',
170
+ name: 'Google Play Store',
171
+ description: 'Fetches app reviews from the Google Play Store.',
172
+ version: '1.0.0',
173
+ faviconDomain: 'play.google.com',
174
+ authSchema: {
175
+ methods: [{ type: 'none' }],
176
+ },
177
+ feeds: {
178
+ reviews: {
179
+ key: 'reviews',
180
+ name: 'App Reviews',
181
+ description: 'Fetch reviews for an Android app.',
182
+ configSchema: {
183
+ type: 'object',
184
+ required: ['app_id'],
185
+ properties: {
186
+ app_id: {
187
+ type: 'string',
188
+ minLength: 1,
189
+ description: 'Google Play package name (e.g., "com.spotify.music")',
190
+ },
191
+ country: {
192
+ type: 'string',
193
+ minLength: 2,
194
+ maxLength: 2,
195
+ default: 'us',
196
+ description: 'ISO country code',
197
+ },
198
+ lang: {
199
+ type: 'string',
200
+ minLength: 2,
201
+ maxLength: 5,
202
+ default: 'en',
203
+ description: 'Language code',
204
+ },
205
+ },
206
+ },
207
+ eventKinds: {
208
+ review: {
209
+ description: 'A Google Play Store app review',
210
+ metadataSchema: {
211
+ type: 'object',
212
+ properties: {
213
+ rating: { type: 'number', description: 'Star rating (1-5)' },
214
+ thumbs_up: { type: 'number', description: 'Thumbs up count' },
215
+ version: { type: 'string', description: 'App version reviewed' },
216
+ reply: { type: 'string', description: 'Developer reply text' },
217
+ reply_date: { type: 'string', description: 'Developer reply date' },
218
+ },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ optionsSchema: {
225
+ type: 'object',
226
+ required: ['app_id'],
227
+ properties: {
228
+ app_id: {
229
+ type: 'string',
230
+ minLength: 1,
231
+ description: 'Google Play package name (e.g., "com.spotify.music")',
232
+ },
233
+ country: {
234
+ type: 'string',
235
+ minLength: 2,
236
+ maxLength: 2,
237
+ default: 'us',
238
+ description: 'ISO country code',
239
+ },
240
+ lang: {
241
+ type: 'string',
242
+ minLength: 2,
243
+ maxLength: 5,
244
+ default: 'en',
245
+ description: 'Language code',
246
+ },
247
+ },
248
+ },
249
+ };
250
+
251
+ async sync(ctx: SyncContext): Promise<SyncResult> {
252
+ const app_id = ctx.config.app_id as string;
253
+ const country = (ctx.config.country as string) || 'us';
254
+ const lang = (ctx.config.lang as string) || 'en';
255
+ const checkpoint = (ctx.checkpoint ?? {}) as GooglePlayCheckpoint;
256
+
257
+ const MAX_REVIEWS = 500;
258
+ const allReviews: RawReview[] = [];
259
+ let nextToken: string | null = checkpoint.pagination_token ?? null;
260
+ const lastTimestamp = checkpoint.last_timestamp
261
+ ? new Date(checkpoint.last_timestamp).getTime()
262
+ : null;
263
+ let hitCheckpoint = false;
264
+
265
+ while (allReviews.length < MAX_REVIEWS) {
266
+ const page = await fetchReviewsPage(app_id, SORT.HELPFULNESS, lang, country, nextToken);
267
+
268
+ if (page.reviews.length === 0) break;
269
+
270
+ if (lastTimestamp) {
271
+ for (const review of page.reviews) {
272
+ const reviewTime = review.date ? new Date(review.date).getTime() : 0;
273
+ if (reviewTime > lastTimestamp) {
274
+ allReviews.push(review);
275
+ } else {
276
+ hitCheckpoint = true;
277
+ break;
278
+ }
279
+ }
280
+ if (hitCheckpoint) break;
281
+ } else {
282
+ allReviews.push(...page.reviews);
283
+ }
284
+
285
+ nextToken = page.nextToken;
286
+ if (!nextToken) break;
287
+
288
+ // Rate-limit delay between pagination requests
289
+ await new Promise((resolve) => setTimeout(resolve, 1000));
290
+ }
291
+
292
+ // Transform to EventEnvelope format — skip reviews without text
293
+ const events: EventEnvelope[] = allReviews
294
+ .filter((review) => review.text)
295
+ .map((review) => {
296
+ const rating = review.score || 0;
297
+ const thumbsUp = review.thumbsUp || 0;
298
+ const replyCount = review.replyDate ? 1 : 0;
299
+
300
+ return {
301
+ origin_id: review.id,
302
+ payload_text: review.text,
303
+ author_name: review.userName || undefined,
304
+ occurred_at: review.date ? new Date(review.date) : new Date(),
305
+ origin_type: 'review',
306
+ score: calculateEngagementScore('google_play', {
307
+ rating,
308
+ helpful_count: thumbsUp,
309
+ reply_count: replyCount,
310
+ }),
311
+ source_url: review.url,
312
+ metadata: {
313
+ rating,
314
+ thumbs_up: thumbsUp,
315
+ version: review.version,
316
+ reply: review.replyText,
317
+ reply_date: review.replyDate,
318
+ },
319
+ };
320
+ });
321
+
322
+ // Sort by published date descending
323
+ events.sort((a, b) => b.occurred_at.getTime() - a.occurred_at.getTime());
324
+
325
+ const newCheckpoint: GooglePlayCheckpoint =
326
+ events.length > 0
327
+ ? {
328
+ last_timestamp: events[0].occurred_at.toISOString(),
329
+ pagination_token: nextToken ?? undefined,
330
+ }
331
+ : {
332
+ last_timestamp: checkpoint.last_timestamp,
333
+ pagination_token: checkpoint.pagination_token,
334
+ };
335
+
336
+ return {
337
+ events,
338
+ checkpoint: newCheckpoint as Record<string, unknown>,
339
+ metadata: {
340
+ items_found: allReviews.length,
341
+ items_skipped: 0,
342
+ },
343
+ };
344
+ }
345
+
346
+ async execute(_ctx: ActionContext): Promise<ActionResult> {
347
+ return { success: false, error: 'Actions not supported' };
348
+ }
349
+ }