@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,125 @@
1
+ /**
2
+ * WhatsApp (local) Connector — Lobu for Mac only.
3
+ *
4
+ * Reads messages directly from the WhatsApp Desktop app's local SQLite store
5
+ * at `~/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/
6
+ * ChatStorage.sqlite`. Lobu for Mac snapshots the DB read-only, walks new
7
+ * rows since the last `Z_PK` checkpoint, and emits events that share the
8
+ * `whatsapp` connector's metadata shape so downstream entity links work
9
+ * identically.
10
+ *
11
+ * Differences from the QR-paired `whatsapp` connector:
12
+ * - No Baileys, no socket, no phone-offline auto-unlink (WA Desktop itself
13
+ * is the linked device).
14
+ * - Ciphertext never leaves the Mac.
15
+ * - Bound to one specific Mac; requires WhatsApp Desktop installed.
16
+ */
17
+
18
+ import {
19
+ type ActionResult,
20
+ type ConnectorDefinition,
21
+ ConnectorRuntime,
22
+ IDENTITY,
23
+ type SyncContext,
24
+ type SyncResult,
25
+ } from '@lobu/connector-sdk';
26
+
27
+ const BRIDGE_ONLY =
28
+ 'WhatsApp (local) runs only on a worker advertising capability "whatsapp_local" (Lobu for Mac with WhatsApp Desktop installed).';
29
+
30
+ export default class WhatsAppLocalConnector extends ConnectorRuntime {
31
+ readonly definition: ConnectorDefinition = {
32
+ key: 'whatsapp.local',
33
+ name: 'WhatsApp (this Mac)',
34
+ description:
35
+ "Reads messages from the WhatsApp Desktop app's local archive on this Mac. No QR pairing, no phone-offline auto-unlink — the desktop app is itself the linked device.",
36
+ version: '0.1.0',
37
+ faviconDomain: 'whatsapp.com',
38
+ requiredCapability: 'whatsapp_local',
39
+ runtime: { platforms: ['macos'] },
40
+ authSchema: { methods: [{ type: 'none' }] },
41
+ feeds: {
42
+ messages: {
43
+ key: 'messages',
44
+ name: 'Messages',
45
+ description:
46
+ 'Personal WhatsApp messages from 1:1 and group chats, sourced from WhatsApp Desktop.',
47
+ configSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ chat_filter: {
51
+ type: 'string',
52
+ enum: ['all', 'individual', 'group'],
53
+ default: 'all',
54
+ description: 'Which chats to include.',
55
+ },
56
+ max_messages_per_sync: {
57
+ type: 'integer',
58
+ minimum: 1,
59
+ maximum: 500000,
60
+ default: 5000,
61
+ description:
62
+ 'Safety cap on messages collected per sync. The first sync drains all messages up to this cap; subsequent syncs ingest only new messages, so the cap rarely binds.',
63
+ },
64
+ },
65
+ },
66
+ eventKinds: {
67
+ message: {
68
+ description: 'A WhatsApp message (text, caption, or system).',
69
+ metadataSchema: {
70
+ type: 'object',
71
+ properties: {
72
+ source: { type: 'string', const: 'whatsapp_local' },
73
+ chat_jid: { type: 'string' },
74
+ is_group: { type: 'boolean' },
75
+ from_me: { type: 'boolean' },
76
+ participant: { type: 'string' },
77
+ sender_jid: { type: 'string' },
78
+ sender_phone: { type: 'string' },
79
+ push_name: { type: 'string' },
80
+ media_type: { type: 'string' },
81
+ quoted_id: { type: 'string' },
82
+ is_forwarded: { type: 'boolean' },
83
+ is_starred: { type: 'boolean' },
84
+ is_system_event: { type: 'boolean' },
85
+ voice_note_skipped: {
86
+ type: 'string',
87
+ enum: ['not_downloaded', 'too_large', 'empty', 'read_error', 'invalid_path'],
88
+ },
89
+ },
90
+ },
91
+ entityLinks: [
92
+ {
93
+ entityType: 'person',
94
+ autoCreate: true,
95
+ titlePath: 'metadata.push_name',
96
+ identities: [
97
+ { namespace: IDENTITY.WA_JID, eventPath: 'metadata.sender_jid' },
98
+ { namespace: IDENTITY.PHONE, eventPath: 'metadata.sender_phone' },
99
+ ],
100
+ traits: {
101
+ push_name: {
102
+ eventPath: 'metadata.push_name',
103
+ behavior: 'prefer_non_empty',
104
+ },
105
+ last_seen_at: {
106
+ eventPath: 'occurred_at',
107
+ behavior: 'overwrite',
108
+ },
109
+ },
110
+ },
111
+ ],
112
+ },
113
+ },
114
+ },
115
+ },
116
+ };
117
+
118
+ async sync(_ctx: SyncContext): Promise<SyncResult> {
119
+ throw new Error(BRIDGE_ONLY);
120
+ }
121
+
122
+ async execute(): Promise<ActionResult> {
123
+ throw new Error(BRIDGE_ONLY);
124
+ }
125
+ }
@@ -0,0 +1,536 @@
1
+ /**
2
+ * X (Twitter) Connector (V1 runtime)
3
+ *
4
+ * Supports two auth modes:
5
+ * - OAuth 2.0 user context against the X API v2 (preferred when available)
6
+ * - Browser session cookies for scraping/network interception fallback
7
+ */
8
+
9
+ import {
10
+ type ActionContext,
11
+ type ActionResult,
12
+ browserNetworkSync,
13
+ type ConnectorDefinition,
14
+ ConnectorRuntime,
15
+ calculateEngagementScore,
16
+ type EventEnvelope,
17
+ type SyncContext,
18
+ type SyncResult,
19
+ } from '@lobu/connector-sdk';
20
+ import {
21
+ getBrowserCdpUrl,
22
+ getBrowserCookies,
23
+ getBrowserUserDataDir,
24
+ validateCookieNotExpired,
25
+ } from './browser-scraper-utils';
26
+
27
+ interface XCheckpoint {
28
+ last_tweet_id?: string;
29
+ last_timestamp?: Date | string;
30
+ }
31
+
32
+ interface XTweet {
33
+ id: string;
34
+ text: string;
35
+ username: string;
36
+ likes: number;
37
+ retweets: number;
38
+ replies: number;
39
+ quotes: number;
40
+ publishedAt: Date;
41
+ isRetweet: boolean;
42
+ isReply: boolean;
43
+ isQuote: boolean;
44
+ conversationId?: string;
45
+ inReplyToId?: string;
46
+ }
47
+
48
+ interface XApiTweetRecord {
49
+ id: string;
50
+ text?: string;
51
+ author_id?: string;
52
+ created_at?: string;
53
+ conversation_id?: string;
54
+ public_metrics?: {
55
+ like_count?: number;
56
+ retweet_count?: number;
57
+ reply_count?: number;
58
+ quote_count?: number;
59
+ };
60
+ referenced_tweets?: Array<{ type?: string; id?: string }>;
61
+ }
62
+
63
+ interface XApiUserRecord {
64
+ id: string;
65
+ username?: string;
66
+ name?: string;
67
+ }
68
+
69
+ interface XApiListResponse {
70
+ data?: XApiTweetRecord[];
71
+ includes?: {
72
+ users?: XApiUserRecord[];
73
+ };
74
+ meta?: {
75
+ next_token?: string;
76
+ result_count?: number;
77
+ };
78
+ errors?: Array<{ detail?: string; message?: string }>;
79
+ }
80
+
81
+ function normalizeHandle(input: string | undefined): string | null {
82
+ if (!input) return null;
83
+ const trimmed = input.trim().replace(/^@+/, '');
84
+ if (!trimmed) return null;
85
+ const match = trimmed.match(/^[A-Za-z0-9_]{1,15}/);
86
+ return match?.[0] ?? null;
87
+ }
88
+
89
+ function buildSearchQuery(config: Record<string, unknown>): string {
90
+ const explicitSearchQuery =
91
+ typeof config.search_query === 'string' ? config.search_query.trim() : '';
92
+ if (explicitSearchQuery.length > 0) {
93
+ return explicitSearchQuery;
94
+ }
95
+
96
+ const accountHandle = normalizeHandle(
97
+ typeof config.account_handle === 'string' ? config.account_handle : undefined
98
+ );
99
+ if (!accountHandle) {
100
+ throw new Error('search_query or account_handle is required');
101
+ }
102
+
103
+ return `from:${accountHandle}`;
104
+ }
105
+
106
+ function buildApiTweet(
107
+ tweet: XApiTweetRecord,
108
+ usernameById: Map<string, string>,
109
+ defaultUsername?: string
110
+ ): XTweet | null {
111
+ if (!tweet.id || !tweet.text || !tweet.created_at) return null;
112
+
113
+ const referenced = tweet.referenced_tweets ?? [];
114
+ const publicMetrics = tweet.public_metrics ?? {};
115
+ const inReplyToId = referenced.find((ref) => ref.type === 'replied_to')?.id;
116
+
117
+ return {
118
+ id: tweet.id,
119
+ text: tweet.text,
120
+ username: usernameById.get(tweet.author_id ?? '') ?? defaultUsername ?? '',
121
+ likes: publicMetrics.like_count ?? 0,
122
+ retweets: publicMetrics.retweet_count ?? 0,
123
+ replies: publicMetrics.reply_count ?? 0,
124
+ quotes: publicMetrics.quote_count ?? 0,
125
+ publishedAt: new Date(tweet.created_at),
126
+ isRetweet: referenced.some((ref) => ref.type === 'retweeted'),
127
+ isReply: Boolean(inReplyToId),
128
+ isQuote: referenced.some((ref) => ref.type === 'quoted'),
129
+ conversationId: tweet.conversation_id,
130
+ inReplyToId,
131
+ };
132
+ }
133
+
134
+ function parseApiListResponse(json: XApiListResponse, defaultUsername?: string): XTweet[] {
135
+ const users = json.includes?.users ?? [];
136
+ const usernameById = new Map(users.map((user) => [user.id, user.username ?? '']));
137
+
138
+ return (json.data ?? [])
139
+ .map((tweet) => buildApiTweet(tweet, usernameById, defaultUsername))
140
+ .filter((tweet): tweet is XTweet => tweet !== null);
141
+ }
142
+
143
+ /** Extract tweets from X's GraphQL SearchTimeline response */
144
+ function parseBrowserSearchResponse(_url: string, json: unknown): XTweet[] {
145
+ const tweets: XTweet[] = [];
146
+ const data = json as any;
147
+
148
+ const instructions =
149
+ data?.data?.search_by_raw_query?.search_timeline?.timeline?.instructions ?? [];
150
+
151
+ for (const instruction of instructions) {
152
+ const entries = instruction.entries ?? instruction.moduleItems ?? [];
153
+ for (const entry of entries) {
154
+ const result =
155
+ entry?.content?.itemContent?.tweet_results?.result ??
156
+ entry?.item?.itemContent?.tweet_results?.result;
157
+ if (!result) continue;
158
+
159
+ const legacy = result.legacy ?? result.tweet?.legacy;
160
+ if (!legacy?.full_text) continue;
161
+
162
+ const userResult =
163
+ result.core?.user_results?.result ?? result.tweet?.core?.user_results?.result;
164
+ const screenName = userResult?.core?.screen_name ?? userResult?.legacy?.screen_name ?? '';
165
+
166
+ tweets.push({
167
+ id: legacy.id_str ?? result.rest_id ?? entry.entryId,
168
+ text: legacy.full_text,
169
+ username: screenName,
170
+ likes: legacy.favorite_count ?? 0,
171
+ retweets: legacy.retweet_count ?? 0,
172
+ replies: legacy.reply_count ?? 0,
173
+ quotes: legacy.quote_count ?? 0,
174
+ publishedAt: new Date(legacy.created_at),
175
+ isRetweet: !!legacy.retweeted_status_result,
176
+ isReply: !!legacy.in_reply_to_status_id_str,
177
+ isQuote: !!legacy.is_quote_status,
178
+ conversationId: legacy.conversation_id_str,
179
+ inReplyToId: legacy.in_reply_to_status_id_str,
180
+ });
181
+ }
182
+ }
183
+
184
+ return tweets;
185
+ }
186
+
187
+ function tweetToEvent(tweet: XTweet): EventEnvelope {
188
+ const engagementData = {
189
+ reply_count: tweet.replies,
190
+ upvotes: tweet.likes,
191
+ score: tweet.retweets * 2 + tweet.likes,
192
+ };
193
+
194
+ return {
195
+ origin_id: tweet.id,
196
+ payload_text: tweet.text,
197
+ author_name: tweet.username ? `@${tweet.username}` : undefined,
198
+ occurred_at: tweet.publishedAt,
199
+ origin_type: tweet.isReply ? 'reply' : 'tweet',
200
+ score: calculateEngagementScore('x', engagementData),
201
+ source_url: `https://x.com/${tweet.username || 'i'}/status/${tweet.id}`,
202
+ origin_parent_id: tweet.inReplyToId || undefined,
203
+ metadata: {
204
+ ...engagementData,
205
+ retweet_count: tweet.retweets,
206
+ quote_count: tweet.quotes,
207
+ is_retweet: tweet.isRetweet,
208
+ is_reply: tweet.isReply,
209
+ is_quote: tweet.isQuote,
210
+ ...(tweet.conversationId ? { conversation_id: tweet.conversationId } : {}),
211
+ },
212
+ };
213
+ }
214
+
215
+ function finalizeSyncResult(
216
+ tweets: XTweet[],
217
+ checkpoint: XCheckpoint,
218
+ metadata: Record<string, unknown>,
219
+ authUpdate?: Record<string, unknown> | null
220
+ ): SyncResult {
221
+ const seenIds = new Set<string>();
222
+ const deduped = tweets.filter((tweet) => {
223
+ if (!tweet.id || !tweet.text || seenIds.has(tweet.id)) return false;
224
+ seenIds.add(tweet.id);
225
+ if (checkpoint.last_tweet_id && tweet.id === checkpoint.last_tweet_id) return false;
226
+ return true;
227
+ });
228
+
229
+ const events: EventEnvelope[] = deduped.map(tweetToEvent);
230
+ events.sort((a, b) => new Date(b.occurred_at).getTime() - new Date(a.occurred_at).getTime());
231
+
232
+ const newestTweetId = events.length > 0 ? events[0].origin_id : checkpoint.last_tweet_id;
233
+ const newCheckpoint: XCheckpoint = {
234
+ last_tweet_id: newestTweetId,
235
+ last_timestamp: events.length > 0 ? events[0].occurred_at : checkpoint.last_timestamp,
236
+ };
237
+
238
+ return {
239
+ events,
240
+ checkpoint: newCheckpoint as unknown as Record<string, unknown>,
241
+ ...(authUpdate ? { auth_update: authUpdate } : {}),
242
+ metadata: {
243
+ items_found: events.length,
244
+ items_skipped: tweets.length - deduped.length,
245
+ ...metadata,
246
+ },
247
+ };
248
+ }
249
+
250
+ async function fetchJson<T>(url: URL, accessToken: string): Promise<T> {
251
+ const response = await fetch(url, {
252
+ headers: {
253
+ Authorization: `Bearer ${accessToken}`,
254
+ 'Content-Type': 'application/json',
255
+ },
256
+ });
257
+
258
+ if (!response.ok) {
259
+ const text = await response.text();
260
+ throw new Error(`X API request failed (${response.status}): ${text}`);
261
+ }
262
+
263
+ return (await response.json()) as T;
264
+ }
265
+
266
+ async function resolveUserId(handle: string, accessToken: string): Promise<string> {
267
+ const url = new URL(`https://api.x.com/2/users/by/username/${encodeURIComponent(handle)}`);
268
+ const json = await fetchJson<{ data?: { id?: string } }>(url, accessToken);
269
+ const userId = json.data?.id;
270
+ if (!userId) {
271
+ throw new Error(`Could not resolve X user id for @${handle}`);
272
+ }
273
+ return userId;
274
+ }
275
+
276
+ async function syncViaOAuthApi(
277
+ ctx: SyncContext,
278
+ config: Record<string, unknown>,
279
+ checkpoint: XCheckpoint
280
+ ): Promise<SyncResult> {
281
+ const accessToken = ctx.credentials?.accessToken;
282
+ if (!accessToken) {
283
+ throw new Error('OAuth access token missing for X connector');
284
+ }
285
+
286
+ const maxPages = Math.max(1, Math.min(50, Number(config.max_scrolls ?? 10) || 10));
287
+ const accountHandle = normalizeHandle(
288
+ typeof config.account_handle === 'string' ? config.account_handle : undefined
289
+ );
290
+ const explicitSearchQuery =
291
+ typeof config.search_query === 'string' ? config.search_query.trim() : '';
292
+
293
+ const tweets: XTweet[] = [];
294
+ let pageCount = 0;
295
+ let nextToken: string | undefined;
296
+
297
+ if (explicitSearchQuery.length === 0 && accountHandle) {
298
+ const userId = await resolveUserId(accountHandle, accessToken);
299
+
300
+ for (let page = 0; page < maxPages; page += 1) {
301
+ const url = new URL(`https://api.x.com/2/users/${encodeURIComponent(userId)}/tweets`);
302
+ url.searchParams.set('max_results', '100');
303
+ url.searchParams.set(
304
+ 'tweet.fields',
305
+ 'author_id,conversation_id,created_at,public_metrics,referenced_tweets'
306
+ );
307
+ if (checkpoint.last_tweet_id) {
308
+ url.searchParams.set('since_id', checkpoint.last_tweet_id);
309
+ }
310
+ if (nextToken) {
311
+ url.searchParams.set('pagination_token', nextToken);
312
+ }
313
+
314
+ const json = await fetchJson<XApiListResponse>(url, accessToken);
315
+ tweets.push(...parseApiListResponse(json, accountHandle));
316
+ pageCount += 1;
317
+
318
+ nextToken = json.meta?.next_token;
319
+ if (!nextToken) break;
320
+ }
321
+ } else {
322
+ const searchQuery = buildSearchQuery(config);
323
+
324
+ for (let page = 0; page < maxPages; page += 1) {
325
+ const url = new URL('https://api.x.com/2/tweets/search/recent');
326
+ url.searchParams.set('query', searchQuery);
327
+ url.searchParams.set('max_results', '100');
328
+ url.searchParams.set(
329
+ 'tweet.fields',
330
+ 'author_id,conversation_id,created_at,public_metrics,referenced_tweets'
331
+ );
332
+ url.searchParams.set('expansions', 'author_id');
333
+ url.searchParams.set('user.fields', 'username');
334
+ if (checkpoint.last_tweet_id) {
335
+ url.searchParams.set('since_id', checkpoint.last_tweet_id);
336
+ }
337
+ if (nextToken) {
338
+ url.searchParams.set('next_token', nextToken);
339
+ }
340
+
341
+ const json = await fetchJson<XApiListResponse>(url, accessToken);
342
+ tweets.push(...parseApiListResponse(json));
343
+ pageCount += 1;
344
+
345
+ nextToken = json.meta?.next_token;
346
+ if (!nextToken) break;
347
+ }
348
+ }
349
+
350
+ return finalizeSyncResult(tweets, checkpoint, {
351
+ backend: 'oauth_api',
352
+ api_calls: pageCount,
353
+ });
354
+ }
355
+
356
+ async function syncViaBrowser(
357
+ ctx: SyncContext,
358
+ config: Record<string, unknown>,
359
+ checkpoint: XCheckpoint
360
+ ): Promise<SyncResult> {
361
+ const searchQuery = buildSearchQuery(config);
362
+ const maxScrolls = (config.max_scrolls as number) ?? 10;
363
+ const searchFilter = (config.search_filter as string) ?? 'live';
364
+ const searchUrl = `https://x.com/search?q=${encodeURIComponent(searchQuery)}&src=typed_query&f=${searchFilter}`;
365
+
366
+ const userDataDir = getBrowserUserDataDir(ctx.sessionState);
367
+ const cdpUrl = getBrowserCdpUrl(ctx.sessionState) ?? 'auto';
368
+ let cookies: any[] = [];
369
+ if (!userDataDir) {
370
+ try {
371
+ cookies = getBrowserCookies(ctx.checkpoint as any, ctx.sessionState as any, 'x');
372
+ validateCookieNotExpired(cookies, 'auth_token', 'x');
373
+ } catch {
374
+ // No stored cookies — CDP will be the only path
375
+ }
376
+ }
377
+
378
+ const result = await browserNetworkSync<XTweet>({
379
+ config: {
380
+ interceptPatterns: [/\/i\/api\/graphql\/.*Search/],
381
+ authDomains: ['x.com', '.x.com'],
382
+ maxScrolls,
383
+ scrollDelayMs: 2000,
384
+ responseTimeoutMs: 5000,
385
+ navigationTimeoutMs: 15000,
386
+ },
387
+ url: searchUrl,
388
+ cdpUrl,
389
+ cookies,
390
+ userDataDir,
391
+ parseResponse: parseBrowserSearchResponse,
392
+ checkAuth: async (page) => {
393
+ const url = page.url();
394
+ return !url.includes('/login') && !url.includes('/i/flow/login');
395
+ },
396
+ });
397
+
398
+ return finalizeSyncResult(
399
+ result.items,
400
+ checkpoint,
401
+ {
402
+ backend: result.backend,
403
+ api_calls: result.apiCallCount,
404
+ },
405
+ { cookies: result.cookies }
406
+ );
407
+ }
408
+
409
+ const configSchema = {
410
+ type: 'object',
411
+ anyOf: [{ required: ['search_query'] }, { required: ['account_handle'] }],
412
+ properties: {
413
+ search_query: {
414
+ type: 'string',
415
+ minLength: 1,
416
+ description: 'Search query for tweets (e.g., "nodejs", "#programming", "from:user")',
417
+ },
418
+ account_handle: {
419
+ type: 'string',
420
+ minLength: 1,
421
+ description:
422
+ 'Optional X handle to track directly (e.g. "openai" or "@openai"). Used when search_query is omitted.',
423
+ },
424
+ search_filter: {
425
+ type: 'string',
426
+ enum: ['live', 'top'],
427
+ default: 'live',
428
+ description:
429
+ 'Search tab: "live" for Latest (chronological), "top" for Top (popular/algorithmic)',
430
+ },
431
+ max_scrolls: {
432
+ type: 'integer',
433
+ minimum: 1,
434
+ maximum: 50,
435
+ default: 10,
436
+ description: 'Maximum pagination iterations (default: 10, API pages or browser scrolls)',
437
+ },
438
+ },
439
+ };
440
+
441
+ const engagementMetadataSchema = {
442
+ type: 'object',
443
+ properties: {
444
+ reply_count: { type: 'number' },
445
+ upvotes: { type: 'number', description: 'Likes' },
446
+ score: { type: 'number' },
447
+ retweet_count: { type: 'number' },
448
+ quote_count: { type: 'number' },
449
+ is_retweet: { type: 'boolean' },
450
+ is_reply: { type: 'boolean' },
451
+ is_quote: { type: 'boolean' },
452
+ },
453
+ };
454
+
455
+ export default class XConnector extends ConnectorRuntime {
456
+ readonly definition: ConnectorDefinition = {
457
+ key: 'x',
458
+ name: 'X (Twitter)',
459
+ description: 'Fetches tweets via the X API v2 with browser-cookie fallback.',
460
+ version: '2.1.0',
461
+ faviconDomain: 'x.com',
462
+ authSchema: {
463
+ methods: [
464
+ {
465
+ type: 'oauth',
466
+ provider: 'twitter',
467
+ requiredScopes: ['tweet.read', 'users.read', 'offline.access'],
468
+ optionalScopes: ['users.email', 'follows.read', 'like.read', 'bookmark.read'],
469
+ loginScopes: ['users.read', 'tweet.read', 'offline.access', 'users.email'],
470
+ authorizationUrl: 'https://x.com/i/oauth2/authorize',
471
+ tokenUrl: 'https://api.x.com/2/oauth2/token',
472
+ userinfoUrl: 'https://api.x.com/2/users/me?user.fields=username',
473
+ tokenEndpointAuthMethod: 'client_secret_basic',
474
+ usePkce: true,
475
+ clientIdKey: 'TWITTER_CLIENT_ID',
476
+ clientSecretKey: 'TWITTER_CLIENT_SECRET',
477
+ description:
478
+ 'Preferred auth mode. Uses the X OAuth 2.0 API for server-side syncs and login.',
479
+ setupInstructions:
480
+ 'Create an X OAuth 2.0 app, add {{redirect_uri}} as the callback URL, then paste the client ID and client secret below.',
481
+ loginProvisioning: {
482
+ autoCreateConnection: true,
483
+ },
484
+ },
485
+ {
486
+ type: 'browser',
487
+ capture: 'cli',
488
+ requiredDomains: ['x.com', '.x.com'],
489
+ description:
490
+ 'Fallback for browser-based scraping when API access is unavailable or insufficient.',
491
+ },
492
+ ],
493
+ },
494
+ feeds: {
495
+ tweets: {
496
+ key: 'tweets',
497
+ name: 'Tweets',
498
+ requiredScopes: ['tweet.read', 'users.read'],
499
+ description: 'Search and sync tweets matching a query or a specific account handle.',
500
+ configSchema,
501
+ eventKinds: {
502
+ tweet: {
503
+ description: 'A tweet (original post)',
504
+ metadataSchema: engagementMetadataSchema,
505
+ },
506
+ reply: {
507
+ description: 'A reply to a tweet',
508
+ metadataSchema: {
509
+ ...engagementMetadataSchema,
510
+ properties: {
511
+ ...engagementMetadataSchema.properties,
512
+ conversation_id: { type: 'string' },
513
+ },
514
+ },
515
+ },
516
+ },
517
+ },
518
+ },
519
+ optionsSchema: configSchema,
520
+ };
521
+
522
+ async sync(ctx: SyncContext): Promise<SyncResult> {
523
+ const config = ctx.config as Record<string, unknown>;
524
+ const checkpoint = (ctx.checkpoint ?? {}) as XCheckpoint;
525
+
526
+ if (ctx.credentials?.accessToken) {
527
+ return syncViaOAuthApi(ctx, config, checkpoint);
528
+ }
529
+
530
+ return syncViaBrowser(ctx, config, checkpoint);
531
+ }
532
+
533
+ async execute(_ctx: ActionContext): Promise<ActionResult> {
534
+ return { success: false, error: 'Actions not supported' };
535
+ }
536
+ }