@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,572 @@
1
+ /**
2
+ * Revolut Connector (V1 runtime)
3
+ *
4
+ * Revolut has no public personal-banking API, so this connector drives the
5
+ * Revolut web app and captures the JSON it fetches from
6
+ * `app.revolut.com/api/retail/user/current/transactions/last?...` while
7
+ * paginating the transaction list (the `to=<ms>` param walks back in time).
8
+ *
9
+ * Auth: CDP only. Revolut's `app.revolut.com` access token (`credentials`
10
+ * cookie) is bound to the browser that minted it (a per-request `x-device-id`
11
+ * header + Cloudflare/TLS fingerprint), so exported cookies replayed in a fresh
12
+ * headless browser get a 401 on `/api/retail/...` and bounce to
13
+ * `sso.revolut.com/passcode`. The connector therefore connects over CDP to a
14
+ * Chrome that already holds the live session — see the auth-schema notes.
15
+ *
16
+ * The emitted event shape matches the original file-import Revolut connector
17
+ * (`semantic_type: "transaction"`, metadata `{ date, description, amount,
18
+ * direction, balance, currency }`) so historical imports stay uniform.
19
+ */
20
+
21
+ import {
22
+ type ActionContext,
23
+ type ActionResult,
24
+ browserNetworkSync,
25
+ type ConnectorDefinition,
26
+ ConnectorRuntime,
27
+ type EventEnvelope,
28
+ type SyncContext,
29
+ type SyncResult,
30
+ } from "@lobu/connector-sdk";
31
+ import {
32
+ getBrowserCdpUrl,
33
+ getBrowserCookies,
34
+ getBrowserUserDataDir,
35
+ validateCookieNotExpired,
36
+ } from "./browser-scraper-utils";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Types
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export interface RevolutCheckpoint {
43
+ last_transaction_id?: string;
44
+ last_timestamp?: string;
45
+ }
46
+
47
+ export interface RevolutTransaction {
48
+ id: string;
49
+ description: string;
50
+ /** Absolute value in major currency units (e.g. 20.0 for £20.00). */
51
+ amount: number;
52
+ direction: "in" | "out";
53
+ /** Account balance after the transaction, in major units (may be absent). */
54
+ balance?: number;
55
+ currency: string;
56
+ /** ISO calendar date (YYYY-MM-DD) the transaction settled / started. */
57
+ date: string;
58
+ /** Full settlement timestamp. */
59
+ occurredAt: Date;
60
+ /** Revolut transaction type, e.g. CARD_PAYMENT, TRANSFER, TOPUP. */
61
+ type?: string;
62
+ /** Revolut state, e.g. COMPLETED, PENDING. */
63
+ state?: string;
64
+ }
65
+
66
+ // Currencies with zero minor units — Revolut returns these amounts unscaled.
67
+ const ZERO_DECIMAL_CURRENCIES = new Set([
68
+ "JPY",
69
+ "KRW",
70
+ "VND",
71
+ "CLP",
72
+ "ISK",
73
+ "XAF",
74
+ "XOF",
75
+ "BIF",
76
+ "DJF",
77
+ "GNF",
78
+ "KMF",
79
+ "MGA",
80
+ "PYG",
81
+ "RWF",
82
+ "UGX",
83
+ "VUV",
84
+ "XPF",
85
+ ]);
86
+
87
+ // Transaction states worth keeping. DECLINED/FAILED/REVERTED never settled.
88
+ const KEPT_STATES = new Set(["COMPLETED", "PENDING", "CONFIRMED", "SETTLED"]);
89
+
90
+ // Fields the web app uses for the transaction timestamp, best first.
91
+ const TIMESTAMP_FIELDS = [
92
+ "completedDate",
93
+ "completed_date",
94
+ "completedAt",
95
+ "bookingDate",
96
+ "booking_date",
97
+ "valueDate",
98
+ "value_date",
99
+ "startedDate",
100
+ "started_date",
101
+ "createdDate",
102
+ "created_date",
103
+ "createdAt",
104
+ "date",
105
+ ];
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Parsing
109
+ // ---------------------------------------------------------------------------
110
+
111
+ function minorUnitsToMajor(raw: number, currency: string): number {
112
+ const exponent = ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase()) ? 0 : 2;
113
+ return raw / 10 ** exponent;
114
+ }
115
+
116
+ function coerceTimestamp(value: unknown): Date | null {
117
+ if (typeof value === "number" && Number.isFinite(value)) {
118
+ // Revolut uses ms-epoch; treat 10-digit values as seconds defensively.
119
+ const ms = value < 1e12 ? value * 1000 : value;
120
+ const d = new Date(ms);
121
+ return Number.isNaN(d.getTime()) ? null : d;
122
+ }
123
+ if (typeof value === "string" && value.trim()) {
124
+ const d = new Date(value);
125
+ return Number.isNaN(d.getTime()) ? null : d;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ function extractAmountAndCurrency(
131
+ record: Record<string, unknown>,
132
+ ): { amount: number; currency: string } | null {
133
+ // Flat shape: { amount: -2000, currency: "GBP" }
134
+ if (
135
+ typeof record.amount === "number" &&
136
+ typeof record.currency === "string"
137
+ ) {
138
+ return { amount: record.amount, currency: record.currency };
139
+ }
140
+ // Nested money shape: { amount: { value: -2000, currency: "GBP" } } or
141
+ // { amount: { amount: -20.0, currency: "GBP" } }.
142
+ const amt = record.amount;
143
+ if (amt && typeof amt === "object") {
144
+ const obj = amt as Record<string, unknown>;
145
+ const value =
146
+ typeof obj.value === "number"
147
+ ? obj.value
148
+ : typeof obj.amount === "number"
149
+ ? obj.amount
150
+ : null;
151
+ const currency = typeof obj.currency === "string" ? obj.currency : null;
152
+ if (value !== null && currency) return { amount: value, currency };
153
+ }
154
+ return null;
155
+ }
156
+
157
+ function nameOf(node: unknown): string | null {
158
+ if (!node || typeof node !== "object") return null;
159
+ const obj = node as Record<string, unknown>;
160
+ for (const key of ["name", "legalName", "username", "displayName"]) {
161
+ const v = obj[key];
162
+ if (typeof v === "string" && v.trim()) return v.trim();
163
+ }
164
+ return null;
165
+ }
166
+
167
+ function describeTransaction(record: Record<string, unknown>): string {
168
+ // Card payments carry a clean `merchant.name` ("OpenAI") alongside a noisy
169
+ // raw descriptor ("Openai *chatgpt Subscr") — prefer the merchant name, which
170
+ // is also what the Revolut UI shows and what the legacy import used. Transfers
171
+ // and top-ups have no merchant, so fall back to the human description.
172
+ const merchant = nameOf(record.merchant);
173
+ if (merchant) return merchant;
174
+ for (const key of [
175
+ "description",
176
+ "localisedDescription",
177
+ "reference",
178
+ "comment",
179
+ ]) {
180
+ const v = record[key];
181
+ if (typeof v === "string" && v.trim()) return v.trim();
182
+ }
183
+ for (const key of [
184
+ "counterpart",
185
+ "counterparty",
186
+ "recipient",
187
+ "sender",
188
+ "beneficiary",
189
+ ]) {
190
+ const v = nameOf(record[key]);
191
+ if (v) return v;
192
+ }
193
+ const type = record.type;
194
+ return typeof type === "string" && type.trim()
195
+ ? type.replace(/_/g, " ")
196
+ : "Transaction";
197
+ }
198
+
199
+ function extractBalance(
200
+ record: Record<string, unknown>,
201
+ currency: string,
202
+ ): number | undefined {
203
+ const raw =
204
+ typeof record.balance === "number"
205
+ ? record.balance
206
+ : record.balance && typeof record.balance === "object"
207
+ ? ((record.balance as Record<string, unknown>).value ??
208
+ (record.balance as Record<string, unknown>).amount)
209
+ : undefined;
210
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return undefined;
211
+ return Number.isInteger(raw) ? minorUnitsToMajor(raw, currency) : raw;
212
+ }
213
+
214
+ function parseTransactionRecord(
215
+ record: Record<string, unknown>,
216
+ ): RevolutTransaction | null {
217
+ const money = extractAmountAndCurrency(record);
218
+ if (!money) return null;
219
+
220
+ const id = record.id ?? record.legId ?? record.transactionId ?? record.code;
221
+ if (typeof id !== "string" && typeof id !== "number") return null;
222
+
223
+ let occurredAt: Date | null = null;
224
+ for (const field of TIMESTAMP_FIELDS) {
225
+ occurredAt = coerceTimestamp(record[field]);
226
+ if (occurredAt) break;
227
+ }
228
+ if (!occurredAt) return null;
229
+
230
+ const state =
231
+ typeof record.state === "string" ? record.state.toUpperCase() : undefined;
232
+ if (state && !KEPT_STATES.has(state)) return null;
233
+
234
+ const currency = money.currency.toUpperCase();
235
+ // Revolut's retail API returns integer minor units; some endpoints return a
236
+ // decimal already in major units — fractional values mean "already major".
237
+ const value = Number.isInteger(money.amount)
238
+ ? minorUnitsToMajor(money.amount, currency)
239
+ : money.amount;
240
+
241
+ return {
242
+ id: String(id),
243
+ description: describeTransaction(record),
244
+ amount: Math.abs(value),
245
+ direction: value < 0 ? "out" : "in",
246
+ balance: extractBalance(record, currency),
247
+ currency,
248
+ date: occurredAt.toISOString().slice(0, 10),
249
+ occurredAt,
250
+ type: typeof record.type === "string" ? record.type : undefined,
251
+ state,
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Walk an arbitrary JSON value and pull out anything that looks like a Revolut
257
+ * transaction. The web app's responses vary (bare arrays, `{ items: [...] }`,
258
+ * paginated envelopes, single-transaction detail endpoints), so we recurse
259
+ * rather than assume one shape. A record only counts as a transaction if it
260
+ * carries both an amount/currency and a timestamp, which keeps merchant/budget
261
+ * objects out.
262
+ */
263
+ export function extractTransactionsFromResponse(
264
+ json: unknown,
265
+ ): RevolutTransaction[] {
266
+ const found: RevolutTransaction[] = [];
267
+ const seen = new Set<object>();
268
+
269
+ const visit = (node: unknown): void => {
270
+ if (!node || typeof node !== "object") return;
271
+ if (seen.has(node as object)) return;
272
+ seen.add(node as object);
273
+
274
+ if (Array.isArray(node)) {
275
+ for (const item of node) {
276
+ const parsed =
277
+ item && typeof item === "object" && !Array.isArray(item)
278
+ ? parseTransactionRecord(item as Record<string, unknown>)
279
+ : null;
280
+ if (parsed) found.push(parsed);
281
+ else visit(item);
282
+ }
283
+ return;
284
+ }
285
+
286
+ const record = node as Record<string, unknown>;
287
+ const asTxn = parseTransactionRecord(record);
288
+ if (asTxn) {
289
+ found.push(asTxn);
290
+ return;
291
+ }
292
+ for (const value of Object.values(record)) visit(value);
293
+ };
294
+
295
+ visit(json);
296
+ return found;
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Checkpoint filtering
301
+ // ---------------------------------------------------------------------------
302
+
303
+ export function filterTransactionsSinceCheckpoint(
304
+ transactions: RevolutTransaction[],
305
+ checkpoint: RevolutCheckpoint | null | undefined,
306
+ ): RevolutTransaction[] {
307
+ const lastTs = checkpoint?.last_timestamp
308
+ ? new Date(checkpoint.last_timestamp).getTime()
309
+ : null;
310
+ const lastId = checkpoint?.last_transaction_id;
311
+ const seen = new Set<string>();
312
+ return transactions.filter((t) => {
313
+ if (seen.has(t.id)) return false;
314
+ seen.add(t.id);
315
+ if (lastId && t.id === lastId) return false;
316
+ if (
317
+ lastTs !== null &&
318
+ Number.isFinite(lastTs) &&
319
+ t.occurredAt.getTime() <= lastTs
320
+ ) {
321
+ return false;
322
+ }
323
+ return true;
324
+ });
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // Event mapping (matches the original file-import Revolut connector)
329
+ // ---------------------------------------------------------------------------
330
+
331
+ function currencySymbol(currency: string): string {
332
+ switch (currency.toUpperCase()) {
333
+ case "GBP":
334
+ return "£";
335
+ case "USD":
336
+ return "$";
337
+ case "EUR":
338
+ return "€";
339
+ default:
340
+ return `${currency} `;
341
+ }
342
+ }
343
+
344
+ export function transactionToEvent(t: RevolutTransaction): EventEnvelope {
345
+ const sign = t.direction === "out" ? "-" : "+";
346
+ return {
347
+ origin_id: `revolut-${t.id}`,
348
+ payload_text: `${t.description} ${sign}${currencySymbol(t.currency)}${t.amount} on ${t.date}`,
349
+ occurred_at: t.occurredAt,
350
+ semantic_type: "transaction",
351
+ metadata: {
352
+ date: t.date,
353
+ description: t.description,
354
+ amount: t.amount,
355
+ direction: t.direction,
356
+ ...(t.balance !== undefined ? { balance: t.balance } : {}),
357
+ currency: t.currency,
358
+ ...(t.type ? { transaction_type: t.type } : {}),
359
+ ...(t.state ? { state: t.state } : {}),
360
+ },
361
+ };
362
+ }
363
+
364
+ // ---------------------------------------------------------------------------
365
+ // Sync
366
+ // ---------------------------------------------------------------------------
367
+
368
+ // The Revolut web app fetches account history from
369
+ // `app.revolut.com/api/retail/user/current/transactions/last?count=N&to=<ms>&internalPocketId=<uuid>`
370
+ // (the `to` param walks back in time as you scroll). These patterns also cover
371
+ // plausible alternates without catching unrelated `/api/retail/...` calls.
372
+ const TRANSACTION_API_PATTERNS: RegExp[] = [
373
+ /\/api\/retail\/.*transactions?(?:\/|\b)/i,
374
+ /\/api\/.*\/transactions(?:\b|\?|\/|$)/i,
375
+ /transactions?[./](?:last|recent|history|search)/i,
376
+ ];
377
+
378
+ const REVOLUT_AUTH_DOMAINS = ["app.revolut.com", ".revolut.com"];
379
+ // `/transactions` shows the full, infinitely-scrollable history for the default
380
+ // account; `/home` only shows the latest ~10. Per-pocket history lives at
381
+ // `/transactions?accountType=pocket&walletId=<uuid>&pocketId=<uuid>` — point a
382
+ // second feed's `start_url` there to sync a non-default currency pocket.
383
+ const DEFAULT_START_URL = "https://app.revolut.com/transactions";
384
+
385
+ function isLoggedIn(url: string): boolean {
386
+ let host: string;
387
+ try {
388
+ host = new URL(url).hostname;
389
+ } catch {
390
+ return false;
391
+ }
392
+ // An unauthenticated session is bounced to sso.revolut.com/passcode.
393
+ if (host !== "app.revolut.com") return false;
394
+ return !/\/(?:start|signin|login|verify|onboarding)\b/i.test(url);
395
+ }
396
+
397
+ const configSchema = {
398
+ type: "object",
399
+ properties: {
400
+ start_url: {
401
+ type: "string",
402
+ default: DEFAULT_START_URL,
403
+ description:
404
+ "Revolut web app URL to open. Defaults to the full transactions view for the primary account; set it to a per-pocket /transactions?...pocketId=<uuid> URL to sync a different currency pocket.",
405
+ },
406
+ currency_filter: {
407
+ type: "string",
408
+ description:
409
+ 'If set, keep only transactions in this ISO 4217 currency (e.g. "GBP").',
410
+ },
411
+ max_scrolls: {
412
+ type: "integer",
413
+ minimum: 1,
414
+ maximum: 100,
415
+ default: 20,
416
+ description:
417
+ "Maximum scroll iterations to paginate older transactions (default: 20).",
418
+ },
419
+ },
420
+ };
421
+
422
+ const transactionMetadataSchema = {
423
+ type: "object",
424
+ properties: {
425
+ date: { type: "string", format: "date" },
426
+ description: { type: "string" },
427
+ amount: { type: "number" },
428
+ direction: { type: "string", enum: ["in", "out"] },
429
+ balance: { type: "number" },
430
+ currency: { type: "string" },
431
+ transaction_type: { type: "string" },
432
+ state: { type: "string" },
433
+ },
434
+ };
435
+
436
+ export default class RevolutConnector extends ConnectorRuntime {
437
+ readonly definition: ConnectorDefinition = {
438
+ key: "revolut",
439
+ name: "Revolut",
440
+ description:
441
+ "Syncs Revolut account transactions from the Revolut web app (no public API). Requires a Chrome instance, with remote debugging enabled, that stays logged in to app.revolut.com.",
442
+ version: "2.0.0",
443
+ faviconDomain: "app.revolut.com",
444
+ authSchema: {
445
+ methods: [
446
+ // CDP only — *not* `cli` cookie capture. Revolut's `app.revolut.com`
447
+ // access token (the `credentials` cookie) is bound to the browser that
448
+ // minted it (device-id header + Cloudflare/TLS fingerprint), so cookies
449
+ // exported from Chrome and replayed in a fresh headless browser get a 401
450
+ // on /api/retail/... and bounce to sso.revolut.com/passcode. The only
451
+ // path that authenticates is connecting over CDP to the *same* Chrome
452
+ // that holds the live session — keep one logged in and reachable.
453
+ {
454
+ type: "browser",
455
+ capture: "cdp",
456
+ defaultCdpUrl: "http://127.0.0.1:9222",
457
+ requiredDomains: REVOLUT_AUTH_DOMAINS,
458
+ description:
459
+ "Connect over CDP to a Chrome logged in to app.revolut.com: lobu memory browser-auth --connector revolut --launch-cdp (log in there, re-enter the passcode whenever Revolut expires the session).",
460
+ },
461
+ ],
462
+ },
463
+ feeds: {
464
+ transactions: {
465
+ key: "transactions",
466
+ name: "Transactions",
467
+ description: "Account transactions pulled from the Revolut web app.",
468
+ configSchema,
469
+ eventKinds: {
470
+ transaction: {
471
+ description: "A bank transaction",
472
+ metadataSchema: transactionMetadataSchema,
473
+ },
474
+ },
475
+ },
476
+ },
477
+ optionsSchema: configSchema,
478
+ };
479
+
480
+ async sync(ctx: SyncContext): Promise<SyncResult> {
481
+ const config = (ctx.config ?? {}) as Record<string, unknown>;
482
+ const checkpoint = (ctx.checkpoint ?? {}) as RevolutCheckpoint;
483
+
484
+ const startUrl =
485
+ typeof config.start_url === "string" && config.start_url.trim()
486
+ ? config.start_url.trim()
487
+ : DEFAULT_START_URL;
488
+ const currencyFilter =
489
+ typeof config.currency_filter === "string" &&
490
+ config.currency_filter.trim()
491
+ ? config.currency_filter.trim().toUpperCase()
492
+ : null;
493
+ const maxScrolls = Math.max(
494
+ 1,
495
+ Math.min(100, Number(config.max_scrolls ?? 20) || 20),
496
+ );
497
+
498
+ // Primary auth is CDP (connect to the Chrome that holds the live Revolut
499
+ // session). Stored cookies are only a best-effort fallback for the
500
+ // Playwright path — see the auth-schema comment on why they rarely suffice
501
+ // for Revolut. Don't fail the sync just because there are none.
502
+ const userDataDir = getBrowserUserDataDir(ctx.sessionState);
503
+ const cdpUrl = getBrowserCdpUrl(ctx.sessionState) ?? "auto";
504
+ let cookies: ReturnType<typeof getBrowserCookies> = [];
505
+ if (!userDataDir) {
506
+ try {
507
+ cookies = getBrowserCookies(
508
+ ctx.checkpoint as Record<string, unknown> | null,
509
+ ctx.sessionState,
510
+ "revolut",
511
+ );
512
+ validateCookieNotExpired(cookies, "credentials", "revolut");
513
+ } catch {
514
+ cookies = [];
515
+ }
516
+ }
517
+
518
+ const result = await browserNetworkSync<RevolutTransaction>({
519
+ config: {
520
+ interceptPatterns: TRANSACTION_API_PATTERNS,
521
+ authDomains: REVOLUT_AUTH_DOMAINS,
522
+ maxScrolls,
523
+ scrollDelayMs: 2500,
524
+ responseTimeoutMs: 8000,
525
+ navigationTimeoutMs: 20000,
526
+ stealth: true,
527
+ },
528
+ url: startUrl,
529
+ cdpUrl,
530
+ cookies,
531
+ userDataDir,
532
+ parseResponse: (_url, json) => extractTransactionsFromResponse(json),
533
+ checkAuth: async (page) => isLoggedIn(page.url()),
534
+ });
535
+
536
+ let transactions = filterTransactionsSinceCheckpoint(
537
+ result.items,
538
+ checkpoint,
539
+ );
540
+ if (currencyFilter) {
541
+ transactions = transactions.filter((t) => t.currency === currencyFilter);
542
+ }
543
+ transactions.sort(
544
+ (a, b) => b.occurredAt.getTime() - a.occurredAt.getTime(),
545
+ );
546
+
547
+ const events: EventEnvelope[] = transactions.map(transactionToEvent);
548
+ const newest = transactions[0];
549
+ const newCheckpoint: RevolutCheckpoint = newest
550
+ ? {
551
+ last_transaction_id: newest.id,
552
+ last_timestamp: newest.occurredAt.toISOString(),
553
+ }
554
+ : checkpoint;
555
+
556
+ return {
557
+ events,
558
+ checkpoint: newCheckpoint as unknown as Record<string, unknown>,
559
+ auth_update: { cookies: result.cookies },
560
+ metadata: {
561
+ items_found: events.length,
562
+ api_calls: result.apiCallCount,
563
+ backend: result.backend,
564
+ ...(currencyFilter ? { currency_filter: currencyFilter } : {}),
565
+ },
566
+ };
567
+ }
568
+
569
+ async execute(_ctx: ActionContext): Promise<ActionResult> {
570
+ return { success: false, error: "Actions not supported" };
571
+ }
572
+ }