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,165 +1,204 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import { FLOW_URL } from './paths.mjs';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
await page.
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
page.
|
|
39
|
-
page.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
await
|
|
46
|
-
await page.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
page.locator('
|
|
64
|
-
page.locator('
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
await
|
|
71
|
-
await
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
page.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { FLOW_URL } from './paths.mjs';
|
|
4
|
+
import { openGeneratedMediaById } from './flow-media.mjs';
|
|
5
|
+
|
|
6
|
+
export async function openFlow(page) {
|
|
7
|
+
await page.goto(FLOW_URL, { waitUntil: 'domcontentloaded', timeout: 90000 });
|
|
8
|
+
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function capture(page, run, name) {
|
|
12
|
+
const filePath = path.join(run.screenshotsDir, `${name}.png`);
|
|
13
|
+
await page.screenshot({ path: filePath, fullPage: true }).catch(() => null);
|
|
14
|
+
return filePath;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function inspectFlowState(page) {
|
|
18
|
+
const title = await page.title().catch(() => '');
|
|
19
|
+
const url = page.url();
|
|
20
|
+
const text = (await page.locator('body').innerText({ timeout: 5000 }).catch(() => '')).slice(0, 5000);
|
|
21
|
+
const lower = text.toLowerCase();
|
|
22
|
+
const manualActions = [];
|
|
23
|
+
if (lower.includes('sign in') || lower.includes('log in')) {
|
|
24
|
+
manualActions.push('Sign into Google/Flow in the opened browser.');
|
|
25
|
+
}
|
|
26
|
+
if (lower.includes('captcha') || lower.includes('verify')) {
|
|
27
|
+
manualActions.push('Complete the verification challenge in the browser.');
|
|
28
|
+
}
|
|
29
|
+
if (lower.includes('quota') || lower.includes('limit') || lower.includes('credits')) {
|
|
30
|
+
manualActions.push('Review Flow quota or credits message in the browser.');
|
|
31
|
+
}
|
|
32
|
+
return { title, url, manualActions, textSample: text };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function ensureProjectEditor(page) {
|
|
36
|
+
if (page.url().includes('/project/')) return waitForProjectEditor(page);
|
|
37
|
+
const candidates = [
|
|
38
|
+
page.getByRole('button', { name: /new project/i }).first(),
|
|
39
|
+
page.locator('button').filter({ hasText: /new project/i }).first(),
|
|
40
|
+
page.getByText('New project').first(),
|
|
41
|
+
];
|
|
42
|
+
for (const locator of candidates) {
|
|
43
|
+
if (!(await locator.count().catch(() => 0))) continue;
|
|
44
|
+
try {
|
|
45
|
+
await locator.click({ timeout: 10000 });
|
|
46
|
+
await page.waitForURL(/\/project\//, { timeout: 30000 }).catch(() => {});
|
|
47
|
+
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
|
|
48
|
+
return page.url().includes('/project/') && await waitForProjectEditor(page);
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function waitForProjectEditor(page) {
|
|
55
|
+
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
|
|
56
|
+
const ready = page.locator('body').filter({ hasText: /what do you want to create|start creating|add media/i });
|
|
57
|
+
await ready.waitFor({ state: 'visible', timeout: 60000 }).catch(() => {});
|
|
58
|
+
return (await page.locator('div[role="textbox"], textarea:not(.g-recaptcha-response)').count().catch(() => 0)) > 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function fillPrompt(page, prompt) {
|
|
62
|
+
const candidates = [
|
|
63
|
+
page.locator('textarea:not(.g-recaptcha-response):not([name*="recaptcha"]):visible').last(),
|
|
64
|
+
page.locator('[contenteditable="true"][role="textbox"]:visible').last(),
|
|
65
|
+
page.locator('div[role="textbox"]:visible').last(),
|
|
66
|
+
];
|
|
67
|
+
for (const locator of candidates) {
|
|
68
|
+
if (!(await locator.count().catch(() => 0))) continue;
|
|
69
|
+
try {
|
|
70
|
+
await locator.click({ timeout: 8000 });
|
|
71
|
+
if (!(await fillEditable(locator, page, prompt))) continue;
|
|
72
|
+
if (await editableContains(locator, prompt)) return true;
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function fillEditable(locator, page, prompt) {
|
|
79
|
+
if (await locator.fill(prompt, { timeout: 8000 }).then(() => true).catch(() => false)) return true;
|
|
80
|
+
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A').catch(() => {});
|
|
81
|
+
await page.keyboard.insertText(prompt).catch(async () => {
|
|
82
|
+
await page.keyboard.type(prompt, { delay: 5 });
|
|
83
|
+
});
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function editableContains(locator, prompt) {
|
|
88
|
+
const expected = compactText(prompt).slice(0, 80);
|
|
89
|
+
const actual = await locator.evaluate(element => {
|
|
90
|
+
const value = element.value || element.innerText || element.textContent || '';
|
|
91
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
92
|
+
}).catch(() => '');
|
|
93
|
+
return compactText(actual).includes(expected);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function compactText(value) {
|
|
97
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function submitGeneration(page) {
|
|
101
|
+
const preferred = [
|
|
102
|
+
page.locator('button').filter({ hasText: /arrow_forward\s*Create/i }).last(),
|
|
103
|
+
page.getByRole('button', { name: /^Create$/i }).last(),
|
|
104
|
+
page.locator('button').filter({ hasText: /^Create$/i }).last(),
|
|
105
|
+
];
|
|
106
|
+
for (const button of preferred) {
|
|
107
|
+
if (!(await button.count().catch(() => 0))) continue;
|
|
108
|
+
try {
|
|
109
|
+
if (await button.isDisabled().catch(() => false)) continue;
|
|
110
|
+
await button.click({ timeout: 8000 });
|
|
111
|
+
return true;
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
const labels = /submit|start/i;
|
|
115
|
+
const buttons = [
|
|
116
|
+
page.getByRole('button', { name: labels }).first(),
|
|
117
|
+
page.locator('button').filter({ hasText: labels }).first(),
|
|
118
|
+
];
|
|
119
|
+
for (const button of buttons) {
|
|
120
|
+
if (!(await button.count().catch(() => 0))) continue;
|
|
121
|
+
try {
|
|
122
|
+
if (await button.isDisabled().catch(() => false)) continue;
|
|
123
|
+
await button.click({ timeout: 8000 });
|
|
124
|
+
return true;
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function tryDownloadAssets(page, run, maxDownloads = 1, options = {}) {
|
|
131
|
+
const assets = [];
|
|
132
|
+
const allowRenderedImageFallback = options.allowRenderedImageFallback !== false;
|
|
133
|
+
await openGeneratedMedia(page, options.mediaIds || []);
|
|
134
|
+
for (let i = 0; i < maxDownloads; i += 1) {
|
|
135
|
+
const button = downloadButton(page);
|
|
136
|
+
if (!(await button.count().catch(() => 0))) break;
|
|
137
|
+
const [completed] = await Promise.all([
|
|
138
|
+
page.waitForEvent('download', { timeout: 30000 }).catch(() => null),
|
|
139
|
+
button.click({ timeout: 5000 }).catch(() => null),
|
|
140
|
+
]);
|
|
141
|
+
if (!completed) {
|
|
142
|
+
if (!allowRenderedImageFallback) break;
|
|
143
|
+
const imagePath = await downloadLargestRenderedImage(page, run, i).catch(() => null);
|
|
144
|
+
if (!imagePath) break;
|
|
145
|
+
assets.push(imagePath);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const suggested = completed.suggestedFilename();
|
|
149
|
+
const filePath = path.join(run.downloadsDir, suggested);
|
|
150
|
+
await completed.saveAs(filePath);
|
|
151
|
+
assets.push(filePath);
|
|
152
|
+
}
|
|
153
|
+
return assets;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function downloadLargestRenderedImage(page, run, index) {
|
|
157
|
+
const images = await page.locator('img').evaluateAll(items => items
|
|
158
|
+
.map(img => ({ src: img.src, width: img.naturalWidth, height: img.naturalHeight }))
|
|
159
|
+
.filter(item => item.src && item.width >= 256 && item.height >= 256)
|
|
160
|
+
.sort((a, b) => (b.width * b.height) - (a.width * a.height)));
|
|
161
|
+
if (!images.length) return null;
|
|
162
|
+
const response = await page.context().request.get(images[0].src, { timeout: 30000 });
|
|
163
|
+
if (!response.ok()) return null;
|
|
164
|
+
const filePath = path.join(run.downloadsDir, `flow-image-${index + 1}.png`);
|
|
165
|
+
fs.writeFileSync(filePath, await response.body());
|
|
166
|
+
return filePath;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function openGeneratedMedia(page, mediaIds = []) {
|
|
170
|
+
if (await downloadButton(page).count().catch(() => 0)) return true;
|
|
171
|
+
for (const mediaId of mediaIds) {
|
|
172
|
+
if (await openGeneratedMediaById(page, mediaId)) return true;
|
|
173
|
+
}
|
|
174
|
+
const candidates = [
|
|
175
|
+
page.getByAltText(/video thumbnail|generated image/i).first(),
|
|
176
|
+
page.locator('img[src*="media.getMediaUrlRedirect"]').last(),
|
|
177
|
+
page.locator('[role="button"]').filter({ hasText: /play_circle/i }).first(),
|
|
178
|
+
page.locator('button').filter({ hasText: /play_circle/i }).first(),
|
|
179
|
+
];
|
|
180
|
+
for (const locator of candidates) {
|
|
181
|
+
if (!(await locator.count().catch(() => 0))) continue;
|
|
182
|
+
try {
|
|
183
|
+
await locator.click({ timeout: 10000 });
|
|
184
|
+
await page.waitForTimeout(3000);
|
|
185
|
+
return true;
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function hasText(page, pattern) {
|
|
192
|
+
return (await page.locator('body').filter({ hasText: pattern }).count().catch(() => 0)) > 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function downloadButton(page) {
|
|
196
|
+
return page.locator([
|
|
197
|
+
'button[aria-label*="Download" i]',
|
|
198
|
+
'[role="button"][aria-label*="Download" i]',
|
|
199
|
+
'button[title*="Download" i]',
|
|
200
|
+
'[role="button"][title*="Download" i]',
|
|
201
|
+
'button:has-text("download")',
|
|
202
|
+
'[role="button"]:has-text("download")',
|
|
203
|
+
].join(', ')).first();
|
|
204
|
+
}
|