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.
Files changed (68) hide show
  1. package/.agents/plugins/marketplace.json +22 -22
  2. package/README.md +217 -422
  3. package/assets/.agent/skills/remotion/rules/assets/charts-bar-chart.tsx +173 -173
  4. package/assets/.agent/skills/remotion/rules/assets/text-animations-typewriter.tsx +100 -100
  5. package/assets/.agent/skills/remotion/rules/text-animations.md +20 -20
  6. package/assets/.agent/skills/remotion/rules/videos.md +171 -171
  7. package/assets/.agent/skills/remotion-real-ui-video/SKILL.md +264 -0
  8. package/assets/.agent/skills/remotion-real-ui-video/references/anti-patterns.md +114 -0
  9. package/assets/.agent/skills/remotion-real-ui-video/references/completion-checklist.md +75 -0
  10. package/assets/.agent/skills/remotion-real-ui-video/references/real-ui-bridge-example.md +93 -0
  11. package/assets/.agent/skills/takomi-flow/SKILL.md +244 -235
  12. package/docs/features/TakomiFlow_Portable_Plugin.md +82 -78
  13. package/docs/takomi-flow-onboarding.md +86 -76
  14. package/docs/takomi-flow-public-distribution.md +94 -94
  15. package/package.json +1 -1
  16. package/plugins/takomi-flow/.mcp.json +11 -11
  17. package/plugins/takomi-flow/assets/capabilities.json +203 -171
  18. package/plugins/takomi-flow/assets/collection.schema.json +29 -29
  19. package/plugins/takomi-flow/assets/examples.json +94 -94
  20. package/plugins/takomi-flow/assets/request.schema.json +91 -67
  21. package/plugins/takomi-flow/assets/result.schema.json +46 -45
  22. package/plugins/takomi-flow/assets/templates/image-request.json +17 -13
  23. package/plugins/takomi-flow/assets/templates/video-request.json +19 -15
  24. package/plugins/takomi-flow/package.json +38 -38
  25. package/plugins/takomi-flow/pnpm-lock.yaml +813 -813
  26. package/plugins/takomi-flow/references/flow-provider-contract.md +349 -326
  27. package/plugins/takomi-flow/scripts/lib/agent-plan.mjs +140 -130
  28. package/plugins/takomi-flow/scripts/lib/api.mjs +113 -113
  29. package/plugins/takomi-flow/scripts/lib/args.mjs +66 -57
  30. package/plugins/takomi-flow/scripts/lib/audit.mjs +65 -65
  31. package/plugins/takomi-flow/scripts/lib/browser.mjs +78 -39
  32. package/plugins/takomi-flow/scripts/lib/capabilities.mjs +11 -11
  33. package/plugins/takomi-flow/scripts/lib/collect.mjs +53 -53
  34. package/plugins/takomi-flow/scripts/lib/commands.mjs +192 -175
  35. package/plugins/takomi-flow/scripts/lib/doctor.mjs +77 -77
  36. package/plugins/takomi-flow/scripts/lib/examples.mjs +17 -17
  37. package/plugins/takomi-flow/scripts/lib/flow-media.mjs +39 -0
  38. package/plugins/takomi-flow/scripts/lib/flow-outcome.mjs +115 -90
  39. package/plugins/takomi-flow/scripts/lib/flow-project-session.mjs +153 -0
  40. package/plugins/takomi-flow/scripts/lib/flow-ui.mjs +204 -165
  41. package/plugins/takomi-flow/scripts/lib/generation.mjs +190 -90
  42. package/plugins/takomi-flow/scripts/lib/inspect.mjs +81 -81
  43. package/plugins/takomi-flow/scripts/lib/mcp-prompts.mjs +39 -39
  44. package/plugins/takomi-flow/scripts/lib/mcp-resources.mjs +16 -16
  45. package/plugins/takomi-flow/scripts/lib/mcp-tools.mjs +165 -150
  46. package/plugins/takomi-flow/scripts/lib/media.mjs +78 -78
  47. package/plugins/takomi-flow/scripts/lib/observe.mjs +54 -54
  48. package/plugins/takomi-flow/scripts/lib/paths.mjs +37 -37
  49. package/plugins/takomi-flow/scripts/lib/playwright-loader.mjs +23 -23
  50. package/plugins/takomi-flow/scripts/lib/prompts.mjs +72 -72
  51. package/plugins/takomi-flow/scripts/lib/report.mjs +141 -141
  52. package/plugins/takomi-flow/scripts/lib/request-validator.mjs +73 -58
  53. package/plugins/takomi-flow/scripts/lib/request.mjs +66 -55
  54. package/plugins/takomi-flow/scripts/lib/resource-files.mjs +69 -69
  55. package/plugins/takomi-flow/scripts/lib/result.mjs +31 -31
  56. package/plugins/takomi-flow/scripts/lib/review.mjs +44 -44
  57. package/plugins/takomi-flow/scripts/lib/selftest.mjs +153 -153
  58. package/plugins/takomi-flow/scripts/lib/settings-plan.mjs +44 -39
  59. package/plugins/takomi-flow/scripts/lib/templates.mjs +37 -37
  60. package/plugins/takomi-flow/scripts/lib/trusted-chrome.mjs +67 -67
  61. package/plugins/takomi-flow/scripts/lib/workflow.mjs +56 -56
  62. package/plugins/takomi-flow/scripts/mcp-server.mjs +18 -18
  63. package/plugins/takomi-flow/scripts/mcp-smoke.mjs +144 -142
  64. package/plugins/takomi-flow/scripts/takomi-flow.mjs +17 -17
  65. package/plugins/takomi-flow/skills/takomi-flow/SKILL.md +244 -235
  66. package/plugins/takomi-flow/skills/takomi-flow/agents/openai.yaml +7 -7
  67. package/scripts/install-takomi-flow.ps1 +85 -85
  68. 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
