takomi 2.1.39 → 2.1.40

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 (26) hide show
  1. package/README.md +217 -422
  2. package/assets/.agent/skills/takomi-flow/SKILL.md +10 -1
  3. package/docs/features/TakomiFlow_Portable_Plugin.md +7 -3
  4. package/docs/takomi-flow-onboarding.md +10 -0
  5. package/package.json +1 -1
  6. package/plugins/takomi-flow/assets/capabilities.json +33 -1
  7. package/plugins/takomi-flow/assets/request.schema.json +24 -0
  8. package/plugins/takomi-flow/assets/result.schema.json +1 -0
  9. package/plugins/takomi-flow/assets/templates/image-request.json +4 -0
  10. package/plugins/takomi-flow/assets/templates/video-request.json +4 -0
  11. package/plugins/takomi-flow/references/flow-provider-contract.md +25 -2
  12. package/plugins/takomi-flow/scripts/lib/agent-plan.mjs +10 -0
  13. package/plugins/takomi-flow/scripts/lib/args.mjs +12 -3
  14. package/plugins/takomi-flow/scripts/lib/browser.mjs +42 -3
  15. package/plugins/takomi-flow/scripts/lib/commands.mjs +28 -11
  16. package/plugins/takomi-flow/scripts/lib/flow-media.mjs +39 -0
  17. package/plugins/takomi-flow/scripts/lib/flow-outcome.mjs +37 -12
  18. package/plugins/takomi-flow/scripts/lib/flow-project-session.mjs +153 -0
  19. package/plugins/takomi-flow/scripts/lib/flow-ui.mjs +54 -15
  20. package/plugins/takomi-flow/scripts/lib/generation.mjs +128 -28
  21. package/plugins/takomi-flow/scripts/lib/mcp-tools.mjs +15 -0
  22. package/plugins/takomi-flow/scripts/lib/request-validator.mjs +15 -0
  23. package/plugins/takomi-flow/scripts/lib/request.mjs +12 -1
  24. package/plugins/takomi-flow/scripts/lib/settings-plan.mjs +6 -1
  25. package/plugins/takomi-flow/scripts/mcp-smoke.mjs +2 -0
  26. package/plugins/takomi-flow/skills/takomi-flow/SKILL.md +10 -1
@@ -69,7 +69,6 @@ If Git is unavailable, ask the user to download the VibeCode Protocol Suite ZIP
69
69
  ## Agent Tool Surface
70
70
 
71
71
  Prefer MCP tools when they are available in the active Codex session. Use the CLI commands as the stable fallback.
72
-
73
72
  MCP tools:
74
73
 
75
74
  - `takomi_flow_capabilities`
@@ -116,7 +115,13 @@ Browser-opening MCP tools require `allowBrowser=true`. Generation still requires
116
115
  - Use public Flow UI automation only.
117
116
  - Do not bypass captchas, login challenges, quotas, safety checks, rate limits, or hidden endpoints.
118
117
  - Prefer headed mode first so the user can handle Google login, consent, quota, and safety prompts.
118
+ - Browser commands should start from trusted Chrome/CDP by default; use Playwright-launched browsers only as an explicit fallback.
119
119
  - Never submit a paid generation unless the user explicitly requested it and `allowSpend` or `TAKOMI_FLOW_ALLOW_SPEND=true` is set.
120
+ - Reuse the current Flow project or pass `projectUrl`; create new projects only when the user asks or `allowNewProject` is set.
121
+ - Treat Flow as one active generation at a time: wait for the current approved generation to finish and download it before submitting another paid generation.
122
+ - A gray preview placeholder with scheduled, queue, or ready-shortly copy means the generation is in progress, not failed.
123
+ - If stale scheduled or failure text remains after media is ready, probe/open the generated media and use the top toolbar Download control instead of waiting for the full timeout.
124
+ - Download generated media by opening the media and clicking the top toolbar `Download` icon/button.
120
125
  - Keep credentials out of prompts, logs, metadata, and project files.
121
126
  - Store run artifacts in a predictable folder and report the exact result paths.
122
127
 
@@ -138,6 +143,7 @@ node scripts/takomi-flow.mjs smoke
138
143
  node scripts/takomi-flow.mjs template --kind video
139
144
  node scripts/takomi-flow.mjs prepare --kind video --prompt "cinematic AI lab scene" --variations 2
140
145
  node scripts/takomi-flow.mjs workflow --kind video --prompt "cinematic AI lab scene" --variations 2
146
+ node scripts/takomi-flow.mjs workflow --kind video --prompt "cinematic AI lab scene" --project-url "<Flow project URL>" --reuse-current-project
141
147
  node scripts/takomi-flow.mjs validate --request output/takomi-flow/requests/<file>.json
142
148
  node scripts/takomi-flow.mjs generate --request output/takomi-flow/requests/<file>.json
143
149
  node scripts/takomi-flow.mjs inspect --run output/takomi-flow/<runId>/run.json
