@lobu/cli 6.0.1 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +20 -27
  2. package/dist/bundled-skills/lobu/SKILL.md +11 -11
  3. package/dist/commands/_lib/apply/apply-cmd.d.ts +2 -0
  4. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  5. package/dist/commands/_lib/apply/apply-cmd.js +26 -0
  6. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  7. package/dist/commands/_lib/apply/client.d.ts +1 -1
  8. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  9. package/dist/commands/_lib/apply/desired-state.js +4 -4
  10. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  11. package/dist/commands/agent.d.ts +7 -0
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +65 -1
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/chat.d.ts +12 -9
  16. package/dist/commands/chat.d.ts.map +1 -1
  17. package/dist/commands/chat.js +117 -56
  18. package/dist/commands/chat.js.map +1 -1
  19. package/dist/commands/dev.d.ts +15 -7
  20. package/dist/commands/dev.d.ts.map +1 -1
  21. package/dist/commands/dev.js +79 -44
  22. package/dist/commands/dev.js.map +1 -1
  23. package/dist/commands/doctor.d.ts +1 -0
  24. package/dist/commands/doctor.d.ts.map +1 -1
  25. package/dist/commands/doctor.js +136 -0
  26. package/dist/commands/doctor.js.map +1 -1
  27. package/dist/commands/eval.d.ts +8 -0
  28. package/dist/commands/eval.d.ts.map +1 -1
  29. package/dist/commands/eval.js +56 -1
  30. package/dist/commands/eval.js.map +1 -1
  31. package/dist/commands/init.d.ts +20 -5
  32. package/dist/commands/init.d.ts.map +1 -1
  33. package/dist/commands/init.js +332 -183
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/commands/link.d.ts +11 -0
  36. package/dist/commands/link.d.ts.map +1 -0
  37. package/dist/commands/link.js +28 -0
  38. package/dist/commands/link.js.map +1 -0
  39. package/dist/commands/login.d.ts.map +1 -1
  40. package/dist/commands/login.js +14 -2
  41. package/dist/commands/login.js.map +1 -1
  42. package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
  43. package/dist/commands/memory/_lib/browser-auth-cmd.js +3 -3
  44. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  45. package/dist/commands/memory/_lib/mcp.d.ts +2 -2
  46. package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
  47. package/dist/commands/memory/_lib/mcp.js +24 -12
  48. package/dist/commands/memory/_lib/mcp.js.map +1 -1
  49. package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
  50. package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
  51. package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
  52. package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
  53. package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
  54. package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
  55. package/dist/commands/memory/_lib/schema.d.ts +1 -1
  56. package/dist/commands/memory/_lib/schema.js +1 -1
  57. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  58. package/dist/commands/memory/_lib/seed-cmd.js +5 -6
  59. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  60. package/dist/commands/memory/run.d.ts.map +1 -1
  61. package/dist/commands/memory/run.js +2 -2
  62. package/dist/commands/memory/run.js.map +1 -1
  63. package/dist/commands/platforms/platform-prompts.d.ts +0 -1
  64. package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
  65. package/dist/commands/platforms/platform-prompts.js +54 -8
  66. package/dist/commands/platforms/platform-prompts.js.map +1 -1
  67. package/dist/commands/telemetry.d.ts +10 -0
  68. package/dist/commands/telemetry.d.ts.map +1 -0
  69. package/dist/commands/telemetry.js +68 -0
  70. package/dist/commands/telemetry.js.map +1 -0
  71. package/dist/commands/whoami.d.ts.map +1 -1
  72. package/dist/commands/whoami.js +1 -1
  73. package/dist/commands/whoami.js.map +1 -1
  74. package/dist/connectors/README.md +534 -0
  75. package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
  76. package/dist/connectors/browser-scraper-utils.ts +214 -0
  77. package/dist/connectors/capterra.ts +273 -0
  78. package/dist/connectors/g2.ts +286 -0
  79. package/dist/connectors/github.ts +1553 -0
  80. package/dist/connectors/glassdoor.ts +291 -0
  81. package/dist/connectors/gmaps.ts +197 -0
  82. package/dist/connectors/google_calendar.ts +631 -0
  83. package/dist/connectors/google_gmail.ts +751 -0
  84. package/dist/connectors/google_photos.ts +776 -0
  85. package/dist/connectors/google_play.ts +342 -0
  86. package/dist/connectors/hackernews.ts +471 -0
  87. package/dist/connectors/index.ts +23 -0
  88. package/dist/connectors/ios_appstore.ts +226 -0
  89. package/dist/connectors/linkedin.ts +471 -0
  90. package/dist/connectors/microsoft_outlook.ts +410 -0
  91. package/dist/connectors/producthunt.ts +471 -0
  92. package/dist/connectors/reddit.ts +600 -0
  93. package/dist/connectors/rss.ts +448 -0
  94. package/dist/connectors/spotify.ts +590 -0
  95. package/dist/connectors/trustpilot.ts +199 -0
  96. package/dist/connectors/website.ts +629 -0
  97. package/dist/connectors/whatsapp.ts +1073 -0
  98. package/dist/connectors/x.ts +526 -0
  99. package/dist/connectors/youtube.ts +666 -0
  100. package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
  101. package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
  102. package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
  103. package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
  104. package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
  105. package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
  106. package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
  107. package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
  108. package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
  109. package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
  110. package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
  111. package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
  112. package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
  113. package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
  114. package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
  115. package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
  116. package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
  117. package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
  118. package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
  119. package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
  120. package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
  121. package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
  122. package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
  123. package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
  124. package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
  125. package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
  126. package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
  127. package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
  128. package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
  129. package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
  130. package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
  131. package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
  132. package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
  133. package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
  134. package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
  135. package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
  136. package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
  137. package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
  138. package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
  139. package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
  140. package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
  141. package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
  142. package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
  143. package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
  144. package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
  145. package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +147 -23
  148. package/dist/index.js.map +1 -1
  149. package/dist/internal/api-client.d.ts +4 -8
  150. package/dist/internal/api-client.d.ts.map +1 -1
  151. package/dist/internal/api-client.js +1 -1
  152. package/dist/internal/api-client.js.map +1 -1
  153. package/dist/internal/context.js +2 -2
  154. package/dist/internal/context.js.map +1 -1
  155. package/dist/internal/credentials.d.ts.map +1 -1
  156. package/dist/internal/credentials.js +6 -1
  157. package/dist/internal/credentials.js.map +1 -1
  158. package/dist/internal/index.d.ts +2 -3
  159. package/dist/internal/index.d.ts.map +1 -1
  160. package/dist/internal/index.js +2 -2
  161. package/dist/internal/index.js.map +1 -1
  162. package/dist/internal/oauth.d.ts +6 -5
  163. package/dist/internal/oauth.d.ts.map +1 -1
  164. package/dist/internal/oauth.js +2 -2
  165. package/dist/internal/project-link.d.ts +10 -0
  166. package/dist/internal/project-link.d.ts.map +1 -0
  167. package/dist/internal/project-link.js +48 -0
  168. package/dist/internal/project-link.js.map +1 -0
  169. package/dist/providers.json +2 -2
  170. package/dist/server.bundle.mjs +3090 -4321
  171. package/dist/start-local.bundle.mjs +71481 -0
  172. package/dist/templates/README.md.tmpl +10 -11
  173. package/package.json +14 -12
  174. package/dist/__tests__/chat.integration.test.d.ts +0 -2
  175. package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
  176. package/dist/__tests__/chat.integration.test.js +0 -337
  177. package/dist/__tests__/chat.integration.test.js.map +0 -1
  178. package/dist/__tests__/dev.test.d.ts +0 -2
  179. package/dist/__tests__/dev.test.d.ts.map +0 -1
  180. package/dist/__tests__/dev.test.js +0 -25
  181. package/dist/__tests__/dev.test.js.map +0 -1
  182. package/dist/__tests__/init-memory.test.d.ts +0 -2
  183. package/dist/__tests__/init-memory.test.d.ts.map +0 -1
  184. package/dist/__tests__/init-memory.test.js +0 -45
  185. package/dist/__tests__/init-memory.test.js.map +0 -1
  186. package/dist/__tests__/token.test.d.ts +0 -2
  187. package/dist/__tests__/token.test.d.ts.map +0 -1
  188. package/dist/__tests__/token.test.js +0 -52
  189. package/dist/__tests__/token.test.js.map +0 -1
  190. package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
  191. package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
  192. package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
  193. package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
  194. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
  195. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
  196. package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
  197. package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
  198. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
  199. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
  200. package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
  201. package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
  202. package/dist/commands/apply.d.ts +0 -3
  203. package/dist/commands/apply.d.ts.map +0 -1
  204. package/dist/commands/apply.js +0 -5
  205. package/dist/commands/apply.js.map +0 -1
  206. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
  207. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
  208. package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
  209. package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
  210. package/dist/internal/__tests__/api-client.test.d.ts +0 -2
  211. package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
  212. package/dist/internal/__tests__/api-client.test.js +0 -95
  213. package/dist/internal/__tests__/api-client.test.js.map +0 -1
  214. package/dist/internal/__tests__/context.test.d.ts +0 -2
  215. package/dist/internal/__tests__/context.test.d.ts.map +0 -1
  216. package/dist/internal/__tests__/context.test.js +0 -77
  217. package/dist/internal/__tests__/context.test.js.map +0 -1
