@lobu/cli 6.1.1 → 7.1.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 (177) hide show
  1. package/dist/commands/_lib/apply/apply-cmd.d.ts +36 -0
  2. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  3. package/dist/commands/_lib/apply/apply-cmd.js +696 -40
  4. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  5. package/dist/commands/_lib/apply/client.d.ts +285 -0
  6. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  7. package/dist/commands/_lib/apply/client.js +469 -28
  8. package/dist/commands/_lib/apply/client.js.map +1 -1
  9. package/dist/commands/_lib/apply/desired-state.d.ts +187 -3
  10. package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
  11. package/dist/commands/_lib/apply/desired-state.js +879 -88
  12. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  13. package/dist/commands/_lib/apply/diff.d.ts +72 -3
  14. package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
  15. package/dist/commands/_lib/apply/diff.js +473 -84
  16. package/dist/commands/_lib/apply/diff.js.map +1 -1
  17. package/dist/commands/_lib/apply/prompt.d.ts +6 -0
  18. package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
  19. package/dist/commands/_lib/apply/prompt.js +16 -0
  20. package/dist/commands/_lib/apply/prompt.js.map +1 -1
  21. package/dist/commands/_lib/apply/render.d.ts +9 -0
  22. package/dist/commands/_lib/apply/render.d.ts.map +1 -1
  23. package/dist/commands/_lib/apply/render.js +80 -3
  24. package/dist/commands/_lib/apply/render.js.map +1 -1
  25. package/dist/commands/_lib/connector-loader.d.ts +3 -0
  26. package/dist/commands/_lib/connector-loader.d.ts.map +1 -0
  27. package/dist/commands/_lib/connector-loader.js +129 -0
  28. package/dist/commands/_lib/connector-loader.js.map +1 -0
  29. package/dist/commands/_lib/connector-run-cmd.d.ts +35 -0
  30. package/dist/commands/_lib/connector-run-cmd.d.ts.map +1 -0
  31. package/dist/commands/_lib/connector-run-cmd.js +351 -0
  32. package/dist/commands/_lib/connector-run-cmd.js.map +1 -0
  33. package/dist/commands/_lib/export/export-cmd.d.ts +35 -0
  34. package/dist/commands/_lib/export/export-cmd.d.ts.map +1 -0
  35. package/dist/commands/_lib/export/export-cmd.js +329 -0
  36. package/dist/commands/_lib/export/export-cmd.js.map +1 -0
  37. package/dist/commands/agent.d.ts.map +1 -1
  38. package/dist/commands/agent.js +11 -14
  39. package/dist/commands/agent.js.map +1 -1
  40. package/dist/commands/chat.d.ts.map +1 -1
  41. package/dist/commands/chat.js +28 -7
  42. package/dist/commands/chat.js.map +1 -1
  43. package/dist/commands/connector.d.ts +3 -0
  44. package/dist/commands/connector.d.ts.map +1 -0
  45. package/dist/commands/connector.js +5 -0
  46. package/dist/commands/connector.js.map +1 -0
  47. package/dist/commands/dev.d.ts +23 -0
  48. package/dist/commands/dev.d.ts.map +1 -1
  49. package/dist/commands/dev.js +273 -8
  50. package/dist/commands/dev.js.map +1 -1
  51. package/dist/commands/doctor.d.ts.map +1 -1
  52. package/dist/commands/doctor.js +2 -3
  53. package/dist/commands/doctor.js.map +1 -1
  54. package/dist/commands/eval.d.ts.map +1 -1
  55. package/dist/commands/eval.js +28 -18
  56. package/dist/commands/eval.js.map +1 -1
  57. package/dist/commands/init.d.ts +2 -0
  58. package/dist/commands/init.d.ts.map +1 -1
  59. package/dist/commands/init.js +29 -1
  60. package/dist/commands/init.js.map +1 -1
  61. package/dist/commands/login.d.ts.map +1 -1
  62. package/dist/commands/login.js +22 -16
  63. package/dist/commands/login.js.map +1 -1
  64. package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
  65. package/dist/commands/memory/_lib/browser-auth-cmd.js +15 -144
  66. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  67. package/dist/commands/memory/_lib/schema.d.ts +28 -1
  68. package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
  69. package/dist/commands/memory/_lib/schema.js +120 -4
  70. package/dist/commands/memory/_lib/schema.js.map +1 -1
  71. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  72. package/dist/commands/memory/_lib/seed-cmd.js +41 -18
  73. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  74. package/dist/commands/org.d.ts +4 -0
  75. package/dist/commands/org.d.ts.map +1 -1
  76. package/dist/commands/org.js +10 -0
  77. package/dist/commands/org.js.map +1 -1
  78. package/dist/commands/token.d.ts +9 -0
  79. package/dist/commands/token.d.ts.map +1 -1
  80. package/dist/commands/token.js +54 -3
  81. package/dist/commands/token.js.map +1 -1
  82. package/dist/commands/validate.d.ts.map +1 -1
  83. package/dist/commands/validate.js +4 -13
  84. package/dist/commands/validate.js.map +1 -1
  85. package/dist/config/loader.js +2 -2
  86. package/dist/config/loader.js.map +1 -1
  87. package/dist/connectors/README.md +2 -3
  88. package/dist/connectors/apple_health.ts +138 -0
  89. package/dist/connectors/apple_photos.ts +178 -0
  90. package/dist/connectors/apple_screen_time.ts +82 -0
  91. package/dist/connectors/browser/evaluate.ts +120 -0
  92. package/dist/connectors/browser/fill_form.ts +107 -0
  93. package/dist/connectors/browser/page_text.ts +108 -0
  94. package/dist/connectors/browser-scraper-utils.ts +111 -3
  95. package/dist/connectors/capterra.ts +5 -1
  96. package/dist/connectors/chrome_tabs.ts +74 -0
  97. package/dist/connectors/g2.ts +5 -1
  98. package/dist/connectors/github.ts +16 -38
  99. package/dist/connectors/glassdoor.ts +5 -1
  100. package/dist/connectors/google_calendar.ts +28 -6
  101. package/dist/connectors/google_gmail.ts +6 -3
  102. package/dist/connectors/google_play.ts +32 -5
  103. package/dist/connectors/hackernews.ts +37 -2
  104. package/dist/connectors/index.ts +14 -1
  105. package/dist/connectors/linkedin.ts +32 -9
  106. package/dist/connectors/local_directory.ts +91 -0
  107. package/dist/connectors/reddit.ts +1 -0
  108. package/dist/connectors/revolut.ts +569 -0
  109. package/dist/connectors/rss.ts +33 -8
  110. package/dist/connectors/trustpilot.ts +36 -21
  111. package/dist/connectors/website.ts +8 -69
  112. package/dist/connectors/whatsapp.ts +21 -22
  113. package/dist/connectors/whatsapp_local.ts +125 -0
  114. package/dist/connectors/x.ts +17 -7
  115. package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
  116. package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
  117. package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
  118. package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
  119. package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
  120. package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
  121. package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
  122. package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
  123. package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
  124. package/dist/db/migrations/20260514130000_connection_action_modes.sql +103 -0
  125. package/dist/db/migrations/20260514160000_auth_profiles_mirror_mode.sql +32 -0
  126. package/dist/db/migrations/20260515120000_agents_per_org_pk.sql +66 -0
  127. package/dist/db/migrations/20260515150000_geo_enrichment.sql +208 -0
  128. package/dist/db/migrations/20260515160000_drop_agents_org_id_unique.sql +24 -0
  129. package/dist/db/migrations/20260515170000_auth_profiles_default_for_connector.sql +23 -0
  130. package/dist/db/migrations/20260516120000_agents_per_org_pk_swap.sql +125 -0
  131. package/dist/db/migrations/20260516200000_events_search_tsv.sql +134 -0
  132. package/dist/db/migrations/20260516200100_events_lifecycle_changes_index.sql +25 -0
  133. package/dist/db/migrations/20260517010000_drop_unused_indexes.sql +49 -0
  134. package/dist/db/migrations/20260517020000_softdelete_orphan_feeds.sql +56 -0
  135. package/dist/db/migrations/20260517030000_pat_worker_id_binding.sql +27 -0
  136. package/dist/db/migrations/20260517040000_archive_orphan_watchers.sql +30 -0
  137. package/dist/db/migrations/20260517050000_watcher_agent_id_not_null.sql +34 -0
  138. package/dist/db/migrations/20260517060000_watcher_schema_additions.sql +78 -0
  139. package/dist/db/migrations/20260517150000_goals_primitive.sql +55 -0
  140. package/dist/db/migrations/20260517160000_drop_goals_primitive.sql +45 -0
  141. package/dist/db/migrations/20260518000000_pending_interactions.sql +49 -0
  142. package/dist/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql +22 -0
  143. package/dist/eval/client.d.ts.map +1 -1
  144. package/dist/eval/client.js +11 -0
  145. package/dist/eval/client.js.map +1 -1
  146. package/dist/eval/grader.js +2 -1
  147. package/dist/eval/grader.js.map +1 -1
  148. package/dist/eval/types.d.ts +2 -0
  149. package/dist/eval/types.d.ts.map +1 -1
  150. package/dist/index.d.ts +11 -0
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.js +115 -114
  153. package/dist/index.js.map +1 -1
  154. package/dist/internal/context.d.ts +9 -0
  155. package/dist/internal/context.d.ts.map +1 -1
  156. package/dist/internal/context.js +41 -6
  157. package/dist/internal/context.js.map +1 -1
  158. package/dist/internal/credentials.d.ts +5 -0
  159. package/dist/internal/credentials.d.ts.map +1 -1
  160. package/dist/internal/credentials.js +75 -1
  161. package/dist/internal/credentials.js.map +1 -1
  162. package/dist/internal/gateway-url.d.ts +14 -0
  163. package/dist/internal/gateway-url.d.ts.map +1 -1
  164. package/dist/internal/gateway-url.js +19 -0
  165. package/dist/internal/gateway-url.js.map +1 -1
  166. package/dist/internal/index.d.ts +1 -1
  167. package/dist/internal/index.d.ts.map +1 -1
  168. package/dist/internal/index.js +1 -1
  169. package/dist/internal/index.js.map +1 -1
  170. package/dist/internal/local-env.d.ts.map +1 -1
  171. package/dist/internal/local-env.js +9 -2
  172. package/dist/internal/local-env.js.map +1 -1
  173. package/dist/server.bundle.mjs +42251 -36931
  174. package/dist/start-local.bundle.mjs +16437 -9882
  175. package/dist/templates/TESTING.md.tmpl +9 -9
  176. package/package.json +8 -6
  177. package/dist/connectors/google_photos.ts +0 -776
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Browser Evaluate Connector — Owletto for Chrome only.
3
+ *
4
+ * Runs on the Owletto Chrome extension, which advertises capability
5
+ * `browser.debugger`. The extension attaches `chrome.debugger` to a tab,
6
+ * optionally navigates + waits for a selector, runs the supplied JS via
7
+ * `Runtime.evaluate`, and emits one event with the JSON-serialised result.
8
+ *
9
+ * This is the generic "agent runs JS in a user's signed-in Chrome" primitive
10
+ * — most bridge connectors (Revolut feed, banking, sites that fingerprint
11
+ * a managed Chromium) compose on top of `browser.evaluate` rather than
12
+ * shipping their own connector. The trust boundary is `config.script`: only
13
+ * the gateway-side connector author should mint it. The extension defaults
14
+ * to opening a fresh background tab so a compromised gateway / leaked token
15
+ * can't drive the tab a user is actively using; see executor.js in
16
+ * owletto-web for the full threat model.
17
+ *
18
+ * Cloud-side `sync()` / `execute()` throw — actual work happens in the
19
+ * extension's service worker (lobu-ai/owletto: apps/chrome/executor.js).
20
+ */
21
+
22
+ import {
23
+ type ActionResult,
24
+ type ConnectorDefinition,
25
+ ConnectorRuntime,
26
+ type SyncContext,
27
+ type SyncResult,
28
+ } from '@lobu/connector-sdk';
29
+
30
+ const BRIDGE_ONLY =
31
+ 'browser.evaluate runs only on a worker advertising capability "browser.debugger" (Owletto for Chrome).';
32
+
33
+ export default class BrowserEvaluateConnector extends ConnectorRuntime {
34
+ readonly definition: ConnectorDefinition = {
35
+ key: 'browser.evaluate',
36
+ name: 'Browser Evaluate',
37
+ description:
38
+ 'Runs a JS snippet in a page via chrome.debugger and emits the result. The primitive most bridge connectors build on.',
39
+ version: '0.1.0',
40
+ faviconDomain: 'google.com',
41
+ requiredCapability: 'browser.debugger',
42
+ runtime: { platforms: ['chrome-extension'] },
43
+ authSchema: { methods: [{ type: 'none' }] },
44
+ feeds: {
45
+ evaluate: {
46
+ key: 'evaluate',
47
+ name: 'Evaluate JS',
48
+ description:
49
+ 'Executes a JS expression in the page and emits one event with the JSON-serialised return value.',
50
+ // `script` is required and gateway-author-supplied. Auto-wire would
51
+ // insert a feed row with config=NULL and produce a runs-but-fails
52
+ // loop. Bridge connectors (Revolut, banking, etc.) compose by
53
+ // creating explicit feed instances per call site.
54
+ userManaged: true,
55
+ configSchema: {
56
+ type: 'object',
57
+ required: ['script'],
58
+ properties: {
59
+ url: {
60
+ type: 'string',
61
+ format: 'uri',
62
+ description: 'If set, navigate the tab here before evaluating.',
63
+ },
64
+ script: {
65
+ type: 'string',
66
+ description:
67
+ 'JS expression evaluated with Runtime.evaluate(awaitPromise: true). Return value is JSON-serialised — keep it small.',
68
+ },
69
+ wait_for_selector: {
70
+ type: 'string',
71
+ description:
72
+ 'CSS selector to wait for before evaluating (polled every 200ms via Runtime.evaluate).',
73
+ },
74
+ wait_timeout_ms: {
75
+ type: 'integer',
76
+ minimum: 100,
77
+ maximum: 60_000,
78
+ description: 'Timeout for wait_for_selector. Default 10000.',
79
+ },
80
+ open_in_new_tab: {
81
+ type: 'boolean',
82
+ description:
83
+ 'Open a fresh background tab instead of driving the active tab. DEFAULT TRUE — opt out only when you specifically need the user-active tab.',
84
+ },
85
+ close_tab_after: {
86
+ type: 'boolean',
87
+ description:
88
+ 'Close the tab when the run completes. Defaults to true when open_in_new_tab is true.',
89
+ },
90
+ },
91
+ },
92
+ eventKinds: {
93
+ browser_evaluate: {
94
+ description:
95
+ 'One event per run with the JSON-serialised Runtime.evaluate result.',
96
+ metadataSchema: {
97
+ type: 'object',
98
+ required: ['source', 'origin_id'],
99
+ properties: {
100
+ source: { type: 'string', const: 'browser_evaluate' },
101
+ origin_id: { type: 'string' },
102
+ url: { type: 'string' },
103
+ title: { type: 'string' },
104
+ tab_id: { type: 'integer' },
105
+ },
106
+ },
107
+ },
108
+ },
109
+ },
110
+ },
111
+ };
112
+
113
+ async sync(_ctx: SyncContext): Promise<SyncResult> {
114
+ throw new Error(BRIDGE_ONLY);
115
+ }
116
+
117
+ async execute(): Promise<ActionResult> {
118
+ throw new Error(BRIDGE_ONLY);
119
+ }
120
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Browser Fill Form Connector — Owletto for Chrome only.
3
+ *
4
+ * Thin wrapper around browser.evaluate that bakes in a "fill these inputs
5
+ * by selector and dispatch the right input/change events" script.
6
+ *
7
+ * The extension's executor branch for `browser.fill_form` substitutes the
8
+ * canonical fill-form script when this connector_key is dispatched. The
9
+ * server-side definition just exposes the URL + fields config to the
10
+ * admin UI.
11
+ *
12
+ * Cloud-side `sync()` / `execute()` throw — actual work happens in the
13
+ * extension's service worker (lobu-ai/owletto: apps/chrome/executor.js).
14
+ */
15
+
16
+ import {
17
+ type ActionResult,
18
+ type ConnectorDefinition,
19
+ ConnectorRuntime,
20
+ type SyncContext,
21
+ type SyncResult,
22
+ } from '@lobu/connector-sdk';
23
+
24
+ const BRIDGE_ONLY =
25
+ 'browser.fill_form runs only on a worker advertising capability "browser.debugger" (Owletto for Chrome).';
26
+
27
+ export default class BrowserFillFormConnector extends ConnectorRuntime {
28
+ readonly definition: ConnectorDefinition = {
29
+ key: 'browser.fill_form',
30
+ name: 'Browser Fill Form',
31
+ description:
32
+ 'Fills inputs on a page by CSS selector and dispatches input/change events. Returns the filled field count.',
33
+ version: '0.1.0',
34
+ faviconDomain: 'google.com',
35
+ requiredCapability: 'browser.debugger',
36
+ runtime: { platforms: ['chrome-extension'] },
37
+ authSchema: { methods: [{ type: 'none' }] },
38
+ feeds: {
39
+ fill: {
40
+ key: 'fill',
41
+ name: 'Fill form',
42
+ description:
43
+ 'Sets values on input/textarea/select elements matched by CSS selector.',
44
+ // Required url + fields; instances are minted by composing bridge
45
+ // connectors, not auto-wired by device-reconcile.
46
+ userManaged: true,
47
+ configSchema: {
48
+ type: 'object',
49
+ required: ['url', 'fields'],
50
+ properties: {
51
+ url: {
52
+ type: 'string',
53
+ format: 'uri',
54
+ description: 'Page to load before filling.',
55
+ },
56
+ fields: {
57
+ type: 'object',
58
+ description:
59
+ 'Map of CSS selector → value to set. e.g. { "#email": "x@y.com", "#submit": "click" } — the literal string "click" triggers a click instead of a value set.',
60
+ additionalProperties: { type: 'string' },
61
+ },
62
+ wait_for_selector: {
63
+ type: 'string',
64
+ description:
65
+ 'CSS selector to wait for before filling (defaults to the first key of fields).',
66
+ },
67
+ wait_timeout_ms: {
68
+ type: 'integer',
69
+ minimum: 100,
70
+ maximum: 60_000,
71
+ },
72
+ submit_selector: {
73
+ type: 'string',
74
+ description:
75
+ 'Optional selector to click after filling all fields (e.g. "button[type=submit]").',
76
+ },
77
+ },
78
+ },
79
+ eventKinds: {
80
+ form_filled: {
81
+ description:
82
+ 'One event per run with the count of fields filled + whether submit was clicked.',
83
+ metadataSchema: {
84
+ type: 'object',
85
+ required: ['source', 'origin_id', 'url', 'filled_count'],
86
+ properties: {
87
+ source: { type: 'string', const: 'browser_fill_form' },
88
+ origin_id: { type: 'string' },
89
+ url: { type: 'string', format: 'uri' },
90
+ filled_count: { type: 'integer' },
91
+ submitted: { type: 'boolean' },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ },
98
+ };
99
+
100
+ async sync(_ctx: SyncContext): Promise<SyncResult> {
101
+ throw new Error(BRIDGE_ONLY);
102
+ }
103
+
104
+ async execute(): Promise<ActionResult> {
105
+ throw new Error(BRIDGE_ONLY);
106
+ }
107
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Browser Page Text Connector — Owletto for Chrome only.
3
+ *
4
+ * Thin wrapper around browser.evaluate that bakes in a "return cleaned-up
5
+ * page text" script. Saves the connector author from re-deriving the
6
+ * text-extraction recipe for every page-scrape feed.
7
+ *
8
+ * The extension's executor branch for `browser.page_text` is responsible
9
+ * for substituting the canonical script when this connector_key is
10
+ * dispatched — gateway-side this connector definition just exposes the
11
+ * URL + selector-scope config to the admin UI.
12
+ *
13
+ * Cloud-side `sync()` / `execute()` throw — actual work happens in the
14
+ * extension's service worker (lobu-ai/owletto: apps/chrome/executor.js).
15
+ */
16
+
17
+ import {
18
+ type ActionResult,
19
+ type ConnectorDefinition,
20
+ ConnectorRuntime,
21
+ type SyncContext,
22
+ type SyncResult,
23
+ } from '@lobu/connector-sdk';
24
+
25
+ const BRIDGE_ONLY =
26
+ 'browser.page_text runs only on a worker advertising capability "browser.debugger" (Owletto for Chrome).';
27
+
28
+ export default class BrowserPageTextConnector extends ConnectorRuntime {
29
+ readonly definition: ConnectorDefinition = {
30
+ key: 'browser.page_text',
31
+ name: 'Browser Page Text',
32
+ description:
33
+ 'Fetches a page in the paired Chrome and returns its readable text content. Wraps browser.evaluate with a canonical text-extraction script.',
34
+ version: '0.1.0',
35
+ faviconDomain: 'google.com',
36
+ requiredCapability: 'browser.debugger',
37
+ runtime: { platforms: ['chrome-extension'] },
38
+ authSchema: { methods: [{ type: 'none' }] },
39
+ feeds: {
40
+ page: {
41
+ key: 'page',
42
+ name: 'Page text',
43
+ description: 'Snapshot of the text content of a single page.',
44
+ // Required url; instances are minted by composing bridge connectors,
45
+ // not auto-wired by device-reconcile.
46
+ userManaged: true,
47
+ configSchema: {
48
+ type: 'object',
49
+ required: ['url'],
50
+ properties: {
51
+ url: {
52
+ type: 'string',
53
+ format: 'uri',
54
+ description: 'Page to load and read text from.',
55
+ },
56
+ selector: {
57
+ type: 'string',
58
+ description:
59
+ 'CSS selector to scope the extraction to (defaults to body.innerText).',
60
+ },
61
+ wait_for_selector: {
62
+ type: 'string',
63
+ description:
64
+ 'CSS selector to wait for before reading (defaults to body).',
65
+ },
66
+ wait_timeout_ms: {
67
+ type: 'integer',
68
+ minimum: 100,
69
+ maximum: 60_000,
70
+ },
71
+ max_chars: {
72
+ type: 'integer',
73
+ minimum: 100,
74
+ maximum: 1_000_000,
75
+ description: 'Truncate output past this length. Default 200000.',
76
+ },
77
+ },
78
+ },
79
+ eventKinds: {
80
+ page_text: {
81
+ description:
82
+ 'One event per run containing the page text (truncated to max_chars).',
83
+ metadataSchema: {
84
+ type: 'object',
85
+ required: ['source', 'origin_id', 'url'],
86
+ properties: {
87
+ source: { type: 'string', const: 'browser_page_text' },
88
+ origin_id: { type: 'string' },
89
+ url: { type: 'string', format: 'uri' },
90
+ title: { type: 'string' },
91
+ char_count: { type: 'integer' },
92
+ truncated: { type: 'boolean' },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ },
98
+ },
99
+ };
100
+
101
+ async sync(_ctx: SyncContext): Promise<SyncResult> {
102
+ throw new Error(BRIDGE_ONLY);
103
+ }
104
+
105
+ async execute(): Promise<ActionResult> {
106
+ throw new Error(BRIDGE_ONLY);
107
+ }
108
+ }
@@ -24,12 +24,42 @@ export function getBrowserCookies(
24
24
  ): any[] {
25
25
  const sessionCookies = (sessionState?.cookies as any[]) ?? [];
26
26
  const cookies = (checkpoint as any)?.cookies ?? sessionCookies;
27
- if (!cookies || cookies.length === 0) {
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) {
28
30
  throw new Error(
29
31
  `No browser cookies found. Run: lobu memory browser-auth --connector ${connectorKey} --auth-profile-slug <SLUG>`
30
32
  );
31
33
  }
32
- return cookies;
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;
33
63
  }
34
64
 
35
65
  export function validateCookieNotExpired(
@@ -52,6 +82,82 @@ export function validateCookieNotExpired(
52
82
  // URL validation
53
83
  // -----------------------------------------------------------------------------
54
84
 
85
+ /**
86
+ * Validates a URL is safe for server-side fetching.
87
+ * Blocks private/internal network addresses to prevent SSRF attacks.
88
+ *
89
+ * Returns silently when the URL is safe; throws with a descriptive message
90
+ * otherwise. Connectors that fetch URLs derived from remote/untrusted input
91
+ * (sitemaps, HN story links, RSS feeds configured by users, etc.) MUST call
92
+ * this at the trust boundary before issuing the request.
93
+ */
94
+ export function validatePublicUrl(url: string): void {
95
+ let parsed: URL;
96
+ try {
97
+ parsed = new URL(url);
98
+ } catch {
99
+ throw new Error(`Invalid URL: ${url}`);
100
+ }
101
+
102
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
103
+ throw new Error(`URL must use http: or https: protocol, got ${parsed.protocol}`);
104
+ }
105
+
106
+ const hostname = parsed.hostname.toLowerCase();
107
+
108
+ // Block localhost variants
109
+ if (hostname === 'localhost' || hostname === '[::1]' || hostname.endsWith('.localhost')) {
110
+ throw new Error(`URL must not point to localhost: ${hostname}`);
111
+ }
112
+
113
+ // IPv4 private/loopback/link-local/cloud-metadata/CGNAT ranges
114
+ const ipv4Match = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
115
+ if (ipv4Match) {
116
+ const [, a, b] = ipv4Match.map(Number);
117
+ if (
118
+ a === 127 || // loopback
119
+ a === 10 || // private
120
+ (a === 172 && b >= 16 && b <= 31) || // private
121
+ (a === 192 && b === 168) || // private
122
+ (a === 169 && b === 254) || // link-local incl. 169.254.169.254 cloud metadata
123
+ (a === 100 && b >= 64 && b <= 127) || // CGNAT 100.64.0.0/10
124
+ a === 0
125
+ ) {
126
+ throw new Error(`URL must not point to a private/internal IP address: ${hostname}`);
127
+ }
128
+ }
129
+
130
+ // IPv6 private ranges (bracketed notation)
131
+ if (hostname.startsWith('[')) {
132
+ const ipv6 = hostname.slice(1, -1).toLowerCase();
133
+ // Link-local fe80::/10 covers fe80:..fec0: (first byte 1111 1110 1x).
134
+ const linkLocalPrefix = /^fe[89ab][0-9a-f]?:/;
135
+ // Multicast ff00::/8 — any address starting with ff.
136
+ const multicastPrefix = /^ff[0-9a-f]{2}:/;
137
+ if (
138
+ ipv6 === '::1' ||
139
+ linkLocalPrefix.test(ipv6) ||
140
+ multicastPrefix.test(ipv6) ||
141
+ ipv6.startsWith('fc') || // unique local fc00::/7
142
+ ipv6.startsWith('fd') ||
143
+ ipv6 === '::' ||
144
+ ipv6.startsWith('::ffff:') // IPv4-mapped IPv6
145
+ ) {
146
+ throw new Error(`URL must not point to a private/internal IPv6 address: ${hostname}`);
147
+ }
148
+ }
149
+
150
+ // Common internal hostnames
151
+ if (
152
+ hostname.endsWith('.internal') ||
153
+ hostname.endsWith('.local') ||
154
+ hostname.endsWith('.corp') ||
155
+ hostname.endsWith('.lan')
156
+ ) {
157
+ throw new Error(`URL must not point to an internal hostname: ${hostname}`);
158
+ }
159
+ }
160
+
55
161
  /**
56
162
  * Validate that a URL is well-formed, uses HTTPS, and belongs to the expected
57
163
  * domain (hostname ends with `expectedDomain`).
@@ -103,12 +209,14 @@ export async function openStealthBrowser(opts?: {
103
209
  cdpUrl?: string | 'auto' | null;
104
210
  cookies?: Cookie[];
105
211
  authDomains?: string[];
212
+ userDataDir?: string;
106
213
  }): Promise<BrowserSession> {
107
214
  const acquired = await acquireBrowser({
108
- cdpUrl: opts?.cdpUrl ?? null,
215
+ cdpUrl: opts?.userDataDir ? null : (opts?.cdpUrl ?? null),
109
216
  cookies: opts?.cookies ?? [],
110
217
  authDomains: opts?.authDomains ?? [],
111
218
  stealth: true,
219
+ userDataDir: opts?.userDataDir,
112
220
  });
113
221
 
114
222
  const page = acquired.cdpPage ?? acquired.page;
@@ -14,6 +14,8 @@ import {
14
14
  type SyncResult,
15
15
  } from '@lobu/connector-sdk';
16
16
  import {
17
+ getBrowserCdpUrl,
18
+ getBrowserUserDataDir,
17
19
  handleCookieConsent,
18
20
  openStealthBrowser,
19
21
  validateUrlDomain,
@@ -132,7 +134,9 @@ export default class CapterraConnector extends ConnectorRuntime {
132
134
  : `https://www.capterra.com/p/${productId}/reviews`;
133
135
  validateUrlDomain(baseUrl, 'capterra.com');
134
136
 
135
- const session = await openStealthBrowser({ cdpUrl: 'auto' });
137
+ const userDataDir = getBrowserUserDataDir(ctx.sessionState);
138
+ const cdpUrl = getBrowserCdpUrl(ctx.sessionState) ?? 'auto';
139
+ const session = await openStealthBrowser({ cdpUrl, userDataDir });
136
140
 
137
141
  return withBrowserErrorCapture(session, 'capterra-sync', async (page) => {
138
142
  await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Chrome Tabs Connector — Owletto for Chrome only.
3
+ *
4
+ * Runs on the Owletto Chrome extension, which advertises capability
5
+ * `browser.tabs`. The extension uses `chrome.tabs.query()` to list the tabs
6
+ * currently open in the user's paired Chrome profile. No persistent backfill
7
+ * — each sync returns the live tab list at that moment.
8
+ *
9
+ * This connector is the smallest end-to-end demo of the Chrome-extension
10
+ * device protocol: it proves a connector definition can declare a browser
11
+ * capability, get auto-wired into the user's personal org when a
12
+ * `chrome-extension` device polls, and route runs to it.
13
+ *
14
+ * The cloud-side `sync()` / `execute()` throw — actual work happens in the
15
+ * extension's service worker (lobu-ai/owletto: apps/chrome/background.js).
16
+ */
17
+
18
+ import {
19
+ type ActionResult,
20
+ type ConnectorDefinition,
21
+ ConnectorRuntime,
22
+ type SyncContext,
23
+ type SyncResult,
24
+ } from '@lobu/connector-sdk';
25
+
26
+ const BRIDGE_ONLY =
27
+ 'Chrome Tabs runs only on a worker advertising capability "browser.tabs" (Owletto for Chrome).';
28
+
29
+ export default class ChromeTabsConnector extends ConnectorRuntime {
30
+ readonly definition: ConnectorDefinition = {
31
+ key: 'chrome.tabs',
32
+ name: 'Chrome Tabs',
33
+ description:
34
+ 'Lists the tabs currently open in the paired Chrome profile. Read-only; no history or content.',
35
+ version: '0.1.0',
36
+ faviconDomain: 'google.com',
37
+ requiredCapability: 'browser.tabs',
38
+ runtime: { platforms: ['chrome-extension'] },
39
+ authSchema: { methods: [{ type: 'none' }] },
40
+ feeds: {
41
+ open_tabs: {
42
+ key: 'open_tabs',
43
+ name: 'Open tabs',
44
+ description: 'Snapshot of the tabs currently open in this Chrome profile.',
45
+ configSchema: { type: 'object', properties: {} },
46
+ eventKinds: {
47
+ tab_snapshot: {
48
+ description: 'One row per tab observed in the active poll cycle.',
49
+ metadataSchema: {
50
+ type: 'object',
51
+ required: ['source', 'origin_id', 'url'],
52
+ properties: {
53
+ source: { type: 'string', const: 'chrome_tabs' },
54
+ origin_id: { type: 'string' },
55
+ url: { type: 'string', format: 'uri' },
56
+ title: { type: 'string' },
57
+ window_id: { type: 'integer' },
58
+ active: { type: 'boolean' },
59
+ },
60
+ },
61
+ },
62
+ },
63
+ },
64
+ },
65
+ };
66
+
67
+ async sync(_ctx: SyncContext): Promise<SyncResult> {
68
+ throw new Error(BRIDGE_ONLY);
69
+ }
70
+
71
+ async execute(): Promise<ActionResult> {
72
+ throw new Error(BRIDGE_ONLY);
73
+ }
74
+ }
@@ -15,6 +15,8 @@ import {
15
15
  type SyncResult,
16
16
  } from '@lobu/connector-sdk';
17
17
  import {
18
+ getBrowserCdpUrl,
19
+ getBrowserUserDataDir,
18
20
  handleCookieConsent,
19
21
  openStealthBrowser,
20
22
  validateUrlDomain,
@@ -114,7 +116,9 @@ export default class G2Connector extends ConnectorRuntime {
114
116
  const baseUrl = productUrl;
115
117
  const allEvents: EventEnvelope[] = [];
116
118
 
117
- const session = await openStealthBrowser({ cdpUrl: 'auto' });
119
+ const userDataDir = getBrowserUserDataDir(ctx.sessionState);
120
+ const cdpUrl = getBrowserCdpUrl(ctx.sessionState) ?? 'auto';
121
+ const session = await openStealthBrowser({ cdpUrl, userDataDir });
118
122
 
119
123
  return withBrowserErrorCapture(session, 'g2-sync', async (page) => {
120
124
  const maxPages = 5;