@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,186 @@
1
+ import { describe, expect, mock, test } from 'bun:test';
2
+
3
+ // browser-scraper-utils.ts pulls runtime symbols (acquireBrowser,
4
+ // captureErrorArtifacts) from @lobu/connector-sdk, which itself pulls in
5
+ // playwright. Stub the SDK so the pure helpers (validateUrlDomain,
6
+ // getBrowserCookies, validateCookieNotExpired, filterByCheckpoint) can be
7
+ // imported without spinning up a real browser stack.
8
+ mock.module('@lobu/connector-sdk', () => ({
9
+ acquireBrowser: () => {
10
+ throw new Error('not used in unit tests');
11
+ },
12
+ captureErrorArtifacts: () => {
13
+ throw new Error('not used in unit tests');
14
+ },
15
+ }));
16
+
17
+ const {
18
+ filterByCheckpoint,
19
+ getBrowserCookies,
20
+ validateCookieNotExpired,
21
+ validateUrlDomain,
22
+ } = await import('../browser-scraper-utils.ts');
23
+
24
+ describe('validateUrlDomain', () => {
25
+ test('accepts a well-formed https URL on the expected domain', () => {
26
+ expect(() =>
27
+ validateUrlDomain('https://www.trustpilot.com/review/foo', 'trustpilot.com')
28
+ ).not.toThrow();
29
+ });
30
+
31
+ test('accepts a subdomain of the expected domain', () => {
32
+ expect(() => validateUrlDomain('https://api.example.com/x', 'example.com')).not.toThrow();
33
+ });
34
+
35
+ test('rejects a malformed URL', () => {
36
+ expect(() => validateUrlDomain('not a url', 'example.com')).toThrow(/Invalid example.com URL/);
37
+ });
38
+
39
+ test('rejects http (non-https) URLs', () => {
40
+ expect(() => validateUrlDomain('http://www.example.com/', 'example.com')).toThrow(
41
+ /must use https: protocol/
42
+ );
43
+ });
44
+
45
+ test('rejects a URL on a different domain', () => {
46
+ expect(() => validateUrlDomain('https://evil.com/foo', 'example.com')).toThrow(
47
+ /must be on example.com/
48
+ );
49
+ });
50
+
51
+ test('rejects a substring-match hostname (security: notexample.com is NOT example.com)', () => {
52
+ expect(() => validateUrlDomain('https://notexample.com/x', 'example.com')).toThrow(
53
+ /must be on example.com/
54
+ );
55
+ expect(() => validateUrlDomain('https://eviltrustpilot.com/x', 'trustpilot.com')).toThrow(
56
+ /must be on trustpilot.com/
57
+ );
58
+ });
59
+
60
+ test('accepts the apex domain itself (no subdomain)', () => {
61
+ expect(() => validateUrlDomain('https://example.com/x', 'example.com')).not.toThrow();
62
+ });
63
+ });
64
+
65
+ describe('getBrowserCookies', () => {
66
+ test('prefers checkpoint cookies over session-state cookies', () => {
67
+ const cookies = getBrowserCookies(
68
+ { cookies: [{ name: 'checkpoint-cookie' }] },
69
+ { cookies: [{ name: 'session-cookie' }] },
70
+ 'connector.x'
71
+ );
72
+ expect(cookies).toEqual([{ name: 'checkpoint-cookie' }]);
73
+ });
74
+
75
+ test('falls back to session-state cookies when checkpoint has none', () => {
76
+ const cookies = getBrowserCookies(
77
+ null,
78
+ { cookies: [{ name: 'session-cookie' }] },
79
+ 'connector.x'
80
+ );
81
+ expect(cookies).toEqual([{ name: 'session-cookie' }]);
82
+ });
83
+
84
+ test('throws a descriptive error when no cookies are present anywhere', () => {
85
+ expect(() => getBrowserCookies(null, null, 'connector.x')).toThrow(
86
+ /No browser cookies found/
87
+ );
88
+ expect(() => getBrowserCookies(null, null, 'connector.x')).toThrow(/connector\.x/);
89
+ });
90
+
91
+ test('throws when checkpoint cookies array is empty and no session', () => {
92
+ expect(() => getBrowserCookies({ cookies: [] }, undefined, 'connector.x')).toThrow(
93
+ /No browser cookies found/
94
+ );
95
+ });
96
+
97
+ test('handles undefined sessionState explicitly', () => {
98
+ expect(() => getBrowserCookies(null, undefined, 'connector.x')).toThrow(
99
+ /No browser cookies found/
100
+ );
101
+ });
102
+ });
103
+
104
+ describe('validateCookieNotExpired', () => {
105
+ test('does nothing when the cookie is missing entirely', () => {
106
+ expect(() =>
107
+ validateCookieNotExpired([{ name: 'other' }], 'session', 'connector.x')
108
+ ).not.toThrow();
109
+ });
110
+
111
+ test('does nothing when the cookie has no expires field', () => {
112
+ expect(() =>
113
+ validateCookieNotExpired([{ name: 'session' }], 'session', 'connector.x')
114
+ ).not.toThrow();
115
+ });
116
+
117
+ test('does nothing when expires is 0 (session cookie)', () => {
118
+ expect(() =>
119
+ validateCookieNotExpired([{ name: 'session', expires: 0 }], 'session', 'connector.x')
120
+ ).not.toThrow();
121
+ });
122
+
123
+ test('does nothing when the cookie expires in the future', () => {
124
+ const futureUnix = Math.floor(Date.now() / 1000) + 60 * 60;
125
+ expect(() =>
126
+ validateCookieNotExpired(
127
+ [{ name: 'session', expires: futureUnix }],
128
+ 'session',
129
+ 'connector.x'
130
+ )
131
+ ).not.toThrow();
132
+ });
133
+
134
+ test('throws when the cookie has expired', () => {
135
+ const pastUnix = Math.floor(Date.now() / 1000) - 60 * 60 * 24;
136
+ expect(() =>
137
+ validateCookieNotExpired(
138
+ [{ name: 'session', expires: pastUnix }],
139
+ 'session',
140
+ 'connector.x'
141
+ )
142
+ ).toThrow(/session expired on/);
143
+ });
144
+
145
+ test('error message includes the connector slug for the re-auth hint', () => {
146
+ const pastUnix = Math.floor(Date.now() / 1000) - 60 * 60 * 24;
147
+ expect(() =>
148
+ validateCookieNotExpired(
149
+ [{ name: 'session', expires: pastUnix }],
150
+ 'session',
151
+ 'connector.x'
152
+ )
153
+ ).toThrow(/--connector connector\.x/);
154
+ });
155
+ });
156
+
157
+ describe('filterByCheckpoint', () => {
158
+ const events = [
159
+ { occurred_at: new Date('2024-01-01T00:00:00Z') } as any,
160
+ { occurred_at: new Date('2024-06-01T00:00:00Z') } as any,
161
+ { occurred_at: new Date('2024-12-31T00:00:00Z') } as any,
162
+ ];
163
+
164
+ test('returns every event when no checkpoint is set', () => {
165
+ expect(filterByCheckpoint(events, null)).toEqual(events);
166
+ });
167
+
168
+ test('returns every event when checkpoint has no last_timestamp', () => {
169
+ expect(filterByCheckpoint(events, {})).toEqual(events);
170
+ });
171
+
172
+ test('keeps only events strictly newer than last_timestamp', () => {
173
+ const filtered = filterByCheckpoint(events, {
174
+ last_timestamp: '2024-06-01T00:00:00Z',
175
+ });
176
+ // strict `>` — event at exactly the cutoff is filtered out
177
+ expect(filtered).toHaveLength(1);
178
+ expect(filtered[0]).toBe(events[2]);
179
+ });
180
+
181
+ test('returns empty array when cutoff is past every event', () => {
182
+ expect(
183
+ filterByCheckpoint(events, { last_timestamp: '2099-01-01T00:00:00Z' })
184
+ ).toEqual([]);
185
+ });
186
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Apple Health Connector (V1 runtime) — Lobu device app.
3
+ *
4
+ * Runs on a device bridge with HealthKit access. On iOS this reads the phone's
5
+ * Health store directly; on supported macOS installs HealthKit exposes the
6
+ * per-user store synced from iPhone and Apple Watch via iCloud Health. The
7
+ * Lobu device bridge holds the HealthKit entitlement, requests read permission
8
+ * once via a system sheet, and queries:
9
+ *
10
+ * - `daily_summaries`: daily totals for steps, distance, active energy,
11
+ * exercise minutes, and resting heart rate.
12
+ * - `workouts`: individual workout sessions (type, duration, energy, distance).
13
+ *
14
+ * The connector DEFINITION (feeds, event kinds, options) is the source of truth
15
+ * for what data ends up in Lobu and how it's shaped. The EXECUTION lives in the
16
+ * Lobu device app, which polls /api/workers/* as a user-scoped worker
17
+ * advertising the `healthkit` capability and streams events back through the
18
+ * standard worker protocol — same `runs` lifecycle as every other connector.
19
+ *
20
+ * The TS sync()/execute() here are safety stubs: if a server-side worker
21
+ * somehow bypassed the capability gate (`required_capability='healthkit'`),
22
+ * the run would throw immediately instead of silently producing no events.
23
+ */
24
+
25
+ import {
26
+ type ActionResult,
27
+ type ConnectorDefinition,
28
+ ConnectorRuntime,
29
+ type SyncContext,
30
+ type SyncResult,
31
+ } from '@lobu/connector-sdk';
32
+
33
+ const BRIDGE_ONLY_MESSAGE =
34
+ 'apple.health runs only on a worker advertising capability "healthkit" (Lobu with Apple Health permission). ' +
35
+ 'This run was claimed by a worker without that capability — check connector_definitions.required_capability and the poll-time capability filter.';
36
+
37
+ export default class AppleHealthConnector extends ConnectorRuntime {
38
+ readonly definition: ConnectorDefinition = {
39
+ key: 'apple.health',
40
+ name: 'Apple Health',
41
+ description:
42
+ 'Sync Apple Health daily activity summaries and workouts from Lobu on your device. ' +
43
+ 'macOS reads HealthKit data synced from the user\'s iPhone (and Apple Watch) via iCloud Health.',
44
+ version: '0.1.0',
45
+ faviconDomain: 'apple.com',
46
+ requiredCapability: 'healthkit',
47
+ runtime: {
48
+ platforms: ['ios', 'macos'],
49
+ scopes: ['steps', 'distance', 'active-calories', 'exercise-minutes', 'workouts', 'resting-heart-rate'],
50
+ },
51
+ authSchema: {
52
+ methods: [{ type: 'none' }],
53
+ },
54
+ feeds: {
55
+ daily_summaries: {
56
+ key: 'daily_summaries',
57
+ name: 'Daily summaries',
58
+ description:
59
+ 'Daily Apple Health activity summaries: steps, distance, active energy, ' +
60
+ 'exercise minutes, and resting heart rate.',
61
+ configSchema: {
62
+ type: 'object',
63
+ properties: {
64
+ backfill_days: {
65
+ type: 'integer',
66
+ minimum: 1,
67
+ maximum: 3650,
68
+ default: 30,
69
+ description: 'How many days the bridge backfills on a fresh sync (incremental syncs only re-query changed days).',
70
+ },
71
+ },
72
+ },
73
+ eventKinds: {
74
+ health_daily_summary: {
75
+ description: 'A daily summary of Apple Health activity data.',
76
+ metadataSchema: {
77
+ type: 'object',
78
+ required: ['source', 'origin_id', 'date'],
79
+ properties: {
80
+ source: { type: 'string', const: 'apple_health' },
81
+ origin_id: { type: 'string' },
82
+ date: { type: 'string', format: 'date' },
83
+ steps: { type: 'number' },
84
+ distance_m: { type: 'number' },
85
+ active_energy_kcal: { type: 'number' },
86
+ exercise_minutes: { type: 'number' },
87
+ resting_heart_rate_bpm: { type: ['number', 'null'] },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ workouts: {
94
+ key: 'workouts',
95
+ name: 'Workouts',
96
+ description: 'Workout sessions recorded in Apple Health.',
97
+ configSchema: {
98
+ type: 'object',
99
+ properties: {
100
+ backfill_days: {
101
+ type: 'integer',
102
+ minimum: 1,
103
+ maximum: 3650,
104
+ default: 30,
105
+ description: 'How many days the bridge backfills on a fresh sync.',
106
+ },
107
+ },
108
+ },
109
+ eventKinds: {
110
+ health_workout: {
111
+ description: 'A workout recorded in Apple Health.',
112
+ metadataSchema: {
113
+ type: 'object',
114
+ required: ['source', 'origin_id', 'workout_type'],
115
+ properties: {
116
+ source: { type: 'string', const: 'apple_health' },
117
+ origin_id: { type: 'string' },
118
+ workout_type: { type: 'string' },
119
+ started_at: { type: 'string' },
120
+ duration_s: { type: 'number' },
121
+ active_energy_kcal: { type: ['number', 'null'] },
122
+ distance_m: { type: ['number', 'null'] },
123
+ },
124
+ },
125
+ },
126
+ },
127
+ },
128
+ },
129
+ };
130
+
131
+ async sync(_ctx: SyncContext): Promise<SyncResult> {
132
+ throw new Error(BRIDGE_ONLY_MESSAGE);
133
+ }
134
+
135
+ async execute(): Promise<ActionResult> {
136
+ throw new Error(BRIDGE_ONLY_MESSAGE);
137
+ }
138
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Apple Screen Time Connector (V1 runtime) — Lobu for Mac only.
3
+ *
4
+ * Runs on Lobu for Mac, which reads `~/Library/Application Support/
5
+ * Knowledge/knowledgeC.db` (the on-device Knowledge store backing Apple's
6
+ * Settings → Screen Time UI). With Full Disk Access granted, the Mac can
7
+ * pull per-app foreground time by day for both Mac usage and (if the user
8
+ * enables Screen Time iCloud sync) iOS usage.
9
+ *
10
+ * iOS does NOT advertise the `screentime` capability — Apple's
11
+ * FamilyControls + DeviceActivityReport design prevents per-app data from
12
+ * leaving the device on iOS. The Mac path is the workable one.
13
+ */
14
+
15
+ import {
16
+ type ActionResult,
17
+ type ConnectorDefinition,
18
+ ConnectorRuntime,
19
+ type SyncContext,
20
+ type SyncResult,
21
+ } from '@lobu/connector-sdk';
22
+
23
+ const BRIDGE_ONLY =
24
+ 'Apple Screen Time runs only on a worker advertising capability "screentime" (Lobu for Mac with Full Disk Access).';
25
+
26
+ export default class AppleScreenTimeConnector extends ConnectorRuntime {
27
+ readonly definition: ConnectorDefinition = {
28
+ key: 'apple.screen_time',
29
+ name: 'Apple Screen Time',
30
+ description:
31
+ 'Daily per-app usage totals from Lobu for Mac, sourced from the Apple Knowledge store. Captures both Mac usage and (if Screen Time iCloud sync is on) the user\'s iOS device usage.',
32
+ version: '0.1.0',
33
+ faviconDomain: 'apple.com',
34
+ requiredCapability: 'screentime',
35
+ runtime: { platforms: ['macos'] },
36
+ authSchema: { methods: [{ type: 'none' }] },
37
+ feeds: {
38
+ daily_app_usage: {
39
+ key: 'daily_app_usage',
40
+ name: 'Daily app usage',
41
+ description:
42
+ 'Per-day total foreground time for each application (identified by bundle id).',
43
+ configSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ backfill_days: {
47
+ type: 'integer',
48
+ minimum: 1,
49
+ maximum: 90,
50
+ default: 14,
51
+ description: 'How many days the bridge should backfill on each sync.',
52
+ },
53
+ },
54
+ },
55
+ eventKinds: {
56
+ screen_time_daily_app: {
57
+ description: 'Total time the user spent in one application on a given day.',
58
+ metadataSchema: {
59
+ type: 'object',
60
+ required: ['source', 'origin_id', 'date', 'bundle_id', 'seconds'],
61
+ properties: {
62
+ source: { type: 'string', const: 'apple_screen_time' },
63
+ origin_id: { type: 'string' },
64
+ date: { type: 'string', format: 'date' },
65
+ bundle_id: { type: 'string' },
66
+ seconds: { type: 'number', minimum: 0 },
67
+ },
68
+ },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ };
74
+
75
+ async sync(_ctx: SyncContext): Promise<SyncResult> {
76
+ throw new Error(BRIDGE_ONLY);
77
+ }
78
+
79
+ async execute(): Promise<ActionResult> {
80
+ throw new Error(BRIDGE_ONLY);
81
+ }
82
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Shared utilities for browser-based scraper connectors.
3
+ *
4
+ * Provides common patterns used across Trustpilot, G2, Glassdoor, Capterra,
5
+ * and similar connectors that launch a stealth browser and scrape review pages.
6
+ */
7
+
8
+ import {
9
+ acquireBrowser,
10
+ type CdpPage,
11
+ captureErrorArtifacts,
12
+ type EventEnvelope,
13
+ } from '@lobu/connector-sdk';
14
+ import type { Browser, Cookie, Page } from 'playwright';
15
+
16
+ // -----------------------------------------------------------------------------
17
+ // Browser auth cookie helpers
18
+ // -----------------------------------------------------------------------------
19
+
20
+ export function getBrowserCookies(
21
+ checkpoint: Record<string, unknown> | null,
22
+ sessionState: Record<string, unknown> | null | undefined,
23
+ connectorKey: string
24
+ ): any[] {
25
+ const sessionCookies = (sessionState?.cookies as any[]) ?? [];
26
+ const cookies = (checkpoint as any)?.cookies ?? sessionCookies;
27
+ // Device-bound browser profiles ship cookies via --user-data-dir on disk
28
+ // rather than this jsonb blob; the persistent context loads them itself.
29
+ if ((!cookies || cookies.length === 0) && !sessionState?.user_data_dir) {
30
+ throw new Error(
31
+ `No browser cookies found. Run: lobu memory browser-auth --connector ${connectorKey} --auth-profile-slug <SLUG>`
32
+ );
33
+ }
34
+ return cookies ?? [];
35
+ }
36
+
37
+ /**
38
+ * Pull the device-bound managed --user-data-dir from session_state, if the
39
+ * connection's auth profile is owned by a device worker. When set, callers
40
+ * should pass it to openStealthBrowser instead of relying on the cookies/CDP
41
+ * cascade — Chrome reads cookies from that profile dir directly.
42
+ */
43
+ export function getBrowserUserDataDir(
44
+ sessionState: Record<string, unknown> | null | undefined
45
+ ): string | undefined {
46
+ const value = sessionState?.user_data_dir;
47
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
48
+ }
49
+
50
+ /**
51
+ * Pull the device-bound CDP endpoint URL from session_state (set when the
52
+ * user picked "Attach via CDP" mode on their browser profile). When set,
53
+ * callers should pass it through as `cdpUrl` so the connector attaches to
54
+ * the exact running Chrome the user chose — instead of `'auto'`, which can
55
+ * land on the wrong browser when several debuggable Chromiums are running
56
+ * or a non-default port was configured.
57
+ */
58
+ export function getBrowserCdpUrl(
59
+ sessionState: Record<string, unknown> | null | undefined
60
+ ): string | undefined {
61
+ const value = sessionState?.cdp_url;
62
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
63
+ }
64
+
65
+ export function validateCookieNotExpired(
66
+ cookies: any[],
67
+ cookieName: string,
68
+ connectorKey: string
69
+ ): void {
70
+ const cookie = cookies.find((c: any) => c.name === cookieName);
71
+ if (cookie?.expires && cookie.expires > 0) {
72
+ const expiresAt = new Date(cookie.expires * 1000);
73
+ if (expiresAt < new Date()) {
74
+ throw new Error(
75
+ `${cookieName} expired on ${expiresAt.toISOString()}. Re-run: lobu memory browser-auth --connector ${connectorKey} --auth-profile-slug <SLUG>`
76
+ );
77
+ }
78
+ }
79
+ }
80
+
81
+ // -----------------------------------------------------------------------------
82
+ // URL validation
83
+ // -----------------------------------------------------------------------------
84
+
85
+ /**
86
+ * Validate that a URL is well-formed, uses HTTPS, and belongs to the expected
87
+ * domain (hostname ends with `expectedDomain`).
88
+ *
89
+ * @throws If the URL is invalid, not HTTPS, or on the wrong domain.
90
+ */
91
+ export function validateUrlDomain(url: string, expectedDomain: string): void {
92
+ let parsed: URL;
93
+ try {
94
+ parsed = new URL(url);
95
+ } catch {
96
+ throw new Error(`Invalid ${expectedDomain} URL: ${url}`);
97
+ }
98
+ if (parsed.protocol !== 'https:') {
99
+ throw new Error(`${expectedDomain} URL must use https: protocol, got ${parsed.protocol}`);
100
+ }
101
+ if (
102
+ parsed.hostname !== expectedDomain &&
103
+ !parsed.hostname.endsWith(`.${expectedDomain}`)
104
+ ) {
105
+ throw new Error(`URL must be on ${expectedDomain}, got ${parsed.hostname}`);
106
+ }
107
+ }
108
+
109
+ // -----------------------------------------------------------------------------
110
+ // Browser lifecycle
111
+ // -----------------------------------------------------------------------------
112
+
113
+ export interface BrowserSession {
114
+ /** Playwright Browser (null when using raw CDP). */
115
+ browser: Browser | null;
116
+ /** Page handle — Playwright Page or CdpPage. Both support goto/evaluate/waitForSelector. */
117
+ page: Page | CdpPage;
118
+ screenshotDir: string;
119
+ /** Which backend was used ('cdp' or 'playwright'). */
120
+ backend: 'cdp' | 'playwright';
121
+ /** If false, don't close the browser (CDP — it's the user's Chrome). */
122
+ ownsBrowser: boolean;
123
+ }
124
+
125
+ /**
126
+ * Acquire a stealth browser session.
127
+ *
128
+ * By default launches a fresh Playwright browser (safe for DOM scraping).
129
+ * Pass `cdpUrl: 'auto'` to try CDP first — uses raw CDP protocol to avoid
130
+ * Playwright's connectOverCDP crash on browsers with many tabs.
131
+ */
132
+ export async function openStealthBrowser(opts?: {
133
+ cdpUrl?: string | 'auto' | null;
134
+ cookies?: Cookie[];
135
+ authDomains?: string[];
136
+ userDataDir?: string;
137
+ }): Promise<BrowserSession> {
138
+ const acquired = await acquireBrowser({
139
+ cdpUrl: opts?.userDataDir ? null : (opts?.cdpUrl ?? null),
140
+ cookies: opts?.cookies ?? [],
141
+ authDomains: opts?.authDomains ?? [],
142
+ stealth: true,
143
+ userDataDir: opts?.userDataDir,
144
+ });
145
+
146
+ const page = acquired.cdpPage ?? acquired.page;
147
+ if (!page) throw new Error('No page available from browser acquisition');
148
+
149
+ return {
150
+ browser: acquired.browser,
151
+ page,
152
+ screenshotDir: acquired.screenshotDir,
153
+ backend: acquired.backend,
154
+ ownsBrowser: acquired.ownsBrowser,
155
+ };
156
+ }
157
+
158
+ // -----------------------------------------------------------------------------
159
+ // Cookie consent
160
+ // -----------------------------------------------------------------------------
161
+
162
+ /**
163
+ * Attempt to dismiss a cookie consent banner by clicking an accept button.
164
+ *
165
+ * @param page - Playwright page instance
166
+ * @param selector - CSS selector for the accept/dismiss button
167
+ * @param timeout - How long to wait for the button to appear (ms, default 2000)
168
+ */
169
+ export async function handleCookieConsent(
170
+ page: Page | CdpPage,
171
+ selector: string,
172
+ timeout = 2000
173
+ ): Promise<void> {
174
+ try {
175
+ const found = await page.waitForSelector(selector, { timeout });
176
+ if (found) {
177
+ // CdpPage.waitForSelector returns boolean, Playwright returns ElementHandle
178
+ if (typeof found === 'boolean') {
179
+ await (page as CdpPage).click(selector);
180
+ } else {
181
+ await found.click();
182
+ }
183
+ }
184
+ } catch {
185
+ // No cookie banner found or already dismissed — continue
186
+ }
187
+ }
188
+
189
+ // -----------------------------------------------------------------------------
190
+ // Checkpoint filtering
191
+ // -----------------------------------------------------------------------------
192
+
193
+ /**
194
+ * Filter events that are newer than the checkpoint's `last_timestamp`.
195
+ * If no checkpoint is set, all events are returned.
196
+ */
197
+ export function filterByCheckpoint(
198
+ events: EventEnvelope[],
199
+ checkpoint: Record<string, unknown> | null
200
+ ): EventEnvelope[] {
201
+ const lastTimestamp = checkpoint?.last_timestamp as string | undefined;
202
+ if (!lastTimestamp) return events;
203
+
204
+ const cutoff = new Date(lastTimestamp);
205
+ return events.filter((e) => e.occurred_at > cutoff);
206
+ }
207
+
208
+ // -----------------------------------------------------------------------------
209
+ // Error handling with browser cleanup
210
+ // -----------------------------------------------------------------------------
211
+
212
+ /**
213
+ * Run a scraper function inside a try/catch that captures error artifacts
214
+ * (screenshot + HTML snapshot) and ensures the browser is always closed.
215
+ *
216
+ * @param session - The browser session from `openStealthBrowser()`
217
+ * @param connectorName - Short name used for artifact filenames (e.g. "trustpilot-sync")
218
+ * @param fn - The async scraper logic receiving the page
219
+ * @returns - Whatever `fn` returns
220
+ */
221
+ export async function withBrowserErrorCapture<T>(
222
+ session: BrowserSession,
223
+ connectorName: string,
224
+ fn: (page: Page | CdpPage) => Promise<T>
225
+ ): Promise<T> {
226
+ try {
227
+ return await fn(session.page);
228
+ } catch (error: any) {
229
+ // captureErrorArtifacts only works with Playwright pages
230
+ if (session.backend === 'playwright' && session.page) {
231
+ await captureErrorArtifacts(
232
+ session.page as Page,
233
+ error,
234
+ connectorName,
235
+ session.screenshotDir
236
+ );
237
+ }
238
+ throw error;
239
+ } finally {
240
+ if (session.backend === 'cdp') {
241
+ await (session.page as CdpPage).close();
242
+ } else if (session.ownsBrowser && session.browser) {
243
+ await session.browser.close();
244
+ }
245
+ }
246
+ }