takomi 2.1.37 → 2.1.39
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 +32 -0
- package/.pi/agents/architect.md +4 -2
- package/.pi/agents/designer.md +2 -0
- package/.pi/agents/orchestrator.md +4 -0
- package/.pi/prompts/design-prompt.md +3 -1
- package/.pi/prompts/genesis-prompt.md +2 -0
- package/.pi/prompts/takomi-prompt.md +6 -3
- package/.pi/takomi/policies/subagent-routing.md +9 -1
- package/.pi/takomi/policies/takomi-lifecycle-routing.md +3 -1
- package/README.md +27 -5
- package/assets/.agent/skills/takomi-flow/SKILL.md +235 -0
- package/assets/.agent/workflows/README.md +3 -3
- package/assets/.agent/workflows/mode-architect.md +7 -5
- package/assets/.agent/workflows/vibe-design.md +3 -1
- package/assets/.agent/workflows/vibe-genesis.md +2 -0
- package/assets/Takomi-Agents/custom_modes.yaml +1253 -1252
- package/assets/Takomi-Agents/vibe-architect.yaml +11 -6
- package/assets/Takomi-Agents/vibe-orchestrator.yaml +2 -2
- package/docs/features/TakomiFlow_Portable_Plugin.md +78 -0
- package/docs/takomi-flow-onboarding.md +76 -0
- package/docs/takomi-flow-public-distribution.md +94 -0
- package/package.json +19 -2
- package/plugins/takomi-codex/.codex-plugin/plugin.json +37 -0
- package/plugins/takomi-codex/assets/composer-icon.png +0 -0
- package/plugins/takomi-codex/assets/logo-dark.png +0 -0
- package/plugins/takomi-codex/assets/logo.png +0 -0
- package/plugins/takomi-codex/scripts/takomi-board.ps1 +197 -0
- package/plugins/takomi-codex/scripts/takomi-detect.ps1 +125 -0
- package/plugins/takomi-codex/scripts/takomi-doctor.ps1 +88 -0
- package/plugins/takomi-codex/scripts/takomi-harness.ps1 +52 -0
- package/plugins/takomi-codex/scripts/takomi-pi-dispatch.ps1 +93 -0
- package/plugins/takomi-codex/scripts/takomi-policy.ps1 +108 -0
- package/plugins/takomi-codex/skills/takomi-codex/SKILL.md +194 -0
- package/plugins/takomi-flow/.codex-plugin/plugin.json +36 -0
- package/plugins/takomi-flow/.mcp.json +11 -0
- package/plugins/takomi-flow/assets/capabilities.json +171 -0
- package/plugins/takomi-flow/assets/collection.schema.json +29 -0
- package/plugins/takomi-flow/assets/composer-icon.png +0 -0
- package/plugins/takomi-flow/assets/examples.json +94 -0
- package/plugins/takomi-flow/assets/logo-dark.png +0 -0
- package/plugins/takomi-flow/assets/logo.png +0 -0
- package/plugins/takomi-flow/assets/request.schema.json +67 -0
- package/plugins/takomi-flow/assets/result.schema.json +45 -0
- package/plugins/takomi-flow/assets/templates/image-request.json +13 -0
- package/plugins/takomi-flow/assets/templates/video-request.json +15 -0
- package/plugins/takomi-flow/package.json +38 -0
- package/plugins/takomi-flow/pnpm-lock.yaml +813 -0
- package/plugins/takomi-flow/references/flow-provider-contract.md +326 -0
- package/plugins/takomi-flow/scripts/lib/agent-plan.mjs +130 -0
- package/plugins/takomi-flow/scripts/lib/api.mjs +113 -0
- package/plugins/takomi-flow/scripts/lib/args.mjs +57 -0
- package/plugins/takomi-flow/scripts/lib/audit.mjs +65 -0
- package/plugins/takomi-flow/scripts/lib/browser.mjs +39 -0
- package/plugins/takomi-flow/scripts/lib/capabilities.mjs +11 -0
- package/plugins/takomi-flow/scripts/lib/collect.mjs +53 -0
- package/plugins/takomi-flow/scripts/lib/commands.mjs +175 -0
- package/plugins/takomi-flow/scripts/lib/doctor.mjs +77 -0
- package/plugins/takomi-flow/scripts/lib/examples.mjs +17 -0
- package/plugins/takomi-flow/scripts/lib/flow-outcome.mjs +90 -0
- package/plugins/takomi-flow/scripts/lib/flow-ui.mjs +165 -0
- package/plugins/takomi-flow/scripts/lib/generation.mjs +90 -0
- package/plugins/takomi-flow/scripts/lib/inspect.mjs +81 -0
- package/plugins/takomi-flow/scripts/lib/mcp-prompts.mjs +39 -0
- package/plugins/takomi-flow/scripts/lib/mcp-resources.mjs +16 -0
- package/plugins/takomi-flow/scripts/lib/mcp-tools.mjs +150 -0
- package/plugins/takomi-flow/scripts/lib/media.mjs +78 -0
- package/plugins/takomi-flow/scripts/lib/observe.mjs +54 -0
- package/plugins/takomi-flow/scripts/lib/paths.mjs +37 -0
- package/plugins/takomi-flow/scripts/lib/playwright-loader.mjs +23 -0
- package/plugins/takomi-flow/scripts/lib/prompts.mjs +72 -0
- package/plugins/takomi-flow/scripts/lib/report.mjs +141 -0
- package/plugins/takomi-flow/scripts/lib/request-validator.mjs +58 -0
- package/plugins/takomi-flow/scripts/lib/request.mjs +55 -0
- package/plugins/takomi-flow/scripts/lib/resource-files.mjs +69 -0
- package/plugins/takomi-flow/scripts/lib/result.mjs +31 -0
- package/plugins/takomi-flow/scripts/lib/review.mjs +44 -0
- package/plugins/takomi-flow/scripts/lib/selftest.mjs +153 -0
- package/plugins/takomi-flow/scripts/lib/settings-plan.mjs +39 -0
- package/plugins/takomi-flow/scripts/lib/templates.mjs +37 -0
- package/plugins/takomi-flow/scripts/lib/trusted-chrome.mjs +67 -0
- package/plugins/takomi-flow/scripts/lib/workflow.mjs +56 -0
- package/plugins/takomi-flow/scripts/mcp-server.mjs +18 -0
- package/plugins/takomi-flow/scripts/mcp-smoke.mjs +142 -0
- package/plugins/takomi-flow/scripts/takomi-flow.mjs +17 -0
- package/plugins/takomi-flow/skills/takomi-flow/SKILL.md +235 -0
- package/plugins/takomi-flow/skills/takomi-flow/agents/openai.yaml +7 -0
- package/scripts/install-takomi-flow.ps1 +85 -0
- package/src/pi-takomi-core/orchestration.ts +1 -1
- package/src/pi-takomi-core/workflows.ts +4 -4
- package/src/skills-catalog.js +11 -7
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { defaultProfileDir, FLOW_URL, readJson, resolvePath } from './paths.mjs';
|
|
3
|
+
import { normalizeRequest, canSpend } from './request.mjs';
|
|
4
|
+
import { closeFlowBrowser, launchFlowBrowser } from './browser.mjs';
|
|
5
|
+
import { createRun, baseResult, saveResult } from './result.mjs';
|
|
6
|
+
import { capture, ensureProjectEditor, fillPrompt, inspectFlowState, openFlow, submitGeneration, tryDownloadAssets } from './flow-ui.mjs';
|
|
7
|
+
import { handleGenerationFollowups, waitForGenerationOutcome } from './flow-outcome.mjs';
|
|
8
|
+
import { catalogAssets } from './media.mjs';
|
|
9
|
+
import { createSettingsPlan } from './settings-plan.mjs';
|
|
10
|
+
|
|
11
|
+
export async function generateFromRequest(args = {}) {
|
|
12
|
+
if (!args.request) throw new Error('generate requires a request path.');
|
|
13
|
+
const request = normalizeRequest(readJson(path.resolve(args.request)));
|
|
14
|
+
const run = createRun(request, 'generate');
|
|
15
|
+
const result = baseResult('blocked', run, {
|
|
16
|
+
command: 'generate',
|
|
17
|
+
kind: request.kind,
|
|
18
|
+
prompt: request.prompt,
|
|
19
|
+
flowUrl: FLOW_URL,
|
|
20
|
+
settingsPlan: createSettingsPlan(request),
|
|
21
|
+
});
|
|
22
|
+
if (!canSpend(request)) {
|
|
23
|
+
result.manualActions.push('Set request allowSpend=true or TAKOMI_FLOW_ALLOW_SPEND=true to submit a Flow generation.');
|
|
24
|
+
saveResult(run, result);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
const browser = await launchFlowBrowser({
|
|
28
|
+
profileDir: resolvePath(args.profileDir, defaultProfileDir()),
|
|
29
|
+
downloadsDir: run.downloadsDir,
|
|
30
|
+
browserChannel: args.browserChannel,
|
|
31
|
+
cdpUrl: args.cdpUrl,
|
|
32
|
+
headless: Boolean(args.headless),
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
await openFlow(browser.page);
|
|
36
|
+
result.screenshots.push(await capture(browser.page, run, 'before-prompt'));
|
|
37
|
+
const state = await inspectFlowState(browser.page);
|
|
38
|
+
if (state.manualActions.length) {
|
|
39
|
+
requireManual(result, state.manualActions);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
if (!(await ensureProjectEditor(browser.page))) {
|
|
43
|
+
requireManual(result, ['New project button or project editor was not found. Open a Flow project manually, then retry with --cdp-url.']);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
result.projectUrl = browser.page.url();
|
|
47
|
+
result.screenshots.push(await capture(browser.page, run, 'project-editor'));
|
|
48
|
+
if (!(await fillPrompt(browser.page, request.prompt))) {
|
|
49
|
+
requireManual(result, ['Prompt box was not found. Update Flow selectors or paste the prompt manually.']);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
result.screenshots.push(await capture(browser.page, run, 'prompt-filled'));
|
|
53
|
+
if (!(await submitGeneration(browser.page))) {
|
|
54
|
+
requireManual(result, ['Generate/Create button was not found. Submit manually in the opened browser.']);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
result.projectUrl = browser.page.url();
|
|
58
|
+
await handleGenerationFollowups(browser.page, request);
|
|
59
|
+
result.generationOutcome = await waitForGenerationOutcome(browser.page, request);
|
|
60
|
+
result.projectUrl = browser.page.url();
|
|
61
|
+
result.screenshots.push(await capture(browser.page, run, 'after-wait'));
|
|
62
|
+
result.assets = await tryDownloadAssets(browser.page, run, request.variations);
|
|
63
|
+
if (result.assets.length) {
|
|
64
|
+
result.assetCatalogPath = (await catalogAssets(run, result.assets, { frames: request.extractFrames || 0 })).catalogPath;
|
|
65
|
+
}
|
|
66
|
+
result.status = result.assets.length ? 'downloaded' : 'manual_action_required';
|
|
67
|
+
if (!result.assets.length) {
|
|
68
|
+
result.manualActions.push(nextActionForOutcome(result.generationOutcome));
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
result.status = 'failed';
|
|
73
|
+
result.errors.push(error.message);
|
|
74
|
+
return result;
|
|
75
|
+
} finally {
|
|
76
|
+
await closeFlowBrowser(browser);
|
|
77
|
+
saveResult(run, result);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function requireManual(result, actions) {
|
|
82
|
+
result.status = 'manual_action_required';
|
|
83
|
+
result.manualActions.push(...actions);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function nextActionForOutcome(outcome = {}) {
|
|
87
|
+
if (outcome.status === 'failed') return 'Flow reported generation failure. Inspect the browser/run screenshots.';
|
|
88
|
+
if (outcome.status === 'timeout') return 'Generation did not finish before the wait timeout. Leave trusted Chrome open and run inspect/observe again.';
|
|
89
|
+
return 'Generation may still be running or download controls changed. Inspect the browser/run screenshots.';
|
|
90
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { defaultRunsDir, readJson } from './paths.mjs';
|
|
4
|
+
|
|
5
|
+
export function inspectRun(target) {
|
|
6
|
+
const runPath = resolveRunJson(target);
|
|
7
|
+
const data = readJson(runPath);
|
|
8
|
+
return summarize(data, runPath);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function latestRun(outputDir = defaultRunsDir()) {
|
|
12
|
+
const root = path.resolve(outputDir);
|
|
13
|
+
if (!fs.existsSync(root)) {
|
|
14
|
+
throw new Error(`Output directory does not exist: ${root}`);
|
|
15
|
+
}
|
|
16
|
+
const candidates = fs.readdirSync(root, { withFileTypes: true })
|
|
17
|
+
.filter(entry => entry.isDirectory())
|
|
18
|
+
.map(entry => path.join(root, entry.name, 'run.json'))
|
|
19
|
+
.filter(filePath => fs.existsSync(filePath))
|
|
20
|
+
.sort();
|
|
21
|
+
if (!candidates.length) {
|
|
22
|
+
throw new Error(`No TakomiFlow runs found in: ${root}`);
|
|
23
|
+
}
|
|
24
|
+
return summarize(readJson(candidates[candidates.length - 1]), candidates[candidates.length - 1]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function listRuns(outputDir = defaultRunsDir(), limit = 20) {
|
|
28
|
+
const root = path.resolve(outputDir);
|
|
29
|
+
if (!fs.existsSync(root)) {
|
|
30
|
+
return { schemaVersion: 1, outputDir: root, runs: [] };
|
|
31
|
+
}
|
|
32
|
+
const runFiles = fs.readdirSync(root, { withFileTypes: true })
|
|
33
|
+
.filter(entry => entry.isDirectory())
|
|
34
|
+
.map(entry => path.join(root, entry.name, 'run.json'))
|
|
35
|
+
.filter(filePath => fs.existsSync(filePath))
|
|
36
|
+
.sort()
|
|
37
|
+
.reverse()
|
|
38
|
+
.slice(0, Math.max(1, Number.parseInt(limit || '20', 10) || 20));
|
|
39
|
+
return {
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
outputDir: root,
|
|
42
|
+
runs: runFiles.map(filePath => summarize(readJson(filePath), filePath)),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveRunJson(target) {
|
|
47
|
+
if (!target) throw new Error('inspect requires --run <run.json|run-dir>');
|
|
48
|
+
const resolved = path.resolve(target);
|
|
49
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
50
|
+
return path.join(resolved, 'run.json');
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveRun(target) {
|
|
56
|
+
const runJsonPath = resolveRunJson(target);
|
|
57
|
+
const data = readJson(runJsonPath);
|
|
58
|
+
return {
|
|
59
|
+
runId: data.runId || path.basename(path.dirname(runJsonPath)),
|
|
60
|
+
runDir: path.dirname(runJsonPath),
|
|
61
|
+
downloadsDir: path.join(path.dirname(runJsonPath), 'downloads'),
|
|
62
|
+
screenshotsDir: path.join(path.dirname(runJsonPath), 'screenshots'),
|
|
63
|
+
data,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function summarize(data, runJsonPath) {
|
|
68
|
+
return {
|
|
69
|
+
status: data.status,
|
|
70
|
+
runId: data.runId,
|
|
71
|
+
command: data.command,
|
|
72
|
+
kind: data.kind,
|
|
73
|
+
projectUrl: data.projectUrl,
|
|
74
|
+
flowUrl: data.flowUrl,
|
|
75
|
+
assets: data.assets || [],
|
|
76
|
+
screenshots: data.screenshots || [],
|
|
77
|
+
errors: data.errors || [],
|
|
78
|
+
manualActions: data.manualActions || [],
|
|
79
|
+
metadataPath: data.metadataPath || runJsonPath,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { collectPrompt, imagePrompt, reviewPrompt, videoPrompt } from './prompts.mjs';
|
|
3
|
+
|
|
4
|
+
export function registerPrompts(server) {
|
|
5
|
+
server.registerPrompt('takomi_flow_video_workflow', {
|
|
6
|
+
description: 'Guide an agent through a safe TakomiFlow video generation workflow.',
|
|
7
|
+
argsSchema: {
|
|
8
|
+
topic: z.string().optional(),
|
|
9
|
+
aspectRatio: z.string().optional(),
|
|
10
|
+
variations: z.string().optional(),
|
|
11
|
+
},
|
|
12
|
+
}, args => videoPrompt(args));
|
|
13
|
+
|
|
14
|
+
server.registerPrompt('takomi_flow_image_workflow', {
|
|
15
|
+
description: 'Guide an agent through a safe TakomiFlow image generation workflow.',
|
|
16
|
+
argsSchema: {
|
|
17
|
+
topic: z.string().optional(),
|
|
18
|
+
aspectRatio: z.string().optional(),
|
|
19
|
+
variations: z.string().optional(),
|
|
20
|
+
},
|
|
21
|
+
}, args => imagePrompt(args));
|
|
22
|
+
|
|
23
|
+
server.registerPrompt('takomi_flow_review_workflow', {
|
|
24
|
+
description: 'Guide an agent through reviewing TakomiFlow run results and assets.',
|
|
25
|
+
argsSchema: {
|
|
26
|
+
run: z.string().optional(),
|
|
27
|
+
frames: z.string().optional(),
|
|
28
|
+
},
|
|
29
|
+
}, args => reviewPrompt(args));
|
|
30
|
+
|
|
31
|
+
server.registerPrompt('takomi_flow_collect_workflow', {
|
|
32
|
+
description: 'Guide an agent through collecting reviewed TakomiFlow outputs into a downstream folder.',
|
|
33
|
+
argsSchema: {
|
|
34
|
+
run: z.string().optional(),
|
|
35
|
+
targetDir: z.string().optional(),
|
|
36
|
+
includeFrames: z.string().optional(),
|
|
37
|
+
},
|
|
38
|
+
}, args => collectPrompt(args));
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { RESOURCES, readResourceFile } from './resource-files.mjs';
|
|
2
|
+
|
|
3
|
+
export function registerResources(server) {
|
|
4
|
+
for (const resource of RESOURCES) {
|
|
5
|
+
server.registerResource(resource.name, resource.uri, {
|
|
6
|
+
mimeType: resource.mimeType,
|
|
7
|
+
description: resource.description,
|
|
8
|
+
}, async () => ({
|
|
9
|
+
contents: [{
|
|
10
|
+
uri: resource.uri,
|
|
11
|
+
mimeType: resource.mimeType,
|
|
12
|
+
text: readResourceFile(resource.file),
|
|
13
|
+
}],
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,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
|
+
submit: z.boolean().optional(),
|
|
32
|
+
targetDir: z.string().optional(),
|
|
33
|
+
}, args => api.plan(args));
|
|
34
|
+
registerRequestTools(register);
|
|
35
|
+
registerBrowserTools(register);
|
|
36
|
+
registerResultTools(register);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function registerRequestTools(register) {
|
|
40
|
+
register('takomi_flow_template', 'Return or write a video/image request template.', {
|
|
41
|
+
kind: z.enum(['video', 'image']).optional(),
|
|
42
|
+
outputDir: z.string().optional(),
|
|
43
|
+
}, args => api.template(args));
|
|
44
|
+
const requestShape = {
|
|
45
|
+
kind: z.enum(['video', 'image']),
|
|
46
|
+
prompt: z.string(),
|
|
47
|
+
variations: z.number().int().positive().optional(),
|
|
48
|
+
aspectRatio: z.string().optional(),
|
|
49
|
+
durationSeconds: z.number().positive().optional(),
|
|
50
|
+
mode: z.string().optional(),
|
|
51
|
+
modelHint: z.string().optional(),
|
|
52
|
+
outputDir: z.string().optional(),
|
|
53
|
+
allowSpend: z.boolean().optional(),
|
|
54
|
+
extractFrames: z.number().int().nonnegative().optional(),
|
|
55
|
+
sourceAssets: z.array(z.string()).optional(),
|
|
56
|
+
notes: z.string().optional(),
|
|
57
|
+
};
|
|
58
|
+
register('takomi_flow_prepare', 'Create a prepared request JSON file for a Flow image or video generation.', requestShape, args => api.prepare(args));
|
|
59
|
+
register('takomi_flow_workflow', 'Prepare, validate, and optionally submit a Flow image/video generation from one agent call.', {
|
|
60
|
+
...requestShape,
|
|
61
|
+
submit: z.boolean().optional(),
|
|
62
|
+
allowBrowser: z.boolean().optional(),
|
|
63
|
+
profileDir: z.string().optional(),
|
|
64
|
+
browserChannel: z.string().optional(),
|
|
65
|
+
cdpUrl: z.string().optional(),
|
|
66
|
+
headless: z.boolean().optional(),
|
|
67
|
+
}, args => api.workflow(args));
|
|
68
|
+
register('takomi_flow_validate', 'Validate a TakomiFlow request JSON before opening Flow or spending credits.', {
|
|
69
|
+
request: z.string(),
|
|
70
|
+
}, args => api.validate(args));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function registerBrowserTools(register) {
|
|
74
|
+
register('takomi_flow_observe', 'Open Flow and observe UI controls/screenshots. Requires allowBrowser=true.', {
|
|
75
|
+
allowBrowser: z.boolean(),
|
|
76
|
+
profileDir: z.string().optional(),
|
|
77
|
+
outputDir: z.string().optional(),
|
|
78
|
+
browserChannel: z.string().optional(),
|
|
79
|
+
cdpUrl: z.string().optional(),
|
|
80
|
+
headless: z.boolean().optional(),
|
|
81
|
+
}, args => requireBrowser(args, () => api.observe(args)));
|
|
82
|
+
register('takomi_flow_generate', 'Run guarded Flow generation from a request file. Requires allowBrowser=true and request allowSpend=true to submit.', {
|
|
83
|
+
allowBrowser: z.boolean(),
|
|
84
|
+
request: z.string(),
|
|
85
|
+
profileDir: z.string().optional(),
|
|
86
|
+
browserChannel: z.string().optional(),
|
|
87
|
+
cdpUrl: z.string().optional(),
|
|
88
|
+
headless: z.boolean().optional(),
|
|
89
|
+
}, args => requireBrowser(args, () => api.generate(args)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function registerResultTools(register) {
|
|
93
|
+
register('takomi_flow_selftest', 'Run deterministic no-spend TakomiFlow checks, including media catalog/frame extraction.', {
|
|
94
|
+
outputDir: z.string().optional(),
|
|
95
|
+
}, args => api.selftest(args));
|
|
96
|
+
register('takomi_flow_inspect', 'Summarize a TakomiFlow run.json or run directory.', { run: z.string() }, args => api.inspect(args));
|
|
97
|
+
register('takomi_flow_latest', 'Summarize the latest TakomiFlow run under an output directory.', {
|
|
98
|
+
outputDir: z.string().optional(),
|
|
99
|
+
}, args => api.latest(args));
|
|
100
|
+
register('takomi_flow_runs', 'List recent TakomiFlow runs under an output directory.', {
|
|
101
|
+
outputDir: z.string().optional(),
|
|
102
|
+
limit: z.number().int().positive().optional(),
|
|
103
|
+
}, args => api.runs(args));
|
|
104
|
+
register('takomi_flow_assets', 'Catalog downloaded run assets and optionally extract video review frames.', {
|
|
105
|
+
run: z.string(),
|
|
106
|
+
frames: z.number().int().nonnegative().optional(),
|
|
107
|
+
}, args => api.assets(args));
|
|
108
|
+
register('takomi_flow_review', 'Inspect a run, catalog assets, extract review frames, and write a Markdown handoff.', {
|
|
109
|
+
run: z.string(),
|
|
110
|
+
frames: z.number().int().nonnegative().optional(),
|
|
111
|
+
reportPath: z.string().optional(),
|
|
112
|
+
}, args => api.review(args));
|
|
113
|
+
register('takomi_flow_collect', 'Copy reviewed assets, frames, report, and manifest into a downstream target folder.', {
|
|
114
|
+
run: z.string(),
|
|
115
|
+
targetDir: z.string(),
|
|
116
|
+
frames: z.number().int().nonnegative().optional(),
|
|
117
|
+
includeFrames: z.boolean().optional(),
|
|
118
|
+
reportPath: z.string().optional(),
|
|
119
|
+
}, args => api.collect(args));
|
|
120
|
+
register('takomi_flow_report', 'Create a Markdown report for a run or recent TakomiFlow history.', {
|
|
121
|
+
run: z.string().optional(),
|
|
122
|
+
outputDir: z.string().optional(),
|
|
123
|
+
limit: z.number().int().positive().optional(),
|
|
124
|
+
reportPath: z.string().optional(),
|
|
125
|
+
}, args => api.report(args));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toolRegister(server) {
|
|
129
|
+
return (name, description, inputSchema, handler) => {
|
|
130
|
+
server.registerTool(name, { description, inputSchema }, async args => {
|
|
131
|
+
const structuredContent = await handler(args || {});
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: 'text', text: JSON.stringify(structuredContent, null, 2) }],
|
|
134
|
+
structuredContent,
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function requireBrowser(args, handler) {
|
|
141
|
+
if (!args?.allowBrowser) {
|
|
142
|
+
return {
|
|
143
|
+
schemaVersion: 1,
|
|
144
|
+
status: 'blocked',
|
|
145
|
+
errors: [],
|
|
146
|
+
manualActions: ['Set allowBrowser=true to permit TakomiFlow to open or drive a browser.'],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return handler();
|
|
150
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
export const FLOW_URL = process.env.TAKOMI_FLOW_URL || 'https://labs.google/fx/tools/flow';
|
|
6
|
+
|
|
7
|
+
export function defaultProfileDir() {
|
|
8
|
+
return path.join(os.homedir(), '.takomi-flow', 'browser-profile');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function defaultRunsDir() {
|
|
12
|
+
return path.join(os.homedir(), '.takomi-flow', 'runs');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function timestampId(label = 'flow') {
|
|
16
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace('.', '-').replace('T', '-').replace('Z', '');
|
|
17
|
+
return `${stamp}-${label}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ensureDir(dirPath) {
|
|
21
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
22
|
+
return dirPath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolvePath(value, fallback) {
|
|
26
|
+
return path.resolve(value || fallback);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeJson(filePath, data) {
|
|
30
|
+
ensureDir(path.dirname(filePath));
|
|
31
|
+
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
|
|
32
|
+
return filePath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readJson(filePath) {
|
|
36
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
37
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function loadPlaywright() {
|
|
5
|
+
try {
|
|
6
|
+
return await import('playwright');
|
|
7
|
+
} catch (firstError) {
|
|
8
|
+
try {
|
|
9
|
+
const requireFromCwd = createRequire(path.join(process.cwd(), 'package.json'));
|
|
10
|
+
return requireFromCwd('playwright');
|
|
11
|
+
} catch {
|
|
12
|
+
const message = [
|
|
13
|
+
'Playwright is required for TakomiFlow browser automation.',
|
|
14
|
+
'Install it in the plugin folder with:',
|
|
15
|
+
' cd C:\\Users\\johno\\plugins\\takomi-flow',
|
|
16
|
+
' pnpm install',
|
|
17
|
+
'',
|
|
18
|
+
`Original import error: ${firstError.message}`,
|
|
19
|
+
].join('\n');
|
|
20
|
+
throw new Error(message);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function videoPrompt(args = {}) {
|
|
2
|
+
const variations = normalizeVariations(args.variations);
|
|
3
|
+
return promptResult('TakomiFlow video workflow', [
|
|
4
|
+
'Use TakomiFlow to prepare a Google Flow video request.',
|
|
5
|
+
`Topic or prompt: ${args.topic || '<ask the user for the video prompt>'}`,
|
|
6
|
+
`Aspect ratio: ${args.aspectRatio || '16:9'}`,
|
|
7
|
+
`Variations: ${variations}`,
|
|
8
|
+
'Read takomi-flow://contract and takomi-flow://templates/video if available.',
|
|
9
|
+
'Run capabilities, prepare, validate, then generate only with explicit browser and spend approval.',
|
|
10
|
+
'After generation, inspect the run and catalog assets with frame extraction.',
|
|
11
|
+
]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function imagePrompt(args = {}) {
|
|
15
|
+
const variations = normalizeVariations(args.variations);
|
|
16
|
+
return promptResult('TakomiFlow image workflow', [
|
|
17
|
+
'Use TakomiFlow to prepare a Google Flow image request.',
|
|
18
|
+
`Topic or prompt: ${args.topic || '<ask the user for the image prompt>'}`,
|
|
19
|
+
`Aspect ratio: ${args.aspectRatio || '1:1'}`,
|
|
20
|
+
`Variations: ${variations}`,
|
|
21
|
+
'Read takomi-flow://contract and takomi-flow://templates/image if available.',
|
|
22
|
+
'Run capabilities, prepare, validate, then generate only with explicit browser and spend approval.',
|
|
23
|
+
'After generation, inspect the run and catalog downloaded assets.',
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function reviewPrompt(args = {}) {
|
|
28
|
+
return promptResult('TakomiFlow review workflow', [
|
|
29
|
+
'Use TakomiFlow to review a completed or partial Flow run.',
|
|
30
|
+
`Run path: ${args.run || '<ask for the run.json path or run directory>'}`,
|
|
31
|
+
`Review frames: ${normalizeFrames(args.frames)}`,
|
|
32
|
+
'Run inspect or latest if the user did not provide a run path.',
|
|
33
|
+
'Run review with frame extraction to produce assets.json, report.md, and next actions.',
|
|
34
|
+
'If downloaded assets are present and a downstream project needs them, run collect after review.',
|
|
35
|
+
'Report run status, errors, manual actions, asset paths, frame paths, and reportPath.',
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function collectPrompt(args = {}) {
|
|
40
|
+
return promptResult('TakomiFlow collect workflow', [
|
|
41
|
+
'Use TakomiFlow to package reviewed Flow outputs for another project or pipeline.',
|
|
42
|
+
`Run path: ${args.run || '<ask for the run.json path or run directory>'}`,
|
|
43
|
+
`Target folder: ${args.targetDir || '<ask for the downstream target folder>'}`,
|
|
44
|
+
`Include frames: ${args.includeFrames === 'false' ? 'false' : 'true'}`,
|
|
45
|
+
'Run review first when asset quality or errors are unknown.',
|
|
46
|
+
'Run collect with a target directory and include frames when video review frames are useful downstream.',
|
|
47
|
+
'Return the copied asset paths, copied frame paths, report path, and takomi-flow-collection.json manifest path.',
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeVariations(value) {
|
|
52
|
+
const parsed = Number.parseInt(value || '1', 10);
|
|
53
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeFrames(value) {
|
|
57
|
+
const parsed = Number.parseInt(value || '4', 10);
|
|
58
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 4;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function promptResult(description, lines) {
|
|
62
|
+
return {
|
|
63
|
+
description,
|
|
64
|
+
messages: [{
|
|
65
|
+
role: 'user',
|
|
66
|
+
content: {
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: lines.join('\n'),
|
|
69
|
+
},
|
|
70
|
+
}],
|
|
71
|
+
};
|
|
72
|
+
}
|