@mindscraft/branch-video-agent-cli 0.4.6 → 0.5.0
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/README.md +68 -6
- package/dist/commands/workbench.d.ts +2 -0
- package/dist/commands/workbench.js +305 -0
- package/dist/index.js +15 -1
- package/dist/lib/flags.d.ts +8 -0
- package/dist/lib/flags.js +42 -1
- package/dist/lib/http.d.ts +17 -0
- package/dist/lib/http.js +81 -7
- package/dist/playtest/graph.js +43 -2
- package/dist/playtest/runner.d.ts +11 -2
- package/dist/playtest/runner.js +188 -102
- package/dist/playtest/scenarios.js +3 -0
- package/package.json +1 -1
package/dist/lib/http.js
CHANGED
|
@@ -17,6 +17,21 @@ function extractRequestId(response, parsed) {
|
|
|
17
17
|
|| response.headers.get('x-ai-gateway-request-id')
|
|
18
18
|
|| (typeof parsed?.requestId === 'string' ? parsed.requestId : null);
|
|
19
19
|
}
|
|
20
|
+
function parseResponseError(parsed, response) {
|
|
21
|
+
const error = typeof parsed === 'object' && parsed ? parsed?.error : undefined;
|
|
22
|
+
const nested = typeof error === 'object' && error ? error : undefined;
|
|
23
|
+
const message = typeof nested?.message === 'string'
|
|
24
|
+
? nested.message
|
|
25
|
+
: typeof error === 'string'
|
|
26
|
+
? error
|
|
27
|
+
: response.statusText || 'Request failed';
|
|
28
|
+
const code = typeof nested?.code === 'string'
|
|
29
|
+
? nested.code
|
|
30
|
+
: typeof parsed?.code === 'string'
|
|
31
|
+
? String(parsed.code)
|
|
32
|
+
: `HTTP_${response.status}`;
|
|
33
|
+
return { message, code };
|
|
34
|
+
}
|
|
20
35
|
export class CliHttpClient {
|
|
21
36
|
baseUrl;
|
|
22
37
|
token;
|
|
@@ -61,13 +76,8 @@ export class CliHttpClient {
|
|
|
61
76
|
: null;
|
|
62
77
|
const requestId = extractRequestId(response, parsed);
|
|
63
78
|
if (!response.ok) {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
: response.statusText || 'Request failed';
|
|
67
|
-
const errorCode = typeof parsed === 'object' && parsed && typeof parsed.code === 'string'
|
|
68
|
-
? String(parsed.code)
|
|
69
|
-
: `HTTP_${response.status}`;
|
|
70
|
-
throw new CliCommandError(errorMessage, errorCode, parsed, requestId, response.status);
|
|
79
|
+
const error = parseResponseError(parsed, response);
|
|
80
|
+
throw new CliCommandError(error.message, error.code, parsed, requestId, response.status);
|
|
71
81
|
}
|
|
72
82
|
return {
|
|
73
83
|
data: parsed,
|
|
@@ -75,4 +85,68 @@ export class CliHttpClient {
|
|
|
75
85
|
requestId,
|
|
76
86
|
};
|
|
77
87
|
}
|
|
88
|
+
async requestStream(path, options) {
|
|
89
|
+
let response;
|
|
90
|
+
try {
|
|
91
|
+
response = await this.fetchImpl(buildUrl(this.baseUrl, path), {
|
|
92
|
+
method: options.method,
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: `Bearer ${this.token}`,
|
|
95
|
+
'content-type': options.contentType,
|
|
96
|
+
},
|
|
97
|
+
body: options.body,
|
|
98
|
+
duplex: 'half',
|
|
99
|
+
// A redirect to another origin must never receive the bearer token.
|
|
100
|
+
redirect: 'error',
|
|
101
|
+
signal: options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new CliCommandError(error?.message || 'Network request failed', 'NETWORK_ERROR');
|
|
106
|
+
}
|
|
107
|
+
const text = await response.text();
|
|
108
|
+
const parsed = text
|
|
109
|
+
? (() => {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(text);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return text;
|
|
115
|
+
}
|
|
116
|
+
})()
|
|
117
|
+
: null;
|
|
118
|
+
const requestId = extractRequestId(response, parsed);
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const error = parseResponseError(parsed, response);
|
|
121
|
+
throw new CliCommandError(error.message, error.code, parsed, requestId, response.status);
|
|
122
|
+
}
|
|
123
|
+
return { data: parsed, status: response.status, requestId };
|
|
124
|
+
}
|
|
125
|
+
async requestBinary(path, options) {
|
|
126
|
+
let response;
|
|
127
|
+
try {
|
|
128
|
+
response = await this.fetchImpl(buildUrl(this.baseUrl, path), {
|
|
129
|
+
method: options.method,
|
|
130
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
131
|
+
redirect: 'error',
|
|
132
|
+
signal: options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new CliCommandError(error?.message || 'Network request failed', 'NETWORK_ERROR');
|
|
137
|
+
}
|
|
138
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
139
|
+
const requestId = response.headers.get('x-request-id') || response.headers.get('x-correlation-id');
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const text = new TextDecoder().decode(bytes);
|
|
142
|
+
let parsed = text;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(text);
|
|
145
|
+
}
|
|
146
|
+
catch { /* preserve plain error body */ }
|
|
147
|
+
const error = parseResponseError(parsed, response);
|
|
148
|
+
throw new CliCommandError(error.message, error.code, parsed, requestId, response.status);
|
|
149
|
+
}
|
|
150
|
+
return { data: bytes, status: response.status, requestId, contentType: response.headers.get('content-type') };
|
|
151
|
+
}
|
|
78
152
|
}
|
package/dist/playtest/graph.js
CHANGED
|
@@ -99,6 +99,40 @@ function closedChoiceDefault(interaction) {
|
|
|
99
99
|
&& interactive.options.every((option) => typeof option.id === 'string'
|
|
100
100
|
&& (interaction.branches || []).some((branch) => branch.when === option.id && branch.then));
|
|
101
101
|
}
|
|
102
|
+
// Deliberately narrow: the image timer pauses at the first tick and every
|
|
103
|
+
// settlement result leaves this node. Ordinary resumable interactions do not qualify.
|
|
104
|
+
function settlementBlocksImageCompletion(nodeId, node, nodes) {
|
|
105
|
+
if (node.type !== 'image' || node.branchConfig || node.branches || node.events
|
|
106
|
+
|| node.config?.events || node.config?.messageHandlers
|
|
107
|
+
|| (node.config?.loop !== undefined && node.config.loop !== false)
|
|
108
|
+
|| !Array.isArray(node.config?.slides) || node.config.slides.length !== 1
|
|
109
|
+
|| !Array.isArray(node.interactions) || node.interactions.length !== 1)
|
|
110
|
+
return false;
|
|
111
|
+
const slide = node.config.slides[0];
|
|
112
|
+
if (!slide || typeof slide.id !== 'string' || !slide.id.trim()
|
|
113
|
+
|| typeof slide.assetRef !== 'string' || !slide.assetRef.trim()
|
|
114
|
+
|| (slide.duration !== undefined && (!Number.isFinite(slide.duration) || slide.duration < 0))
|
|
115
|
+
|| (slide.duration || 3) <= 0.1)
|
|
116
|
+
return false;
|
|
117
|
+
const interaction = node.interactions[0];
|
|
118
|
+
if (!interaction || interaction.interactive?.type !== 'score_settlement'
|
|
119
|
+
|| interaction.branchConfig || interaction.events
|
|
120
|
+
|| (interaction.timeLimit !== undefined && (!Number.isFinite(interaction.timeLimit) || interaction.timeLimit > 0))
|
|
121
|
+
|| !(interaction.trigger?.type === 'time' && interaction.trigger.value === 0
|
|
122
|
+
|| interaction.trigger?.type === 'slide' && interaction.trigger.index === 0 && interaction.trigger.offsetSec === 0)
|
|
123
|
+
|| !Array.isArray(interaction.branches))
|
|
124
|
+
return false;
|
|
125
|
+
const outcomes = settlementConditions(interaction.interactive);
|
|
126
|
+
if (!outcomes)
|
|
127
|
+
return false;
|
|
128
|
+
const leavesNode = (action) => action?.type === 'end'
|
|
129
|
+
|| action?.type === 'goto' && typeof action.target === 'string'
|
|
130
|
+
&& Boolean(nodes[action.target.trim()]) && action.target.trim() !== nodeId;
|
|
131
|
+
if (interaction.branches.some((branch) => !branch || branch.condition || !leavesNode(branch.then))
|
|
132
|
+
|| [interaction.fallback?.onDefault, interaction.fallback?.onTimeout].some(action => action && !leavesNode(action)))
|
|
133
|
+
return false;
|
|
134
|
+
return [...outcomes.keys()].every(result => interaction.branches.some((branch) => (typeof branch.when === 'string' && branch.when.trim().toLowerCase() === result)));
|
|
135
|
+
}
|
|
102
136
|
export function extractPlaytestEdges(script) {
|
|
103
137
|
const graph = script?.graph;
|
|
104
138
|
const nodes = graph?.nodes || {};
|
|
@@ -108,14 +142,21 @@ export function extractPlaytestEdges(script) {
|
|
|
108
142
|
if (node.type === 'start')
|
|
109
143
|
addEdge(edges, nodeId, 'node', nodeId, { type: 'enter' }, node.config?.next);
|
|
110
144
|
const avgCompletionInteraction = node.type === 'avg' && (node.interactions || []).some((interaction) => interaction.trigger?.type === 'complete');
|
|
111
|
-
|
|
145
|
+
const mediaCompletion = node.type === 'video' || node.type === 'image' || node.type === 'avg';
|
|
146
|
+
if (mediaCompletion && !node.branchConfig && !avgCompletionInteraction) {
|
|
112
147
|
addEdge(edges, nodeId, 'node', nodeId, { type: 'complete' }, node.config?.onComplete);
|
|
148
|
+
if (settlementBlocksImageCompletion(nodeId, node, nodes)) {
|
|
149
|
+
const completion = edges.at(-1);
|
|
150
|
+
if (completion?.from === nodeId && completion.owner === 'node' && completion.trigger.type === 'complete') {
|
|
151
|
+
completion.unreachableReason = 'Immediate image settlement pauses completion and every result leaves the node';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
113
154
|
}
|
|
114
155
|
// Completion interactions preempt completion routing, not enter/message rules.
|
|
115
156
|
const nodeBranchConfig = avgCompletionInteraction && node.branchConfig
|
|
116
157
|
? { ...node.branchConfig, rules: (node.branchConfig.rules || []).filter((rule) => rule.trigger?.type !== 'complete'), defaultAction: undefined }
|
|
117
158
|
: node.branchConfig;
|
|
118
|
-
addBranchConfigEdges(edges, nodeId, 'node', nodeId, nodeBranchConfig,
|
|
159
|
+
addBranchConfigEdges(edges, nodeId, 'node', nodeId, nodeBranchConfig, mediaCompletion ? 'complete' : 'default');
|
|
119
160
|
for (const handler of node.config?.messageHandlers || []) {
|
|
120
161
|
addEdge(edges, nodeId, 'node', nodeId, handler.when || { type: 'message' }, handler.then, false, {
|
|
121
162
|
variableActions: handler.variableActions,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Frame } from 'playwright';
|
|
2
|
-
import type { PlaytestContract, PlaytestLimits, PlaytestScenario, PlaytestScenarioResult } from './types.js';
|
|
1
|
+
import { type ElementHandle, type Frame } from 'playwright';
|
|
2
|
+
import type { PlaytestContract, PlaytestEdge, PlaytestLimits, PlaytestScenario, PlaytestScenarioResult } from './types.js';
|
|
3
3
|
type ScriptRecord = Record<string, any>;
|
|
4
4
|
interface RunnerOptions {
|
|
5
5
|
script: ScriptRecord;
|
|
@@ -15,8 +15,17 @@ interface RunnerOptions {
|
|
|
15
15
|
height: number;
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
interface VerifiedMedia {
|
|
19
|
+
element: ElementHandle;
|
|
20
|
+
source: string;
|
|
21
|
+
nodeId: string;
|
|
22
|
+
}
|
|
18
23
|
export declare function startPlayback(frame: Frame, entryNodeId: string, entryNodeType: string, timeout: number): Promise<void>;
|
|
24
|
+
export declare function assertNodeMedia(frame: Frame, node: ScriptRecord, deadline: number): Promise<VerifiedMedia | undefined>;
|
|
25
|
+
export declare function driveVerifiedVideo(media: VerifiedMedia | undefined, trigger: 'time' | 'complete' | 'complete-interaction', time?: number): Promise<void>;
|
|
26
|
+
export declare function executeWebEdge(frame: Frame, node: ScriptRecord, edge: PlaytestEdge, contract: PlaytestContract, deadline: number): Promise<void>;
|
|
19
27
|
export declare function completeAvgPlayback(frame: Frame, nodeId: string, deadline: number): Promise<void>;
|
|
28
|
+
export declare function executeEdge(frame: Frame, edge: PlaytestEdge, options: RunnerOptions, deadline: number): Promise<void>;
|
|
20
29
|
export declare function runPlaytestScenarios(options: RunnerOptions): Promise<PlaytestScenarioResult[]>;
|
|
21
30
|
export declare function createDraftPlayerUrl(baseUrl: string): {
|
|
22
31
|
playerUrl: string;
|