screenhand 0.1.1 → 0.3.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 (241) hide show
  1. package/README.md +193 -109
  2. package/bin/darwin-arm64/macos-bridge +0 -0
  3. package/dist/mcp-desktop.js +5876 -0
  4. package/dist/scripts/codex-monitor-daemon.js +335 -0
  5. package/dist/scripts/export-help-center.js +112 -0
  6. package/dist/scripts/marketing-loop.js +117 -0
  7. package/dist/scripts/observer-daemon.js +288 -0
  8. package/dist/scripts/orchestrator-daemon.js +399 -0
  9. package/dist/scripts/supervisor-daemon.js +272 -0
  10. package/dist/scripts/threads-campaign.js +208 -0
  11. package/dist/scripts/worker-daemon.js +228 -0
  12. package/dist/src/agent/cli.js +82 -0
  13. package/dist/src/agent/loop.js +274 -0
  14. package/dist/src/community/fetcher.js +109 -0
  15. package/dist/src/community/index.js +6 -0
  16. package/dist/src/community/publisher.js +191 -0
  17. package/dist/src/community/remote-api.js +121 -0
  18. package/dist/src/community/types.js +3 -0
  19. package/dist/src/community/validator.js +95 -0
  20. package/{src/config.ts → dist/src/config.js} +5 -10
  21. package/dist/src/context-tracker.js +489 -0
  22. package/{src/index.ts → dist/src/index.js} +32 -52
  23. package/dist/src/ingestion/coverage-auditor.js +233 -0
  24. package/dist/src/ingestion/doc-parser.js +164 -0
  25. package/dist/src/ingestion/index.js +8 -0
  26. package/dist/src/ingestion/menu-scanner.js +152 -0
  27. package/dist/src/ingestion/reference-merger.js +186 -0
  28. package/dist/src/ingestion/shortcut-extractor.js +180 -0
  29. package/dist/src/ingestion/tutorial-extractor.js +170 -0
  30. package/dist/src/ingestion/types.js +3 -0
  31. package/dist/src/jobs/manager.js +305 -0
  32. package/dist/src/jobs/runner.js +806 -0
  33. package/dist/src/jobs/store.js +102 -0
  34. package/dist/src/jobs/types.js +30 -0
  35. package/dist/src/jobs/worker.js +97 -0
  36. package/dist/src/learning/engine.js +356 -0
  37. package/dist/src/learning/index.js +9 -0
  38. package/dist/src/learning/locator-policy.js +120 -0
  39. package/dist/src/learning/pattern-policy.js +89 -0
  40. package/dist/src/learning/recovery-policy.js +116 -0
  41. package/dist/src/learning/sensor-policy.js +115 -0
  42. package/dist/src/learning/timing-model.js +204 -0
  43. package/dist/src/learning/topology-policy.js +90 -0
  44. package/dist/src/learning/types.js +9 -0
  45. package/dist/src/logging/timeline-logger.js +48 -0
  46. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  47. package/dist/src/mcp/server.js +363 -0
  48. package/dist/src/mcp-entry.js +60 -0
  49. package/dist/src/memory/playbook-seeds.js +200 -0
  50. package/dist/src/memory/recall.js +222 -0
  51. package/dist/src/memory/research.js +104 -0
  52. package/dist/src/memory/seeds.js +101 -0
  53. package/dist/src/memory/service.js +446 -0
  54. package/dist/src/memory/session.js +169 -0
  55. package/dist/src/memory/store.js +451 -0
  56. package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
  57. package/dist/src/monitor/codex-monitor.js +382 -0
  58. package/dist/src/monitor/task-queue.js +97 -0
  59. package/dist/src/monitor/types.js +62 -0
  60. package/dist/src/native/bridge-client.js +412 -0
  61. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  62. package/dist/src/observer/state.js +199 -0
  63. package/dist/src/observer/types.js +43 -0
  64. package/dist/src/orchestrator/state.js +68 -0
  65. package/dist/src/orchestrator/types.js +22 -0
  66. package/dist/src/perception/ax-source.js +162 -0
  67. package/dist/src/perception/cdp-source.js +162 -0
  68. package/dist/src/perception/coordinator.js +771 -0
  69. package/dist/src/perception/frame-differ.js +287 -0
  70. package/dist/src/perception/index.js +22 -0
  71. package/dist/src/perception/manager.js +199 -0
  72. package/dist/src/perception/types.js +47 -0
  73. package/dist/src/perception/vision-source.js +399 -0
  74. package/dist/src/planner/deterministic.js +298 -0
  75. package/dist/src/planner/executor.js +870 -0
  76. package/dist/src/planner/goal-store.js +92 -0
  77. package/dist/src/planner/index.js +21 -0
  78. package/dist/src/planner/planner.js +520 -0
  79. package/dist/src/planner/tool-registry.js +71 -0
  80. package/dist/src/planner/types.js +22 -0
  81. package/dist/src/platform/explorer.js +213 -0
  82. package/dist/src/platform/help-center-markdown.js +527 -0
  83. package/dist/src/platform/learner.js +257 -0
  84. package/dist/src/playbook/engine.js +486 -0
  85. package/dist/src/playbook/index.js +20 -0
  86. package/dist/src/playbook/mcp-recorder.js +204 -0
  87. package/dist/src/playbook/recorder.js +536 -0
  88. package/dist/src/playbook/runner.js +408 -0
  89. package/dist/src/playbook/store.js +312 -0
  90. package/dist/src/playbook/types.js +17 -0
  91. package/dist/src/recovery/detectors.js +156 -0
  92. package/dist/src/recovery/engine.js +327 -0
  93. package/dist/src/recovery/index.js +20 -0
  94. package/dist/src/recovery/strategies.js +274 -0
  95. package/dist/src/recovery/types.js +20 -0
  96. package/dist/src/runtime/accessibility-adapter.js +430 -0
  97. package/dist/src/runtime/app-adapter.js +64 -0
  98. package/dist/src/runtime/applescript-adapter.js +305 -0
  99. package/dist/src/runtime/ax-role-map.js +96 -0
  100. package/dist/src/runtime/browser-adapter.js +52 -0
  101. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  102. package/dist/src/runtime/composite-adapter.js +221 -0
  103. package/dist/src/runtime/execution-contract.js +159 -0
  104. package/dist/src/runtime/executor.js +286 -0
  105. package/dist/src/runtime/locator-cache.js +50 -0
  106. package/dist/src/runtime/planning-loop.js +63 -0
  107. package/dist/src/runtime/service.js +432 -0
  108. package/dist/src/runtime/session-manager.js +63 -0
  109. package/dist/src/runtime/state-observer.js +121 -0
  110. package/dist/src/runtime/vision-adapter.js +225 -0
  111. package/dist/src/state/app-map-types.js +72 -0
  112. package/dist/src/state/app-map.js +1974 -0
  113. package/dist/src/state/entity-tracker.js +108 -0
  114. package/dist/src/state/fusion.js +96 -0
  115. package/dist/src/state/index.js +21 -0
  116. package/dist/src/state/ladder-generator.js +236 -0
  117. package/dist/src/state/persistence.js +156 -0
  118. package/dist/src/state/types.js +17 -0
  119. package/dist/src/state/world-model.js +1456 -0
  120. package/dist/src/supervisor/locks.js +186 -0
  121. package/dist/src/supervisor/supervisor.js +403 -0
  122. package/dist/src/supervisor/types.js +30 -0
  123. package/dist/src/test-mcp-protocol.js +154 -0
  124. package/dist/src/types.js +17 -0
  125. package/dist/src/util/atomic-write.js +133 -0
  126. package/dist/src/util/sanitize.js +146 -0
  127. package/dist-app-maps/com.figma.Desktop.json +959 -0
  128. package/dist-app-maps/com.hnc.Discord.json +1146 -0
  129. package/dist-app-maps/notion.id.json +2831 -0
  130. package/dist-playbooks/canva-screenhand-carousel.json +445 -0
  131. package/dist-playbooks/codex-desktop.json +76 -0
  132. package/dist-playbooks/competitor-research-stack.json +122 -0
  133. package/dist-playbooks/davinci-color-grade.json +153 -0
  134. package/dist-playbooks/davinci-edit-timeline.json +162 -0
  135. package/dist-playbooks/davinci-render.json +114 -0
  136. package/dist-playbooks/devto.json +52 -0
  137. package/dist-playbooks/discord.json +41 -0
  138. package/dist-playbooks/google-flow-create-project.json +59 -0
  139. package/dist-playbooks/google-flow-edit-image.json +90 -0
  140. package/dist-playbooks/google-flow-edit-video.json +90 -0
  141. package/dist-playbooks/google-flow-generate-image.json +68 -0
  142. package/dist-playbooks/google-flow-generate-video.json +191 -0
  143. package/dist-playbooks/google-flow-open-project.json +48 -0
  144. package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
  145. package/dist-playbooks/google-flow-search-assets.json +64 -0
  146. package/dist-playbooks/instagram.json +57 -0
  147. package/dist-playbooks/linkedin.json +52 -0
  148. package/dist-playbooks/n8n.json +43 -0
  149. package/dist-playbooks/reddit.json +52 -0
  150. package/dist-playbooks/threads.json +59 -0
  151. package/dist-playbooks/x-twitter.json +59 -0
  152. package/dist-playbooks/youtube.json +59 -0
  153. package/dist-references/canva.json +646 -0
  154. package/dist-references/codex-desktop.json +305 -0
  155. package/dist-references/davinci-resolve-keyboard.json +594 -0
  156. package/dist-references/davinci-resolve-menu-map.json +1139 -0
  157. package/dist-references/davinci-resolve-menus-batch1.json +116 -0
  158. package/dist-references/davinci-resolve-menus-batch2.json +372 -0
  159. package/dist-references/davinci-resolve-menus-batch3.json +330 -0
  160. package/dist-references/davinci-resolve-menus-batch4.json +297 -0
  161. package/dist-references/davinci-resolve-shortcuts.json +333 -0
  162. package/dist-references/devto.json +317 -0
  163. package/dist-references/discord.json +549 -0
  164. package/dist-references/figma.json +1186 -0
  165. package/dist-references/finder.json +146 -0
  166. package/dist-references/google-ads-transparency.json +95 -0
  167. package/dist-references/google-flow.json +649 -0
  168. package/dist-references/instagram.json +341 -0
  169. package/dist-references/linkedin.json +324 -0
  170. package/dist-references/meta-ad-library.json +86 -0
  171. package/dist-references/n8n.json +387 -0
  172. package/dist-references/notes.json +27 -0
  173. package/dist-references/notion.json +163 -0
  174. package/dist-references/reddit.json +341 -0
  175. package/dist-references/threads.json +337 -0
  176. package/dist-references/x-twitter.json +403 -0
  177. package/dist-references/youtube.json +373 -0
  178. package/native/macos-bridge/Package.swift +1 -0
  179. package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
  180. package/native/macos-bridge/Sources/AppManagement.swift +212 -2
  181. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
  182. package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
  183. package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
  184. package/native/macos-bridge/Sources/main.swift +169 -16
  185. package/native/windows-bridge/Program.cs +5 -0
  186. package/native/windows-bridge/ScreenCapture.cs +124 -0
  187. package/package.json +29 -4
  188. package/scripts/postinstall.cjs +127 -0
  189. package/.claude/commands/automate.md +0 -28
  190. package/.claude/commands/debug-ui.md +0 -19
  191. package/.claude/commands/screenshot.md +0 -15
  192. package/.github/FUNDING.yml +0 -1
  193. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  194. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  195. package/.mcp.json +0 -8
  196. package/DESKTOP_MCP_GUIDE.md +0 -92
  197. package/SECURITY.md +0 -44
  198. package/docs/architecture.md +0 -47
  199. package/install-skills.sh +0 -19
  200. package/mcp-bridge.ts +0 -271
  201. package/mcp-desktop.ts +0 -1221
  202. package/playbooks/instagram.json +0 -41
  203. package/playbooks/instagram_v2.json +0 -201
  204. package/playbooks/x_v1.json +0 -211
  205. package/scripts/devpost-live-loop.mjs +0 -421
  206. package/src/logging/timeline-logger.ts +0 -55
  207. package/src/mcp/server.ts +0 -449
  208. package/src/memory/recall.ts +0 -191
  209. package/src/memory/research.ts +0 -146
  210. package/src/memory/seeds.ts +0 -123
  211. package/src/memory/session.ts +0 -201
  212. package/src/memory/store.ts +0 -434
  213. package/src/memory/types.ts +0 -69
  214. package/src/native/bridge-client.ts +0 -239
  215. package/src/runtime/accessibility-adapter.ts +0 -487
  216. package/src/runtime/app-adapter.ts +0 -169
  217. package/src/runtime/applescript-adapter.ts +0 -376
  218. package/src/runtime/ax-role-map.ts +0 -102
  219. package/src/runtime/browser-adapter.ts +0 -129
  220. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  221. package/src/runtime/composite-adapter.ts +0 -274
  222. package/src/runtime/executor.ts +0 -396
  223. package/src/runtime/planning-loop.ts +0 -81
  224. package/src/runtime/service.ts +0 -448
  225. package/src/runtime/session-manager.ts +0 -50
  226. package/src/runtime/state-observer.ts +0 -136
  227. package/src/runtime/vision-adapter.ts +0 -297
  228. package/src/types.ts +0 -297
  229. package/tests/bridge-client.test.ts +0 -176
  230. package/tests/browser-stealth.test.ts +0 -210
  231. package/tests/composite-adapter.test.ts +0 -64
  232. package/tests/mcp-server.test.ts +0 -151
  233. package/tests/memory-recall.test.ts +0 -339
  234. package/tests/memory-research.test.ts +0 -159
  235. package/tests/memory-seeds.test.ts +0 -120
  236. package/tests/memory-store.test.ts +0 -392
  237. package/tests/types.test.ts +0 -92
  238. package/tsconfig.check.json +0 -17
  239. package/tsconfig.json +0 -19
  240. package/vitest.config.ts +0 -8
  241. /package/{playbooks → dist-references}/devpost.json +0 -0