@@ -173,6 +179,9 @@ Important defaults:
173
179
  - Keep that Chrome window open and use `--cdp-url http://127.0.0.1:9222` for observe/generate.
174
180
  - A signed-in dashboard should show project cards and a `New project` button during `observe`.
175
181
  - The project editor prompt textbox currently contains `What do you want to create?`.
182
+ - Leave Chrome on the desired project editor or pass `--project-url`; TakomiFlow does not click `New project` unless `--allow-new-project` is set.
183
+ - Rename a project by editing the top-left header title/date-time input.
184
+ - If the chat breaks, keep the same project and let `freshChatOnFailure` try one fresh chat before manual recovery.
176
185
  - Prefer video durations `4`, `6`, `8`, or `10` seconds to avoid a Flow follow-up question.
177
186
  - Use `bootstrap` only when Google accepts the launched browser.
178
187
  - Ask the user to log into Google Flow manually in the opened browser.
@@ -26,6 +26,8 @@ Move the proven TakomiFlow Google Flow automation into this repository as the so
26
26
  - First-run agents should run `doctor`, then check whether `http://127.0.0.1:9222` is already serving Chrome DevTools Protocol.
27
27
  - If CDP is alive, agents should reuse it.
28
28
  - If CDP is missing, agents should start `trusted-chrome` and ask the user to sign into Google Flow manually.
29
+ - For repeated generations, agents should reuse the current Flow project tab or pass `projectUrl`; clicking `New project` requires explicit `allowNewProject`.
30
+ - If a project chat is stale or broken, agents should recover by creating a fresh chat inside the same project before asking for manual intervention.
29
31
  - Agents should prefer MCP tools when the harness supports MCP and the plugin is installed.
30
32
  - Agents should offer plugin installation/repair in Codex only after user approval.
31
33
  - Agents should fall back to CLI commands and standalone skill instructions when MCP or plugins are absent.
@@ -44,9 +46,10 @@ Move the proven TakomiFlow Google Flow automation into this repository as the so
44
46
  3. Agent checks for an existing trusted Chrome CDP instance.
45
47
  4. Agent reuses the instance or launches trusted Chrome.
46
48
  5. User signs in manually when needed.
47
- 6. Agent prepares and validates a Flow request.
48
- 7. Agent submits only with explicit spend approval.
49
- 8. Agent records `projectUrl`, downloads assets, catalogs outputs, and writes a report.
49
+ 6. Agent resolves the target project from `projectUrl` or the current project tab.
50
+ 7. Agent prepares and validates a Flow request.
51
+ 8. Agent submits only with explicit spend approval.
52
+ 9. Agent records `projectUrl`, downloads assets, catalogs outputs, and writes a report.
50
53
 
51
54
  ## Database Schema
52
55
 
@@ -66,6 +69,7 @@ Persistent state is file-based:
66
69
  - A fresh computer can bootstrap by running the repo installer or directly invoking the plugin CLI from this repo.
67
70
  - Global/user writes must be explicit installer actions, not hidden side effects.
68
71
  - Project URLs must be captured for every generated Flow project.
72
+ - New Flow projects are opt-in; project reuse is the default.
69
73
 
70
74
  ## Verification
71
75
 
@@ -44,6 +44,16 @@ If all you have is a skill, use the bundled `takomi-flow` skill. It is the singl
44
44
  node plugins/takomi-flow/scripts/takomi-flow.mjs observe --allow-browser --cdp-url http://127.0.0.1:9222
