browser-workspace 0.4.2 → 0.4.5
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/README.md +15 -1
- package/SKILL.md +22 -21
- package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
- package/package.json +1 -1
- package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
- package/platforms/chatgpt/tests/test_contract.py +7 -0
- package/pyproject.toml +6 -2
- package/session_daemon.py +91 -3
- package/src/browser_workspace.egg-info/PKG-INFO +2 -1
- package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
- package/src/browser_workspace.egg-info/entry_points.txt +2 -0
- package/src/browser_workspace.egg-info/requires.txt +1 -0
- package/src/browser_workspace.egg-info/top_level.txt +1 -1
- package/src/browser_workspace_app/__init__.py +1 -0
- package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
- package/src/browser_workspace_app/cli.py +236 -0
- package/src/browser_workspace_app/extension/manifest.json +26 -0
- package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
- package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
- package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
- package/src/browser_workspace_app/extension/workspace.html +8 -0
- package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
- package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
- package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
- package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
- package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
- package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
- package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
- package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
- package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
- package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
- package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
- package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
- package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
- package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
- package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
- package/src/browser_workspace_app/managed_chrome.py +244 -0
- package/src/browser_workspace_app/node/admin.mjs +44 -0
- package/src/browser_workspace_app/package.json +36 -0
- package/src/browser_workspace_app/platform_runner.py +204 -0
- package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
- package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
- package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
- package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
- package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
- package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
- package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
- package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
- package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
- package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
- package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
- package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
- package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
- package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
- package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
- package/src/browser_workspace_app/pyproject.toml +33 -0
- package/src/browser_workspace_app/session_client.py +107 -0
- package/src/browser_workspace_app/session_daemon.py +431 -0
- package/src/browser_harness/__init__.py +0 -2
- package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/browser_harness/_ipc.py +0 -201
- package/src/browser_harness/admin.py +0 -1588
- package/src/browser_harness/auth.py +0 -546
- package/src/browser_harness/daemon.py +0 -894
- package/src/browser_harness/helpers.py +0 -668
- package/src/browser_harness/macos.py +0 -131
- package/src/browser_harness/mcp_cli.py +0 -16
- package/src/browser_harness/paths.py +0 -49
- package/src/browser_harness/recorder.py +0 -343
- package/src/browser_harness/run.py +0 -413
- package/src/browser_harness/telemetry.py +0 -308
- package/src/browser_harness/video-template.html +0 -970
- package/src/browser_harness/video.py +0 -749
- package/src/browser_harness/video_render.py +0 -524
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
platform: xhs
|
|
2
|
+
status: migrated_unverified
|
|
3
|
+
last_verified: null
|
|
4
|
+
actions:
|
|
5
|
+
post: "actions/_post.py"
|
|
6
|
+
manage: "actions/_manage.py"
|
|
7
|
+
verification:
|
|
8
|
+
status: migrated_unverified
|
|
9
|
+
last_verified: null
|
|
10
|
+
evidence: Existing post and creator-manager mechanics migrated from the working
|
|
11
|
+
post adapter. Current Browser Workspace live verification pending.
|
|
12
|
+
url_patterns:
|
|
13
|
+
- https://creator.xiaohongshu.com/*
|
|
14
|
+
authentication:
|
|
15
|
+
required: true
|
|
16
|
+
notes: Use the caller's authenticated browser context; never store credentials.
|
|
17
|
+
provenance:
|
|
18
|
+
source: skills/post/scripts/platforms/xhs
|
|
19
|
+
migrated_code: true
|
|
20
|
+
flows:
|
|
21
|
+
post:
|
|
22
|
+
status: migrated_unverified
|
|
23
|
+
evidence: Migrated action; current Browser Workspace live verification pending.
|
|
24
|
+
manage:
|
|
25
|
+
status: migrated_unverified
|
|
26
|
+
evidence: Migrated manager action; current Browser Workspace live verification
|
|
27
|
+
pending.
|
|
28
|
+
display_name: Xiaohongshu
|
|
29
|
+
aliases:
|
|
30
|
+
- xiaohongshu
|
|
31
|
+
media:
|
|
32
|
+
modes:
|
|
33
|
+
- video
|
|
34
|
+
- images
|
|
35
|
+
video:
|
|
36
|
+
required_when_mode: video
|
|
37
|
+
formats:
|
|
38
|
+
- mp4
|
|
39
|
+
images:
|
|
40
|
+
required_when_mode: images
|
|
41
|
+
formats:
|
|
42
|
+
- jpg
|
|
43
|
+
- png
|
|
44
|
+
- webp
|
|
45
|
+
max_count: 18
|
|
46
|
+
mutually_exclusive:
|
|
47
|
+
- video
|
|
48
|
+
- images
|
|
49
|
+
fields:
|
|
50
|
+
title:
|
|
51
|
+
cli: --title
|
|
52
|
+
required: true
|
|
53
|
+
type: string
|
|
54
|
+
max_chars: 20
|
|
55
|
+
implemented: true
|
|
56
|
+
body:
|
|
57
|
+
cli: --body
|
|
58
|
+
required: true
|
|
59
|
+
type: string
|
|
60
|
+
max_chars: 1000
|
|
61
|
+
implemented: true
|
|
62
|
+
tags:
|
|
63
|
+
cli: --tags
|
|
64
|
+
required: false
|
|
65
|
+
type: string_list
|
|
66
|
+
max_count: 5
|
|
67
|
+
implemented: true
|
|
68
|
+
render: 'append as #tag'
|
|
69
|
+
publish:
|
|
70
|
+
cli: --publish
|
|
71
|
+
required: false
|
|
72
|
+
type: boolean
|
|
73
|
+
default: false
|
|
74
|
+
implemented: true
|
|
75
|
+
collection:
|
|
76
|
+
required: false
|
|
77
|
+
type: string
|
|
78
|
+
max_chars: 20
|
|
79
|
+
implemented: true
|
|
80
|
+
collection_description:
|
|
81
|
+
required: false
|
|
82
|
+
type: string
|
|
83
|
+
max_chars: 50
|
|
84
|
+
implemented: true
|
|
85
|
+
capabilities:
|
|
86
|
+
draft: true
|
|
87
|
+
publish: true
|
|
88
|
+
visibility: false
|
|
89
|
+
custom_thumbnail: false
|
|
90
|
+
series_or_collection: true
|
|
91
|
+
post_status: true
|
|
92
|
+
read_comments: true
|
|
93
|
+
write_comment: true
|
|
94
|
+
reply_comments: true
|
|
95
|
+
content_mapping:
|
|
96
|
+
headline: title
|
|
97
|
+
main_text: body
|
|
98
|
+
discovery_terms: tags
|
|
99
|
+
notes:
|
|
100
|
+
- Without --publish, the adapter saves a draft.
|
|
101
|
+
- After --publish, the XHS adapter automatically verifies the created note in creator
|
|
102
|
+
note manager and reports published/reviewing/rejected state.
|
|
103
|
+
- comments/comment/reply resolve the post from the signed-in My Posts profile list
|
|
104
|
+
and use that card's live xsec detail route.
|
|
105
|
+
- The opened post detail view is the canonical place to read comments, write a top-level
|
|
106
|
+
comment, and reply to comments.
|
|
107
|
+
- Optional `collection` selects an existing exact-name collection or creates and joins it when absent.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
HERE = Path(__file__).resolve().parents[1]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_actions_do_not_use_page_info_as_tab_url_authority():
|
|
8
|
+
for name in ("_post.py", "_manage.py"):
|
|
9
|
+
source = (HERE / "actions" / name).read_text()
|
|
10
|
+
assert 'page_info()["url"]' not in source
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_actions_use_current_tab_url_for_navigation_identity():
|
|
14
|
+
post = (HERE / "actions" / "_post.py").read_text()
|
|
15
|
+
manage = (HERE / "actions" / "_manage.py").read_text()
|
|
16
|
+
assert '(current_tab().get("url") or "") != url' in post
|
|
17
|
+
assert '(current_tab().get("url") or "") != url' in manage
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_video_readiness_uses_editor_state_not_preview():
|
|
21
|
+
source=(HERE/'actions'/'_post.py').read_text()
|
|
22
|
+
assert '上传中' in source
|
|
23
|
+
assert '重新上传' in source
|
|
24
|
+
assert 'Upload processed and editor-ready!' in source
|
|
25
|
+
|
|
26
|
+
def test_collection_and_receipt_contracts_are_present():
|
|
27
|
+
source=(HERE/'actions'/'_post.py').read_text()
|
|
28
|
+
manifest=(HERE/'manifest.yaml').read_text()
|
|
29
|
+
assert 'choose_collection' in source
|
|
30
|
+
assert 'creator_manager_receipt' in source
|
|
31
|
+
assert 'XHS_RECEIPT' in source
|
|
32
|
+
assert 'duplicate_count' in source
|
|
33
|
+
assert 'series_or_collection: true' in manifest
|
|
34
|
+
assert 'collection:' in manifest
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: youtube-platform
|
|
3
|
+
description: "YouTube Studio Browser Workspace upload mechanics."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# YouTube
|
|
7
|
+
|
|
8
|
+
Use the parent Browser Workspace skill contract first. The
|
|
9
|
+
`actions/` directory contains the canonical migrated Studio upload
|
|
10
|
+
mechanics; `skills/post` retains payload and publishing orchestration.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import time, json
|
|
2
|
+
|
|
3
|
+
CFG = json.load(open("__CFG_PATH__"))
|
|
4
|
+
|
|
5
|
+
tags = CFG.get("tags", [])
|
|
6
|
+
thumbnail = CFG.get("thumbnail", "")
|
|
7
|
+
has_extras = bool(tags or thumbnail)
|
|
8
|
+
total_steps = 7 + (1 if tags else 0) + (1 if thumbnail else 0)
|
|
9
|
+
step = [0]
|
|
10
|
+
def next_step(label):
|
|
11
|
+
step[0] += 1
|
|
12
|
+
print(f"[{step[0]}/{total_steps}] {label}")
|
|
13
|
+
|
|
14
|
+
def click_btn(text):
|
|
15
|
+
text_s = json.dumps(text)
|
|
16
|
+
for _ in range(5):
|
|
17
|
+
clicked = js(
|
|
18
|
+
'(function(){ var btns=document.querySelectorAll("ytcp-button"); '
|
|
19
|
+
'var b=Array.from(btns).find(function(el){return el.textContent.trim()===' + text_s + ' && !el.disabled;}); '
|
|
20
|
+
'if(!b)return false; b.click(); return true; })()'
|
|
21
|
+
)
|
|
22
|
+
if clicked:
|
|
23
|
+
return True
|
|
24
|
+
time.sleep(1)
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
next_step("Opening YouTube Studio upload...")
|
|
28
|
+
new_tab("https://www.youtube.com/upload")
|
|
29
|
+
wait_for_load()
|
|
30
|
+
time.sleep(3)
|
|
31
|
+
|
|
32
|
+
url = page_info()["url"]
|
|
33
|
+
if "accounts.google.com" in url:
|
|
34
|
+
print("ERROR: Not logged in")
|
|
35
|
+
capture_screenshot()
|
|
36
|
+
raise SystemExit(1)
|
|
37
|
+
|
|
38
|
+
next_step("Uploading video...")
|
|
39
|
+
upload_file("input[type=file]", CFG["video"])
|
|
40
|
+
time.sleep(5)
|
|
41
|
+
|
|
42
|
+
next_step("Waiting for processing...")
|
|
43
|
+
time.sleep(3)
|
|
44
|
+
|
|
45
|
+
next_step("Setting title: " + CFG["title"])
|
|
46
|
+
title_escaped = json.dumps(CFG["title"])
|
|
47
|
+
js('var b=document.querySelectorAll("#textbox")[0];b.textContent=' + title_escaped + ';b.dispatchEvent(new Event("input",{bubbles:true}));""')
|
|
48
|
+
time.sleep(0.5)
|
|
49
|
+
|
|
50
|
+
next_step("Setting description...")
|
|
51
|
+
desc_escaped = json.dumps(CFG["desc"])
|
|
52
|
+
js('var b=document.querySelectorAll("#textbox")[1];b.textContent=' + desc_escaped + ';b.dispatchEvent(new Event("input",{bubbles:true}));""')
|
|
53
|
+
time.sleep(0.5)
|
|
54
|
+
|
|
55
|
+
# Upload thumbnail if provided
|
|
56
|
+
if thumbnail:
|
|
57
|
+
next_step("Uploading thumbnail...")
|
|
58
|
+
# Click "Upload thumbnail" button to reveal file input
|
|
59
|
+
js('var btn=document.querySelector("#still-picker ytcp-button,#upload-prompt");if(btn)btn.click();""')
|
|
60
|
+
time.sleep(1)
|
|
61
|
+
upload_file("#file-loader input[type=file],input[accept*=image]", thumbnail)
|
|
62
|
+
time.sleep(3)
|
|
63
|
+
|
|
64
|
+
# Add tags via "Show more" → tags input
|
|
65
|
+
if tags:
|
|
66
|
+
next_step("Adding tags: " + ", ".join(tags))
|
|
67
|
+
# Expand "Show more" section
|
|
68
|
+
js('var btn=document.querySelector("#toggle-button ytcp-button,ytcp-button#toggle-button");if(btn)btn.click();""')
|
|
69
|
+
time.sleep(1)
|
|
70
|
+
# Find tags input and type comma-separated tags
|
|
71
|
+
tag_str = ",".join(tags)
|
|
72
|
+
tag_escaped = json.dumps(tag_str)
|
|
73
|
+
js('var inp=document.querySelector("input.tags-input,input[aria-label*=Tag],input[placeholder*=Tag]");if(inp){var ns=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value").set;ns.call(inp,' + tag_escaped + ');inp.dispatchEvent(new Event("input",{bubbles:true}));inp.dispatchEvent(new Event("change",{bubbles:true}))};"done"')
|
|
74
|
+
time.sleep(0.5)
|
|
75
|
+
|
|
76
|
+
next_step("Setting audience" + (" (MADE FOR KIDS)" if CFG.get("made_for_kids") else "") + " + advancing wizard...")
|
|
77
|
+
if CFG.get("made_for_kids"):
|
|
78
|
+
# COPPA: select explicitly by name — never rely on radio order
|
|
79
|
+
js('var r=document.querySelector("tp-yt-paper-radio-button[name=VIDEO_MADE_FOR_KIDS_MFK]");if(r){r.click()}')
|
|
80
|
+
else:
|
|
81
|
+
js('var g=document.querySelector(".made-for-kids-group");if(g){g.querySelectorAll("tp-yt-paper-radio-button")[1].click()}')
|
|
82
|
+
time.sleep(1)
|
|
83
|
+
|
|
84
|
+
for w_step in ["Video elements", "Initial check", "Visibility"]:
|
|
85
|
+
print(" -> " + w_step)
|
|
86
|
+
click_btn("Next")
|
|
87
|
+
time.sleep(2)
|
|
88
|
+
|
|
89
|
+
vis = CFG["vis"]
|
|
90
|
+
next_step("Setting visibility to " + vis + " and saving...")
|
|
91
|
+
for _ in range(5):
|
|
92
|
+
found = js('document.querySelector("[name=' + vis + ']") ? "found" : "notfound"')
|
|
93
|
+
if found == "found":
|
|
94
|
+
js('document.querySelector("[name=' + vis + ']").click()')
|
|
95
|
+
time.sleep(1)
|
|
96
|
+
break
|
|
97
|
+
time.sleep(1)
|
|
98
|
+
|
|
99
|
+
click_btn("Save")
|
|
100
|
+
time.sleep(5)
|
|
101
|
+
|
|
102
|
+
capture_screenshot()
|
|
103
|
+
print("Done! Video saved as " + vis + ".")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
platform: youtube
|
|
2
|
+
status: migrated_unverified
|
|
3
|
+
last_verified: null
|
|
4
|
+
actions:
|
|
5
|
+
post: actions/_post.py
|
|
6
|
+
verification:
|
|
7
|
+
status: migrated_unverified
|
|
8
|
+
last_verified: null
|
|
9
|
+
evidence: Existing Studio upload mechanics migrated from the working post adapter.
|
|
10
|
+
Current Browser Workspace live verification pending.
|
|
11
|
+
url_patterns:
|
|
12
|
+
- https://www.youtube.com/upload
|
|
13
|
+
- https://studio.youtube.com/*
|
|
14
|
+
authentication:
|
|
15
|
+
required: true
|
|
16
|
+
notes: Use the caller's authenticated browser context; never store credentials.
|
|
17
|
+
provenance:
|
|
18
|
+
source: skills/post/scripts/platforms/youtube
|
|
19
|
+
migrated_code: true
|
|
20
|
+
flows:
|
|
21
|
+
post:
|
|
22
|
+
status: migrated_unverified
|
|
23
|
+
evidence: Migrated action; current Browser Workspace live verification pending.
|
|
24
|
+
display_name: YouTube
|
|
25
|
+
aliases:
|
|
26
|
+
- yt
|
|
27
|
+
media:
|
|
28
|
+
modes:
|
|
29
|
+
- video
|
|
30
|
+
video:
|
|
31
|
+
required: true
|
|
32
|
+
fields:
|
|
33
|
+
title:
|
|
34
|
+
cli: --title
|
|
35
|
+
required: true
|
|
36
|
+
type: string
|
|
37
|
+
max_chars: 100
|
|
38
|
+
implemented: true
|
|
39
|
+
desc:
|
|
40
|
+
cli: --desc
|
|
41
|
+
required: false
|
|
42
|
+
type: string
|
|
43
|
+
max_chars: 5000
|
|
44
|
+
implemented: true
|
|
45
|
+
tags:
|
|
46
|
+
cli: --tags
|
|
47
|
+
required: false
|
|
48
|
+
type: string_list
|
|
49
|
+
max_total_chars: 500
|
|
50
|
+
max_chars_each: 30
|
|
51
|
+
implemented: true
|
|
52
|
+
thumbnail:
|
|
53
|
+
cli: --thumbnail
|
|
54
|
+
required: false
|
|
55
|
+
type: file
|
|
56
|
+
formats:
|
|
57
|
+
- jpg
|
|
58
|
+
- png
|
|
59
|
+
max_size_mb: 2
|
|
60
|
+
implemented: true
|
|
61
|
+
visibility:
|
|
62
|
+
cli_group:
|
|
63
|
+
- --private
|
|
64
|
+
- --unlisted
|
|
65
|
+
- --public
|
|
66
|
+
required: false
|
|
67
|
+
type: enum
|
|
68
|
+
values:
|
|
69
|
+
- PRIVATE
|
|
70
|
+
- UNLISTED
|
|
71
|
+
- PUBLIC
|
|
72
|
+
default: PRIVATE
|
|
73
|
+
implemented: true
|
|
74
|
+
made_for_kids:
|
|
75
|
+
cli: --made-for-kids
|
|
76
|
+
required: false
|
|
77
|
+
type: boolean
|
|
78
|
+
default: false
|
|
79
|
+
implemented: true
|
|
80
|
+
capabilities:
|
|
81
|
+
draft: false
|
|
82
|
+
publish: true
|
|
83
|
+
visibility: true
|
|
84
|
+
custom_thumbnail: true
|
|
85
|
+
audience_made_for_kids: true
|
|
86
|
+
post_status: false
|
|
87
|
+
read_comments: false
|
|
88
|
+
write_comment: false
|
|
89
|
+
reply_comments: false
|
|
90
|
+
content_mapping:
|
|
91
|
+
headline: title
|
|
92
|
+
main_text: desc
|
|
93
|
+
discovery_terms: tags
|
|
94
|
+
visual_packaging: thumbnail
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools==84.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "browser-workspace"
|
|
7
|
+
version = "0.4.5"
|
|
8
|
+
description = "Agent browser workspace with persistent Python sessions, tab leasing, snapshots, and platform workflows."
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"browser-harness @ git+https://github.com/lalalic/browser-harness.git@80b74e116b9126f5159c84e5e05a750573e5f192",
|
|
12
|
+
"cdp-use==1.4.5",
|
|
13
|
+
"fetch-use==0.4.0",
|
|
14
|
+
"pillow==12.3.0",
|
|
15
|
+
"PyYAML==6.0.2",
|
|
16
|
+
"websockets==15.0.1",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
browser-workspace = "browser_workspace_app.cli:main"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
package-dir = {"" = "src"}
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
browser_workspace_app = ["**/*"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
pythonpath = ["src"]
|
|
33
|
+
addopts = "--import-mode=importlib"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import fcntl, json, os, shutil, socket, subprocess, sys, time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ROOT=Path(__file__).resolve().parent
|
|
7
|
+
RUNTIME_DIR=Path.home()/'.config/browser-workspace/runtime'
|
|
8
|
+
SOCKET_PATH=(Path(os.environ['BROWSER_WORKSPACE_SESSION_SOCKET']) if os.environ.get('BROWSER_WORKSPACE_TEST_MODE')=='1' and os.environ.get('BROWSER_WORKSPACE_SESSION_SOCKET') else RUNTIME_DIR/'session.sock')
|
|
9
|
+
|
|
10
|
+
def package_version():
|
|
11
|
+
try:
|
|
12
|
+
return json.loads((ROOT/'package.json').read_text()).get('version','dev')
|
|
13
|
+
except Exception:
|
|
14
|
+
return 'dev'
|
|
15
|
+
|
|
16
|
+
def runtime_source():
|
|
17
|
+
configured=os.environ.get('BROWSER_WORKSPACE_RUNTIME_SOURCE')
|
|
18
|
+
if configured: return Path(configured)
|
|
19
|
+
if not (ROOT/'package.json').exists(): return ROOT
|
|
20
|
+
target=RUNTIME_DIR/'packages'/package_version()
|
|
21
|
+
marker=target/'.ready'
|
|
22
|
+
if marker.exists(): return target
|
|
23
|
+
target.parent.mkdir(parents=True,exist_ok=True)
|
|
24
|
+
tmp=target.with_name(target.name+'.tmp')
|
|
25
|
+
shutil.rmtree(tmp,ignore_errors=True)
|
|
26
|
+
tmp.mkdir(parents=True)
|
|
27
|
+
for name in ['pyproject.toml','session_daemon.py','session_client.py','platform_runner.py','managed_chrome.py','src','agent-workspace','interaction-skills','platforms','extension','licenses']:
|
|
28
|
+
source=ROOT/name
|
|
29
|
+
if not source.exists(): continue
|
|
30
|
+
destination=tmp/name
|
|
31
|
+
if source.is_dir(): shutil.copytree(source,destination)
|
|
32
|
+
else: shutil.copy2(source,destination)
|
|
33
|
+
(tmp/'.ready').write_text(package_version())
|
|
34
|
+
shutil.rmtree(target,ignore_errors=True)
|
|
35
|
+
tmp.rename(target)
|
|
36
|
+
return target
|
|
37
|
+
|
|
38
|
+
SOURCE=runtime_source()
|
|
39
|
+
PYTHON=Path(os.environ.get('BROWSER_WORKSPACE_SESSION_PYTHON',RUNTIME_DIR/'venv'/package_version()/'bin/python'))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def ensure_runtime_python():
|
|
44
|
+
if PYTHON.exists():
|
|
45
|
+
return PYTHON
|
|
46
|
+
RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
lock_path=RUNTIME_DIR/'runtime-bootstrap.lock'
|
|
48
|
+
with open(lock_path,'a+b') as lock:
|
|
49
|
+
fcntl.flock(lock,fcntl.LOCK_EX)
|
|
50
|
+
if PYTHON.exists():
|
|
51
|
+
return PYTHON
|
|
52
|
+
uv=shutil.which('uv')
|
|
53
|
+
if uv:
|
|
54
|
+
subprocess.run([uv,'venv','--python','3.11',str(PYTHON.parent.parent)],check=True)
|
|
55
|
+
subprocess.run([uv,'pip','install','--python',str(PYTHON),'-e',str(SOURCE)],check=True)
|
|
56
|
+
return PYTHON
|
|
57
|
+
if sys.version_info < (3,11):
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
'Browser Workspace needs Python 3.11+ for first-use bootstrap. '
|
|
60
|
+
'Install uv or Python 3.11+, then retry.'
|
|
61
|
+
)
|
|
62
|
+
subprocess.run([sys.executable,'-m','venv',str(PYTHON.parent.parent)],check=True)
|
|
63
|
+
subprocess.run([str(PYTHON),'-m','pip','install','-e',str(SOURCE)],check=True)
|
|
64
|
+
return PYTHON
|
|
65
|
+
|
|
66
|
+
def request(payload, ensure=True):
|
|
67
|
+
if ensure: ensure_daemon()
|
|
68
|
+
s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
|
|
69
|
+
try: s.connect(str(SOCKET_PATH))
|
|
70
|
+
except Exception:
|
|
71
|
+
s.close(); raise
|
|
72
|
+
with s:
|
|
73
|
+
f=s.makefile('rwb'); f.write((json.dumps(payload,ensure_ascii=False)+'\n').encode()); f.flush(); line=f.readline()
|
|
74
|
+
if not line: raise RuntimeError('browser-workspace session daemon closed without a response')
|
|
75
|
+
return json.loads(line)
|
|
76
|
+
|
|
77
|
+
def ping():
|
|
78
|
+
try: return request({'op':'ping'},ensure=False)
|
|
79
|
+
except Exception: return None
|
|
80
|
+
|
|
81
|
+
def owner_is_running():
|
|
82
|
+
"""An exclusive owner lock means a live daemon already exists."""
|
|
83
|
+
owner_path=RUNTIME_DIR/'session.lock'
|
|
84
|
+
with open(owner_path,'a+b') as owner:
|
|
85
|
+
try:
|
|
86
|
+
fcntl.flock(owner,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
|
87
|
+
except BlockingIOError:
|
|
88
|
+
return True
|
|
89
|
+
finally:
|
|
90
|
+
try: fcntl.flock(owner,fcntl.LOCK_UN)
|
|
91
|
+
except OSError: pass
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def ensure_daemon():
|
|
95
|
+
p=ping()
|
|
96
|
+
if p and p.get('ok'): return p
|
|
97
|
+
RUNTIME_DIR.mkdir(parents=True,exist_ok=True)
|
|
98
|
+
if owner_is_running():
|
|
99
|
+
raise RuntimeError('browser-workspace daemon is alive but its socket is unreachable; refusing duplicate startup')
|
|
100
|
+
runtime_python=ensure_runtime_python()
|
|
101
|
+
log=open(RUNTIME_DIR/'session-daemon.log','ab',buffering=0)
|
|
102
|
+
subprocess.Popen([str(runtime_python),str(SOURCE/'session_daemon.py')],stdin=subprocess.DEVNULL,stdout=log,stderr=log,start_new_session=True,close_fds=True)
|
|
103
|
+
deadline=time.time()+15
|
|
104
|
+
while time.time()<deadline:
|
|
105
|
+
time.sleep(.1); p=ping()
|
|
106
|
+
if p and p.get('ok'): return p
|
|
107
|
+
raise RuntimeError(f'browser-workspace session daemon did not start; see {RUNTIME_DIR / "session-daemon.log"}')
|