@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/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 errorMessage = typeof parsed === 'object' && parsed && typeof parsed.error === 'string'
65
- ? String(parsed.error)
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
  }
@@ -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
- if (node.type === 'video' || node.type === 'image' || (node.type === 'avg' && !node.branchConfig && !avgCompletionInteraction)) {
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, node.type === 'avg' ? 'complete' : 'default');
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;