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,90 +1,115 @@
1
- import { hasText, openGeneratedMedia } from './flow-ui.mjs';
2
-
3
- export async function handleGenerationFollowups(page, request) {
4
- if (await hasText(page, /which of these durations would you prefer/i)) {
5
- await chooseDuration(page, request.durationSeconds || 4);
6
- await page.waitForTimeout(2000);
7
- }
8
- if (await hasText(page, /costing\s+\d+\s+credits/i)) {
9
- return clickApprove(page);
10
- }
11
- return false;
12
- }
13
-
14
- export async function waitForGenerationOutcome(page, request, options = {}) {
15
- const timeoutMs = Number(options.timeoutMs || process.env.TAKOMI_FLOW_WAIT_MS || 300000);
16
- const intervalMs = Number(options.intervalMs || process.env.TAKOMI_FLOW_POLL_MS || 5000);
17
- const started = Date.now();
18
- let approved = false;
19
- while (Date.now() - started < timeoutMs) {
20
- if (await hasText(page, /which of these durations would you prefer/i)) {
21
- await chooseDuration(page, request.durationSeconds || 4);
22
- }
23
- if (await hasText(page, /costing\s+\d+\s+credits/i)) {
24
- approved = await clickApprove(page) || approved;
25
- }
26
- if (await page.locator('button').filter({ hasText: /download\s*Download/i }).count().catch(() => 0)) {
27
- return { status: 'download_ready', approved };
28
- }
29
- if (await page.locator('img[src*="media.getMediaUrlRedirect"], video').count().catch(() => 0)) {
30
- await openGeneratedMedia(page);
31
- return { status: 'media_ready', approved };
32
- }
33
- if (await hasVisibleText(page, /\b\d{1,3}%\b|generating|waiting in the queue|been scheduled/i)) {
34
- await page.waitForTimeout(intervalMs);
35
- continue;
36
- }
37
- if (await hasVisibleText(page, /failed|oops, something went wrong/i)) {
38
- return { status: 'failed', approved };
39
- }
40
- await page.waitForTimeout(intervalMs);
41
- }
42
- return { status: 'timeout', approved };
43
- }
44
-
45
- async function hasVisibleText(page, pattern) {
46
- return page.locator('body').evaluate((body, source) => {
47
- const regex = new RegExp(source, 'i');
48
- const isVisible = element => {
49
- const style = window.getComputedStyle(element);
50
- const rect = element.getBoundingClientRect();
51
- return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
52
- };
53
- const walker = document.createTreeWalker(body, NodeFilter.SHOW_ELEMENT);
54
- let node = walker.nextNode();
55
- while (node) {
56
- const visibleChildCount = [...node.children].filter(isVisible).length;
57
- if (visibleChildCount === 0 && isVisible(node) && regex.test(node.innerText || node.textContent || '')) {
58
- return true;
59
- }
60
- node = walker.nextNode();
61
- }
62
- return false;
63
- }, pattern.source).catch(() => false);
64
- }
65
-
66
- async function chooseDuration(page, requested) {
67
- const options = [4, 6, 8, 10];
68
- const closest = options.reduce((best, item) => (
69
- Math.abs(item - requested) < Math.abs(best - requested) ? item : best
70
- ), options[0]);
71
- const option = page.getByText(new RegExp(`^${closest} seconds$`, 'i')).first();
72
- if (await option.count().catch(() => 0)) await option.click({ timeout: 10000 }).catch(() => {});
73
- }
74
-
75
- async function clickApprove(page) {
76
- 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(),
80
- ];
81
- for (const locator of candidates) {
82
- if (!(await locator.count().catch(() => 0))) continue;
83
- try {
84
- await locator.click({ timeout: 10000, force: true });
85
- await page.waitForTimeout(1000);
86
- return true;
87
- } catch {}
88
- }
89
- return false;
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
+ }