@@ -0,0 +1,341 @@
1
+ {
2
+ "id": "reddit",
3
+ "name": "Reddit Automation",
4
+ "description": "Battle-tested playbook for Reddit browser automation via CDP. Covers feed browsing, upvote/downvote, comment, create post, search, join subreddit, and profile viewing. Reddit uses shadow DOM extensively (shreddit-post, faceplate-textarea-input) and Lexical editor for text input.",
5
+ "platform": "reddit",
6
+ "version": "1.0.0",
7
+ "urlPatterns": [
8
+ "*reddit.com*"
9
+ ],
10
+ "tags": [
11
+ "reddit",
12
+ "social",
13
+ "browser",
14
+ "cdp",
15
+ "shadow-dom"
16
+ ],
17
+ "successCount": 0,
18
+ "failCount": 0,
19
+ "urls": {
20
+ "home": "https://www.reddit.com/",
21
+ "popular": "https://www.reddit.com/r/popular/",
22
+ "subreddit": "https://www.reddit.com/r/{subreddit}/",
23
+ "post": "https://www.reddit.com/r/{subreddit}/comments/{postId}/{slug}/",
24
+ "submit": "https://www.reddit.com/r/{subreddit}/submit/?type=TEXT",
25
+ "submit_link": "https://www.reddit.com/r/{subreddit}/submit/?type=LINK",
26
+ "submit_image": "https://www.reddit.com/r/{subreddit}/submit/?type=IMAGE",
27
+ "search": "https://www.reddit.com/search/?q={query}&type=link",
28
+ "search_subreddit": "https://www.reddit.com/r/{subreddit}/search/?q={query}",
29
+ "profile": "https://www.reddit.com/user/{username}/",
30
+ "settings": "https://www.reddit.com/settings/",
31
+ "messages": "https://www.reddit.com/message/inbox/",
32
+ "notifications": "https://www.reddit.com/notifications/"
33
+ },
34
+ "selectors": {
35
+ "nav": {
36
+ "home": "left-nav-top-section button:has-text('Home')",
37
+ "popular": "left-nav-top-section button:has-text('Popular')",
38
+ "search": "input[type='search'], #search-input, reddit-search-large input",
39
+ "create_post": "a[href='/submit'], button:has-text('Create Post')",
40
+ "user_menu": "button:has-text('Expand user menu')"
41
+ },
42
+ "post": {
43
+ "container": "shreddit-post",
44
+ "title_attr": "[post-title]",
45
+ "author_attr": "[author]",
46
+ "score_attr": "[score]",
47
+ "permalink_attr": "[permalink]",
48
+ "subreddit_attr": "[subreddit-prefixed-name]"
49
+ },
50
+ "post_actions_shadow": {
51
+ "upvote": "shreddit-post >> shadowRoot >> button:has-text('Upvote')",
52
+ "downvote": "shreddit-post >> shadowRoot >> button:has-text('Downvote')",
53
+ "comments": "shreddit-post >> shadowRoot >> button:has-text('Go to comments')",
54
+ "share": "shreddit-post >> shadowRoot >> button:has-text('Share')",
55
+ "note": "These selectors are conceptual — use JS to access shadow roots: post.shadowRoot.querySelectorAll('button')"
56
+ },
57
+ "comment": {
58
+ "composer": "shreddit-composer",
59
+ "textbox": "div[role='textbox'][contenteditable='true'][aria-placeholder='Join the conversation']",
60
+ "submit_button": "button:has-text('Comment')",
61
+ "comment_element": "shreddit-comment",
62
+ "comment_author": "shreddit-comment[author]",
63
+ "comment_text": "shreddit-comment [slot='comment']"
64
+ },
65
+ "create_post": {
66
+ "title_wrapper": "faceplate-textarea-input[name='title']",
67
+ "title_textarea": "faceplate-textarea-input[name='title'] >> shadowRoot >> textarea",
68
+ "body_textbox": "div[role='textbox'][aria-placeholder='Body text (optional)']",
69
+ "submit_button": "button:has-text('Post')",
70
+ "flair_button": "button:has-text('Add flair')",
71
+ "type_text": "[type='TEXT']",
72
+ "type_link": "[type='LINK']",
73
+ "type_image": "[type='IMAGE']",
74
+ "note": "Title textarea is inside shadow root of faceplate-textarea-input — use native value setter via JS"
75
+ },
76
+ "subreddit": {
77
+ "header": "shreddit-subreddit-header",
78
+ "join_button": "button:has-text('Join')",
79
+ "joined_button": "button:has-text('Joined')",
80
+ "sort_dropdown": "shreddit-sort-dropdown"
81
+ },
82
+ "search": {
83
+ "input": "input[type='search']",
84
+ "results": "shreddit-post, a[href*='/comments/']",
85
+ "filter_posts": "button:has-text('Posts')",
86
+ "filter_communities": "button:has-text('Communities')",
87
+ "filter_comments": "button:has-text('Comments')",
88
+ "filter_people": "button:has-text('People')"
89
+ },
90
+ "feed": {
91
+ "post_list": "shreddit-feed, main",
92
+ "post_item": "shreddit-post",
93
+ "ad_post": "shreddit-ad-post"
94
+ }
95
+ },
96
+ "detection": {
97
+ "is_logged_in": "!!document.querySelector('button[aria-label*=\"Expand user menu\"], button:has-text(\"Expand user menu\")')",
98
+ "is_home": "window.location.pathname === '/' || window.location.pathname === '/home/'",
99
+ "is_subreddit": "/^\\/r\\/[a-zA-Z0-9_]+\\/?$/.test(window.location.pathname)",
100
+ "is_post": "/\\/comments\\//.test(window.location.pathname)",
101
+ "is_submit": "window.location.pathname.includes('/submit')",
102
+ "is_search": "window.location.pathname === '/search/'",
103
+ "is_profile": "window.location.pathname.startsWith('/user/')"
104
+ },
105
+ "flows": {
106
+ "upvote_post": {
107
+ "steps": [
108
+ "Find post element: document.querySelector('shreddit-post')",
109
+ "Access shadow root: post.shadowRoot",
110
+ "Find Upvote button: post.shadowRoot.querySelectorAll('button') — find by textContent 'Upvote'",
111
+ "Click via JS: button.click()",
112
+ "Verify: button text or style changes (filled arrow)"
113
+ ],
114
+ "guards": [
115
+ "Must be logged in",
116
+ "Post must not already be upvoted"
117
+ ],
118
+ "why": "Reddit uses shadow DOM for post action buttons. Regular CSS selectors cannot reach them — must use element.shadowRoot.querySelectorAll()."
119
+ },
120
+ "downvote_post": {
121
+ "steps": [
122
+ "Find post element: document.querySelector('shreddit-post')",
123
+ "Access shadow root: post.shadowRoot",
124
+ "Find Downvote button by textContent",
125
+ "Click via JS: button.click()"
126
+ ],
127
+ "guards": [
128
+ "Must be logged in"
129
+ ]
130
+ },
131
+ "comment_on_post": {
132
+ "steps": [
133
+ "Navigate to post page: reddit.com/r/{subreddit}/comments/{postId}/{slug}/",
134
+ "Scroll down to find comment composer",
135
+ "Click on 'Join the conversation' placeholder area using screen coordinates (CDP click at element center)",
136
+ "Wait 1s for Lexical editor to activate and show Cancel/Comment buttons",
137
+ "Use browser_fill_form with 80ms delay on div[role='textbox'][aria-placeholder='Join the conversation']",
138
+ "Verify textbox content is set",
139
+ "Click Comment button via JS: find visible button with textContent 'Comment'",
140
+ "Wait 2s for comment to post — composer collapses when successful"
141
+ ],
142
+ "guards": [
143
+ "Must be logged in",
144
+ "Comment composer must be visible (scroll to it)"
145
+ ],
146
+ "why": "Reddit uses Lexical editor which rejects execCommand, clipboard paste, and fast CDP key events. browser_fill_form with 80ms delay works. The composer must be activated by clicking the placeholder area first — it starts collapsed with 0 dimensions until clicked."
147
+ },
148
+ "create_post": {
149
+ "steps": [
150
+ "Navigate to reddit.com/r/{subreddit}/submit/?type=TEXT",
151
+ "Wait 2s for submit page to load",
152
+ "Set title via JS: access faceplate-textarea-input[name='title'].shadowRoot.querySelector('textarea'), use native value setter (Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set), dispatch input event",
153
+ "Click body textbox via browser_human_click: div[role='textbox'][aria-placeholder='Body text (optional)']",
154
+ "Type body via browser_fill_form with 80ms delay",
155
+ "Optionally add flair",
156
+ "Click Post button"
157
+ ],
158
+ "guards": [
159
+ "Must be logged in",
160
+ "Must have sufficient karma for subreddit"
161
+ ],
162
+ "why": "Title is inside shadow root of faceplate-textarea-input custom element — cannot use CSS selectors directly. Must use native value setter. Body uses Lexical editor — same approach as comments."
163
+ },
164
+ "search": {
165
+ "steps": [
166
+ "Navigate to reddit.com/search/?q={query}&type=link",
167
+ "Wait 2s for results to load",
168
+ "Extract post data from shreddit-post elements using attributes: post-title, author, score, permalink, subreddit-prefixed-name",
169
+ "Filter by type: Posts, Communities, Comments, People tabs"
170
+ ],
171
+ "guards": [],
172
+ "why": "Direct URL navigation is more reliable than typing in Reddit's search input. Post metadata is stored as HTML attributes on shreddit-post elements."
173
+ },
174
+ "join_subreddit": {
175
+ "steps": [
176
+ "Navigate to reddit.com/r/{subreddit}/",
177
+ "Wait 2s for subreddit to load",
178
+ "Find Join button — may be in shreddit-subreddit-header shadow root or main DOM",
179
+ "Click Join button",
180
+ "Verify button changes to 'Joined'"
181
+ ],
182
+ "guards": [
183
+ "Must be logged in",
184
+ "Must not already be a member"
185
+ ],
186
+ "why": "Join button location varies — sometimes in shadow DOM of shreddit-subreddit-header, sometimes in sidebar. Check both."
187
+ },
188
+ "scroll_and_extract_feed": {
189
+ "steps": [
190
+ "Navigate to reddit.com or reddit.com/r/{subreddit}/",
191
+ "Wait 2s for feed to load",
192
+ "Extract posts from shreddit-post elements using attributes: post-title, author, score, permalink, subreddit-prefixed-name",
193
+ "Access shadow roots for upvote/downvote/comment counts",
194
+ "Scroll down for more posts",
195
+ "Wait 1.5s for lazy-loaded content"
196
+ ],
197
+ "guards": []
198
+ },
199
+ "view_profile": {
200
+ "steps": [
201
+ "Navigate to reddit.com/user/{username}/",
202
+ "Wait 2s for profile to load",
203
+ "Extract karma, account age, recent posts and comments",
204
+ "Scroll for more activity"
205
+ ],
206
+ "guards": [
207
+ "Profile must not be private or suspended"
208
+ ]
209
+ },
210
+ "extract_post_data": {
211
+ "steps": [
212
+ "Query shreddit-post elements",
213
+ "Read attributes: post.getAttribute('post-title'), post.getAttribute('author'), post.getAttribute('score'), post.getAttribute('permalink'), post.getAttribute('subreddit-prefixed-name')",
214
+ "For action buttons, access post.shadowRoot and find buttons by textContent",
215
+ "For comment count, find button with 'Go to comments' text in shadow root"
216
+ ],
217
+ "guards": [],
218
+ "why": "Reddit stores all post metadata as HTML attributes on the shreddit-post custom element. This is the most reliable way to extract data."
219
+ }
220
+ },
221
+ "errors": [
222
+ {
223
+ "error": "Login required",
224
+ "context": "Any action requiring authentication (upvote, comment, post)",
225
+ "solution": "Log in manually first. Reddit uses email/password or Google/Apple SSO.",
226
+ "severity": "high"
227
+ },
228
+ {
229
+ "error": "Shadow DOM blocks CSS selectors for action buttons",
230
+ "context": "Upvote, Downvote, Share, Comment buttons are inside shreddit-post.shadowRoot",
231
+ "solution": "Access via JS: document.querySelector('shreddit-post').shadowRoot.querySelectorAll('button'). Find by textContent ('Upvote', 'Downvote', etc.).",
232
+ "severity": "high"
233
+ },
234
+ {
235
+ "error": "Lexical editor rejects execCommand, clipboard paste, and fast typing",
236
+ "context": "Comment and post body use Lexical editor (contenteditable div with data-lexical-editor attribute)",
237
+ "solution": "Use browser_fill_form with 80ms+ delay. The composer must be activated first by clicking the placeholder area — it starts collapsed with 0 dimensions.",
238
+ "severity": "high"
239
+ },
240
+ {
241
+ "error": "Title textarea inside shadow root of faceplate-textarea-input",
242
+ "context": "Post creation title field is not accessible via regular CSS selectors",
243
+ "solution": "Use JS: document.querySelector('faceplate-textarea-input[name=\"title\"]').shadowRoot.querySelector('textarea'). Set value with native setter: Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(textarea, text). Dispatch input event.",
244
+ "severity": "high"
245
+ },
246
+ {
247
+ "error": "Comment composer has 0 dimensions until activated",
248
+ "context": "The shreddit-composer and its textbox start with 0x0 dimensions",
249
+ "solution": "Scroll to the comment area and click on the 'Join the conversation' placeholder using screen coordinates. Wait for composer to expand before typing.",
250
+ "severity": "medium"
251
+ },
252
+ {
253
+ "error": "browser_human_click returns (0,0) for shadow DOM elements",
254
+ "context": "Elements inside shadow roots report 0,0 coordinates to CDP selectors",
255
+ "solution": "Use browser_js to access shadow root, get bounding rect, then click by coordinates. Or use JS click: element.click().",
256
+ "severity": "medium"
257
+ },
258
+ {
259
+ "error": "Join button not found in expected location",
260
+ "context": "Join/Joined button may be in shadow root of shreddit-subreddit-header or rendered lazily",
261
+ "solution": "Search both main DOM and all shadow roots for button with 'Join' textContent. May need to scroll sidebar into view.",
262
+ "severity": "medium"
263
+ },
264
+ {
265
+ "error": "Ad posts mixed with regular posts",
266
+ "context": "Reddit inserts shreddit-ad-post elements between regular shreddit-post elements",
267
+ "solution": "Filter by tag name: use shreddit-post (not shreddit-ad-post) for real content.",
268
+ "severity": "low"
269
+ },
270
+ {
271
+ "error": "Reddit ads open new tabs on click",
272
+ "context": "Clicking on promoted posts navigates to external URLs",
273
+ "solution": "Skip shreddit-ad-post elements when interacting with feed.",
274
+ "severity": "low"
275
+ },
276
+ {
277
+ "error": "Frontmost app switches to VS Code when approving tool calls",
278
+ "context": "AccessibilityAdapter attaches to frontmost app",
279
+ "solution": "Use CDP browser tools — they work regardless of which app is frontmost.",
280
+ "severity": "high"
281
+ }
282
+ ],
283
+ "policyNotes": {
284
+ "rate_limits": [
285
+ "Posts: ~5/day for new accounts, more for established",
286
+ "Comments: ~50/day",
287
+ "Upvotes/Downvotes: no strict limit but don't spam",
288
+ "New accounts have karma requirements for many subreddits",
289
+ "Some subreddits require minimum account age"
290
+ ],
291
+ "safety": [
292
+ "Never automate login",
293
+ "Reddit aggressively detects automation — add 3-10s random delays",
294
+ "Use browser_stealth before interacting",
295
+ "Respect subreddit rules — many ban self-promotion",
296
+ "New accounts have very limited posting ability",
297
+ "Use browser_fill_form with 80ms+ delays for Lexical editor text input",
298
+ "Reddit may require CAPTCHA for suspicious activity"
299
+ ],
300
+ "tool_preferences": [
301
+ "browser_fill_form (80ms+ delay) — for Lexical editor text (comments, post body)",
302
+ "browser_js with native value setter — for shadow DOM inputs (post title)",
303
+ "browser_js with shadowRoot access — for upvote/downvote/action buttons",
304
+ "browser_human_click — for activating collapsed composers (needs real coordinates)",
305
+ "browser_navigate — for search (direct URL), subreddit/post navigation"
306
+ ]
307
+ },
308
+ "steps": [
309
+ {
310
+ "action": "navigate",
311
+ "url": "https://www.reddit.com/",
312
+ "description": "Open Reddit home feed"
313
+ },
314
+ {
315
+ "action": "wait",
316
+ "ms": 2000,
317
+ "description": "Wait for feed to load"
318
+ },
319
+ {
320
+ "action": "extract",
321
+ "target": "shreddit-post",
322
+ "format": "attributes",
323
+ "description": "Extract post titles, authors, scores from shreddit-post attributes"
324
+ },
325
+ {
326
+ "action": "scroll",
327
+ "direction": "down",
328
+ "amount": 5,
329
+ "description": "Scroll for more posts"
330
+ },
331
+ {
332
+ "action": "wait",
333
+ "ms": 1500,
334
+ "description": "Wait for lazy-loaded posts"
335
+ },
336
+ {
337
+ "action": "screenshot",
338
+ "description": "Capture current feed state"
339
+ }
340
+ ]
341
+ }
@@ -0,0 +1,337 @@
1
+ {
2
+ "id": "threads",
3
+ "name": "Threads Automation",
4
+ "description": "Battle-tested playbook for Threads (threads.com) browser automation via CDP. Covers feed, like, reply, repost, share, create post, search, follow/unfollow, and profile viewing.",
5
+ "platform": "threads",
6
+ "version": "1.0.0",
7
+ "urlPatterns": [
8
+ "*threads.com*",
9
+ "*threads.net*"
10
+ ],
11
+ "tags": [
12
+ "threads",
13
+ "social",
14
+ "browser",
15
+ "cdp",
16
+ "meta"
17
+ ],
18
+ "successCount": 0,
19
+ "failCount": 0,
20
+ "urls": {
21
+ "home": "https://www.threads.com/",
22
+ "search": "https://www.threads.com/search",
23
+ "activity": "https://www.threads.com/activity",
24
+ "profile": "https://www.threads.com/@{username}",
25
+ "post": "https://www.threads.com/@{username}/post/{postId}",
26
+ "settings": "https://www.threads.com/settings"
27
+ },
28
+ "selectors": {
29
+ "nav": {
30
+ "home": "svg[aria-label='Home']",
31
+ "search": "svg[aria-label='Search']",
32
+ "create": "svg[aria-label='Create']",
33
+ "notifications": "svg[aria-label='Notifications']",
34
+ "profile": "svg[aria-label='Profile']"
35
+ },
36
+ "search": {
37
+ "input": "input[placeholder='Search']",
38
+ "result_link": "a[href^='/@']"
39
+ },
40
+ "post_actions": {
41
+ "like_button": "svg[aria-label='Like']",
42
+ "unlike_button": "svg[aria-label='Unlike']",
43
+ "reply_button": "svg[aria-label='Reply']",
44
+ "repost_button": "svg[aria-label='Repost']",
45
+ "share_button": "svg[aria-label='Share']",
46
+ "more_options": "svg[aria-label='More']"
47
+ },
48
+ "feed": {
49
+ "post_container": "div[data-pressable-container='true']",
50
+ "username": "a[href^='/@'] span",
51
+ "follow_button": "svg[aria-label='Follow']"
52
+ },
53
+ "profile": {
54
+ "follow_button": "div[role='button']:has-text('Follow')",
55
+ "following_button": "div[role='button']:has-text('Following')",
56
+ "unfollow_button": "div[role='button']:has-text('Unfollow')"
57
+ },
58
+ "create_post": {
59
+ "textbox": "div[role='textbox'][contenteditable='true']",
60
+ "post_button": "button:has-text('Post'), div[role='button']:has-text('Post')",
61
+ "cancel_button": "button:has-text('Cancel')",
62
+ "attach_media": "button:has-text('Attach media')",
63
+ "add_gif": "button:has-text('Add a GIF')",
64
+ "add_emoji": "button:has-text('Add an emoji')",
65
+ "add_poll": "button:has-text('Add a poll')",
66
+ "add_location": "button:has-text('Add a location')"
67
+ },
68
+ "reply": {
69
+ "textbox": "div[role='textbox'][contenteditable='true']",
70
+ "post_button": "button:has-text('Post'), div[role='button']:has-text('Post')"
71
+ },
72
+ "repost_menu": {
73
+ "repost": "div[role='button']:has-text('RepostRepost')",
74
+ "quote": "div[role='button']:has-text('QuoteQuote')"
75
+ },
76
+ "share_menu": {
77
+ "copy_link": "div[role='button']:has-text('Copy link')",
78
+ "copy_as_image": "div[role='button']:has-text('Copy as image')",
79
+ "add_to_story": "div[role='button']:has-text('Add to Instagram story')"
80
+ }
81
+ },
82
+ "detection": {
83
+ "is_logged_in": "!!document.querySelector('svg[aria-label=\"Home\"]')",
84
+ "is_feed": "window.location.pathname === '/' || window.location.pathname === '/for_you' || window.location.pathname === '/following'",
85
+ "is_search": "window.location.pathname === '/search'",
86
+ "is_profile": "/^\\/@[a-zA-Z0-9._]+\\/?$/.test(window.location.pathname)",
87
+ "is_post": "/^\\/@[a-zA-Z0-9._]+\\/post\\//.test(window.location.pathname)",
88
+ "has_dialog_open": "!!document.querySelector('div[role=\"dialog\"]')"
89
+ },
90
+ "flows": {
91
+ "like_post": {
92
+ "steps": [
93
+ "Find post container: div[data-pressable-container='true']",
94
+ "Find Like button within post: svg[aria-label='Like']",
95
+ "Use browser_human_click on Like SVG",
96
+ "Verify: svg[aria-label='Unlike'] now exists in that post"
97
+ ],
98
+ "guards": [
99
+ "Must be logged in",
100
+ "Post must not already be liked"
101
+ ]
102
+ },
103
+ "reply_to_post": {
104
+ "steps": [
105
+ "Find Reply button in post: svg[aria-label='Reply']",
106
+ "Use browser_human_click on Reply SVG",
107
+ "Wait 1.5s for reply dialog to open",
108
+ "Find textbox: div[role='textbox'] with placeholder 'Reply to {username}...'",
109
+ "Type reply via browser_fill_form with ~80ms delay",
110
+ "Find Post button in dialog and click via browser_js",
111
+ "Wait 1s for reply to post"
112
+ ],
113
+ "guards": [
114
+ "Must be logged in"
115
+ ]
116
+ },
117
+ "repost": {
118
+ "steps": [
119
+ "Find Repost button in post: svg[aria-label='Repost']",
120
+ "Use browser_human_click on Repost SVG",
121
+ "Wait 1s for repost menu to appear",
122
+ "Click 'Repost' option (textContent contains 'RepostRepost')",
123
+ "Wait for repost confirmation"
124
+ ],
125
+ "guards": [
126
+ "Must be logged in",
127
+ "Post must not already be reposted"
128
+ ]
129
+ },
130
+ "quote_post": {
131
+ "steps": [
132
+ "Find Repost button in post: svg[aria-label='Repost']",
133
+ "Use browser_human_click on Repost SVG",
134
+ "Wait 1s for repost menu to appear",
135
+ "Click 'Quote' option (textContent contains 'QuoteQuote')",
136
+ "Wait for quote dialog to open",
137
+ "Type quote text via browser_fill_form",
138
+ "Click Post button"
139
+ ],
140
+ "guards": [
141
+ "Must be logged in"
142
+ ]
143
+ },
144
+ "share_copy_link": {
145
+ "steps": [
146
+ "Find Share button in post: svg[aria-label='Share']",
147
+ "Use browser_human_click on Share SVG",
148
+ "Wait 1s for share menu to appear",
149
+ "Click 'Copy link' button",
150
+ "Link is now in clipboard"
151
+ ],
152
+ "guards": [
153
+ "Must be logged in"
154
+ ]
155
+ },
156
+ "create_post": {
157
+ "steps": [
158
+ "Click Create button in sidebar: svg[aria-label='Create'] via parent element",
159
+ "Wait 1.5s for create dialog to open",
160
+ "Find textbox: div[role='textbox'] with placeholder 'What's new?'",
161
+ "Type post content via browser_fill_form with ~60ms delay",
162
+ "Optionally attach media, GIF, emoji, poll, or location",
163
+ "Click Post button in dialog",
164
+ "Wait for post to publish"
165
+ ],
166
+ "guards": [
167
+ "Must be logged in"
168
+ ],
169
+ "why": "Create dialog offers: Attach media, Add a GIF, Add an emoji, Add a poll, Attach text, Add a location, Add to thread, Reply options"
170
+ },
171
+ "search_profiles": {
172
+ "steps": [
173
+ "Navigate to threads.com/search",
174
+ "Wait 1s for search page to load",
175
+ "Find search input: input[placeholder='Search']",
176
+ "Type query via browser_fill_form with ~100ms delay",
177
+ "Wait 2s for search results",
178
+ "Extract profile results from a[href^='/@'] links"
179
+ ],
180
+ "guards": [
181
+ "Must be logged in"
182
+ ]
183
+ },
184
+ "follow_profile": {
185
+ "steps": [
186
+ "Navigate to threads.com/@{username}",
187
+ "Wait 2s for profile to load",
188
+ "Find Follow button: div[role='button'] with text 'Follow'",
189
+ "Click Follow via browser_js",
190
+ "Verify button changed to 'Following'"
191
+ ],
192
+ "guards": [
193
+ "Must be logged in",
194
+ "Must not already be following"
195
+ ]
196
+ },
197
+ "unfollow_profile": {
198
+ "steps": [
199
+ "Find Following button: div[role='button'] with text 'Following'",
200
+ "Click Following button",
201
+ "Wait 1s for unfollow confirmation to appear",
202
+ "Click Unfollow button",
203
+ "Verify button changed back to 'Follow'"
204
+ ],
205
+ "guards": [
206
+ "Must be logged in",
207
+ "Must currently be following"
208
+ ]
209
+ },
210
+ "scroll_and_extract_feed": {
211
+ "steps": [
212
+ "Navigate to threads.com",
213
+ "Verify logged in (check for Home icon)",
214
+ "Extract visible posts from div[data-pressable-container='true'] elements",
215
+ "Get username from a[href^='/@'] span within each post",
216
+ "Scroll down to load more content",
217
+ "Wait 1s for lazy-loaded posts",
218
+ "Extract next batch of posts"
219
+ ],
220
+ "guards": [
221
+ "Must be logged in"
222
+ ]
223
+ },
224
+ "view_profile": {
225
+ "steps": [
226
+ "Navigate to threads.com/@{username}",
227
+ "Wait 2s for profile to load",
228
+ "Extract follow/following status",
229
+ "Extract profile bio and thread count",
230
+ "Scroll to view recent threads"
231
+ ],
232
+ "guards": [
233
+ "Must be logged in"
234
+ ]
235
+ }
236
+ },
237
+ "errors": [
238
+ {
239
+ "error": "Login required / redirected to login",
240
+ "context": "Any action when not logged in",
241
+ "solution": "Log in manually first. ScreenHand cannot bypass login. Threads uses Instagram login.",
242
+ "severity": "high"
243
+ },
244
+ {
245
+ "error": "Rate limited / Action Blocked",
246
+ "context": "Too many likes, reposts, or replies",
247
+ "solution": "Wait 15-30 minutes. Threads shares rate limits with Instagram.",
248
+ "severity": "high"
249
+ },
250
+ {
251
+ "error": "Repost menu item text is 'RepostRepost' not 'Repost'",
252
+ "context": "Threads renders icon + text together causing doubled text in textContent",
253
+ "solution": "Match on textContent containing 'RepostRepost' for Repost option, 'QuoteQuote' for Quote option.",
254
+ "severity": "medium"
255
+ },
256
+ {
257
+ "error": "Search sidebar doesn't open search input",
258
+ "context": "Clicking Search SVG in sidebar navigates instead of opening inline search",
259
+ "solution": "Navigate directly to threads.com/search to get the search input.",
260
+ "severity": "medium"
261
+ },
262
+ {
263
+ "error": "Follow button is a div[role='button'] not a button element",
264
+ "context": "Threads uses div role=button for Follow/Following",
265
+ "solution": "Query div[role='button'] in addition to button elements.",
266
+ "severity": "low"
267
+ },
268
+ {
269
+ "error": "Frontmost app switches to VS Code when approving tool calls",
270
+ "context": "AccessibilityAdapter attaches to frontmost app",
271
+ "solution": "Use CDP browser tools (browser_js, browser_human_click, browser_fill_form) — they work regardless of which app is frontmost.",
272
+ "severity": "high"
273
+ }
274
+ ],
275
+ "policyNotes": {
276
+ "rate_limits": [
277
+ "Shares rate limits with Instagram",
278
+ "Likes: ~60/hr, ~150/day",
279
+ "Reposts: ~20/hr",
280
+ "Replies: ~20/hr, ~60/day",
281
+ "Follows: ~20/hr, ~100/day"
282
+ ],
283
+ "safety": [
284
+ "Never automate login (uses Instagram SSO)",
285
+ "Add 2-5s random delays between actions",
286
+ "Use browser_stealth before interacting",
287
+ "Don't exceed rate limits",
288
+ "Use browser_human_click for Like/Repost/Follow",
289
+ "Use browser_fill_form with 50-100ms delays for typing"
290
+ ],
291
+ "tool_preferences": [
292
+ "browser_human_click — for Like, Repost, Share, Follow buttons",
293
+ "browser_fill_form — for reply/post textboxes and search input",
294
+ "browser_js — for finding elements by textContent, clicking menu items, extracting data",
295
+ "browser_navigate — for page navigation (works in background)"
296
+ ]
297
+ },
298
+ "steps": [
299
+ {
300
+ "action": "navigate",
301
+ "url": "https://www.threads.com/",
302
+ "description": "Open Threads home feed"
303
+ },
304
+ {
305
+ "action": "wait",
306
+ "ms": 1500,
307
+ "description": "Wait for feed to load"
308
+ },
309
+ {
310
+ "action": "extract",
311
+ "target": "div[data-pressable-container='true']",
312
+ "format": "text",
313
+ "description": "Extract first visible post content"
314
+ },
315
+ {
316
+ "action": "scroll",
317
+ "direction": "down",
318
+ "amount": 5,
319
+ "description": "Scroll down to see more posts"
320
+ },
321
+ {
322
+ "action": "wait",
323
+ "ms": 1000,
324
+ "description": "Wait for new posts to load"
325
+ },
326
+ {
327
+ "action": "extract",
328
+ "target": "div[data-pressable-container='true']",
329
+ "format": "text",
330
+ "description": "Extract posts after scroll"
331
+ },
332
+ {
333
+ "action": "screenshot",
334
+ "description": "Capture current feed state"
335
+ }
336
+ ]
337
+ }