Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ Direct browser control via CDP. Read helpers.py — that's where the functions l
## Usage

```bash
browser-harness <<'PY'
new_tab("https://docs.browser-use.com")
browser-harness -c "
new_tab('https://docs.browser-use.com')
wait_for_load()
print(page_info())
PY
"
```

- Invoke as browser-harness — it's on $PATH. No cd, no uv run.
Expand All @@ -30,30 +30,32 @@ Available domain skills:
## Tool call shape

```bash
browser-harness <<'PY'
browser-harness -c "
# any python. helpers pre-imported. daemon auto-starts.
PY
"
```

run.py calls ensure_daemon() before exec — you never start/stop manually unless you want to.

**Quoting:** The outer shell quotes are double-quotes, so use single quotes for Python string literals inside `-c "..."`. If you need a literal single quote inside a string, escape it as `\'`.

### Remote browsers

Use remote for parallel sub-agents (each gets its own isolated browser via a distinct BU_NAME) or on a headless server. BROWSER_USE_API_KEY must be set. start_remote_daemon, list_cloud_profiles, list_local_profiles, sync_local_profile are pre-imported.

```bash
browser-harness <<'PY'
start_remote_daemon("work") # default — clean browser, no profile
# start_remote_daemon("work", profileName="my-work") # reuse a cloud profile (already logged in)
# start_remote_daemon("work", profileId="<uuid>") # same, but by UUID
# start_remote_daemon("work", proxyCountryCode="de", timeout=120) # DE proxy, 2-hour timeout
# start_remote_daemon("work", proxyCountryCode=None) # disable the Browser Use proxy
PY

BU_NAME=work browser-harness <<'PY'
new_tab("https://example.com")
browser-harness -c "
start_remote_daemon('work') # default — clean browser, no profile
# start_remote_daemon('work', profileName='my-work') # reuse a cloud profile (already logged in)
# start_remote_daemon('work', profileId='<uuid>') # same, but by UUID
# start_remote_daemon('work', proxyCountryCode='de', timeout=120) # DE proxy, 2-hour timeout
# start_remote_daemon('work', proxyCountryCode=None) # disable the Browser Use proxy
"

BU_NAME=work browser-harness -c "
new_tab('https://example.com')
print(page_info())
PY
"
```

start_remote_daemon prints liveUrl and auto-opens it in the local browser (if a GUI is detected) so the user can watch along. Headless servers print only — share the URL with the user. The daemon PATCHes the cloud browser to stop on shutdown, which persists profile state. Running remote daemons bill until timeout.
Expand Down
86 changes: 86 additions & 0 deletions domain-skills/notion/db-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Notion — read a database's full layout schematic (v3 API)

Given any Notion database URL, extract the complete page-layout schematic — pinned properties (in order), standalone/dedicated property modules, module order, named property groups + membership + order, property visibility, sub-items wiring, and automation ids — none of which the public REST API exposes.

**Mechanism:** call Notion's internal v3 API from inside the user's logged-in tab via `js()` fetch. The session cookie rides along in-browser; no token is ever extracted, stored, or logged. Read-only endpoint (`syncRecordValues`).

## Where layout state lives (internal data model)

- **`block`** (the DB page, type `collection_view_page`) → `format.collection_pointer` → the collection id + spaceId. Use this to resolve a URL to a collection.
- **`collection`** (= public API "data source") → `schema` (property id → {name, type}) and `format`:
- `property_groups`: `[{id, title, propertyIds[]}]` — named groups, ordered, with ordered membership.
- `layout_pointer`: `{table: "layout", id, spaceId}` — present **only when the page layout was customized**; absent = default layout.
- `property_visibility`: `[{property, visibility: show|hide|hide_if_empty}]`.
- `collection_page_properties`: ordered `[{property, visible}]` (legacy page-property list; keeps **ghost entries for deleted properties**).
- `collection_page_sections`, `page_section_visibility` (comments/backlinks config), `subitem_property` (sub-items relation id), `automation_ids[]`.
- **`layout`** (own record type, parent = collection) → `modules.page_layout_schema`: ordered module list:
- `{type: "titleWithIcon", propertyIds[]}` — **the pinned chips, in display order**.
- `{type: "property", propertyId, config}` — a **standalone/dedicated property module** (e.g. `property_file` with style).
- `{type: "properties"}` — the property section; `relationsGroup`, `cover`, `discussions`, `editor`.
- **`automation`** (own record type, ids from `automation_ids`) → `trigger`, `action_ids`, `status`, `properties` — DB automations ARE readable here (the public API has none of this).

