@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.
- package/dist/commands/_lib/apply/apply-cmd.d.ts +36 -0
- package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
- package/dist/commands/_lib/apply/apply-cmd.js +696 -40
- package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
- package/dist/commands/_lib/apply/client.d.ts +285 -0
- package/dist/commands/_lib/apply/client.d.ts.map +1 -1
- package/dist/commands/_lib/apply/client.js +469 -28
- package/dist/commands/_lib/apply/client.js.map +1 -1
- package/dist/commands/_lib/apply/desired-state.d.ts +187 -3
- package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
- package/dist/commands/_lib/apply/desired-state.js +879 -88
- package/dist/commands/_lib/apply/desired-state.js.map +1 -1
- package/dist/commands/_lib/apply/diff.d.ts +72 -3
- package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
- package/dist/commands/_lib/apply/diff.js +473 -84
- package/dist/commands/_lib/apply/diff.js.map +1 -1
- package/dist/commands/_lib/apply/prompt.d.ts +6 -0
- package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
- package/dist/commands/_lib/apply/prompt.js +16 -0
- package/dist/commands/_lib/apply/prompt.js.map +1 -1
- package/dist/commands/_lib/apply/render.d.ts +9 -0
- package/dist/commands/_lib/apply/render.d.ts.map +1 -1
- package/dist/commands/_lib/apply/render.js +80 -3
- package/dist/commands/_lib/apply/render.js.map +1 -1
- package/dist/commands/_lib/connector-loader.d.ts +3 -0
- package/dist/commands/_lib/connector-loader.d.ts.map +1 -0
- package/dist/commands/_lib/connector-loader.js +129 -0
- package/dist/commands/_lib/connector-loader.js.map +1 -0
- package/dist/commands/_lib/connector-run-cmd.d.ts +35 -0
- package/dist/commands/_lib/connector-run-cmd.d.ts.map +1 -0
- package/dist/commands/_lib/connector-run-cmd.js +351 -0
- package/dist/commands/_lib/connector-run-cmd.js.map +1 -0
- package/dist/commands/_lib/export/export-cmd.d.ts +35 -0
- package/dist/commands/_lib/export/export-cmd.d.ts.map +1 -0
- package/dist/commands/_lib/export/export-cmd.js +329 -0
- package/dist/commands/_lib/export/export-cmd.js.map +1 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +11 -14
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +28 -7
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/connector.d.ts +3 -0
- package/dist/commands/connector.d.ts.map +1 -0
- package/dist/commands/connector.js +5 -0
- package/dist/commands/connector.js.map +1 -0
- package/dist/commands/dev.d.ts +23 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +273 -8
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +28 -18
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +29 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +22 -16
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.js +15 -144
- package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
- package/dist/commands/memory/_lib/schema.d.ts +28 -1
- package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
- package/dist/commands/memory/_lib/schema.js +120 -4
- package/dist/commands/memory/_lib/schema.js.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.js +41 -18
- package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
- package/dist/commands/org.d.ts +4 -0
- package/dist/commands/org.d.ts.map +1 -1
- package/dist/commands/org.js +10 -0
- package/dist/commands/org.js.map +1 -1
- package/dist/commands/token.d.ts +9 -0
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +54 -3
- package/dist/commands/token.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +4 -13
- package/dist/commands/validate.js.map +1 -1
- package/dist/config/loader.js +2 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/connectors/README.md +2 -3
- package/dist/connectors/apple_health.ts +138 -0
- package/dist/connectors/apple_photos.ts +178 -0
- package/dist/connectors/apple_screen_time.ts +82 -0
- package/dist/connectors/browser/evaluate.ts +120 -0
- package/dist/connectors/browser/fill_form.ts +107 -0
- package/dist/connectors/browser/page_text.ts +108 -0
- package/dist/connectors/browser-scraper-utils.ts +111 -3
- package/dist/connectors/capterra.ts +5 -1
- package/dist/connectors/chrome_tabs.ts +74 -0
- package/dist/connectors/g2.ts +5 -1
- package/dist/connectors/github.ts +16 -38
- package/dist/connectors/glassdoor.ts +5 -1
- package/dist/connectors/google_calendar.ts +28 -6
- package/dist/connectors/google_gmail.ts +6 -3
- package/dist/connectors/google_play.ts +32 -5
- package/dist/connectors/hackernews.ts +37 -2
- package/dist/connectors/index.ts +14 -1
- package/dist/connectors/linkedin.ts +32 -9
- package/dist/connectors/local_directory.ts +91 -0
- package/dist/connectors/reddit.ts +1 -0
- package/dist/connectors/revolut.ts +569 -0
- package/dist/connectors/rss.ts +33 -8
- package/dist/connectors/trustpilot.ts +36 -21
- package/dist/connectors/website.ts +8 -69
- package/dist/connectors/whatsapp.ts +21 -22
- package/dist/connectors/whatsapp_local.ts +125 -0
- package/dist/connectors/x.ts +17 -7
- package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
- package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
- package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
- package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
- package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
- package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
- package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
- package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
- package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
- package/dist/db/migrations/20260514130000_connection_action_modes.sql +103 -0
- package/dist/db/migrations/20260514160000_auth_profiles_mirror_mode.sql +32 -0
- package/dist/db/migrations/20260515120000_agents_per_org_pk.sql +66 -0
- package/dist/db/migrations/20260515150000_geo_enrichment.sql +208 -0
- package/dist/db/migrations/20260515160000_drop_agents_org_id_unique.sql +24 -0
- package/dist/db/migrations/20260515170000_auth_profiles_default_for_connector.sql +23 -0
- package/dist/db/migrations/20260516120000_agents_per_org_pk_swap.sql +125 -0
- package/dist/db/migrations/20260516200000_events_search_tsv.sql +134 -0
- package/dist/db/migrations/20260516200100_events_lifecycle_changes_index.sql +25 -0
- package/dist/db/migrations/20260517010000_drop_unused_indexes.sql +49 -0
- package/dist/db/migrations/20260517020000_softdelete_orphan_feeds.sql +56 -0
- package/dist/db/migrations/20260517030000_pat_worker_id_binding.sql +27 -0
- package/dist/db/migrations/20260517040000_archive_orphan_watchers.sql +30 -0
- package/dist/db/migrations/20260517050000_watcher_agent_id_not_null.sql +34 -0
- package/dist/db/migrations/20260517060000_watcher_schema_additions.sql +78 -0
- package/dist/db/migrations/20260517150000_goals_primitive.sql +55 -0
- package/dist/db/migrations/20260517160000_drop_goals_primitive.sql +45 -0
- package/dist/db/migrations/20260518000000_pending_interactions.sql +49 -0
- package/dist/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql +22 -0
- package/dist/eval/client.d.ts.map +1 -1
- package/dist/eval/client.js +11 -0
- package/dist/eval/client.js.map +1 -1
- package/dist/eval/grader.js +2 -1
- package/dist/eval/grader.js.map +1 -1
- package/dist/eval/types.d.ts +2 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +115 -114
- package/dist/index.js.map +1 -1
- package/dist/internal/context.d.ts +9 -0
- package/dist/internal/context.d.ts.map +1 -1
- package/dist/internal/context.js +41 -6
- package/dist/internal/context.js.map +1 -1
- package/dist/internal/credentials.d.ts +5 -0
- package/dist/internal/credentials.d.ts.map +1 -1
- package/dist/internal/credentials.js +75 -1
- package/dist/internal/credentials.js.map +1 -1
- package/dist/internal/gateway-url.d.ts +14 -0
- package/dist/internal/gateway-url.d.ts.map +1 -1
- package/dist/internal/gateway-url.js +19 -0
- package/dist/internal/gateway-url.js.map +1 -1
- package/dist/internal/index.d.ts +1 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/local-env.d.ts.map +1 -1
- package/dist/internal/local-env.js +9 -2
- package/dist/internal/local-env.js.map +1 -1
- package/dist/server.bundle.mjs +42251 -36931
- package/dist/start-local.bundle.mjs +16437 -9882
- package/dist/templates/TESTING.md.tmpl +9 -9
- package/package.json +8 -6
- 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
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/connectors/g2.ts
CHANGED
|
@@ -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
|
|
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;
|