45
45
  ```
46
46
 
47
+ ## Reusing One Flow Project
48
+
49
+ For repeated generations, leave trusted Chrome open on the Flow project editor and reuse it:
50
+
51
+ ```powershell
52
+ node plugins/takomi-flow/scripts/takomi-flow.mjs workflow --kind video --prompt "cinematic lab scene" --project-url "https://labs.google/fx/tools/flow/project/..." --submit --allow-browser --allow-spend --cdp-url http://127.0.0.1:9222
53
+ ```
54
+
55
+ TakomiFlow does not click `New project` unless `--allow-new-project` is set. If a chat surface is stale or broken, it tries one fresh chat inside the same project before asking for manual help.
56
+
47
57
  ## MCP Or CLI
48
58
 
49
59
  - If the harness supports MCP, use TakomiFlow MCP tools.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takomi",
3
- "version": "2.1.39",
3
+ "version": "2.1.40",
4
4
  "description": "🎯 Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -77,6 +77,36 @@
77
77
  "type": "boolean",
78
78
  "required": false,
79
79
  "description": "Explicit user approval to submit a generation that may spend Flow credits."
80
+ },
81
+ {
82
+ "name": "projectUrl",
83
+ "type": "string",
84
+ "required": false,
85
+ "description": "Existing Flow project URL to reuse instead of creating a new project."
86
+ },
87
+ {
88
+ "name": "reuseCurrentProject",
89
+ "type": "boolean",
90
+ "required": false,
91
+ "description": "Prefer the already open Flow project tab when attached to trusted Chrome."
92
+ },
93
+ {
94
+ "name": "allowNewProject",
95
+ "type": "boolean",
96
+ "required": false,
97
+ "description": "Permit clicking New project when no reusable project is available."
98
+ },
99
+ {
100
+ "name": "freshChatOnFailure",
101
+ "type": "boolean",
102
+ "required": false,
103
+ "description": "Recover stale or broken chat state by starting a fresh chat inside the same project."
104
+ },
105
+ {
106
+ "name": "editorWaitMs",
107
+ "type": "integer",
108
+ "required": false,
109
+ "description": "Override the Flow project editor readiness wait budget in milliseconds."
80
110
  }
81
111
  ],
82
112
  "sourceAssets": {
@@ -96,13 +126,15 @@
96
126
  "collection": "takomi-flow://schemas/collection"
97
127
  },
98
128
  "settingsCoverage": {
99
- "automatic": ["prompt", "download folder", "metadata", "asset catalog", "video review frame extraction"],
129
+ "automatic": ["prompt", "project reuse", "same-project chat recovery", "download folder", "metadata", "asset catalog", "video review frame extraction"],
100
130
  "selectorDependent": ["mode", "aspectRatio", "durationSeconds", "modelHint", "sourceAssets", "variations"],
101
131
  "note": "Selector-dependent settings are preserved in settingsPlan metadata until live Flow controls are observed and tuned."
102
132
  },
103
133
  "safety": {
104
134
  "spendGuardDefault": false,
105
135
  "requiresAllowSpend": true,
136
+ "newProjectDefault": false,
137
+ "sameProjectChatRecovery": true,
106
138
  "bypassCaptcha": false,
107
139
  "bypassQuota": false,
108
140
  "automateCredentials": false
@@ -60,6 +60,30 @@
60
60
  "items": { "type": "string" },
61
61
  "default": []
62
62
  },
63
+ "projectUrl": {
64
+ "type": "string",
65
+ "description": "Existing Google Flow project/editor URL to reuse for this generation."
66
+ },
67
+ "reuseCurrentProject": {
68
+ "type": "boolean",
69
+ "default": true,
70
+ "description": "When attached to Chrome, prefer an already open Flow project tab."
71
+ },
72
+ "allowNewProject": {
73
+ "type": "boolean",
74
+ "default": false,
75
+ "description": "Permit TakomiFlow to click New project when no reusable project/editor is available."
76
+ },
77
+ "freshChatOnFailure": {
78
+ "type": "boolean",
79
+ "default": true,
80
+ "description": "Try one same-project fresh chat recovery when prompt or submit controls are broken."
81
+ },
82
+ "editorWaitMs": {
83
+ "type": "integer",
84
+ "minimum": 1000,
85
+ "description": "Maximum time to wait for the Flow project editor to become usable."
86
+ },
63
87
  "notes": {
64
88
  "type": "string"
65
89
  }
@@ -18,6 +18,7 @@
18
18
  "prompt": { "type": "string" },
19
19
  "flowUrl": { "type": "string" },
20
20
  "projectUrl": { "type": "string" },
21
+ "projectSession": { "type": "object" },
21
22
  "settingsPlan": { "type": "object" },
22
23
  "assets": {
23
24
  "type": "array",
@@ -9,5 +9,9 @@
9
9
  "outputDir": "C:/Users/johno/.takomi-flow/runs",
10
10
  "allowSpend": false,
11
11
  "sourceAssets": [],
12
+ "projectUrl": "",
13
+ "reuseCurrentProject": true,
14
+ "allowNewProject": false,
15
+ "freshChatOnFailure": true,
12
16
  "notes": "Set allowSpend=true only after explicit user approval."
13
17
  }
@@ -11,5 +11,9 @@
11
11
  "allowSpend": false,
12
12
  "extractFrames": 4,
13
13
  "sourceAssets": [],
14
+ "projectUrl": "",
15
+ "reuseCurrentProject": true,
16
+ "allowNewProject": false,
17
+ "freshChatOnFailure": true,
14
18
  "notes": "Set allowSpend=true only after explicit user approval."
15
19
  }
@@ -61,7 +61,15 @@ First-run agents should also check for an existing trusted Chrome DevTools endpo
61
61
  curl http://127.0.0.1:9222/json/version
62
62
  ```
63
63
 
