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,150 +1,165 @@
|
|
|
1
|
-
import * as z from 'zod/v4';
|
|
2
|
-
import * as api from './api.mjs';
|
|
3
|
-
|
|
4
|
-
export function registerTools(server) {
|
|
5
|
-
const register = toolRegister(server);
|
|
6
|
-
register('takomi_flow_capabilities', 'List supported TakomiFlow kinds, modes, outputs, commands, and safety rules.', {}, () => api.capabilities());
|
|
7
|
-
register('takomi_flow_doctor', 'Check local TakomiFlow readiness: Playwright, FFmpeg, FFprobe, profile path, and output path.', {
|
|
8
|
-
profileDir: z.string().optional(),
|
|
9
|
-
outputDir: z.string().optional(),
|
|
10
|
-
}, args => api.doctor(args));
|
|
11
|
-
register('takomi_flow_audit', 'Summarize TakomiFlow readiness, safe actions, gated actions, and recent run state.', {
|
|
12
|
-
profileDir: z.string().optional(),
|
|
13
|
-
outputDir: z.string().optional(),
|
|
14
|
-
limit: z.number().int().positive().optional(),
|
|
15
|
-
}, args => api.audit(args));
|
|
16
|
-
register('takomi_flow_examples', 'Return TakomiFlow example requests and downstream handoff patterns.', {
|
|
17
|
-
name: z.string().optional(),
|
|
18
|
-
}, args => api.examples(args));
|
|
19
|
-
register('takomi_flow_plan', 'Create a safe agent execution plan with MCP calls, CLI fallbacks, gates, and expected outputs.', {
|
|
20
|
-
kind: z.enum(['video', 'image']),
|
|
21
|
-
prompt: z.string(),
|
|
22
|
-
variations: z.number().int().positive().optional(),
|
|
23
|
-
aspectRatio: z.string().optional(),
|
|
24
|
-
durationSeconds: z.number().positive().optional(),
|
|
25
|
-
mode: z.string().optional(),
|
|
26
|
-
modelHint: z.string().optional(),
|
|
27
|
-
outputDir: z.string().optional(),
|
|
28
|
-
allowSpend: z.boolean().optional(),
|
|
29
|
-
extractFrames: z.number().int().nonnegative().optional(),
|
|
30
|
-
sourceAssets: z.array(z.string()).optional(),
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
register
|
|
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
|
-
register('
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
register('
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import * as api from './api.mjs';
|
|
3
|
+
|
|
4
|
+
export function registerTools(server) {
|
|
5
|
+
const register = toolRegister(server);
|
|
6
|
+
register('takomi_flow_capabilities', 'List supported TakomiFlow kinds, modes, outputs, commands, and safety rules.', {}, () => api.capabilities());
|
|
7
|
+
register('takomi_flow_doctor', 'Check local TakomiFlow readiness: Playwright, FFmpeg, FFprobe, profile path, and output path.', {
|
|
8
|
+
profileDir: z.string().optional(),
|
|
9
|
+
outputDir: z.string().optional(),
|
|
10
|
+
}, args => api.doctor(args));
|
|
11
|
+
register('takomi_flow_audit', 'Summarize TakomiFlow readiness, safe actions, gated actions, and recent run state.', {
|
|
12
|
+
profileDir: z.string().optional(),
|
|
13
|
+
outputDir: z.string().optional(),
|
|
14
|
+
limit: z.number().int().positive().optional(),
|
|
15
|
+
}, args => api.audit(args));
|
|
16
|
+
register('takomi_flow_examples', 'Return TakomiFlow example requests and downstream handoff patterns.', {
|
|
17
|
+
name: z.string().optional(),
|
|
18
|
+
}, args => api.examples(args));
|
|
19
|
+
register('takomi_flow_plan', 'Create a safe agent execution plan with MCP calls, CLI fallbacks, gates, and expected outputs.', {
|
|
20
|
+
kind: z.enum(['video', 'image']),
|
|
21
|
+
prompt: z.string(),
|
|
22
|
+
variations: z.number().int().positive().optional(),
|
|
23
|
+
aspectRatio: z.string().optional(),
|
|
24
|
+
durationSeconds: z.number().positive().optional(),
|
|
25
|
+
mode: z.string().optional(),
|
|
26
|
+
modelHint: z.string().optional(),
|
|
27
|
+
outputDir: z.string().optional(),
|
|
28
|
+
allowSpend: z.boolean().optional(),
|
|
29
|
+
extractFrames: z.number().int().nonnegative().optional(),
|
|
30
|
+
sourceAssets: z.array(z.string()).optional(),
|
|
31
|
+
projectUrl: z.string().optional(),
|
|
32
|
+
reuseCurrentProject: z.boolean().optional(),
|
|
33
|
+
allowNewProject: z.boolean().optional(),
|
|
34
|
+
freshChatOnFailure: z.boolean().optional(),
|
|
35
|
+
editorWaitMs: z.number().int().positive().optional(),
|
|
36
|
+
submit: z.boolean().optional(),
|
|
37
|
+
targetDir: z.string().optional(),
|
|
38
|
+
}, args => api.plan(args));
|
|
39
|
+
registerRequestTools(register);
|
|
40
|
+
registerBrowserTools(register);
|
|
41
|
+
registerResultTools(register);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function registerRequestTools(register) {
|
|
45
|
+
register('takomi_flow_template', 'Return or write a video/image request template.', {
|
|
46
|
+
kind: z.enum(['video', 'image']).optional(),
|
|
47
|
+
outputDir: z.string().optional(),
|
|
48
|
+
}, args => api.template(args));
|
|
49
|
+
const requestShape = {
|
|
50
|
+
kind: z.enum(['video', 'image']),
|
|
51
|
+
prompt: z.string(),
|
|
52
|
+
variations: z.number().int().positive().optional(),
|
|
53
|
+
aspectRatio: z.string().optional(),
|
|
54
|
+
durationSeconds: z.number().positive().optional(),
|
|
55
|
+
mode: z.string().optional(),
|
|
56
|
+
modelHint: z.string().optional(),
|
|
57
|
+
outputDir: z.string().optional(),
|
|
58
|
+
allowSpend: z.boolean().optional(),
|
|
59
|
+
extractFrames: z.number().int().nonnegative().optional(),
|
|
60
|
+
sourceAssets: z.array(z.string()).optional(),
|
|
61
|
+
projectUrl: z.string().optional(),
|
|
62
|
+
reuseCurrentProject: z.boolean().optional(),
|
|
63
|
+
allowNewProject: z.boolean().optional(),
|
|
64
|
+
freshChatOnFailure: z.boolean().optional(),
|
|
65
|
+
editorWaitMs: z.number().int().positive().optional(),
|
|
66
|
+
notes: z.string().optional(),
|
|
67
|
+
};
|
|
68
|
+
register('takomi_flow_prepare', 'Create a prepared request JSON file for a Flow image or video generation.', requestShape, args => api.prepare(args));
|
|
69
|
+
register('takomi_flow_workflow', 'Prepare, validate, and optionally submit a Flow image/video generation from one agent call.', {
|
|
70
|
+
...requestShape,
|
|
71
|
+
submit: z.boolean().optional(),
|
|
72
|
+
allowBrowser: z.boolean().optional(),
|
|
73
|
+
profileDir: z.string().optional(),
|
|
74
|
+
browserChannel: z.string().optional(),
|
|
75
|
+
cdpUrl: z.string().optional(),
|
|
76
|
+
headless: z.boolean().optional(),
|
|
77
|
+
}, args => api.workflow(args));
|
|
78
|
+
register('takomi_flow_validate', 'Validate a TakomiFlow request JSON before opening Flow or spending credits.', {
|
|
79
|
+
request: z.string(),
|
|
80
|
+
}, args => api.validate(args));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function registerBrowserTools(register) {
|
|
84
|
+
register('takomi_flow_observe', 'Open Flow and observe UI controls/screenshots. Requires allowBrowser=true.', {
|
|
85
|
+
allowBrowser: z.boolean(),
|
|
86
|
+
profileDir: z.string().optional(),
|
|
87
|
+
outputDir: z.string().optional(),
|
|
88
|
+
browserChannel: z.string().optional(),
|
|
89
|
+
cdpUrl: z.string().optional(),
|
|
90
|
+
headless: z.boolean().optional(),
|
|
91
|
+
}, args => requireBrowser(args, () => api.observe(args)));
|
|
92
|
+
register('takomi_flow_generate', 'Run guarded Flow generation from a request file. Requires allowBrowser=true and request allowSpend=true to submit.', {
|
|
93
|
+
allowBrowser: z.boolean(),
|
|
94
|
+
request: z.string(),
|
|
95
|
+
profileDir: z.string().optional(),
|
|
96
|
+
browserChannel: z.string().optional(),
|
|
97
|
+
cdpUrl: z.string().optional(),
|
|
98
|
+
headless: z.boolean().optional(),
|
|
99
|
+
projectUrl: z.string().optional(),
|
|
100
|
+
reuseCurrentProject: z.boolean().optional(),
|
|
101
|
+
allowNewProject: z.boolean().optional(),
|
|
102
|
+
freshChatOnFailure: z.boolean().optional(),
|
|
103
|
+
editorWaitMs: z.number().int().positive().optional(),
|
|
104
|
+
}, args => requireBrowser(args, () => api.generate(args)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function registerResultTools(register) {
|
|
108
|
+
register('takomi_flow_selftest', 'Run deterministic no-spend TakomiFlow checks, including media catalog/frame extraction.', {
|
|
109
|
+
outputDir: z.string().optional(),
|
|
110
|
+
}, args => api.selftest(args));
|
|
111
|
+
register('takomi_flow_inspect', 'Summarize a TakomiFlow run.json or run directory.', { run: z.string() }, args => api.inspect(args));
|
|
112
|
+
register('takomi_flow_latest', 'Summarize the latest TakomiFlow run under an output directory.', {
|
|
113
|
+
outputDir: z.string().optional(),
|
|
114
|
+
}, args => api.latest(args));
|
|
115
|
+
register('takomi_flow_runs', 'List recent TakomiFlow runs under an output directory.', {
|
|
116
|
+
outputDir: z.string().optional(),
|
|
117
|
+
limit: z.number().int().positive().optional(),
|
|
118
|
+
}, args => api.runs(args));
|
|
119
|
+
register('takomi_flow_assets', 'Catalog downloaded run assets and optionally extract video review frames.', {
|
|
120
|
+
run: z.string(),
|
|
121
|
+
frames: z.number().int().nonnegative().optional(),
|
|
122
|
+
}, args => api.assets(args));
|
|
123
|
+
register('takomi_flow_review', 'Inspect a run, catalog assets, extract review frames, and write a Markdown handoff.', {
|
|
124
|
+
run: z.string(),
|
|
125
|
+
frames: z.number().int().nonnegative().optional(),
|
|
126
|
+
reportPath: z.string().optional(),
|
|
127
|
+
}, args => api.review(args));
|
|
128
|
+
register('takomi_flow_collect', 'Copy reviewed assets, frames, report, and manifest into a downstream target folder.', {
|
|
129
|
+
run: z.string(),
|
|
130
|
+
targetDir: z.string(),
|
|
131
|
+
frames: z.number().int().nonnegative().optional(),
|
|
132
|
+
includeFrames: z.boolean().optional(),
|
|
133
|
+
reportPath: z.string().optional(),
|
|
134
|
+
}, args => api.collect(args));
|
|
135
|
+
register('takomi_flow_report', 'Create a Markdown report for a run or recent TakomiFlow history.', {
|
|
136
|
+
run: z.string().optional(),
|
|
137
|
+
outputDir: z.string().optional(),
|
|
138
|
+
limit: z.number().int().positive().optional(),
|
|
139
|
+
reportPath: z.string().optional(),
|
|
140
|
+
}, args => api.report(args));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toolRegister(server) {
|
|
144
|
+
return (name, description, inputSchema, handler) => {
|
|
145
|
+
server.registerTool(name, { description, inputSchema }, async args => {
|
|
146
|
+
const structuredContent = await handler(args || {});
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: 'text', text: JSON.stringify(structuredContent, null, 2) }],
|
|
149
|
+
structuredContent,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function requireBrowser(args, handler) {
|
|
156
|
+
if (!args?.allowBrowser) {
|
|
157
|
+
return {
|
|
158
|
+
schemaVersion: 1,
|
|
159
|
+
status: 'blocked',
|
|
160
|
+
errors: [],
|
|
161
|
+
manualActions: ['Set allowBrowser=true to permit TakomiFlow to open or drive a browser.'],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return handler();
|
|
165
|
+
}
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
import { ensureDir, writeJson } from './paths.mjs';
|
|
6
|
-
|
|
7
|
-
const execFileAsync = promisify(execFile);
|
|
8
|
-
const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.webm', '.mkv', '.avi']);
|
|
9
|
-
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
10
|
-
|
|
11
|
-
export async function catalogAssets(run, assets = [], options = {}) {
|
|
12
|
-
const catalog = [];
|
|
13
|
-
const frameCount = Math.max(0, Number.parseInt(options.frames || '0', 10) || 0);
|
|
14
|
-
for (const assetPath of assets) {
|
|
15
|
-
const absolutePath = path.resolve(assetPath);
|
|
16
|
-
if (!fs.existsSync(absolutePath)) {
|
|
17
|
-
catalog.push({ path: absolutePath, exists: false, errors: ['Asset file not found.'] });
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
const kind = classifyAsset(absolutePath);
|
|
21
|
-
const item = {
|
|
22
|
-
path: absolutePath,
|
|
23
|
-
exists: true,
|
|
24
|
-
kind,
|
|
25
|
-
bytes: fs.statSync(absolutePath).size,
|
|
26
|
-
frames: [],
|
|
27
|
-
metadata: {},
|
|
28
|
-
errors: [],
|
|
29
|
-
};
|
|
30
|
-
if (kind === 'video') {
|
|
31
|
-
item.metadata = await probeMedia(absolutePath).catch(error => ({ probeError: error.message }));
|
|
32
|
-
if (frameCount > 0) {
|
|
33
|
-
item.frames = await extractFrames(absolutePath, path.join(run.runDir, 'frames'), frameCount)
|
|
34
|
-
.catch(error => {
|
|
35
|
-
item.errors.push(error.message);
|
|
36
|
-
return [];
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catalog.push(item);
|
|
41
|
-
}
|
|
42
|
-
const catalogPath = path.join(run.runDir, 'assets.json');
|
|
43
|
-
writeJson(catalogPath, { schemaVersion: 1, runId: run.runId, assets: catalog });
|
|
44
|
-
return { catalogPath, assets: catalog };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function classifyAsset(filePath) {
|
|
48
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
49
|
-
if (VIDEO_EXTENSIONS.has(ext)) return 'video';
|
|
50
|
-
if (IMAGE_EXTENSIONS.has(ext)) return 'image';
|
|
51
|
-
return 'unknown';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function probeMedia(filePath) {
|
|
55
|
-
const { stdout } = await execFileAsync('ffprobe', [
|
|
56
|
-
'-v', 'error',
|
|
57
|
-
'-show_entries', 'format=duration,size:stream=width,height,codec_type,codec_name',
|
|
58
|
-
'-of', 'json',
|
|
59
|
-
filePath,
|
|
60
|
-
]);
|
|
61
|
-
return JSON.parse(stdout);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function extractFrames(filePath, framesDir, count) {
|
|
65
|
-
ensureDir(framesDir);
|
|
66
|
-
const outputPattern = path.join(framesDir, `${path.basename(filePath, path.extname(filePath))}_%03d.jpg`);
|
|
67
|
-
await execFileAsync('ffmpeg', [
|
|
68
|
-
'-y',
|
|
69
|
-
'-i', filePath,
|
|
70
|
-
'-vf', 'fps=1,scale=640:-1',
|
|
71
|
-
'-frames:v', String(count),
|
|
72
|
-
outputPattern,
|
|
73
|
-
]);
|
|
74
|
-
return fs.readdirSync(framesDir)
|
|
75
|
-
.filter(name => name.startsWith(path.basename(filePath, path.extname(filePath))) && name.endsWith('.jpg'))
|
|
76
|
-
.sort()
|
|
77
|
-
.map(name => path.join(framesDir, name));
|
|
78
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { ensureDir, writeJson } from './paths.mjs';
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.webm', '.mkv', '.avi']);
|
|
9
|
+
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
10
|
+
|
|
11
|
+
export async function catalogAssets(run, assets = [], options = {}) {
|
|
12
|
+
const catalog = [];
|
|
13
|
+
const frameCount = Math.max(0, Number.parseInt(options.frames || '0', 10) || 0);
|
|
14
|
+
for (const assetPath of assets) {
|
|
15
|
+
const absolutePath = path.resolve(assetPath);
|
|
16
|
+
if (!fs.existsSync(absolutePath)) {
|
|
17
|
+
catalog.push({ path: absolutePath, exists: false, errors: ['Asset file not found.'] });
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const kind = classifyAsset(absolutePath);
|
|
21
|
+
const item = {
|
|
22
|
+
path: absolutePath,
|
|
23
|
+
exists: true,
|
|
24
|
+
kind,
|
|
25
|
+
bytes: fs.statSync(absolutePath).size,
|
|
26
|
+
frames: [],
|
|
27
|
+
metadata: {},
|
|
28
|
+
errors: [],
|
|
29
|
+
};
|
|
30
|
+
if (kind === 'video') {
|
|
31
|
+
item.metadata = await probeMedia(absolutePath).catch(error => ({ probeError: error.message }));
|
|
32
|
+
if (frameCount > 0) {
|
|
33
|
+
item.frames = await extractFrames(absolutePath, path.join(run.runDir, 'frames'), frameCount)
|
|
34
|
+
.catch(error => {
|
|
35
|
+
item.errors.push(error.message);
|
|
36
|
+
return [];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catalog.push(item);
|
|
41
|
+
}
|
|
42
|
+
const catalogPath = path.join(run.runDir, 'assets.json');
|
|
43
|
+
writeJson(catalogPath, { schemaVersion: 1, runId: run.runId, assets: catalog });
|
|
44
|
+
return { catalogPath, assets: catalog };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function classifyAsset(filePath) {
|
|
48
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
49
|
+
if (VIDEO_EXTENSIONS.has(ext)) return 'video';
|
|
50
|
+
if (IMAGE_EXTENSIONS.has(ext)) return 'image';
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function probeMedia(filePath) {
|
|
55
|
+
const { stdout } = await execFileAsync('ffprobe', [
|
|
56
|
+
'-v', 'error',
|
|
57
|
+
'-show_entries', 'format=duration,size:stream=width,height,codec_type,codec_name',
|
|
58
|
+
'-of', 'json',
|
|
59
|
+
filePath,
|
|
60
|
+
]);
|
|
61
|
+
return JSON.parse(stdout);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function extractFrames(filePath, framesDir, count) {
|
|
65
|
+
ensureDir(framesDir);
|
|
66
|
+
const outputPattern = path.join(framesDir, `${path.basename(filePath, path.extname(filePath))}_%03d.jpg`);
|
|
67
|
+
await execFileAsync('ffmpeg', [
|
|
68
|
+
'-y',
|
|
69
|
+
'-i', filePath,
|
|
70
|
+
'-vf', 'fps=1,scale=640:-1',
|
|
71
|
+
'-frames:v', String(count),
|
|
72
|
+
outputPattern,
|
|
73
|
+
]);
|
|
74
|
+
return fs.readdirSync(framesDir)
|
|
75
|
+
.filter(name => name.startsWith(path.basename(filePath, path.extname(filePath))) && name.endsWith('.jpg'))
|
|
76
|
+
.sort()
|
|
77
|
+
.map(name => path.join(framesDir, name));
|
|
78
|
+
}
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import { defaultProfileDir, defaultRunsDir, FLOW_URL, resolvePath } from './paths.mjs';
|
|
2
|
-
import { closeFlowBrowser, launchFlowBrowser } from './browser.mjs';
|
|
3
|
-
import { createRun, baseResult, saveResult } from './result.mjs';
|
|
4
|
-
import { capture, inspectFlowState, openFlow } from './flow-ui.mjs';
|
|
5
|
-
|
|
6
|
-
export async function observeFlow(options = {}) {
|
|
7
|
-
const outputDir = resolvePath(options.outputDir, defaultRunsDir());
|
|
8
|
-
const run = createRun({ outputDir }, 'observe');
|
|
9
|
-
const profileDir = resolvePath(options.profileDir, defaultProfileDir());
|
|
10
|
-
const browser = await launchFlowBrowser({
|
|
11
|
-
profileDir,
|
|
12
|
-
downloadsDir: run.downloadsDir,
|
|
13
|
-
browserChannel: options.browserChannel,
|
|
14
|
-
cdpUrl: options.cdpUrl,
|
|
15
|
-
headless: options.headless === true,
|
|
16
|
-
});
|
|
17
|
-
const result = baseResult('ok', run, { command: 'observe', flowUrl: FLOW_URL, profileDir });
|
|
18
|
-
try {
|
|
19
|
-
await openFlow(browser.page);
|
|
20
|
-
result.screenshots.push(await capture(browser.page, run, 'observe-flow'));
|
|
21
|
-
Object.assign(result, await inspectFlowState(browser.page));
|
|
22
|
-
result.controls = await collectControls(browser.page);
|
|
23
|
-
if (result.manualActions.length) result.status = 'manual_action_required';
|
|
24
|
-
} catch (error) {
|
|
25
|
-
result.status = 'failed';
|
|
26
|
-
result.errors.push(error.message);
|
|
27
|
-
} finally {
|
|
28
|
-
await closeFlowBrowser(browser);
|
|
29
|
-
saveResult(run, result);
|
|
30
|
-
}
|
|
31
|
-
return result;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function collectControls(page) {
|
|
35
|
-
return page.evaluate(() => {
|
|
36
|
-
const text = node => (node.innerText || node.textContent || node.getAttribute('aria-label') || '').trim();
|
|
37
|
-
const pick = selector => Array.from(document.querySelectorAll(selector))
|
|
38
|
-
.slice(0, 80)
|
|
39
|
-
.map(node => ({
|
|
40
|
-
tag: node.tagName.toLowerCase(),
|
|
41
|
-
text: text(node).slice(0, 120),
|
|
42
|
-
ariaLabel: node.getAttribute('aria-label') || '',
|
|
43
|
-
placeholder: node.getAttribute('placeholder') || '',
|
|
44
|
-
role: node.getAttribute('role') || '',
|
|
45
|
-
type: node.getAttribute('type') || '',
|
|
46
|
-
}))
|
|
47
|
-
.filter(item => item.text || item.ariaLabel || item.placeholder || item.role || item.type);
|
|
48
|
-
return {
|
|
49
|
-
buttons: pick('button,[role="button"]'),
|
|
50
|
-
inputs: pick('input,textarea,[contenteditable="true"],[role="textbox"]'),
|
|
51
|
-
links: pick('a'),
|
|
52
|
-
};
|
|
53
|
-
}).catch(error => ({ error: error.message }));
|
|
54
|
-
}
|
|
1
|
+
import { defaultProfileDir, defaultRunsDir, FLOW_URL, resolvePath } from './paths.mjs';
|
|
2
|
+
import { closeFlowBrowser, launchFlowBrowser } from './browser.mjs';
|
|
3
|
+
import { createRun, baseResult, saveResult } from './result.mjs';
|
|
4
|
+
import { capture, inspectFlowState, openFlow } from './flow-ui.mjs';
|
|
5
|
+
|
|
6
|
+
export async function observeFlow(options = {}) {
|
|
7
|
+
const outputDir = resolvePath(options.outputDir, defaultRunsDir());
|
|
8
|
+
const run = createRun({ outputDir }, 'observe');
|
|
9
|
+
const profileDir = resolvePath(options.profileDir, defaultProfileDir());
|
|
10
|
+
const browser = await launchFlowBrowser({
|
|
11
|
+
profileDir,
|
|
12
|
+
downloadsDir: run.downloadsDir,
|
|
13
|
+
browserChannel: options.browserChannel,
|
|
14
|
+
cdpUrl: options.cdpUrl,
|
|
15
|
+
headless: options.headless === true,
|
|
16
|
+
});
|
|
17
|
+
const result = baseResult('ok', run, { command: 'observe', flowUrl: FLOW_URL, profileDir });
|
|
18
|
+
try {
|
|
19
|
+
await openFlow(browser.page);
|
|
20
|
+
result.screenshots.push(await capture(browser.page, run, 'observe-flow'));
|
|
21
|
+
Object.assign(result, await inspectFlowState(browser.page));
|
|
22
|
+
result.controls = await collectControls(browser.page);
|
|
23
|
+
if (result.manualActions.length) result.status = 'manual_action_required';
|
|
24
|
+
} catch (error) {
|
|
25
|
+
result.status = 'failed';
|
|
26
|
+
result.errors.push(error.message);
|
|
27
|
+
} finally {
|
|
28
|
+
await closeFlowBrowser(browser);
|
|
29
|
+
saveResult(run, result);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function collectControls(page) {
|
|
35
|
+
return page.evaluate(() => {
|
|
36
|
+
const text = node => (node.innerText || node.textContent || node.getAttribute('aria-label') || '').trim();
|
|
37
|
+
const pick = selector => Array.from(document.querySelectorAll(selector))
|
|
38
|
+
.slice(0, 80)
|
|
39
|
+
.map(node => ({
|
|
40
|
+
tag: node.tagName.toLowerCase(),
|
|
41
|
+
text: text(node).slice(0, 120),
|
|
42
|
+
ariaLabel: node.getAttribute('aria-label') || '',
|
|
43
|
+
placeholder: node.getAttribute('placeholder') || '',
|
|
44
|
+
role: node.getAttribute('role') || '',
|
|
45
|
+
type: node.getAttribute('type') || '',
|
|
46
|
+
}))
|
|
47
|
+
.filter(item => item.text || item.ariaLabel || item.placeholder || item.role || item.type);
|
|
48
|
+
return {
|
|
49
|
+
buttons: pick('button,[role="button"]'),
|
|
50
|
+
inputs: pick('input,textarea,[contenteditable="true"],[role="textbox"]'),
|
|
51
|
+
links: pick('a'),
|
|
52
|
+
};
|
|
53
|
+
}).catch(error => ({ error: error.message }));
|
|
54
|
+
}
|