## Recipe

1. `new_tab('https://app.notion.com')` — must be app.notion.com; `notion.so` bounces logged-out visitors to the marketing site. If you land on a workspace page, the session is live. Auth wall → stop and ask the user.
2. Resolve the URL's page id (32-hex in the path, dashed form) to the collection:

```python
browser-harness -c "
import json
r = js('''(async () => {
const bid = '<dashed-page-id>';
const sync=async(reqs)=>(await fetch('/api/v3/syncRecordValues',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({requests:reqs})})).json();
let j=await sync([{pointer:{table:'block',id:bid},version:-1}]); // spaceId optional for block lookup
const blk=j.recordMap.block[bid].value.value;
return JSON.stringify(blk.format.collection_pointer || (blk.format.collection_pointers||[])[0]);

@cubic-dev-ai cubic-dev-ai Bot Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The collection_pointers (plural) fallback field used in step 2's JS code is not documented in the data model section — only collection_pointer (singular) is listed. Add collection_pointers to the block/format docs so someone reading the table can understand both fields.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At domain-skills/notion/db-layout.md, line 35:

<comment>The `collection_pointers` (plural) fallback field used in step 2's JS code is not documented in the data model section — only `collection_pointer` (singular) is listed. Add `collection_pointers` to the block/format docs so someone reading the table can understand both fields.</comment>

<file context>
@@ -0,0 +1,86 @@
+  const sync=async(reqs)=>(await fetch('/api/v3/syncRecordValues',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({requests:reqs})})).json();
+  let j=await sync([{pointer:{table:'block',id:bid},version:-1}]);   // spaceId optional for block lookup
+  const blk=j.recordMap.block[bid].value.value;
+  return JSON.stringify(blk.format.collection_pointer || (blk.format.collection_pointers||[])[0]);
+})()''')
+print(r)
</file context>
Fix with cubic

})()''')
print(r)
"
```

3. Fetch collection (+ layout when `layout_pointer` exists) and render the schematic:

```python
browser-harness -c "
import json
r = js('''(async () => {
const SPACE='<spaceId>', COLL='<collection-id>';
const sync=async(reqs)=>(await fetch('/api/v3/syncRecordValues',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({requests:reqs})})).json();
let j=await sync([{pointer:{table:'collection',id:COLL,spaceId:SPACE},version:-1}]);
const coll=j.recordMap.collection[COLL].value.value;
const nameOf=id=>coll.schema[id]?.name||('<'+id+'>');
const f=coll.format||{};
let out=['DB: '+coll.name[0][0]];
if(f.layout_pointer){
j=await sync([{pointer:f.layout_pointer,version:-1}]);
const lay=j.recordMap.layout[f.layout_pointer.id].value.value;
out.push('PAGE LAYOUT (module order):');
for(const m of lay.modules.page_layout_schema){
if(m.type==='titleWithIcon') out.push(' [title] pinned: '+(m.propertyIds||[]).map(nameOf).join(' | '));
else if(m.type==='property') out.push(' [module] standalone: '+nameOf(m.propertyId)+(m.config?' '+JSON.stringify(m.config):''));
else out.push(' ['+m.type+']');
}
} else out.push('PAGE LAYOUT: default (no layout record)');
out.push('PROPERTY GROUPS:');
for(const g of f.property_groups||[]) out.push(' '+g.title+' ('+g.propertyIds.length+'): '+g.propertyIds.map(nameOf).join(', '));
if(f.subitem_property) out.push('SUB-ITEMS via: '+nameOf(f.subitem_property));
const hidden=(f.property_visibility||[]).filter(p=>p.visibility!=='show');
out.push('HIDDEN PROPS: '+(hidden.length?hidden.map(p=>nameOf(p.property)).join(', '):'none'));
out.push('AUTOMATIONS: '+((f.automation_ids||[]).length));
return JSON.stringify(out);
})()''')
print(chr(10).join(json.loads(r)))
"
```