64
- If the endpoint exists, reuse it with `--cdp-url http://127.0.0.1:9222`. If it does not exist, run `trusted-chrome` and ask the user to sign into Google Flow manually. MCP is optional; every MCP operation has a CLI equivalent through `node scripts/takomi-flow.mjs`.
64
+ If the endpoint exists, reuse it with `--cdp-url http://127.0.0.1:9222`. If it does not exist, run `trusted-chrome` and ask the user to sign into Google Flow manually. Browser commands should start from trusted Chrome/CDP by default; Playwright-launched browser contexts are only an explicit fallback. MCP is optional; every MCP operation has a CLI equivalent through `node scripts/takomi-flow.mjs`.
65
+
66
+ Repeated generations should reuse a Flow project instead of creating a new one. Pass `projectUrl` / `--project-url`, or attach to trusted Chrome while it is already on a `/project/` editor tab. TakomiFlow prefers the current project by default, does not click `New project` unless `allowNewProject=true` / `--allow-new-project` is set, and can recover a stale or broken chat by starting one fresh chat inside the same project.
67
+
68
+ Flow should be treated as one active paid generation at a time. A gray preview placeholder plus scheduled, queue, or ready-shortly copy means the generation was accepted and is still in progress; do not classify it as failed or submit another paid generation until the current media is ready and downloaded.
69
+
70
+ During polling, stale scheduled or failure text may remain visible after the current media is ready. The agent should probe for an open media view or ready media tile and exit as soon as the top toolbar Download control is available, instead of waiting for the full timeout.
71
+
72
+ The Flow project name can be edited from the top-left header title/date-time input after opening the project.
65
73
 
66
74
  Run this after install or edits:
67
75
 
@@ -97,7 +105,8 @@ verified without pressing Create.
97
105
  Live video durations are `4`, `6`, `8`, and `10` seconds. If the agent asks for an unsupported
98
106
  duration, Flow may ask a follow-up. A verified generation also asked for explicit approval before
99
107
  spending 7 credits for one video. After output creation, opening the generated media reveals a
100
- toolbar `download` button that saves the MP4.
108
+ top toolbar download icon. Aim for the button with tooltip, title, aria label, or visible text
109
+ `Download`; clicking it saves the MP4.
101
110
 
102
111
  Use templates when starting a request from scratch:
103
112
 
@@ -137,6 +146,11 @@ Agents can read `takomi-flow://schemas/request` for the JSON Schema version of t
137
146
  "outputDir": "C:/Users/johno/.takomi-flow/runs",
138
147
  "allowSpend": false,
139
148
  "extractFrames": 4,
149
+ "projectUrl": "https://labs.google/fx/tools/flow/project/example",
150
+ "reuseCurrentProject": true,
151
+ "allowNewProject": false,
152
+ "freshChatOnFailure": true,
153
+ "editorWaitMs": 90000,
140
154
  "notes": "Optional agent notes for traceability."
141
155
  }
142
156
  ```
@@ -156,6 +170,11 @@ Agents can discover the same fields from `capabilities.requestFields`.
156
170
  - `allowSpend`: Must be `true` for generation submission.
157
171
  - `extractFrames`: Optional number of review frames to extract from downloaded videos.
158
172
  - `sourceAssets`: Optional array of local file paths for future image/video-to-video flows.
173
+ - `projectUrl`: Optional existing Flow project/editor URL to reuse.
174
+ - `reuseCurrentProject`: Prefer an already open Flow project tab when attached through CDP. Defaults to `true`.
175
+ - `allowNewProject`: Permit clicking `New project` when no reusable project/editor exists. Defaults to `false`.
176
+ - `freshChatOnFailure`: Try one fresh chat inside the same project when prompt or submit controls are broken. Defaults to `true`.
177
+ - `editorWaitMs`: Optional wait budget for Flow editor readiness.
159
178
 
160
179
  ## Validation JSON
161
180
 
@@ -183,6 +202,10 @@ Agents can read `takomi-flow://schemas/result` for the JSON Schema version of ru
183
202
  "kind": "video",
184
203
  "prompt": "cinematic AI lab scene with practical lighting",
185
204
  "projectUrl": "https://labs.google/fx/tools/flow/project/example",
