takomi 2.1.39 → 2.1.41
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/.agents/plugins/marketplace.json +22 -22
- package/README.md +217 -422
- package/assets/.agent/skills/remotion/rules/assets/charts-bar-chart.tsx +173 -173
- package/assets/.agent/skills/remotion/rules/assets/text-animations-typewriter.tsx +100 -100
- package/assets/.agent/skills/remotion/rules/text-animations.md +20 -20
- package/assets/.agent/skills/remotion/rules/videos.md +171 -171
- package/assets/.agent/skills/remotion-real-ui-video/SKILL.md +264 -0
- package/assets/.agent/skills/remotion-real-ui-video/references/anti-patterns.md +114 -0
- package/assets/.agent/skills/remotion-real-ui-video/references/completion-checklist.md +75 -0
- package/assets/.agent/skills/remotion-real-ui-video/references/real-ui-bridge-example.md +93 -0
- package/assets/.agent/skills/takomi-flow/SKILL.md +244 -235
- package/docs/features/TakomiFlow_Portable_Plugin.md +82 -78
- package/docs/takomi-flow-onboarding.md +86 -76
- package/docs/takomi-flow-public-distribution.md +94 -94
- package/package.json +1 -1
- package/plugins/takomi-flow/.mcp.json +11 -11
- package/plugins/takomi-flow/assets/capabilities.json +203 -171
- package/plugins/takomi-flow/assets/collection.schema.json +29 -29
- package/plugins/takomi-flow/assets/examples.json +94 -94
- package/plugins/takomi-flow/assets/request.schema.json +91 -67
- package/plugins/takomi-flow/assets/result.schema.json +46 -45
- package/plugins/takomi-flow/assets/templates/image-request.json +17 -13
- package/plugins/takomi-flow/assets/templates/video-request.json +19 -15
- package/plugins/takomi-flow/package.json +38 -38
- package/plugins/takomi-flow/pnpm-lock.yaml +813 -813
- package/plugins/takomi-flow/references/flow-provider-contract.md +349 -326
- package/plugins/takomi-flow/scripts/lib/agent-plan.mjs +140 -130
- package/plugins/takomi-flow/scripts/lib/api.mjs +113 -113
- package/plugins/takomi-flow/scripts/lib/args.mjs +66 -57
- package/plugins/takomi-flow/scripts/lib/audit.mjs +65 -65
- package/plugins/takomi-flow/scripts/lib/browser.mjs +78 -39
- package/plugins/takomi-flow/scripts/lib/capabilities.mjs +11 -11
- package/plugins/takomi-flow/scripts/lib/collect.mjs +53 -53
- package/plugins/takomi-flow/scripts/lib/commands.mjs +192 -175
- package/plugins/takomi-flow/scripts/lib/doctor.mjs +77 -77
- package/plugins/takomi-flow/scripts/lib/examples.mjs +17 -17
- package/plugins/takomi-flow/scripts/lib/flow-media.mjs +39 -0
- package/plugins/takomi-flow/scripts/lib/flow-outcome.mjs +115 -90
- package/plugins/takomi-flow/scripts/lib/flow-project-session.mjs +153 -0
- package/plugins/takomi-flow/scripts/lib/flow-ui.mjs +204 -165
- package/plugins/takomi-flow/scripts/lib/generation.mjs +190 -90
- package/plugins/takomi-flow/scripts/lib/inspect.mjs +81 -81
- package/plugins/takomi-flow/scripts/lib/mcp-prompts.mjs +39 -39
- package/plugins/takomi-flow/scripts/lib/mcp-resources.mjs +16 -16
- package/plugins/takomi-flow/scripts/lib/mcp-tools.mjs +165 -150
- package/plugins/takomi-flow/scripts/lib/media.mjs +78 -78
- package/plugins/takomi-flow/scripts/lib/observe.mjs +54 -54
- package/plugins/takomi-flow/scripts/lib/paths.mjs +37 -37
- package/plugins/takomi-flow/scripts/lib/playwright-loader.mjs +23 -23
- package/plugins/takomi-flow/scripts/lib/prompts.mjs +72 -72
- package/plugins/takomi-flow/scripts/lib/report.mjs +141 -141
- package/plugins/takomi-flow/scripts/lib/request-validator.mjs +73 -58
- package/plugins/takomi-flow/scripts/lib/request.mjs +66 -55
- package/plugins/takomi-flow/scripts/lib/resource-files.mjs +69 -69
- package/plugins/takomi-flow/scripts/lib/result.mjs +31 -31
- package/plugins/takomi-flow/scripts/lib/review.mjs +44 -44
- package/plugins/takomi-flow/scripts/lib/selftest.mjs +153 -153
- package/plugins/takomi-flow/scripts/lib/settings-plan.mjs +44 -39
- package/plugins/takomi-flow/scripts/lib/templates.mjs +37 -37
- package/plugins/takomi-flow/scripts/lib/trusted-chrome.mjs +67 -67
- package/plugins/takomi-flow/scripts/lib/workflow.mjs +56 -56
- package/plugins/takomi-flow/scripts/mcp-server.mjs +18 -18
- package/plugins/takomi-flow/scripts/mcp-smoke.mjs +144 -142
- package/plugins/takomi-flow/scripts/takomi-flow.mjs +17 -17
- package/plugins/takomi-flow/skills/takomi-flow/SKILL.md +244 -235
- package/plugins/takomi-flow/skills/takomi-flow/agents/openai.yaml +7 -7
- package/scripts/install-takomi-flow.ps1 +85 -85
- package/src/skills-catalog.js +10 -9
|
@@ -1,90 +1,115 @@
|
|
|
1
|
-
import { hasText, openGeneratedMedia } from './flow-ui.mjs';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
await page.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (await hasText(page, /
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
if (await page
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
1
|
+
import { downloadButton, hasText, openGeneratedMedia } from './flow-ui.mjs';
|
|
2
|
+
import { newMediaIds } from './flow-media.mjs';
|
|
3
|
+
|
|
4
|
+
export async function handleGenerationFollowups(page, request) {
|
|
5
|
+
if (await hasText(page, /which of these durations would you prefer/i)) {
|
|
6
|
+
await chooseDuration(page, request.durationSeconds || 4);
|
|
7
|
+
await page.waitForTimeout(2000);
|
|
8
|
+
}
|
|
9
|
+
if (await hasText(page, /costing\s+\d+\s+credits/i)) {
|
|
10
|
+
return clickApprove(page);
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function waitForGenerationOutcome(page, request, options = {}) {
|
|
16
|
+
const timeoutMs = Number(options.timeoutMs || process.env.TAKOMI_FLOW_WAIT_MS || 300000);
|
|
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);
|
|
19
|
+
const started = Date.now();
|
|
20
|
+
let lastReadyProbeAt = 0;
|
|
21
|
+
let approved = false;
|
|
22
|
+
while (Date.now() - started < timeoutMs) {
|
|
23
|
+
if (await hasText(page, /which of these durations would you prefer/i)) {
|
|
24
|
+
await chooseDuration(page, request.durationSeconds || 4);
|
|
25
|
+
}
|
|
26
|
+
if (await hasText(page, /costing\s+\d+\s+credits/i)) {
|
|
27
|
+
approved = await clickApprove(page) || approved;
|
|
28
|
+
}
|
|
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 };
|
|
33
|
+
}
|
|
34
|
+
if (freshMediaIds.length) {
|
|
35
|
+
await openGeneratedMedia(page, freshMediaIds);
|
|
36
|
+
return { status: 'media_ready', approved, mediaIds: freshMediaIds };
|
|
37
|
+
}
|
|
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)) {
|
|
45
|
+
await page.waitForTimeout(intervalMs);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (await hasText(page, /failed|oops, something went wrong/i)) {
|
|
49
|
+
return { status: 'failed', approved };
|
|
50
|
+
}
|
|
51
|
+
await page.waitForTimeout(intervalMs);
|
|
52
|
+
}
|
|
53
|
+
return { status: 'timeout', approved };
|
|
54
|
+
}
|
|
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
|
+
|
|
63
|
+
async function hasVisibleText(page, pattern) {
|
|
64
|
+
return page.locator('body').evaluate((body, source) => {
|
|
65
|
+
const regex = new RegExp(source, 'i');
|
|
66
|
+
const isVisible = element => {
|
|
67
|
+
const style = window.getComputedStyle(element);
|
|
68
|
+
const rect = element.getBoundingClientRect();
|
|
69
|
+
return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
|
70
|
+
};
|
|
71
|
+
const walker = document.createTreeWalker(body, NodeFilter.SHOW_ELEMENT);
|
|
72
|
+
let node = walker.nextNode();
|
|
73
|
+
while (node) {
|
|
74
|
+
const visibleChildCount = [...node.children].filter(isVisible).length;
|
|
75
|
+
if (visibleChildCount === 0 && isVisible(node) && regex.test(node.innerText || node.textContent || '')) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
node = walker.nextNode();
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}, pattern.source).catch(() => false);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function chooseDuration(page, requested) {
|
|
85
|
+
const options = [4, 6, 8, 10];
|
|
86
|
+
const closest = options.reduce((best, item) => (
|
|
87
|
+
Math.abs(item - requested) < Math.abs(best - requested) ? item : best
|
|
88
|
+
), options[0]);
|
|
89
|
+
const option = page.getByText(new RegExp(`^${closest} seconds$`, 'i')).first();
|
|
90
|
+
if (await option.count().catch(() => 0)) await option.click({ timeout: 10000 }).catch(() => {});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function clickApprove(page) {
|
|
94
|
+
const candidates = [
|
|
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(),
|
|
99
|
+
];
|
|
100
|
+
for (const locator of candidates) {
|
|
101
|
+
if (!(await locator.count().catch(() => 0))) continue;
|
|
102
|
+
try {
|
|
103
|
+
await locator.click({ timeout: 10000, force: true });
|
|
104
|
+
await page.waitForTimeout(1000);
|
|
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
|
+
}
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { FLOW_URL } from './paths.mjs';
|
|
2
|
+
import { hasText, openFlow } from './flow-ui.mjs';
|
|
3
|
+
|
|
4
|
+
export async function prepareProjectEditor(browser, request, result) {
|
|
5
|
+
const diagnostics = { projectUrl: undefined, recovered: false };
|
|
6
|
+
const waitMs = Number(request.editorWaitMs || process.env.TAKOMI_FLOW_EDITOR_WAIT_MS || 90000);
|
|
7
|
+
|
|
8
|
+
let mayUseCurrentProject = false;
|
|
9
|
+
if (request.projectUrl) {
|
|
10
|
+
browser.page = await openProjectUrl(browser, request.projectUrl);
|
|
11
|
+
mayUseCurrentProject = true;
|
|
12
|
+
} else if (request.reuseCurrentProject) {
|
|
13
|
+
browser.page = await findProjectPage(browser) || browser.page;
|
|
14
|
+
mayUseCurrentProject = true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (mayUseCurrentProject && isProjectUrl(browser.page.url())) {
|
|
18
|
+
const ready = await waitForProjectEditor(browser.page, waitMs);
|
|
19
|
+
diagnostics.projectUrl = browser.page.url();
|
|
20
|
+
return { ok: ready.ok, diagnostics, manualActions: ready.manualActions };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await openFlow(browser.page);
|
|
24
|
+
if (request.allowNewProject) {
|
|
25
|
+
const opened = await clickNewProject(browser.page);
|
|
26
|
+
if (!opened) {
|
|
27
|
+
return { ok: false, diagnostics, manualActions: ['New project button was not found. Open a Flow project manually, then retry with --project-url.'] };
|
|
28
|
+
}
|
|
29
|
+
const ready = await waitForProjectEditor(browser.page, waitMs);
|
|
30
|
+
diagnostics.projectUrl = browser.page.url();
|
|
31
|
+
return { ok: ready.ok, diagnostics, manualActions: ready.manualActions };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Object.assign(result, await currentFlowState(browser.page));
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
diagnostics,
|
|
38
|
+
manualActions: ['Open an existing Flow project, pass --project-url, or set --allow-new-project to create a new Flow project.'],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function recoverFreshChat(page, request) {
|
|
43
|
+
if (!request.freshChatOnFailure) return { recovered: false, reason: 'freshChatOnFailure is disabled.' };
|
|
44
|
+
if (!isProjectUrl(page.url())) return { recovered: false, reason: 'Current page is not a Flow project.' };
|
|
45
|
+
const before = page.url();
|
|
46
|
+
await leaveMediaEditor(page);
|
|
47
|
+
const candidates = [
|
|
48
|
+
page.getByRole('button', { name: /new chat|new session|start new chat|new generation/i }).first(),
|
|
49
|
+
page.locator('button,[role="button"]').filter({ hasText: /new chat|new session|start new chat|new generation/i }).first(),
|
|
50
|
+
page.locator('[aria-label*="New chat" i], [aria-label*="New session" i], [aria-label*="Start new" i]').first(),
|
|
51
|
+
];
|
|
52
|
+
for (const locator of candidates) {
|
|
53
|
+
if (!(await locator.count().catch(() => 0))) continue;
|
|
54
|
+
try {
|
|
55
|
+
await locator.click({ timeout: 10000 });
|
|
56
|
+
await page.waitForTimeout(1500);
|
|
57
|
+
const ready = await waitForProjectEditor(page, request.editorWaitMs || 60000);
|
|
58
|
+
return { recovered: ready.ok, reason: ready.ok ? 'fresh chat opened' : ready.manualActions.join(' ') };
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
if (await hasText(page, /oops|something went wrong|try again|chat failed/i)) {
|
|
62
|
+
await page.goto(projectBaseUrl(before), { waitUntil: 'domcontentloaded', timeout: 90000 }).catch(() => {});
|
|
63
|
+
const ready = await waitForProjectEditor(page, request.editorWaitMs || 60000);
|
|
64
|
+
return { recovered: ready.ok, reason: ready.ok ? 'project reloaded' : ready.manualActions.join(' ') };
|
|
65
|
+
}
|
|
66
|
+
return { recovered: false, reason: 'No same-project fresh chat control was found.' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function openProjectUrl(browser, projectUrl) {
|
|
70
|
+
const page = (await findProjectPage(browser, projectUrl)) || browser.page;
|
|
71
|
+
await page.goto(projectBaseUrl(projectUrl), { waitUntil: 'domcontentloaded', timeout: 90000 });
|
|
72
|
+
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
|
73
|
+
await leaveMediaEditor(page);
|
|
74
|
+
return page;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function findProjectPage(browser, exactUrl = undefined) {
|
|
78
|
+
const pages = browser.context.pages();
|
|
79
|
+
const match = pages.find(page => exactUrl ? page.url() === exactUrl : isProjectUrl(page.url()));
|
|
80
|
+
return match || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function clickNewProject(page) {
|
|
84
|
+
const candidates = [
|
|
85
|
+
page.getByRole('button', { name: /new project/i }).first(),
|
|
86
|
+
page.locator('button').filter({ hasText: /new project/i }).first(),
|
|
87
|
+
page.getByText('New project').first(),
|
|
88
|
+
];
|
|
89
|
+
for (const locator of candidates) {
|
|
90
|
+
if (!(await locator.count().catch(() => 0))) continue;
|
|
91
|
+
try {
|
|
92
|
+
await locator.click({ timeout: 10000 });
|
|
93
|
+
await page.waitForURL(/\/project\//, { timeout: 30000 }).catch(() => {});
|
|
94
|
+
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
|
95
|
+
return isProjectUrl(page.url());
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function waitForProjectEditor(page, timeoutMs) {
|
|
102
|
+
await leaveMediaEditor(page);
|
|
103
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {});
|
|
104
|
+
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
|
105
|
+
const started = Date.now();
|
|
106
|
+
while (Date.now() - started < timeoutMs) {
|
|
107
|
+
const state = await currentFlowState(page);
|
|
108
|
+
if (state.manualActions.length) return { ok: false, manualActions: state.manualActions };
|
|
109
|
+
const hasEditorText = await hasText(page, /what do you want to create|start creating|add media/i);
|
|
110
|
+
const inputCount = await page.locator('div[role="textbox"], textarea:not(.g-recaptcha-response)').count().catch(() => 0);
|
|
111
|
+
if (hasEditorText && inputCount > 0) return { ok: true, manualActions: [] };
|
|
112
|
+
await page.waitForTimeout(1000);
|
|
113
|
+
}
|
|
114
|
+
return { ok: false, manualActions: ['Flow project editor did not become ready before the wait timeout.'] };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function leaveMediaEditor(page) {
|
|
118
|
+
if (!/\/edit\//.test(page.url())) return;
|
|
119
|
+
const candidates = [
|
|
120
|
+
page.getByRole('button', { name: /^Done$/i }).last(),
|
|
121
|
+
page.locator('button').filter({ hasText: /^Done$/i }).last(),
|
|
122
|
+
page.getByRole('button', { name: /back/i }).first(),
|
|
123
|
+
page.locator('button').filter({ hasText: /arrow_back|back/i }).first(),
|
|
124
|
+
];
|
|
125
|
+
for (const locator of candidates) {
|
|
126
|
+
if (!(await locator.count().catch(() => 0))) continue;
|
|
127
|
+
try {
|
|
128
|
+
await locator.click({ timeout: 10000, force: true });
|
|
129
|
+
await page.waitForURL(url => !/\/edit\//.test(url.toString()), { timeout: 15000 }).catch(() => {});
|
|
130
|
+
if (!/\/edit\//.test(page.url())) return;
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
await page.goto(projectBaseUrl(page.url()), { waitUntil: 'domcontentloaded', timeout: 90000 }).catch(() => {});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function currentFlowState(page) {
|
|
137
|
+
const text = (await page.locator('body').innerText({ timeout: 5000 }).catch(() => '')).toLowerCase();
|
|
138
|
+
const manualActions = [];
|
|
139
|
+
if (text.includes('sign in') || text.includes('log in')) manualActions.push('Sign into Google/Flow in the opened browser.');
|
|
140
|
+
if (text.includes('captcha') || text.includes('verify')) manualActions.push('Complete the verification challenge in the browser.');
|
|
141
|
+
if (/quota|usage limit|not enough credits|insufficient credits|out of credits/.test(text)) {
|
|
142
|
+
manualActions.push('Review Flow quota or credits message in the browser.');
|
|
143
|
+
}
|
|
144
|
+
return { manualActions };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isProjectUrl(url) {
|
|
148
|
+
return /\/project\//.test(url || '');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function projectBaseUrl(url) {
|
|
152
|
+
return String(url || '').replace(/\/edit\/[^/?#]+.*/, '');
|
|
153
|
+
}
|