screenhand 0.2.0 → 0.3.1
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 +165 -446
- package/bin/darwin-arm64/macos-bridge +0 -0
- package/dist/mcp-desktop.js +3615 -400
- package/dist/scripts/export-help-center.js +112 -0
- package/dist/scripts/marketing-loop.js +117 -0
- package/dist/scripts/observer-daemon.js +288 -0
- package/dist/scripts/orchestrator-daemon.js +399 -0
- package/dist/scripts/threads-campaign.js +208 -0
- package/dist/src/community/fetcher.js +109 -0
- package/dist/src/community/index.js +6 -0
- package/dist/src/community/publisher.js +191 -0
- package/dist/src/community/remote-api.js +121 -0
- package/dist/src/community/types.js +3 -0
- package/dist/src/community/validator.js +95 -0
- package/dist/src/context-tracker.js +489 -0
- package/dist/src/ingestion/coverage-auditor.js +233 -0
- package/dist/src/ingestion/doc-parser.js +164 -0
- package/dist/src/ingestion/index.js +8 -0
- package/dist/src/ingestion/menu-scanner.js +152 -0
- package/dist/src/ingestion/reference-merger.js +186 -0
- package/dist/src/ingestion/shortcut-extractor.js +180 -0
- package/dist/src/ingestion/tutorial-extractor.js +170 -0
- package/dist/src/ingestion/types.js +3 -0
- package/dist/src/jobs/manager.js +82 -14
- package/dist/src/jobs/runner.js +138 -15
- package/dist/src/learning/engine.js +356 -0
- package/dist/src/learning/index.js +9 -0
- package/dist/src/learning/locator-policy.js +120 -0
- package/dist/src/learning/pattern-policy.js +89 -0
- package/dist/src/learning/recovery-policy.js +116 -0
- package/dist/src/learning/sensor-policy.js +115 -0
- package/dist/src/learning/timing-model.js +204 -0
- package/dist/src/learning/topology-policy.js +90 -0
- package/dist/src/learning/types.js +9 -0
- package/dist/src/logging/timeline-logger.js +4 -1
- package/dist/src/memory/playbook-seeds.js +200 -0
- package/dist/src/memory/recall.js +60 -8
- package/dist/src/memory/service.js +30 -5
- package/dist/src/memory/store.js +34 -5
- package/dist/src/native/bridge-client.js +253 -31
- package/dist/src/observer/state.js +199 -0
- package/dist/src/observer/types.js +43 -0
- package/dist/src/orchestrator/state.js +68 -0
- package/dist/src/orchestrator/types.js +22 -0
- package/dist/src/perception/ax-source.js +162 -0
- package/dist/src/perception/cdp-source.js +162 -0
- package/dist/src/perception/coordinator.js +771 -0
- package/dist/src/perception/frame-differ.js +287 -0
- package/dist/src/perception/index.js +22 -0
- package/dist/src/perception/manager.js +199 -0
- package/dist/src/perception/types.js +47 -0
- package/dist/src/perception/vision-source.js +399 -0
- package/dist/src/planner/deterministic.js +298 -0
- package/dist/src/planner/executor.js +870 -0
- package/dist/src/planner/goal-store.js +92 -0
- package/dist/src/planner/index.js +21 -0
- package/dist/src/planner/planner.js +520 -0
- package/dist/src/planner/tool-registry.js +71 -0
- package/dist/src/planner/types.js +22 -0
- package/dist/src/platform/explorer.js +213 -0
- package/dist/src/platform/help-center-markdown.js +527 -0
- package/dist/src/platform/learner.js +257 -0
- package/dist/src/playbook/engine.js +296 -11
- package/dist/src/playbook/mcp-recorder.js +204 -0
- package/dist/src/playbook/recorder.js +3 -2
- package/dist/src/playbook/runner.js +1 -1
- package/dist/src/playbook/store.js +139 -10
- package/dist/src/recovery/detectors.js +156 -0
- package/dist/src/recovery/engine.js +327 -0
- package/dist/src/recovery/index.js +20 -0
- package/dist/src/recovery/strategies.js +274 -0
- package/dist/src/recovery/types.js +20 -0
- package/dist/src/runtime/accessibility-adapter.js +55 -18
- package/dist/src/runtime/applescript-adapter.js +8 -2
- package/dist/src/runtime/cdp-chrome-adapter.js +1 -1
- package/dist/src/runtime/executor.js +23 -3
- package/dist/src/runtime/locator-cache.js +24 -2
- package/dist/src/runtime/service.js +59 -15
- package/dist/src/runtime/session-manager.js +4 -1
- package/dist/src/runtime/vision-adapter.js +2 -1
- package/dist/src/state/app-map-types.js +72 -0
- package/dist/src/state/app-map.js +1974 -0
- package/dist/src/state/entity-tracker.js +108 -0
- package/dist/src/state/fusion.js +96 -0
- package/dist/src/state/index.js +21 -0
- package/dist/src/state/ladder-generator.js +236 -0
- package/dist/src/state/persistence.js +156 -0
- package/dist/src/state/types.js +17 -0
- package/dist/src/state/world-model.js +1456 -0
- package/dist/src/util/atomic-write.js +19 -4
- package/dist/src/util/sanitize.js +146 -0
- package/dist-app-maps/com.figma.Desktop.json +959 -0
- package/dist-app-maps/com.hnc.Discord.json +1146 -0
- package/dist-app-maps/notion.id.json +2831 -0
- package/dist-playbooks/canva-screenhand-carousel.json +445 -0
- package/dist-playbooks/codex-desktop.json +76 -0
- package/dist-playbooks/competitor-research-stack.json +122 -0
- package/dist-playbooks/davinci-color-grade.json +153 -0
- package/dist-playbooks/davinci-edit-timeline.json +162 -0
- package/dist-playbooks/davinci-render.json +114 -0
- package/dist-playbooks/devto.json +52 -0
- package/dist-playbooks/discord.json +41 -0
- package/dist-playbooks/google-flow-create-project.json +59 -0
- package/dist-playbooks/google-flow-edit-image.json +90 -0
- package/dist-playbooks/google-flow-edit-video.json +90 -0
- package/dist-playbooks/google-flow-generate-image.json +68 -0
- package/dist-playbooks/google-flow-generate-video.json +191 -0
- package/dist-playbooks/google-flow-open-project.json +48 -0
- package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
- package/dist-playbooks/google-flow-search-assets.json +64 -0
- package/dist-playbooks/instagram.json +57 -0
- package/dist-playbooks/linkedin.json +52 -0
- package/dist-playbooks/n8n.json +43 -0
- package/dist-playbooks/reddit.json +52 -0
- package/dist-playbooks/threads.json +59 -0
- package/dist-playbooks/x-twitter.json +59 -0
- package/dist-playbooks/youtube.json +59 -0
- package/dist-references/canva.json +646 -0
- package/dist-references/codex-desktop.json +305 -0
- package/dist-references/davinci-resolve-keyboard.json +594 -0
- package/dist-references/davinci-resolve-menu-map.json +1139 -0
- package/dist-references/davinci-resolve-menus-batch1.json +116 -0
- package/dist-references/davinci-resolve-menus-batch2.json +372 -0
- package/dist-references/davinci-resolve-menus-batch3.json +330 -0
- package/dist-references/davinci-resolve-menus-batch4.json +297 -0
- package/dist-references/davinci-resolve-shortcuts.json +333 -0
- package/dist-references/devpost.json +186 -0
- package/dist-references/devto.json +317 -0
- package/dist-references/discord.json +549 -0
- package/dist-references/figma.json +1186 -0
- package/dist-references/finder.json +146 -0
- package/dist-references/google-ads-transparency.json +95 -0
- package/dist-references/google-flow.json +649 -0
- package/dist-references/instagram.json +341 -0
- package/dist-references/linkedin.json +324 -0
- package/dist-references/meta-ad-library.json +86 -0
- package/dist-references/n8n.json +387 -0
- package/dist-references/notes.json +27 -0
- package/dist-references/notion.json +163 -0
- package/dist-references/reddit.json +341 -0
- package/dist-references/threads.json +337 -0
- package/dist-references/x-twitter.json +403 -0
- package/dist-references/youtube.json +373 -0
- package/native/macos-bridge/Package.swift +22 -0
- package/native/macos-bridge/Sources/AccessibilityBridge.swift +482 -0
- package/native/macos-bridge/Sources/AppManagement.swift +339 -0
- package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +537 -0
- package/native/macos-bridge/Sources/ObserverBridge.swift +120 -0
- package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
- package/native/macos-bridge/Sources/VisionBridge.swift +238 -0
- package/native/macos-bridge/Sources/main.swift +498 -0
- package/native/windows-bridge/AppManagement.cs +234 -0
- package/native/windows-bridge/InputBridge.cs +436 -0
- package/native/windows-bridge/Program.cs +270 -0
- package/native/windows-bridge/ScreenCapture.cs +453 -0
- package/native/windows-bridge/UIAutomationBridge.cs +571 -0
- package/native/windows-bridge/WindowsBridge.csproj +17 -0
- package/package.json +12 -1
- package/scripts/postinstall.cjs +127 -0
- package/dist/.audit-log.jsonl +0 -55
- package/dist/.screenhand/memory/.lock +0 -1
- package/dist/.screenhand/memory/actions.jsonl +0 -85
- package/dist/.screenhand/memory/errors.jsonl +0 -5
- package/dist/.screenhand/memory/errors.jsonl.bak +0 -4
- package/dist/.screenhand/memory/state.json +0 -35
- package/dist/.screenhand/memory/state.json.bak +0 -35
- package/dist/.screenhand/memory/strategies.jsonl +0 -12
- package/dist/agent/cli.js +0 -73
- package/dist/agent/loop.js +0 -258
- package/dist/config.js +0 -9
- package/dist/index.js +0 -56
- package/dist/logging/timeline-logger.js +0 -29
- package/dist/mcp/mcp-stdio-server.js +0 -448
- package/dist/mcp/server.js +0 -347
- package/dist/mcp-entry.js +0 -59
- package/dist/memory/recall.js +0 -160
- package/dist/memory/research.js +0 -98
- package/dist/memory/seeds.js +0 -89
- package/dist/memory/session.js +0 -161
- package/dist/memory/store.js +0 -391
- package/dist/memory/types.js +0 -4
- package/dist/monitor/codex-monitor.js +0 -377
- package/dist/monitor/task-queue.js +0 -84
- package/dist/monitor/types.js +0 -49
- package/dist/native/bridge-client.js +0 -174
- package/dist/native/macos-bridge-client.js +0 -5
- package/dist/npm-publish-helper.js +0 -117
- package/dist/npm-token-cdp.js +0 -113
- package/dist/npm-token-create.js +0 -135
- package/dist/npm-token-finish.js +0 -126
- package/dist/playbook/engine.js +0 -193
- package/dist/playbook/index.js +0 -4
- package/dist/playbook/recorder.js +0 -519
- package/dist/playbook/runner.js +0 -392
- package/dist/playbook/store.js +0 -166
- package/dist/playbook/types.js +0 -4
- package/dist/runtime/accessibility-adapter.js +0 -377
- package/dist/runtime/app-adapter.js +0 -48
- package/dist/runtime/applescript-adapter.js +0 -283
- package/dist/runtime/ax-role-map.js +0 -80
- package/dist/runtime/browser-adapter.js +0 -36
- package/dist/runtime/cdp-chrome-adapter.js +0 -505
- package/dist/runtime/composite-adapter.js +0 -205
- package/dist/runtime/executor.js +0 -250
- package/dist/runtime/locator-cache.js +0 -12
- package/dist/runtime/planning-loop.js +0 -47
- package/dist/runtime/service.js +0 -372
- package/dist/runtime/session-manager.js +0 -28
- package/dist/runtime/state-observer.js +0 -105
- package/dist/runtime/vision-adapter.js +0 -208
- package/dist/test-mcp-protocol.js +0 -138
- package/dist/types.js +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "google-flow-edit-video",
|
|
3
|
+
"name": "Google Flow — Edit Video",
|
|
4
|
+
"description": "Open a Flow project, switch to video assets, open a matching video edit URL, and apply an Extend or Camera edit through Flow's internal prompt tray. EXTEND, CAMERA_MOTION, and CAMERA_POSITION are fully automated. INSERT and REMOVE are staged because they still need on-frame region selection unless {AUTO_SUBMIT} is explicitly set to 1.",
|
|
5
|
+
"platform": "google-flow",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"urlPatterns": [
|
|
8
|
+
"*labs.google/fx/tools/flow*"
|
|
9
|
+
],
|
|
10
|
+
"tags": [
|
|
11
|
+
"google-flow",
|
|
12
|
+
"browser",
|
|
13
|
+
"cdp",
|
|
14
|
+
"video",
|
|
15
|
+
"edit"
|
|
16
|
+
],
|
|
17
|
+
"successCount": 0,
|
|
18
|
+
"failCount": 0,
|
|
19
|
+
"steps": [
|
|
20
|
+
{
|
|
21
|
+
"action": "navigate",
|
|
22
|
+
"url": "https://labs.google/fx/tools/flow",
|
|
23
|
+
"description": "Open the Flow dashboard"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"action": "wait",
|
|
27
|
+
"ms": 1500,
|
|
28
|
+
"description": "Wait for dashboard project cards"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"action": "browser_js",
|
|
32
|
+
"code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const rawWanted = `{PROJECT_NAME}`.trim(); const wanted = rawWanted.toLowerCase(); const hasWanted = !!rawWanted && rawWanted !== '{PROJECT_NAME}'; const links = [...document.querySelectorAll(\"a[href*='/fx/tools/flow/project/']\")].filter(visible); if (!links.length) throw new Error('No Flow project cards found'); const pick = hasWanted ? links.find(link => ((link.closest('div')?.textContent || link.parentElement?.textContent || '')).toLowerCase().includes(wanted)) : links[0]; if (!(pick instanceof HTMLAnchorElement)) throw new Error(hasWanted ? `Flow project not found: ${rawWanted}` : 'No clickable Flow project found'); const url = pick.href || pick.getAttribute('href'); if (!url) throw new Error('Project card is missing href'); window.location.assign(url); return url; })()",
|
|
33
|
+
"description": "Open the matching project card",
|
|
34
|
+
"verify": "[role='textbox'][contenteditable='true'][data-slate-editor='true']",
|
|
35
|
+
"verifyTimeoutMs": 15000
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"action": "wait",
|
|
39
|
+
"ms": 800,
|
|
40
|
+
"description": "Allow the project editor to settle"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"action": "browser_js",
|
|
44
|
+
"code": "(() => { const button = [...document.querySelectorAll('button')].find(el => (el.textContent || '').includes('View videos')); if (button instanceof HTMLElement) button.click(); return !!button; })()",
|
|
45
|
+
"description": "Switch the asset rail to videos when the filter is present"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"action": "wait",
|
|
49
|
+
"ms": 400,
|
|
50
|
+
"description": "Wait for the video grid filter to apply"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"action": "browser_js",
|
|
54
|
+
"code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const rawUrl = `{ASSET_URL}`.trim(); const rawId = `{ASSET_ID}`.trim(); const rawIndex = `{ASSET_INDEX}`.trim(); const wantedIndex = rawIndex && rawIndex !== '{ASSET_INDEX}' ? Number(rawIndex) : 0; const anchors = [...document.querySelectorAll(\"a[href*='/edit/']\")].filter(visible).filter(anchor => { const tile = anchor.closest(\"[id^='fe_id_']\") || anchor.parentElement; return !!tile?.querySelector('video') || /play_circle/.test(tile?.textContent || ''); }); let pick = null; if (rawUrl && rawUrl !== '{ASSET_URL}') pick = anchors.find(anchor => anchor.href === rawUrl); else if (rawId && rawId !== '{ASSET_ID}') pick = anchors.find(anchor => anchor.href.includes(`/edit/${rawId}`) || anchor.closest(`[id='fe_id_${rawId}']`)); else pick = anchors[Math.max(0, wantedIndex)] || anchors[0]; if (!(pick instanceof HTMLAnchorElement)) throw new Error('No Flow video edit link found'); window.location.assign(pick.href); return pick.href; })()",
|
|
55
|
+
"description": "Open the requested video asset in edit view",
|
|
56
|
+
"verify": "[role='textbox'][contenteditable='true'][data-slate-editor='true']",
|
|
57
|
+
"verifyTimeoutMs": 15000
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"action": "wait",
|
|
61
|
+
"ms": 600,
|
|
62
|
+
"description": "Allow the video editor to settle"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"action": "browser_js",
|
|
66
|
+
"code": "(() => { const modeRaw = `{EDIT_MODE}`.trim().toUpperCase(); const mode = !modeRaw || modeRaw === '{EDIT_MODE}' ? 'EXTEND' : modeRaw; const autoSubmit = `{AUTO_SUBMIT}`.trim() === '1'; const clickButton = text => { const button = [...document.querySelectorAll('button')].find(el => (el.textContent || '').includes(text)); if (!(button instanceof HTMLElement)) throw new Error(`Flow video control not found: ${text}`); button.click(); return text; }; const pickPreset = preferred => { const buttons = [...document.querySelectorAll('button')]; const exact = buttons.find(el => (el.textContent || '').trim() === preferred); const fuzzy = buttons.find(el => (el.textContent || '').includes(preferred)); const button = exact || fuzzy; if (!(button instanceof HTMLElement)) throw new Error(`Flow camera preset not found: ${preferred}`); button.click(); return preferred; }; return new Promise(resolve => { let shouldSubmit = false; let needsPrompt = false; let selected = mode; const finish = extra => { window.__flowVideoEditMode = mode; window.__flowVideoEditShouldSubmit = shouldSubmit; window.__flowVideoEditNeedsPrompt = needsPrompt; resolve(JSON.stringify({ mode, shouldSubmit, needsPrompt, selected, ...extra })); }; if (mode === 'EXTEND') { clickButton('Extend'); shouldSubmit = true; needsPrompt = true; finish({}); return; } if (mode === 'CAMERA_MOTION' || mode === 'CAMERA_POSITION') { clickButton('Camera'); setTimeout(() => { clickButton(mode === 'CAMERA_MOTION' ? 'Camera motion' : 'Camera position'); const rawPreset = `{CAMERA_PRESET}`.trim(); const preferred = rawPreset && rawPreset !== '{CAMERA_PRESET}' ? rawPreset : mode === 'CAMERA_MOTION' ? 'Dolly in' : 'Center'; selected = pickPreset(preferred); shouldSubmit = true; needsPrompt = false; finish({}); }, 120); return; } if (mode === 'INSERT') { clickButton('Insert'); shouldSubmit = autoSubmit; needsPrompt = true; finish({ stagedOnly: !autoSubmit }); return; } if (mode === 'REMOVE') { clickButton('Remove'); shouldSubmit = autoSubmit; needsPrompt = false; finish({ stagedOnly: !autoSubmit }); return; } throw new Error(`Unsupported Flow video edit mode: ${mode}`); }); })()",
|
|
67
|
+
"description": "Choose the video edit mode and stage any camera or region-selection UI"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"action": "browser_js",
|
|
71
|
+
"code": "(() => { if (!window.__flowVideoEditShouldSubmit || !window.__flowVideoEditNeedsPrompt) return JSON.stringify({ skipped: true, reason: window.__flowVideoEditShouldSubmit ? 'no_prompt_needed' : 'staged_only' }); const prompt = `{PROMPT_TEXT}`.trim(); if (!prompt || prompt === '{PROMPT_TEXT}') throw new Error('PROMPT_TEXT variable is required for this Flow video edit mode'); const editorEl = document.querySelector(\"[role='textbox'][contenteditable='true'][data-slate-editor='true']\"); if (!(editorEl instanceof HTMLElement)) throw new Error('Flow video edit prompt box not found'); const fiberKey = Object.keys(editorEl).find(k => k.startsWith('__reactFiber')); let cur = fiberKey ? editorEl[fiberKey] : null; let store = null; for (let i = 0; cur && i < 9; i++, cur = cur.return) { if (i === 7) { store = cur.memoizedProps?.promptBoxStore; break; } } const state = store?.getState?.(); const actions = state?.actions; const textEditor = state?.inputs?.textEditor; if (!actions || !textEditor) throw new Error('Flow video edit store not found'); if (typeof actions.setMode === 'function') actions.setMode('VIDEO'); const currentText = textEditor.children?.[0]?.children?.[0]?.text ?? ''; try { textEditor.select({ anchor: { path: [0, 0], offset: 0 }, focus: { path: [0, 0], offset: currentText.length } }); } catch {} try { textEditor.deleteFragment(); } catch {} textEditor.insertText(prompt); try { textEditor.onChange(); } catch {} const text = (textEditor.children || []).map(node => (node.children || []).map(child => child.text || '').join('')).join('\\n'); window.__flowVideoEditPrompt = text; return JSON.stringify({ prompt: text, mode: store.getState().mode, editMode: window.__flowVideoEditMode || 'EXTEND' }); })()",
|
|
72
|
+
"description": "Insert the video edit prompt when the selected mode needs one"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"action": "browser_js",
|
|
76
|
+
"code": "(() => { if (!window.__flowVideoEditShouldSubmit) return JSON.stringify({ submitted: false, reason: 'staged_only' }); const submitButton = [...document.querySelectorAll('button')].find(el => (el.textContent || '').includes('arrow_forward') && (el.textContent || '').includes('Create')); if (!(submitButton instanceof HTMLElement)) throw new Error('Flow video edit submit button not found'); const fiberKey = Object.keys(submitButton).find(k => k.startsWith('__reactFiber')); let cur = fiberKey ? submitButton[fiberKey] : null; let onSubmit = null; for (let i = 0; cur && i < 6; i++, cur = cur.return) { if (i === 5) { onSubmit = cur.memoizedProps?.onSubmit; break; } } if (typeof onSubmit !== 'function') throw new Error('Flow video edit onSubmit handler not found'); window.__flowVideoEditBaseline = { resourceCount: performance.getEntriesByType('resource').length }; onSubmit(); return JSON.stringify({ submitted: true, baseline: window.__flowVideoEditBaseline, editMode: window.__flowVideoEditMode || 'EXTEND' }); })()",
|
|
77
|
+
"description": "Submit the video edit through Flow's internal handler"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"action": "browser_js",
|
|
81
|
+
"code": "(() => { if (!window.__flowVideoEditShouldSubmit) return JSON.stringify({ staged: true }); const baseline = window.__flowVideoEditBaseline || { resourceCount: 0 }; const seenRequest = () => performance.getEntriesByType('resource').slice(baseline.resourceCount).some(entry => /video:batchAsyncGenerateVideoText|flowWorkflows\\//.test(entry.name)); const start = Date.now(); return new Promise(resolve => { const check = () => { if (seenRequest()) { resolve(JSON.stringify({ submitted: true, generationRequestSeen: true, editMode: window.__flowVideoEditMode || 'EXTEND' })); return; } if (Date.now() - start > 30000) { resolve(JSON.stringify({ submitted: false, generationRequestSeen: false, editMode: window.__flowVideoEditMode || 'EXTEND' })); return; } setTimeout(check, 1000); }; check(); }); })()",
|
|
82
|
+
"description": "Wait for Flow to issue the video edit request"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"action": "screenshot",
|
|
86
|
+
"description": "Capture the video editor after staging or submission",
|
|
87
|
+
"optional": true
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "google-flow-generate-image",
|
|
3
|
+
"name": "Google Flow — Generate Image",
|
|
4
|
+
"description": "Open a Flow project, switch the internal prompt store to IMAGE mode, insert {PROMPT_TEXT} into Flow's Slate text editor, and submit through the prompt tray's internal onSubmit handler. If {PROJECT_NAME} is omitted, the first visible project is used.",
|
|
5
|
+
"platform": "google-flow",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"urlPatterns": [
|
|
8
|
+
"*labs.google/fx/tools/flow*"
|
|
9
|
+
],
|
|
10
|
+
"tags": [
|
|
11
|
+
"google-flow",
|
|
12
|
+
"browser",
|
|
13
|
+
"cdp",
|
|
14
|
+
"image",
|
|
15
|
+
"generate"
|
|
16
|
+
],
|
|
17
|
+
"successCount": 0,
|
|
18
|
+
"failCount": 0,
|
|
19
|
+
"steps": [
|
|
20
|
+
{
|
|
21
|
+
"action": "navigate",
|
|
22
|
+
"url": "https://labs.google/fx/tools/flow",
|
|
23
|
+
"description": "Open the Flow dashboard"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"action": "wait",
|
|
27
|
+
"ms": 1500,
|
|
28
|
+
"description": "Wait for dashboard project cards"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"action": "browser_js",
|
|
32
|
+
"code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const rawWanted = `{PROJECT_NAME}`.trim(); const wanted = rawWanted.toLowerCase(); const hasWanted = !!rawWanted && rawWanted !== '{PROJECT_NAME}'; const links = [...document.querySelectorAll(\"a[href*='/fx/tools/flow/project/']\")].filter(visible); if (!links.length) throw new Error('No Flow project cards found'); const pick = hasWanted ? links.find(link => ((link.closest('div')?.textContent || link.parentElement?.textContent || '')).toLowerCase().includes(wanted)) : links[0]; if (!(pick instanceof HTMLAnchorElement)) throw new Error(hasWanted ? `Flow project not found: ${rawWanted}` : 'No clickable Flow project found'); const url = pick.href || pick.getAttribute('href'); if (!url) throw new Error('Project card is missing href'); window.location.assign(url); return url; })()",
|
|
33
|
+
"description": "Open the matching project card",
|
|
34
|
+
"verify": "[role='textbox'][contenteditable='true'][data-slate-editor='true']",
|
|
35
|
+
"verifyTimeoutMs": 15000
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"action": "wait",
|
|
39
|
+
"ms": 800,
|
|
40
|
+
"description": "Allow the prompt panel to settle"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"action": "browser_js",
|
|
44
|
+
"code": "(() => { const editorEl = document.querySelector(\"[role='textbox'][contenteditable='true'][data-slate-editor='true']\"); if (!(editorEl instanceof HTMLElement)) throw new Error('Flow prompt box not found'); const fiberKey = Object.keys(editorEl).find(k => k.startsWith('__reactFiber')); let cur = fiberKey ? editorEl[fiberKey] : null; let store = null; for (let i = 0; cur && i < 9; i++, cur = cur.return) { if (i === 7) { store = cur.memoizedProps?.promptBoxStore; break; } } const state = store?.getState?.(); const actions = state?.actions; if (!actions) throw new Error('Flow prompt store actions not found'); const aspectRaw = `{ASPECT_RATIO}`.trim(); const aspect = aspectRaw && aspectRaw !== '{ASPECT_RATIO}' ? aspectRaw.toUpperCase() : 'LANDSCAPE'; const outputsRaw = `{OUTPUT_COUNT}`.trim(); const outputs = outputsRaw && outputsRaw !== '{OUTPUT_COUNT}' ? Number(outputsRaw) : 1; actions.setMode('IMAGE'); if (typeof actions.setAspectRatio === 'function') actions.setAspectRatio(aspect === 'PORTRAIT' ? 'PORTRAIT' : 'LANDSCAPE'); if (typeof actions.setOutputsPerPrompt === 'function' && Number.isFinite(outputs) && outputs >= 1) actions.setOutputsPerPrompt(outputs); return JSON.stringify({ mode: store.getState().mode, aspectRatio: store.getState().aspectRatio, outputsPerPrompt: store.getState().outputsPerPrompt }); })()",
|
|
45
|
+
"description": "Set Flow to IMAGE mode through the internal prompt store"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"action": "browser_js",
|
|
49
|
+
"code": "(() => { const prompt = `{PROMPT_TEXT}`.trim(); if (!prompt || prompt === '{PROMPT_TEXT}') throw new Error('PROMPT_TEXT variable is required'); const editorEl = document.querySelector(\"[role='textbox'][contenteditable='true'][data-slate-editor='true']\"); if (!(editorEl instanceof HTMLElement)) throw new Error('Flow prompt box not found'); const fiberKey = Object.keys(editorEl).find(k => k.startsWith('__reactFiber')); let cur = fiberKey ? editorEl[fiberKey] : null; let store = null; for (let i = 0; cur && i < 9; i++, cur = cur.return) { if (i === 7) { store = cur.memoizedProps?.promptBoxStore; break; } } const textEditor = store?.getState?.().inputs?.textEditor; if (!textEditor) throw new Error('Flow text editor not found'); const currentText = textEditor.children?.[0]?.children?.[0]?.text ?? ''; try { textEditor.select({ anchor: { path: [0, 0], offset: 0 }, focus: { path: [0, 0], offset: currentText.length } }); } catch {} try { textEditor.deleteFragment(); } catch {} textEditor.insertText(prompt); try { textEditor.onChange(); } catch {} const text = (textEditor.children || []).map(node => (node.children || []).map(child => child.text || '').join('')).join('\\n'); window.__flowPromptText = text; return JSON.stringify({ prompt: text, mode: store.getState().mode, aspectRatio: store.getState().aspectRatio }); })()",
|
|
50
|
+
"description": "Insert the generation prompt through Flow's internal Slate editor"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"action": "browser_js",
|
|
54
|
+
"code": "(() => { const uniqueTiles = () => [...new Set([...document.querySelectorAll('[data-tile-id]')].map(el => el.getAttribute('data-tile-id')).filter(Boolean))]; const submitButton = [...document.querySelectorAll('button')].find(el => (el.textContent || '').includes('arrow_forward') && (el.textContent || '').includes('Create')); if (!(submitButton instanceof HTMLElement)) throw new Error('Flow submit button not found'); const fiberKey = Object.keys(submitButton).find(k => k.startsWith('__reactFiber')); let cur = fiberKey ? submitButton[fiberKey] : null; let onSubmit = null; for (let i = 0; cur && i < 6; i++, cur = cur.return) { if (i === 5) { onSubmit = cur.memoizedProps?.onSubmit; break; } } if (typeof onSubmit !== 'function') throw new Error('Flow internal onSubmit handler not found'); window.__flowSubmitBaseline = { tileCount: uniqueTiles().length, resourceCount: performance.getEntriesByType('resource').length }; onSubmit(); return JSON.stringify(window.__flowSubmitBaseline); })()",
|
|
55
|
+
"description": "Submit through Flow's internal prompt tray handler"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"action": "browser_js",
|
|
59
|
+
"code": "(() => { const baseline = window.__flowSubmitBaseline || { tileCount: 0, resourceCount: 0 }; const uniqueTiles = () => [...new Set([...document.querySelectorAll('[data-tile-id]')].map(el => el.getAttribute('data-tile-id')).filter(Boolean))]; const hasGenerationRequest = () => performance.getEntriesByType('resource').slice(baseline.resourceCount).some(entry => /flowMedia:batchGenerateImages|flowWorkflows\\//.test(entry.name)); const start = Date.now(); return new Promise(resolve => { const check = () => { const tileCount = uniqueTiles().length; if (tileCount > baseline.tileCount || hasGenerationRequest()) { resolve(JSON.stringify({ submitted: true, tileCount, baselineTileCount: baseline.tileCount, generationRequestSeen: hasGenerationRequest() })); return; } if (Date.now() - start > 30000) { resolve(JSON.stringify({ submitted: false, tileCount, baselineTileCount: baseline.tileCount, generationRequestSeen: false })); return; } setTimeout(check, 1000); }; check(); }); })()",
|
|
60
|
+
"description": "Wait for Flow to create an image generation request or a new tile"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"action": "screenshot",
|
|
64
|
+
"description": "Capture the project after submission",
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "google-flow-generate-video",
|
|
3
|
+
"name": "Google Flow — Generate Video",
|
|
4
|
+
"description": "Open a Flow project, select video settings (model, aspect ratio, generation mode) through UI tab clicks, type {PROMPT_TEXT} via keyboard into the Slate editor, and submit by clicking the Create button. Requires {PROJECT_URL} for direct navigation. Defaults to Veo 3.1 Fast, Portrait, Frames mode for image-to-video workflows.",
|
|
5
|
+
"platform": "google-flow",
|
|
6
|
+
"version": "4.0.0",
|
|
7
|
+
"urlPatterns": [
|
|
8
|
+
"*labs.google/fx/tools/flow*"
|
|
9
|
+
],
|
|
10
|
+
"tags": [
|
|
11
|
+
"google-flow",
|
|
12
|
+
"browser",
|
|
13
|
+
"cdp",
|
|
14
|
+
"video",
|
|
15
|
+
"generate",
|
|
16
|
+
"frames"
|
|
17
|
+
],
|
|
18
|
+
"successCount": 1,
|
|
19
|
+
"failCount": 0,
|
|
20
|
+
"notes": [
|
|
21
|
+
"CRITICAL: For job_create inline steps, browser_js code goes in the 'description' field (not 'text' or 'code'). The job runner calls execBrowserJs(step.description).",
|
|
22
|
+
"CRITICAL: The ONLY reliable submit method is the job runner's 'click' action with target 'Create'. browser_human_click fails with 'Element not found via CDP'. JS-dispatched MouseEvents and programmatic onSubmit() from fiber tree both fire but silently do nothing.",
|
|
23
|
+
"CRITICAL: Settings panel tabs use Material Icons font for icons — the icon name (e.g. 'videocam') may or may not appear in textContent depending on font loading state. Match tabs by their label text ('Video', 'Image', 'Frames', etc.) not icon names.",
|
|
24
|
+
"Settings panel is opened by clicking the button whose text includes crop_16_9 or crop_9_16 plus x[1234]. All settings inside use role='tab' with aria-selected.",
|
|
25
|
+
"Frames mode lets you create video from reference images (start frame, end frame, or both). Ingredients mode lets you reference uploaded media assets.",
|
|
26
|
+
"Video generation costs 20 credits per output on Veo 3.1 Fast. Check weekly limit before generating.",
|
|
27
|
+
"type_text action works for prompt entry after focusing the Slate editor via browser_js and clearing with document.execCommand.",
|
|
28
|
+
"The prompt editor is focused and cleared with: editor.focus(); document.execCommand('selectAll'); document.execCommand('delete');",
|
|
29
|
+
"Wait 800ms+ after opening the settings panel before clicking tabs, otherwise tabs may not be rendered yet."
|
|
30
|
+
],
|
|
31
|
+
"jobStepsTemplate": [
|
|
32
|
+
{
|
|
33
|
+
"action": "navigate",
|
|
34
|
+
"target": "{PROJECT_URL}",
|
|
35
|
+
"description": "Open the Flow project"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"action": "browser_js",
|
|
39
|
+
"description": "(() => { const start = Date.now(); return new Promise(resolve => { const check = () => { if (document.querySelector(\"[role='textbox'][contenteditable='true'][data-slate-editor='true']\")) { resolve('editor ready'); return; } if (Date.now() - start > 15000) { resolve('timeout'); return; } setTimeout(check, 500); }; check(); }); })()"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"action": "browser_js",
|
|
43
|
+
"description": "(() => { const btn = [...document.querySelectorAll('button')].find(el => { const t = el.textContent || ''; return (t.includes('crop_16_9') || t.includes('crop_9_16')) && t.match(/x[1234]/); }); if (!btn) throw new Error('Settings button not found'); btn.click(); return 'opened settings panel'; })()"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"action": "browser_js",
|
|
47
|
+
"description": "(() => { return new Promise(resolve => setTimeout(() => resolve('panel rendered'), 800)); })()"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"action": "browser_js",
|
|
51
|
+
"description": "(() => { const tabs = [...document.querySelectorAll('[role=\"tab\"]')]; const videoTab = tabs.find(el => { const t = (el.textContent || '').toLowerCase(); return t.includes('video') && !t.includes('image'); }); if (!videoTab) throw new Error('Video tab not found among: ' + tabs.map(t => t.textContent?.trim()).join(' | ')); videoTab.click(); return 'Video mode selected (aria-selected=' + videoTab.getAttribute('aria-selected') + ')'; })()"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"action": "browser_js",
|
|
55
|
+
"description": "(() => { return new Promise(resolve => { setTimeout(() => { const tabs = [...document.querySelectorAll('[role=\"tab\"]')]; const framesTab = tabs.find(el => (el.textContent || '').includes('Frames')); if (framesTab && framesTab.getAttribute('aria-selected') !== 'true') framesTab.click(); const portraitTab = tabs.find(el => (el.textContent || '').includes('Portrait')); if (portraitTab && portraitTab.getAttribute('aria-selected') !== 'true') portraitTab.click(); resolve('Frames + Portrait applied'); }, 300); }); })()"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"action": "browser_js",
|
|
59
|
+
"description": "(() => { return new Promise(resolve => { setTimeout(() => { const btn = [...document.querySelectorAll('button')].find(el => { const t = el.textContent || ''; return (t.includes('crop_16_9') || t.includes('crop_9_16')) && t.match(/x[1234]/); }); if (btn) btn.click(); resolve('settings panel closed'); }, 300); }); })()"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"action": "wait",
|
|
63
|
+
"description": "Allow settings to settle"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"action": "browser_js",
|
|
67
|
+
"description": "(() => { const ed = document.querySelector(\"[role='textbox'][contenteditable='true'][data-slate-editor='true']\"); if (ed) { ed.focus(); document.execCommand('selectAll'); document.execCommand('delete'); } return 'prompt editor focused and cleared'; })()"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"action": "type_text",
|
|
71
|
+
"text": "{PROMPT_TEXT}",
|
|
72
|
+
"description": "Type the video generation prompt via keyboard"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"action": "wait",
|
|
76
|
+
"description": "Allow Slate to process typed text"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"action": "click",
|
|
80
|
+
"target": "Create",
|
|
81
|
+
"description": "Click the Create button to start video generation"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"action": "browser_js",
|
|
85
|
+
"description": "(() => { const start = Date.now(); return new Promise(resolve => { const check = () => { const body = document.body.innerText; if (body.includes('Generating') || body.match(/\\d+%/)) { resolve('video generation started'); return; } if (Date.now() - start > 20000) { resolve('timeout waiting for generation indicator'); return; } setTimeout(check, 1000); }; check(); }); })()"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"action": "screenshot",
|
|
89
|
+
"description": "Capture the project after submission"
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"steps": [
|
|
93
|
+
{
|
|
94
|
+
"action": "navigate",
|
|
95
|
+
"url": "{PROJECT_URL}",
|
|
96
|
+
"description": "Navigate directly to the Flow project URL"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"action": "wait",
|
|
100
|
+
"ms": 2000,
|
|
101
|
+
"description": "Wait for page to load"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"action": "browser_js",
|
|
105
|
+
"code": "(() => { const start = Date.now(); return new Promise(resolve => { const check = () => { if (document.querySelector(\"[role='textbox'][contenteditable='true'][data-slate-editor='true']\")) { resolve('editor ready'); return; } if (Date.now() - start > 15000) { resolve('timeout waiting for editor'); return; } setTimeout(check, 500); }; check(); }); })()",
|
|
106
|
+
"description": "Wait for the Slate prompt editor to appear (up to 15s)"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"action": "wait",
|
|
110
|
+
"ms": 500,
|
|
111
|
+
"description": "Allow the prompt panel to settle"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"action": "browser_js",
|
|
115
|
+
"code": "(() => { const settingsBtn = [...document.querySelectorAll('button')].find(el => { const t = (el.textContent || ''); return (t.includes('crop_16_9') || t.includes('crop_9_16')) && t.match(/x[1234]/); }); if (!(settingsBtn instanceof HTMLElement)) throw new Error('Settings button not found'); settingsBtn.click(); return 'opened settings panel'; })()",
|
|
116
|
+
"description": "Open the generation settings panel by clicking the mode/aspect/count button"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"action": "wait",
|
|
120
|
+
"ms": 800,
|
|
121
|
+
"description": "Allow settings panel to fully render before clicking tabs"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"action": "browser_js",
|
|
125
|
+
"code": "(() => { const tabs = [...document.querySelectorAll('[role=\"tab\"]')]; const videoTab = tabs.find(el => { const t = (el.textContent || '').toLowerCase(); return t.includes('video') && !t.includes('image'); }); if (!videoTab) throw new Error('Video tab not found among: ' + tabs.map(t => t.textContent?.trim()).join(' | ')); videoTab.click(); return 'Video mode selected'; })()",
|
|
126
|
+
"description": "Select Video mode"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"action": "wait",
|
|
130
|
+
"ms": 300,
|
|
131
|
+
"description": "Allow mode switch to settle"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"action": "browser_js",
|
|
135
|
+
"code": "(() => { const rawMode = `{GENERATION_MODE}`.trim(); const mode = (!rawMode || rawMode === '{GENERATION_MODE}') ? 'Frames' : rawMode; const tab = [...document.querySelectorAll('[role=\"tab\"]')].find(el => (el.textContent || '').includes(mode)); if (tab && tab.getAttribute('aria-selected') !== 'true') tab.click(); return `${mode} mode selected`; })()",
|
|
136
|
+
"description": "Select Frames or Ingredients mode (defaults to Frames for image-to-video)"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"action": "browser_js",
|
|
140
|
+
"code": "(() => { const rawAspect = `{ASPECT_RATIO}`.trim().toLowerCase(); const aspect = (!rawAspect || rawAspect === '{aspect_ratio}') ? 'Portrait' : (rawAspect === 'landscape' ? 'Landscape' : 'Portrait'); const tab = [...document.querySelectorAll('[role=\"tab\"]')].find(el => (el.textContent || '').includes(aspect)); if (tab && tab.getAttribute('aria-selected') !== 'true') tab.click(); return `${aspect} aspect selected`; })()",
|
|
141
|
+
"description": "Select aspect ratio (defaults to Portrait)"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"action": "browser_js",
|
|
145
|
+
"code": "(() => { const btn = [...document.querySelectorAll('button')].find(el => { const t = (el.textContent || ''); return (t.includes('crop_16_9') || t.includes('crop_9_16')) && t.match(/x[1234]/); }); if (btn) btn.click(); return 'closed settings panel'; })()",
|
|
146
|
+
"description": "Close the settings panel"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"action": "wait",
|
|
150
|
+
"ms": 500,
|
|
151
|
+
"description": "Allow settings to settle"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"action": "browser_click",
|
|
155
|
+
"selector": "[role='textbox'][contenteditable='true'][data-slate-editor='true']",
|
|
156
|
+
"description": "Click/focus the prompt editor before typing"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"action": "browser_type",
|
|
160
|
+
"selector": "[role='textbox'][contenteditable='true'][data-slate-editor='true']",
|
|
161
|
+
"text": "{PROMPT_TEXT}",
|
|
162
|
+
"clear": true,
|
|
163
|
+
"description": "Type the video prompt via CDP keyboard events"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"action": "wait",
|
|
167
|
+
"ms": 300,
|
|
168
|
+
"description": "Allow Slate to process the typed text"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"action": "browser_js",
|
|
172
|
+
"code": "(() => { const btn = [...document.querySelectorAll('button')].find(el => (el.textContent || '').includes('arrow_forward') && (el.textContent || '').includes('Create')); if (!btn) throw new Error('Create button not found'); btn.id = 'flow-create-submit'; return 'tagged Create button'; })()",
|
|
173
|
+
"description": "Assign a unique ID to the Create submit button"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"action": "browser_human_click",
|
|
177
|
+
"selector": "#flow-create-submit",
|
|
178
|
+
"description": "Click the Create button with realistic mouse events to trigger video generation"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"action": "browser_js",
|
|
182
|
+
"code": "(() => { const start = Date.now(); return new Promise(resolve => { const check = () => { const body = document.body.innerText; if (body.includes('Generating') || body.match(/\\d+%/)) { resolve('video generation started'); return; } if (Date.now() - start > 20000) { resolve('timeout waiting for generation indicator'); return; } setTimeout(check, 1000); }; check(); }); })()",
|
|
183
|
+
"description": "Verify that video generation started"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"action": "screenshot",
|
|
187
|
+
"description": "Capture the project after submission",
|
|
188
|
+
"optional": true
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "google-flow-open-project",
|
|
3
|
+
"name": "Google Flow — Open Project",
|
|
4
|
+
"description": "Open a Google Flow project from the dashboard. If {PROJECT_NAME} is provided, the playbook opens the first card whose container text matches it; otherwise it opens the first visible project.",
|
|
5
|
+
"platform": "google-flow",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"urlPatterns": [
|
|
8
|
+
"*labs.google/fx/tools/flow*"
|
|
9
|
+
],
|
|
10
|
+
"tags": [
|
|
11
|
+
"google-flow",
|
|
12
|
+
"browser",
|
|
13
|
+
"cdp",
|
|
14
|
+
"project",
|
|
15
|
+
"open"
|
|
16
|
+
],
|
|
17
|
+
"successCount": 0,
|
|
18
|
+
"failCount": 0,
|
|
19
|
+
"steps": [
|
|
20
|
+
{
|
|
21
|
+
"action": "navigate",
|
|
22
|
+
"url": "https://labs.google/fx/tools/flow",
|
|
23
|
+
"description": "Open the Flow dashboard"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"action": "wait",
|
|
27
|
+
"ms": 1500,
|
|
28
|
+
"description": "Wait for dashboard project cards"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"action": "browser_js",
|
|
32
|
+
"code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const rawWanted = `{PROJECT_NAME}`.trim(); const wanted = rawWanted.toLowerCase(); const hasWanted = !!rawWanted && rawWanted !== '{PROJECT_NAME}'; const links = [...document.querySelectorAll(\"a[href*='/fx/tools/flow/project/']\")].filter(visible); if (!links.length) throw new Error('No Flow project cards found'); const pick = hasWanted ? links.find(link => ((link.closest('div')?.textContent || link.parentElement?.textContent || '')).toLowerCase().includes(wanted)) : links[0]; if (!(pick instanceof HTMLAnchorElement)) throw new Error(hasWanted ? `Flow project not found: ${rawWanted}` : 'No clickable Flow project found'); const url = pick.href || pick.getAttribute('href'); if (!url) throw new Error('Project card is missing href'); window.location.assign(url); return url; })()",
|
|
33
|
+
"description": "Open the matching project card",
|
|
34
|
+
"verify": "input[aria-label='Editable text']",
|
|
35
|
+
"verifyTimeoutMs": 15000
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"action": "wait",
|
|
39
|
+
"ms": 1000,
|
|
40
|
+
"description": "Allow the project editor to settle"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"action": "screenshot",
|
|
44
|
+
"description": "Capture the open project editor",
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "google-flow-open-scenebuilder",
|
|
3
|
+
"name": "Google Flow — Open Scenebuilder",
|
|
4
|
+
"description": "Open a Flow project and switch into Scenebuilder. If {PROJECT_NAME} is omitted, the first visible project is used. This playbook stages scene work but does not reorder clips because the current playbook engine has no drag step.",
|
|
5
|
+
"platform": "google-flow",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"urlPatterns": [
|
|
8
|
+
"*labs.google/fx/tools/flow*"
|
|
9
|
+
],
|
|
10
|
+
"tags": [
|
|
11
|
+
"google-flow",
|
|
12
|
+
"browser",
|
|
13
|
+
"cdp",
|
|
14
|
+
"scene",
|
|
15
|
+
"scenebuilder"
|
|
16
|
+
],
|
|
17
|
+
"successCount": 0,
|
|
18
|
+
"failCount": 0,
|
|
19
|
+
"steps": [
|
|
20
|
+
{
|
|
21
|
+
"action": "navigate",
|
|
22
|
+
"url": "https://labs.google/fx/tools/flow",
|
|
23
|
+
"description": "Open the Flow dashboard"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"action": "wait",
|
|
27
|
+
"ms": 1500,
|
|
28
|
+
"description": "Wait for dashboard project cards"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"action": "browser_js",
|
|
32
|
+
"code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const rawWanted = `{PROJECT_NAME}`.trim(); const wanted = rawWanted.toLowerCase(); const hasWanted = !!rawWanted && rawWanted !== '{PROJECT_NAME}'; const links = [...document.querySelectorAll(\"a[href*='/fx/tools/flow/project/']\")].filter(visible); if (!links.length) throw new Error('No Flow project cards found'); const pick = hasWanted ? links.find(link => ((link.closest('div')?.textContent || link.parentElement?.textContent || '')).toLowerCase().includes(wanted)) : links[0]; if (!(pick instanceof HTMLAnchorElement)) throw new Error(hasWanted ? `Flow project not found: ${rawWanted}` : 'No clickable Flow project found'); const url = pick.href || pick.getAttribute('href'); if (!url) throw new Error('Project card is missing href'); window.location.assign(url); return url; })()",
|
|
33
|
+
"description": "Open the matching project card",
|
|
34
|
+
"verify": "[role='textbox'][contenteditable='true'][data-slate-editor='true']",
|
|
35
|
+
"verifyTimeoutMs": 15000
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"action": "wait",
|
|
39
|
+
"ms": 600,
|
|
40
|
+
"description": "Allow the project toolbar to settle"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"action": "browser_js",
|
|
44
|
+
"code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const button = [...document.querySelectorAll('button,[role=\"button\"]')].filter(visible).find(el => (el.textContent || '').replace(/\\s+/g, ' ').includes('Scenebuilder')); if (!(button instanceof HTMLElement)) throw new Error('Scenebuilder button not found'); button.click(); return 'opened Scenebuilder'; })()",
|
|
45
|
+
"description": "Switch the project into Scenebuilder mode"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"action": "wait",
|
|
49
|
+
"ms": 1000,
|
|
50
|
+
"description": "Allow Scenebuilder to render"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"action": "browser_js",
|
|
54
|
+
"code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const button = [...document.querySelectorAll('button,[role=\"button\"]')].filter(visible).find(el => (el.textContent || '').replace(/\\s+/g, ' ').includes('Arrange')); if (!(button instanceof HTMLElement)) return 'Arrange not visible'; button.click(); return 'opened Arrange'; })()",
|
|
55
|
+
"description": "Open Arrange if it is available in the current scene",
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"action": "screenshot",
|
|
60
|
+
"description": "Capture the Scenebuilder state",
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "google-flow-search-assets",
|
|
3
|
+
"name": "Google Flow — Search Assets",
|
|
4
|
+
"description": "Open a Flow project and search its asset grid for {SEARCH_TERM}. If {PROJECT_NAME} is omitted, the first visible project is used.",
|
|
5
|
+
"platform": "google-flow",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"urlPatterns": [
|
|
8
|
+
"*labs.google/fx/tools/flow*"
|
|
9
|
+
],
|
|
10
|
+
"tags": [
|
|
11
|
+
"google-flow",
|
|
12
|
+
"browser",
|
|
13
|
+
"cdp",
|
|
14
|
+
"assets",
|
|
15
|
+
"search"
|
|
16
|
+
],
|
|
17
|
+
"successCount": 0,
|
|
18
|
+
"failCount": 0,
|
|
19
|
+
"steps": [
|
|
20
|
+
{
|
|
21
|
+
"action": "navigate",
|
|
22
|
+
"url": "https://labs.google/fx/tools/flow",
|
|
23
|
+
"description": "Open the Flow dashboard"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"action": "wait",
|
|
27
|
+
"ms": 1500,
|
|
28
|
+
"description": "Wait for dashboard project cards"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"action": "browser_js",
|
|
32
|
+
"code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const rawWanted = `{PROJECT_NAME}`.trim(); const wanted = rawWanted.toLowerCase(); const hasWanted = !!rawWanted && rawWanted !== '{PROJECT_NAME}'; const links = [...document.querySelectorAll(\"a[href*='/fx/tools/flow/project/']\")].filter(visible); if (!links.length) throw new Error('No Flow project cards found'); const pick = hasWanted ? links.find(link => ((link.closest('div')?.textContent || link.parentElement?.textContent || '')).toLowerCase().includes(wanted)) : links[0]; if (!(pick instanceof HTMLAnchorElement)) throw new Error(hasWanted ? `Flow project not found: ${rawWanted}` : 'No clickable Flow project found'); const url = pick.href || pick.getAttribute('href'); if (!url) throw new Error('Project card is missing href'); window.location.assign(url); return url; })()",
|
|
33
|
+
"description": "Open the matching project card",
|
|
34
|
+
"verify": "form input[type='text']",
|
|
35
|
+
"verifyTimeoutMs": 15000
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"action": "wait",
|
|
39
|
+
"ms": 600,
|
|
40
|
+
"description": "Allow the project toolbar to settle"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"action": "browser_js",
|
|
44
|
+
"code": "(() => { const text = `{SEARCH_TERM}`; if (!text.trim() || text === '{SEARCH_TERM}') throw new Error('SEARCH_TERM variable is required'); const input = document.querySelector(\"form input[type='text']\"); if (!(input instanceof HTMLInputElement)) throw new Error('Flow search input not found'); input.focus(); input.value = ''; input.value = text; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); return input.value; })()",
|
|
45
|
+
"description": "Enter the asset search term"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"action": "wait",
|
|
49
|
+
"ms": 700,
|
|
50
|
+
"description": "Wait for the grid to update"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"action": "browser_js",
|
|
54
|
+
"code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const button = [...document.querySelectorAll('button,[role=\"button\"]')].filter(visible).find(el => (el.textContent || '').replace(/\\s+/g, ' ').includes('Sort & Filter')); if (!(button instanceof HTMLElement)) return 'filter button not visible'; button.click(); return 'opened Sort & Filter'; })()",
|
|
55
|
+
"description": "Open Sort & Filter for manual follow-up if available",
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"action": "screenshot",
|
|
60
|
+
"description": "Capture the filtered asset view",
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "instagram",
|
|
3
|
+
"name": "Instagram Automation",
|
|
4
|
+
"description": "Battle-tested playbook for Instagram browser automation via CDP. Covers feed, search, like, comment, save, DM, create post, follow/unfollow, and profile viewing.",
|
|
5
|
+
"platform": "instagram",
|
|
6
|
+
"version": "4.0.0",
|
|
7
|
+
"urlPatterns": [
|
|
8
|
+
"*instagram.com*"
|
|
9
|
+
],
|
|
10
|
+
"tags": [
|
|
11
|
+
"instagram",
|
|
12
|
+
"social",
|
|
13
|
+
"browser",
|
|
14
|
+
"cdp"
|
|
15
|
+
],
|
|
16
|
+
"successCount": 0,
|
|
17
|
+
"failCount": 0,
|
|
18
|
+
"steps": [
|
|
19
|
+
{
|
|
20
|
+
"action": "navigate",
|
|
21
|
+
"url": "https://www.instagram.com/",
|
|
22
|
+
"description": "Open Instagram home feed"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"action": "wait",
|
|
26
|
+
"ms": 1500,
|
|
27
|
+
"description": "Wait for feed to load"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"action": "extract",
|
|
31
|
+
"target": "article",
|
|
32
|
+
"format": "text",
|
|
33
|
+
"description": "Extract first visible post content"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"action": "scroll",
|
|
37
|
+
"direction": "down",
|
|
38
|
+
"amount": 5,
|
|
39
|
+
"description": "Scroll down to see more posts"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"action": "wait",
|
|
43
|
+
"ms": 1000,
|
|
44
|
+
"description": "Wait for new posts to load"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"action": "extract",
|
|
48
|
+
"target": "article",
|
|
49
|
+
"format": "text",
|
|
50
|
+
"description": "Extract posts after scroll"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"action": "screenshot",
|
|
54
|
+
"description": "Capture current feed state"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|