- export async function openFlow(page) {
6
- await page.goto(FLOW_URL, { waitUntil: 'domcontentloaded', timeout: 90000 });
7
- await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
8
- }
9
-
10
- export async function capture(page, run, name) {
11
- const filePath = path.join(run.screenshotsDir, `${name}.png`);
12
- await page.screenshot({ path: filePath, fullPage: true }).catch(() => null);
13
- return filePath;
14
- }
15
-
16
- export async function inspectFlowState(page) {
17
- const title = await page.title().catch(() => '');
18
- const url = page.url();
19
- const text = (await page.locator('body').innerText({ timeout: 5000 }).catch(() => '')).slice(0, 5000);
20
- const lower = text.toLowerCase();
21
- const manualActions = [];
22
- if (lower.includes('sign in') || lower.includes('log in')) {
23
- manualActions.push('Sign into Google/Flow in the opened browser.');
24
- }
25
- if (lower.includes('captcha') || lower.includes('verify')) {
26
- manualActions.push('Complete the verification challenge in the browser.');
27
- }
28
- if (lower.includes('quota') || lower.includes('limit') || lower.includes('credits')) {
29
- manualActions.push('Review Flow quota or credits message in the browser.');
30
- }
31
- return { title, url, manualActions, textSample: text };
32
- }
33
-
34
- export async function ensureProjectEditor(page) {
35
- if (page.url().includes('/project/')) return waitForProjectEditor(page);
36
- const candidates = [
37
- page.getByRole('button', { name: /new project/i }).first(),
38
- page.locator('button').filter({ hasText: /new project/i }).first(),
39
- page.getByText('New project').first(),
40
- ];
41
- for (const locator of candidates) {
42
- if (!(await locator.count().catch(() => 0))) continue;
43
- try {
44
- await locator.click({ timeout: 10000 });
45
- await page.waitForURL(/\/project\//, { timeout: 30000 }).catch(() => {});
46
- await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
47
- return page.url().includes('/project/') && await waitForProjectEditor(page);
48
- } catch {}
49
- }
50
- return false;
51
- }
52
-
53
- async function waitForProjectEditor(page) {
54
- await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
55
- const ready = page.locator('body').filter({ hasText: /what do you want to create|start creating|add media/i });
56
- await ready.waitFor({ state: 'visible', timeout: 60000 }).catch(() => {});
57
- return (await page.locator('div[role="textbox"], textarea:not(.g-recaptcha-response)').count().catch(() => 0)) > 0;
58
- }
59
-
60
- export async function fillPrompt(page, prompt) {
61
- const candidates = [
62
- page.locator('div[role="textbox"]').filter({ hasText: /what do you want to create/i }).last(),
63
- page.locator('div[role="textbox"]').last(),
64
- page.locator('textarea:not(.g-recaptcha-response):not([name*="recaptcha"])').last(),
65
- ];
66
- for (const locator of candidates) {
67
- if (!(await locator.count().catch(() => 0))) continue;
68
- try {
69
- await locator.click({ timeout: 8000 });
70
- await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A').catch(() => {});
71
- await page.keyboard.type(prompt, { delay: 2 });
72
- const matched = await page.locator('body').filter({ hasText: prompt.slice(0, 40) }).count().catch(() => 0);
73
- if (matched) return true;
74
- } catch {}
75
- }
76
- return false;
77
- }
78
-
79
- export async function submitGeneration(page) {
80
- const preferred = [
81
- page.locator('button').filter({ hasText: /arrow_forward\s*Create/i }).last(),
82
- page.getByRole('button', { name: /^Create$/i }).last(),
83
- page.locator('button').filter({ hasText: /^Create$/i }).last(),
84
- ];
85
- for (const button of preferred) {
86
- if (!(await button.count().catch(() => 0))) continue;
87
- try {
88
- await button.click({ timeout: 8000 });
89
- return true;
90
- } catch {}
91
- }
92
- const labels = /submit|start/i;
93
- const buttons = [
94
- page.getByRole('button', { name: labels }).first(),
95
- page.locator('button').filter({ hasText: labels }).first(),
96
- ];
97
- for (const button of buttons) {
98
- if (!(await button.count().catch(() => 0))) continue;
99
- try {
100
- await button.click({ timeout: 8000 });
101
- return true;
102
- } catch {}
103
- }
104
- return false;
105
- }
106
-
107
- export async function tryDownloadAssets(page, run, maxDownloads = 1) {
108
- const assets = [];
109
- await openGeneratedMedia(page);
110
- for (let i = 0; i < maxDownloads; i += 1) {
111
- const button = page.locator('button').filter({ hasText: /download\s*Download|download/i }).first();
112
- if (!(await button.count().catch(() => 0))) break;
113
- const [completed] = await Promise.all([
114
- page.waitForEvent('download', { timeout: 30000 }).catch(() => null),
115
- button.click({ timeout: 5000 }).catch(() => null),
116
- ]);
117
- if (!completed) {
118
- const imagePath = await downloadLargestRenderedImage(page, run, i).catch(() => null);
119
- if (!imagePath) break;
120
- assets.push(imagePath);
121
- continue;
122
- }
123
- const suggested = completed.suggestedFilename();
124
- const filePath = path.join(run.downloadsDir, suggested);
125
- await completed.saveAs(filePath);
126
- assets.push(filePath);
127
- }
128
- return assets;
129
- }
130
-
131
- async function downloadLargestRenderedImage(page, run, index) {
132
- const images = await page.locator('img').evaluateAll(items => items
133
- .map(img => ({ src: img.src, width: img.naturalWidth, height: img.naturalHeight }))
134
- .filter(item => item.src && item.width >= 256 && item.height >= 256)
135
- .sort((a, b) => (b.width * b.height) - (a.width * a.height)));
136
- if (!images.length) return null;
137
- const response = await page.context().request.get(images[0].src, { timeout: 30000 });
138
- if (!response.ok()) return null;
139
- const filePath = path.join(run.downloadsDir, `flow-image-${index + 1}.png`);
140
- fs.writeFileSync(filePath, await response.body());
141
- return filePath;
142
- }
143
-
144
- export async function openGeneratedMedia(page) {
145
- if (await page.locator('button').filter({ hasText: /download\s*Download/i }).count().catch(() => 0)) return true;
146
- const candidates = [
147
- page.getByAltText(/video thumbnail|generated image/i).first(),
148
- page.locator('img[src*="media.getMediaUrlRedirect"]').last(),
149
- page.locator('[role="button"]').filter({ hasText: /play_circle/i }).first(),
150
- page.locator('button').filter({ hasText: /play_circle/i }).first(),
151
- ];
152
- for (const locator of candidates) {
153
- if (!(await locator.count().catch(() => 0))) continue;
154
- try {
155
- await locator.click({ timeout: 10000 });
156
- await page.waitForTimeout(3000);
157
- return true;
158
- } catch {}
159
- }
160
- return false;
161
- }
162
-
163
- export async function hasText(page, pattern) {
164
- return (await page.locator('body').filter({ hasText: pattern }).count().catch(() => 0)) > 0;
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
+ }