@@ -0,0 +1,342 @@
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
+ const milliStr = String(dateArray[1] ?? '000');
77
+ const totalMs = `${dateArray[0]}${milliStr.substring(0, 3)}`;
78
+ return new Date(Number(totalMs)).toJSON();
79
+ }
80
+
81
+ /**
82
+ * Extract structured review objects from the raw nested-array response data.
83
+ */
84
+ function extractReviews(data: any[], appId: string): RawReview[] {
85
+ const reviewsList: any[] | undefined = data?.[0];
86
+ if (!Array.isArray(reviewsList)) return [];
87
+
88
+ return reviewsList.map((r: any) => ({
89
+ id: r[0] ?? '',
90
+ userName: r[1]?.[0] ?? '',
91
+ userImage: r[1]?.[1]?.[3]?.[2] ?? null,
92
+ date: parseDate(r[5]),
93
+ score: r[2] ?? 0,
94
+ text: r[4] ?? '',
95
+ replyDate: parseDate(r[7]?.[2]),
96
+ replyText: r[7]?.[1] ?? null,
97
+ version: r[10] ?? null,
98
+ thumbsUp: r[6] ?? 0,
99
+ url: `${BASE_URL}/store/apps/details?id=${appId}&reviewId=${r[0] ?? ''}`,
100
+ }));
101
+ }
102
+
103
+ function extractPaginationToken(data: any[]): string | null {
104
+ return data?.[1]?.[1] ?? null;
105
+ }
106
+
107
+ /**
108
+ * Fetch a single page of reviews from the Play Store batchexecute endpoint.
109
+ * Returns the parsed review objects and the next pagination token (if any).
110
+ */
111
+ async function fetchReviewsPage(
112
+ appId: string,
113
+ sort: number,
114
+ lang: string,
115
+ country: string,
116
+ token: string | null,
117
+ numPerPage = 150
118
+ ): Promise<{ reviews: RawReview[]; nextToken: string | null }> {
119
+ const url = buildBatchUrl(lang, country);
120
+ const body = buildRequestBody(appId, sort, numPerPage, token);
121
+
122
+ const res = await fetch(url, {
123
+ method: 'POST',
124
+ headers: {
125
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
126
+ },
127
+ body,
128
+ });
129
+
130
+ if (!res.ok) {
131
+ if (res.status === 404) throw new Error('App not found (404)');
132
+ throw new Error(`Google Play request failed: ${res.status} ${res.statusText}`);
133
+ }
134
+
135
+ const text = await res.text();
136
+
137
+ // Response starts with ")]}'" (security prefix), then a newline, then JSON.
138
+ // The library skips the first 5 characters.
139
+ const outer = JSON.parse(text.substring(5));
140
+ const innerJson: string | null = outer?.[0]?.[2];
141
+
142
+ if (innerJson === null || innerJson === undefined) {
143
+ return { reviews: [], nextToken: null };
144
+ }
145
+
146
+ const data = JSON.parse(innerJson);
147
+ return {
148
+ reviews: extractReviews(data, appId),
149
+ nextToken: extractPaginationToken(data),
150
+ };
151
+ }
152
+
153
+ // ── Connector implementation ────────────────────────────────────────
154
+
155
+ interface GooglePlayCheckpoint {
156
+ last_timestamp?: string;
157
+ pagination_token?: string;
158
+ }
159
+
160
+ export default class GooglePlayConnector extends ConnectorRuntime {
161
+ readonly definition: ConnectorDefinition = {
162
+ key: 'google_play',
163
+ name: 'Google Play Store',
164
+ description: 'Fetches app reviews from the Google Play Store.',
165
+ version: '1.0.0',
166
+ faviconDomain: 'play.google.com',
167
+ authSchema: {
168
+ methods: [{ type: 'none' }],
169
+ },
170
+ feeds: {
171
+ reviews: {
172
+ key: 'reviews',
173
+ name: 'App Reviews',
174
+ description: 'Fetch reviews for an Android app.',
175
+ configSchema: {
176
+ type: 'object',
177
+ required: ['app_id'],
178
+ properties: {
179
+ app_id: {
180
+ type: 'string',
181
+ minLength: 1,
182
+ description: 'Google Play package name (e.g., "com.spotify.music")',
183
+ },
184
+ country: {
185
+ type: 'string',
186
+ minLength: 2,
187
+ maxLength: 2,
188
+ default: 'us',
189
+ description: 'ISO country code',
190
+ },
191
+ lang: {
192
+ type: 'string',
193
+ minLength: 2,
194
+ maxLength: 5,
195
+ default: 'en',
196
+ description: 'Language code',
197
+ },
198
+ },
199
+ },
200
+ eventKinds: {
201
+ review: {
202
+ description: 'A Google Play Store app review',
203
+ metadataSchema: {
204
+ type: 'object',
205
+ properties: {
206
+ rating: { type: 'number', description: 'Star rating (1-5)' },
207
+ thumbs_up: { type: 'number', description: 'Thumbs up count' },
208
+ version: { type: 'string', description: 'App version reviewed' },
209
+ reply: { type: 'string', description: 'Developer reply text' },
210
+ reply_date: { type: 'string', description: 'Developer reply date' },
211
+ },
212
+ },
213
+ },
214
+ },
215
+ },
216
+ },
217
+ optionsSchema: {
218
+ type: 'object',
219
+ required: ['app_id'],
220
+ properties: {
221
+ app_id: {
222
+ type: 'string',
223
+ minLength: 1,
224
+ description: 'Google Play package name (e.g., "com.spotify.music")',
225
+ },
226
+ country: {
227
+ type: 'string',
228
+ minLength: 2,
229
+ maxLength: 2,
230
+ default: 'us',
231
+ description: 'ISO country code',
232
+ },
233
+ lang: {
234
+ type: 'string',
235
+ minLength: 2,
236
+ maxLength: 5,
237
+ default: 'en',
238
+ description: 'Language code',
239
+ },
240
+ },
241
+ },
242
+ };
243
+
244
+ async sync(ctx: SyncContext): Promise<SyncResult> {
245
+ const app_id = ctx.config.app_id as string;
246
+ const country = (ctx.config.country as string) || 'us';
247
+ const lang = (ctx.config.lang as string) || 'en';
248
+ const checkpoint = (ctx.checkpoint ?? {}) as GooglePlayCheckpoint;
249
+
250
+ const MAX_REVIEWS = 500;
251
+ const allReviews: RawReview[] = [];
252
+ let nextToken: string | null = checkpoint.pagination_token ?? null;
253
+ const lastTimestamp = checkpoint.last_timestamp
254
+ ? new Date(checkpoint.last_timestamp).getTime()
255
+ : null;
256
+ let hitCheckpoint = false;
257
+
258
+ while (allReviews.length < MAX_REVIEWS) {
259
+ const page = await fetchReviewsPage(app_id, SORT.HELPFULNESS, lang, country, nextToken);
260
+
261
+ if (page.reviews.length === 0) break;
262
+
263
+ if (lastTimestamp) {
264
+ for (const review of page.reviews) {
265
+ const reviewTime = review.date ? new Date(review.date).getTime() : 0;
266
+ if (reviewTime > lastTimestamp) {
267
+ allReviews.push(review);
268
+ } else {
269
+ hitCheckpoint = true;
270
+ break;
271
+ }
272
+ }
273
+ if (hitCheckpoint) break;
274
+ } else {
275
+ allReviews.push(...page.reviews);
276
+ }
277
+
278
+ nextToken = page.nextToken;
279
+ if (!nextToken) break;
280
+
281
+ // Rate-limit delay between pagination requests
282
+ await new Promise((resolve) => setTimeout(resolve, 1000));
283
+ }
284
+
285
+ // Transform to EventEnvelope format — skip reviews without text
286
+ const events: EventEnvelope[] = allReviews
287
+ .filter((review) => review.text)
288
+ .map((review) => {
289
+ const rating = review.score || 0;
290
+ const thumbsUp = review.thumbsUp || 0;
291
+ const replyCount = review.replyDate ? 1 : 0;
292
+
293
+ return {
294
+ origin_id: review.id,
295
+ payload_text: review.text,
296
+ author_name: review.userName || undefined,
297
+ occurred_at: review.date ? new Date(review.date) : new Date(),
298
+ origin_type: 'review',
299
+ score: calculateEngagementScore('google_play', {
300
+ rating,
301
+ helpful_count: thumbsUp,
302
+ reply_count: replyCount,
303
+ }),
304
+ source_url: review.url,
305
+ metadata: {
306
+ rating,
307
+ thumbs_up: thumbsUp,
308
+ version: review.version,
309
+ reply: review.replyText,
310
+ reply_date: review.replyDate,
311
+ },
312
+ };
313
+ });
314
+
315
+ // Sort by published date descending
316
+ events.sort((a, b) => b.occurred_at.getTime() - a.occurred_at.getTime());
317
+
318
+ const newCheckpoint: GooglePlayCheckpoint =
319
+ events.length > 0
320
+ ? {
321
+ last_timestamp: events[0].occurred_at.toISOString(),
322
+ pagination_token: nextToken ?? undefined,
323
+ }
324
+ : {
325
+ last_timestamp: checkpoint.last_timestamp,
326
+ pagination_token: checkpoint.pagination_token,
327
+ };
328
+
329
+ return {
330
+ events,
331
+ checkpoint: newCheckpoint as Record<string, unknown>,
332
+ metadata: {
333
+ items_found: allReviews.length,
334
+ items_skipped: 0,
335
+ },
336
+ };
337
+ }
338
+
339
+ async execute(_ctx: ActionContext): Promise<ActionResult> {
340
+ return { success: false, error: 'Actions not supported' };
341
+ }
342
+ }