205
+ "projectSession": {
206
+ "projectUrl": "https://labs.google/fx/tools/flow/project/example",
207
+ "recovered": false
208
+ },
186
209
  "settingsPlan": {
187
210
  "requested": {
188
211
  "mode": "text-to-video",
@@ -51,6 +51,11 @@ function mcpSequence(request, options) {
51
51
  allowSpend: request.allowSpend,
52
52
  extractFrames: request.extractFrames,
53
53
  sourceAssets: request.sourceAssets,
54
+ projectUrl: request.projectUrl,
55
+ reuseCurrentProject: request.reuseCurrentProject,
56
+ allowNewProject: request.allowNewProject,
57
+ freshChatOnFailure: request.freshChatOnFailure,
58
+ editorWaitMs: request.editorWaitMs,
54
59
  submit: options.submit,
55
60
  allowBrowser: options.submit,
56
61
  };
@@ -95,6 +100,11 @@ function optionArgs(request, submit) {
95
100
  if (request.outputDir) args.push(`--output-dir "${escapeArg(request.outputDir)}"`);
96
101
  if (request.extractFrames) args.push(`--extract-frames ${request.extractFrames}`);
97
102
  if (request.sourceAssets.length) args.push(`--assets "${escapeArg(request.sourceAssets.join(','))}"`);
103
+ if (request.projectUrl) args.push(`--project-url "${escapeArg(request.projectUrl)}"`);
104
+ if (request.reuseCurrentProject) args.push('--reuse-current-project');
105
+ if (request.allowNewProject) args.push('--allow-new-project');
106
+ if (request.freshChatOnFailure === false) args.push('--fresh-chat-on-failure=false');
107
+ if (request.editorWaitMs) args.push(`--editor-wait-ms ${request.editorWaitMs}`);
98
108
  if (submit) args.push('--submit --allow-browser --allow-spend');
99
109
  return args.length ? ` ${args.join(' ')}` : '';
100
110
  }
@@ -6,7 +6,11 @@ export function parseArgs(argv) {
6
6
  args._.push(token);
7
7
  continue;
8
8
  }
9
- const key = token.slice(2);
9
+ const [key, inlineValue] = token.slice(2).split(/=(.*)/s, 2);
10
+ if (inlineValue !== undefined) {
11
+ args[key] = inlineValue;
12
+ continue;
13
+ }
10
14
  const next = argv[i + 1];
11
15
  if (!next || next.startsWith('--')) {
12
16
  args[key] = true;
@@ -33,10 +37,10 @@ Usage:
33
37
  node scripts/takomi-flow.mjs observe [--profile-dir <path>] [--output-dir <path>] [--browser-channel chrome] [--cdp-url <url>] [--headless]
34
38
  node scripts/takomi-flow.mjs smoke [--profile-dir <path>] [--output-dir <path>] [--browser-channel chrome] [--cdp-url <url>]
35
39
  node scripts/takomi-flow.mjs prepare --kind <video|image> --prompt <text> [--variations 1]
36
- node scripts/takomi-flow.mjs workflow --kind <video|image> --prompt <text> [--submit --allow-browser --allow-spend]
40
+ node scripts/takomi-flow.mjs workflow --kind <video|image> --prompt <text> [--project-url <url>] [--reuse-current-project] [--allow-new-project] [--submit --allow-browser --allow-spend]
37
41
  node scripts/takomi-flow.mjs template --kind <video|image> [--output-dir <path>]
38
42
  node scripts/takomi-flow.mjs validate --request <path>
39
- node scripts/takomi-flow.mjs generate --request <path>
43
+ node scripts/takomi-flow.mjs generate --request <path> [--project-url <url>] [--reuse-current-project] [--allow-new-project]
40
44
  node scripts/takomi-flow.mjs selftest [--output-dir <path>]
41
45
  node scripts/takomi-flow.mjs inspect --run <run.json|run-dir>
42
46
  node scripts/takomi-flow.mjs latest [--output-dir <path>]
@@ -53,5 +57,10 @@ Google login note:
53
57
  If Google says the browser may not be secure, use trusted Chrome attach mode:
54
58
  node scripts/takomi-flow.mjs trusted-chrome
55
59
  Then log in manually and attach with --cdp-url http://127.0.0.1:9222.
60
+
61
+ Project reuse:
62
+ Pass --project-url or attach Chrome while already on a Flow project. TakomiFlow will not click
63
+ New project unless --allow-new-project is set. Use --fresh-chat-on-failure=false to disable
64
+ same-project chat recovery.
56
65
  `);
57
66
  }
@@ -1,17 +1,21 @@
1
1
  import path from 'node:path';
2
2
  import { ensureDir } from './paths.mjs';
3
3
  import { loadPlaywright } from './playwright-loader.mjs';
4
+ import { launchTrustedChrome } from './trusted-chrome.mjs';
5
+
6
+ const DEFAULT_CDP_URL = 'http://127.0.0.1:9222';
4
7
 
5
8
  export async function launchFlowBrowser(options = {}) {
6
9
  const { chromium } = await loadPlaywright();
7
10
  const profileDir = ensureDir(path.resolve(options.profileDir));
8
11
  const downloadsDir = ensureDir(path.resolve(options.downloadsDir));
9
12
  const headless = options.headless === true;
10
- if (options.cdpUrl) {
11
- const browser = await chromium.connectOverCDP(options.cdpUrl);
13
+ const trustedCdpUrl = await trustedChromeCdpUrl(options);
14
+ if (trustedCdpUrl) {
15
+ const browser = await chromium.connectOverCDP(trustedCdpUrl);
12
16
  const context = browser.contexts()[0] || await browser.newContext({ acceptDownloads: true });
13
17
  const page = context.pages()[0] || await context.newPage();
14
- return { browser, context, page, profileDir, downloadsDir, cdpUrl: options.cdpUrl, attached: true };
18
+ return { browser, context, page, profileDir, downloadsDir, cdpUrl: trustedCdpUrl, attached: true };
15
19
  }
16
20
  const channel = options.browserChannel || process.env.TAKOMI_FLOW_BROWSER_CHANNEL || defaultBrowserChannel();
17
21
  const context = await chromium.launchPersistentContext(profileDir, {
@@ -27,12 +31,47 @@ export async function launchFlowBrowser(options = {}) {
27
31
 
28
32
  export async function closeFlowBrowser(browser) {
29
33
  if (browser.attached) {
34
+ if (typeof browser.browser?.disconnect === 'function') {
35
+ browser.browser.disconnect();
36
+ return;
37
+ }
30
38
  await browser.browser?.close?.().catch(() => {});
31
39
  return;
32
40
  }
33
41
  await browser.context.close().catch(() => {});
34
42
  }
35
43
 
44
+ async function trustedChromeCdpUrl(options) {
45
+ if (options.cdpUrl) return options.cdpUrl;
46
+ if (options.headless === true || options.usePlaywrightBrowser === true) return null;
47
+ if (await canConnect(DEFAULT_CDP_URL)) return DEFAULT_CDP_URL;
48
+ try {
49
+ const trusted = launchTrustedChrome({ url: options.url });
50
+ return await waitForCdp(trusted.cdpUrl);
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ async function waitForCdp(cdpUrl) {
57
+ const timeoutMs = 10000;
58
+ const started = Date.now();
59
+ while (Date.now() - started < timeoutMs) {
60
+ if (await canConnect(cdpUrl)) return cdpUrl;
61
+ await new Promise(resolve => setTimeout(resolve, 500));
62
+ }
63
+ return null;
64
+ }
65
+
66
+ async function canConnect(cdpUrl) {
67
+ try {
68
+ const response = await fetch(`${cdpUrl}/json/version`, { signal: AbortSignal.timeout(2000) });
69
+ return response.ok;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
36
75
  function defaultBrowserChannel() {
37
76
  if (process.platform === 'win32' || process.platform === 'darwin') return 'chrome';
38
77
  return undefined;
@@ -10,14 +10,20 @@ export async function handleCommand(command, args) {
10
10
  if (command === 'workflow') return printJson(await api.workflow({
11
11
  ...args,
12
12
  outputDir: args['output-dir'],
13
- allowSpend: Boolean(args['allow-spend']),
13
+ allowSpend: booleanArg(args['allow-spend']),
14
14
  extractFrames: args['extract-frames'],
15
15
  sourceAssets: args.assets,
16
- allowBrowser: Boolean(args['allow-browser']),
16
+ allowBrowser: booleanArg(args['allow-browser']),
17
17
  profileDir: args['profile-dir'],
18
18
  browserChannel: args['browser-channel'],
19
19
  cdpUrl: args['cdp-url'],
20
- submit: Boolean(args.submit),
20
+ headless: booleanArg(args.headless),
21
+ submit: booleanArg(args.submit),
22
+ projectUrl: args['project-url'],
23
+ reuseCurrentProject: args['reuse-current-project'],
24
+ allowNewProject: args['allow-new-project'],
25
+ freshChatOnFailure: args['fresh-chat-on-failure'],
26
+ editorWaitMs: args['editor-wait-ms'],
21
27
  }));
22
28
  if (command === 'template') return template(args);
23
29
  if (command === 'examples') return printJson(api.examples({ name: args.name }));
@@ -26,7 +32,7 @@ export async function handleCommand(command, args) {
26
32
  chromePath: args['chrome-path'],
27
33
  port: args.port,
28
34
  url: args.url,
29
- printCommand: Boolean(args['print-command']),
35
+ printCommand: booleanArg(args['print-command']),
30
36
  }));
31
37
  if (command === 'validate') return printJson(api.validate({ request: args.request }));
32
38
  if (command === 'bootstrap') return bootstrap(args);
@@ -40,10 +46,10 @@ export async function handleCommand(command, args) {
40
46
  if (command === 'plan') return printJson(api.plan({
41
47
  ...args,
42
48
  outputDir: args['output-dir'],
43
- allowSpend: Boolean(args['allow-spend']),
49
+ allowSpend: booleanArg(args['allow-spend']),
44
50
  extractFrames: args['extract-frames'],
45
51
  sourceAssets: args.assets,
46
- submit: Boolean(args.submit),
52
+ submit: booleanArg(args.submit),
47
53
  targetDir: args['target-dir'],
48
54
  }));
49
55
  if (command === 'observe') return observe(args);
@@ -53,7 +59,12 @@ export async function handleCommand(command, args) {
53
59
  profileDir: args['profile-dir'],
54
60
  browserChannel: args['browser-channel'],
55
61
  cdpUrl: args['cdp-url'],
56
- headless: Boolean(args.headless),
62
+ headless: booleanArg(args.headless),
63
+ projectUrl: args['project-url'],
64
+ reuseCurrentProject: args['reuse-current-project'],
65
+ allowNewProject: args['allow-new-project'],
66
+ freshChatOnFailure: args['fresh-chat-on-failure'],
67
+ editorWaitMs: args['editor-wait-ms'],
57
68
  }));
58
69
  if (command === 'selftest') return printJson(await api.selftest({ outputDir: args['output-dir'] }));
59
70
  if (command === 'inspect') return printJson(api.inspect({ run: args.run }));
@@ -69,7 +80,7 @@ export async function handleCommand(command, args) {
69
80
  run: args.run,
70
81
  targetDir: args['target-dir'],
71
82
  frames: args.frames || 0,
72
- includeFrames: Boolean(args['include-frames']),
83
+ includeFrames: booleanArg(args['include-frames']),
73
84
  reportPath: args['report-path'],
74
85
  }));
75
86
  if (command === 'report') return printJson(api.report({
@@ -81,13 +92,19 @@ export async function handleCommand(command, args) {
81
92
  throw new Error(`Unknown command: ${command}`);
82
93
  }
83
94
 
95
+ function booleanArg(value) {
96
+ if (value === undefined || value === null || value === '') return false;
97
+ if (typeof value === 'boolean') return value;
98
+ return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase());
99
+ }
100
+
84
101
  async function observe(cliArgs) {
85
102
  printJson(await api.observe({
86
103
  profileDir: cliArgs['profile-dir'],
87
104
  outputDir: cliArgs['output-dir'],
88
105
  browserChannel: cliArgs['browser-channel'],
89
106
  cdpUrl: cliArgs['cdp-url'],
90
- headless: Boolean(cliArgs.headless),
107
+ headless: booleanArg(cliArgs.headless),
91
108
  }));
92
109
  }
93
110
 
@@ -126,7 +143,7 @@ async function bootstrap(cliArgs) {
126
143
  downloadsDir: run.downloadsDir,
127
144
  browserChannel: cliArgs['browser-channel'],
128
145
  cdpUrl: cliArgs['cdp-url'],
129
- headless: Boolean(cliArgs.headless),
146
+ headless: booleanArg(cliArgs.headless),
130
147
  });
131
148
  try {
132
149
  await openFlow(browser.page);
@@ -151,7 +168,7 @@ async function smoke(cliArgs) {
151
168
  downloadsDir: run.downloadsDir,
152
169
  browserChannel: cliArgs['browser-channel'],
153
170
  cdpUrl: cliArgs['cdp-url'],
154
- headless: Boolean(cliArgs.headless),
171
+ headless: booleanArg(cliArgs.headless),
155
172
  });
156
173
  const result = baseResult('ok', run, { command: 'smoke', flowUrl: FLOW_URL, profileDir });
157
174
  try {
@@ -0,0 +1,39 @@
1
+ export async function listMediaIds(page) {
2
+ const ids = await page.locator('img[src*="media.getMediaUrlRedirect"], video[src*="media.getMediaUrlRedirect"]').evaluateAll(items => (
3
+ items.map(item => mediaIdFromUrl(item.getAttribute('src') || item.src || '')).filter(Boolean)
4
+ )).catch(() => []);
5
+ return [...new Set(ids)];
6
+ }
7
+
8
+ export async function newMediaIds(page, baselineIds = []) {
9
+ const baseline = new Set(baselineIds);
10
+ return (await listMediaIds(page)).filter(id => !baseline.has(id));
11
+ }
12
+
13
+ export async function openGeneratedMediaById(page, mediaId) {
14
+ if (!mediaId) return false;
15
+ const candidates = [
16
+ page.locator(`img[src*="${mediaId}"]`).first(),
17
+ page.locator(`video[src*="${mediaId}"]`).first(),
18
+ ];
19
+ for (const locator of candidates) {
20
+ if (!(await locator.count().catch(() => 0))) continue;
21
+ try {
22
+ await locator.scrollIntoViewIfNeeded().catch(() => {});
23
+ await locator.click({ timeout: 10000, force: true });
24
+ await page.waitForTimeout(3000);
25
+ return true;
26
+ } catch {}
27
+ }
28
+ return false;
29
+ }
30
+
31
+ function mediaIdFromUrl(value) {
32
+ try {
33
+ const url = new URL(value, 'https://labs.google');
34
+ return url.searchParams.get('name');
35
+ } catch {
36
+ const match = /[?&]name=([^&]+)/.exec(value);
37
+ return match ? decodeURIComponent(match[1]) : null;
38
+ }
39
+ }
@@ -1,4 +1,5 @@
1
- import { hasText, openGeneratedMedia } from './flow-ui.mjs';
1
+ import { downloadButton, hasText, openGeneratedMedia } from './flow-ui.mjs';
2
+ import { newMediaIds } from './flow-media.mjs';
2
3
 
3
4
  export async function handleGenerationFollowups(page, request) {
4
5
  if (await hasText(page, /which of these durations would you prefer/i)) {
@@ -14,7 +15,9 @@ export async function handleGenerationFollowups(page, request) {
14
15
  export async function waitForGenerationOutcome(page, request, options = {}) {
15
16
  const timeoutMs = Number(options.timeoutMs || process.env.TAKOMI_FLOW_WAIT_MS || 300000);
16
17
  const intervalMs = Number(options.intervalMs || process.env.TAKOMI_FLOW_POLL_MS || 5000);
18
+ const readyProbeMs = Number(options.readyProbeMs || process.env.TAKOMI_FLOW_READY_PROBE_MS || 45000);
17
19
  const started = Date.now();
20
+ let lastReadyProbeAt = 0;
18
21
  let approved = false;
19
22
  while (Date.now() - started < timeoutMs) {
20
23
  if (await hasText(page, /which of these durations would you prefer/i)) {
@@ -23,18 +26,26 @@ export async function waitForGenerationOutcome(page, request, options = {}) {
23
26
  if (await hasText(page, /costing\s+\d+\s+credits/i)) {
24
27
  approved = await clickApprove(page) || approved;
25
28
  }
26
- if (await page.locator('button').filter({ hasText: /download\s*Download/i }).count().catch(() => 0)) {
27
- return { status: 'download_ready', approved };
29
+ const freshMediaIds = await newMediaIds(page, options.baselineMediaIds || []);
30
+ const downloadReady = await downloadButton(page).count().catch(() => 0);
31
+ if (downloadReady && (!options.baselineMediaIds || freshMediaIds.length || /\/edit\//.test(page.url()))) {
32
+ return { status: 'download_ready', approved, mediaIds: freshMediaIds };
28
33
  }
29
- if (await page.locator('img[src*="media.getMediaUrlRedirect"], video').count().catch(() => 0)) {
30
- await openGeneratedMedia(page);
31
- return { status: 'media_ready', approved };
34
+ if (freshMediaIds.length) {
35
+ await openGeneratedMedia(page, freshMediaIds);
36
+ return { status: 'media_ready', approved, mediaIds: freshMediaIds };
32
37
  }
33
- if (await hasVisibleText(page, /\b\d{1,3}%\b|generating|waiting in the queue|been scheduled/i)) {
38
+ if (Date.now() - started >= readyProbeMs && Date.now() - lastReadyProbeAt >= readyProbeMs) {
39
+ lastReadyProbeAt = Date.now();
40
+ if (await probeDownloadableMedia(page, freshMediaIds)) {
41
+ return { status: 'download_ready', approved, mediaIds: freshMediaIds, probed: true };
42
+ }
43
+ }
44
+ if (await hasVisibleText(page, /\b\d{1,3}%\b|generating|currently in the queue|waiting in the queue|been scheduled|i['’]ve scheduled|ready for you shortly/i)) {
34
45
  await page.waitForTimeout(intervalMs);
35
46
  continue;
36
47
  }
37
- if (await hasVisibleText(page, /failed|oops, something went wrong/i)) {
48
+ if (await hasText(page, /failed|oops, something went wrong/i)) {
38
49
  return { status: 'failed', approved };
39
50
  }
40
51
  await page.waitForTimeout(intervalMs);
@@ -42,6 +53,13 @@ export async function waitForGenerationOutcome(page, request, options = {}) {
42
53
  return { status: 'timeout', approved };
43
54
  }
44
55
 
56
+ async function probeDownloadableMedia(page, mediaIds = []) {
57
+ if (await downloadButton(page).count().catch(() => 0)) return true;
58
+ await openGeneratedMedia(page, mediaIds);
59
+ await page.waitForTimeout(1000);
60
+ return (await downloadButton(page).count().catch(() => 0)) > 0;
61
+ }
62
+
45
63
  async function hasVisibleText(page, pattern) {
46
64
  return page.locator('body').evaluate((body, source) => {
47
65
  const regex = new RegExp(source, 'i');
@@ -74,16 +92,23 @@ async function chooseDuration(page, requested) {
74
92
 
75
93
  async function clickApprove(page) {
76
94
  const candidates = [
77
- page.getByText('Approve', { exact: true }).first(),
78
- page.locator('div,button,[role="button"]').filter({ hasText: /^\s*check\s*Approve\s*$/i }).first(),
79
- page.locator('text=Approve').first(),
95
+ page.getByRole('button', { name: /^Approve$/i }).last(),
96
+ page.locator('button').filter({ hasText: /^Approve$/i }).last(),
97
+ page.locator('[role="button"]').filter({ hasText: /^Approve$/i }).last(),
98
+ page.locator('div,button,[role="button"]').filter({ hasText: /^\s*check\s*Approve\s*$/i }).last(),
80
99
  ];
81
100
  for (const locator of candidates) {
82
101
  if (!(await locator.count().catch(() => 0))) continue;
83
102
  try {
84
103
  await locator.click({ timeout: 10000, force: true });
85
104
  await page.waitForTimeout(1000);
86
- return true;
105
+ const stillVisible = await page.getByRole('button', { name: /^Approve$/i })
106
+ .last()
107
+ .isVisible({ timeout: 1000 })
108
+ .catch(() => false);
109
+ if (!stillVisible || await hasVisibleText(page, /generating|currently in the queue|waiting in the queue|been scheduled|i['’]ve scheduled|ready for you shortly/i)) {
110
+ return true;
111
+ }
87
112
  } catch {}
88
113
  }
89
114
  return false;