4. Automations detail (optional): `sync([{pointer:{table:'automation',id:<id>,spaceId:SPACE},version:-1}])` → `trigger`, `action_ids`, `status`.

@cubic-dev-ai cubic-dev-ai Bot Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The automation properties field is documented in the data model section but omitted from step 4's field list. If properties is intentionally excluded (e.g., rarely useful), note why; otherwise add it for consistency.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At domain-skills/notion/db-layout.md, line 76:

<comment>The automation `properties` field is documented in the data model section but omitted from step 4's field list. If `properties` is intentionally excluded (e.g., rarely useful), note why; otherwise add it for consistency.</comment>

<file context>
@@ -0,0 +1,86 @@
+"
+```
+
+4. Automations detail (optional): `sync([{pointer:{table:'automation',id:<id>,spaceId:SPACE},version:-1}])` → `trigger`, `action_ids`, `status`.
+
+## Traps
</file context>
Suggested change
4. Automations detail (optional): `sync([{pointer:{table:'automation',id:<id>,spaceId:SPACE},version:-1}])``trigger`, `action_ids`, `status`.
4. Automations detail (optional): `sync([{pointer:{table:'automation',id:<id>,spaceId:SPACE},version:-1}])``trigger`, `action_ids`, `status`, `properties`.
Fix with cubic


## Traps

- **Double envelope:** records are at `recordMap.<table>[id].value.value` — the first `.value` wraps `{value, role}`.
- **`layout_pointer` absent ≠ error** — it means the DB uses the default layout (nothing was customized).
- **Ghost properties:** `collection_page_properties` and even layout `propertyIds` can reference deleted properties — `nameOf` misses resolve to `<id>`; report, don't crash.
- **Escaping:** never put `\n` inside the `js('''…''')` string (bash+python eat backslashes → JS syntax error → `js()` returns `None`). Return `JSON.stringify(array)` and join in Python.
- **Ids:** URLs use bare 32-hex; v3 pointers need the dashed UUID form.
- **This is Notion's private, unversioned API** — shapes can change without notice. Field names above verified 2026-07-22.
- Do not write via v3 (`saveTransactions`) from this recipe — read-only.
2 changes: 2 additions & 0 deletions install.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ Chrome / Browser Use cloud -> CDP WS -> daemon.py -> /tmp/bu-<NAME>.sock -> run.
- Try attaching before asking the user to change anything. Decide what to escalate based on the harness's error message, not on whether Chrome is visibly running.
- The remote-debugging checkbox is per-profile sticky in Chrome. If it has ever been ticked on a profile, just launching Chrome is enough — only navigate to `chrome://inspect/#remote-debugging` when `DevToolsActivePort` is genuinely missing.
- The first connect may block on Chrome's `Allow` dialog, and Chrome may also stop first on the profile picker.
- The `Allow remote debugging?` dialog is a **once-per-Chrome-launch** consent gate, not a per-call or per-session prompt, and not a misconfiguration. Symptom: `/json/version` (HTTP) answers, but the CDP **WebSocket** handshake returns **HTTP 403** until the user clicks `Allow`. After one click the daemon stays attached and every later `browser-harness` call in that same Chrome launch connects silently. Seeing it "every time" almost always means Chrome was restarted in between (a fresh launch = a fresh consent).
- **Do NOT try to skip the dialog with `--remote-debugging-port=9222`.** Since Chrome 136 the flag is **silently ignored when Chrome runs on the default user-data-dir** — the port never binds, so it does nothing for the user's real profile. The flag only works with an explicit non-default `--user-data-dir`, i.e. a separate automation profile with no existing logins/extensions. For the user's normal profile, the `chrome://inspect` checkbox + the once-per-launch `Allow` click is the only path. (Verified 2026-06: flag-launched Chrome on the default profile leaves 9222 unbound; same flag with `--user-data-dir=/tmp/...` binds fine.)
- `DevToolsActivePort` can exist before the port is actually listening. Treat connection refused as "still enabling" and keep polling briefly.
- If the port is listening but `/json/version` returns `404`, treat that as expected on newer Chrome builds and retry `browser-harness`.
- Chrome may open the profile picker before any real tab exists.
Expand Down