autokap 1.8.8 → 1.9.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/cli-contract.d.ts +2 -0
- package/dist/cli-runner.d.ts +8 -0
- package/dist/cli-runner.js +46 -26
- package/dist/cli.js +13 -0
- package/dist/execution-schema.d.ts +2 -2
- package/dist/execution-schema.js +7 -1
- package/dist/execution-types.d.ts +20 -2
- package/dist/log-collector.d.ts +5 -1
- package/dist/log-collector.js +2 -1
- package/dist/login-detection.d.ts +52 -0
- package/dist/login-detection.js +126 -0
- package/dist/mockup.d.ts +13 -0
- package/dist/mockup.js +12 -0
- package/dist/opcode-actions.js +10 -9
- package/dist/opcode-runner.js +8 -0
- package/dist/program-signing.d.ts +1 -1
- package/dist/skill-packaging.js +1 -0
- package/dist/video-narration-schema.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli-contract.d.ts
CHANGED
|
@@ -117,6 +117,8 @@ export type ArtifactUploadObjectId = "screenshotRaw" | "screenshot" | "clipGif"
|
|
|
117
117
|
export interface ArtifactUploadMetadata {
|
|
118
118
|
presetId: string;
|
|
119
119
|
runId: string;
|
|
120
|
+
/** Per-invocation session id grouping every charge of one CLI run (migration 267). */
|
|
121
|
+
sessionId?: string | null;
|
|
120
122
|
variantId: string;
|
|
121
123
|
targetId?: string | null;
|
|
122
124
|
targetLabel?: string | null;
|
package/dist/cli-runner.d.ts
CHANGED
|
@@ -49,6 +49,13 @@ export interface CLIRunnerOptions {
|
|
|
49
49
|
* When `false`, skips the failed-run debug logs export (AUT-149).
|
|
50
50
|
*/
|
|
51
51
|
exportDebugLogs?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Shared invocation id for billing. A multi-preset invocation (`run --outdated`,
|
|
54
|
+
* `auto-recapture`) passes one sessionId for every preset so all their charges
|
|
55
|
+
* group into a single "CLI capture" entry (migration 267). Defaults to the
|
|
56
|
+
* run's own id for a single-preset invocation.
|
|
57
|
+
*/
|
|
58
|
+
sessionId?: string;
|
|
52
59
|
}
|
|
53
60
|
export interface CLIRunResult {
|
|
54
61
|
success: boolean;
|
|
@@ -58,3 +65,4 @@ export interface CLIRunResult {
|
|
|
58
65
|
}
|
|
59
66
|
export declare function runCapture(options: CLIRunnerOptions): Promise<CLIRunResult>;
|
|
60
67
|
export declare function buildVideoClipMetadata(videoId: string, result: RunResult, program?: ExecutionProgram, runId?: string): VideoClipMetadata[];
|
|
68
|
+
export declare function resolveEffectiveCliArtifactPlan(artifactPlan: ExecutionProgram['artifactPlan'], deviceFrame?: string | null): ExecutionProgram['artifactPlan'];
|
package/dist/cli-runner.js
CHANGED
|
@@ -25,7 +25,7 @@ import { parseProgram } from './execution-schema.js';
|
|
|
25
25
|
import { buildCursorOverlayScript } from './cursor-overlay-script.js';
|
|
26
26
|
import { CLI_VERSION_HEADER, } from './cli-contract.js';
|
|
27
27
|
import { postProcessClipRecording } from './clip-postprocess.js';
|
|
28
|
-
import { applyDeviceFrame, seedDeviceConfigs } from './mockup.js';
|
|
28
|
+
import { applyDeviceFrame, resolveVariantFrameOptions, seedDeviceConfigs } from './mockup.js';
|
|
29
29
|
import { transformBrowserUrl } from './transform-browser-url.js';
|
|
30
30
|
import { localizeStatusBar } from './status-bar-l10n.js';
|
|
31
31
|
import { logger } from './logger.js';
|
|
@@ -156,6 +156,10 @@ export async function runCapture(options) {
|
|
|
156
156
|
// here; we seed mockup.ts so the export pipeline can apply them locally.
|
|
157
157
|
seedDeviceConfigs(program.deviceConfigs ?? null);
|
|
158
158
|
const runId = randomUUID();
|
|
159
|
+
// A multi-preset invocation shares one sessionId so every preset's charges
|
|
160
|
+
// group into one "CLI capture" billing entry; a lone run groups under its own
|
|
161
|
+
// runId (migration 267).
|
|
162
|
+
const sessionId = options.sessionId ?? runId;
|
|
159
163
|
let videoAudioAssets;
|
|
160
164
|
let videoAudioAssetsByLocale;
|
|
161
165
|
try {
|
|
@@ -164,8 +168,13 @@ export async function runCapture(options) {
|
|
|
164
168
|
catch (error) {
|
|
165
169
|
return { success: false, runId, error: error instanceof Error ? error.message : String(error) };
|
|
166
170
|
}
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
// TTS synthesis is real, billed work — it belongs to a real run only. A dry run validates
|
|
172
|
+
// navigation/opcodes (capture opcodes + upload are skipped below) and must NOT synthesize speech:
|
|
173
|
+
// doing so would bill TTS credits while the run reports "0 credits charged". The rewritten SLEEP
|
|
174
|
+
// durations + audio assets are only consumed on the upload path (signalVideoComplete), which a dry
|
|
175
|
+
// run never reaches, so skipping prep here is side-effect-free for dry.
|
|
176
|
+
if (!options.dryRun && !options.program && program.mediaMode === 'video') {
|
|
177
|
+
const prepareResult = await prepareVideoSpeechForRun(config, options.presetId, runId, options.regenerateTts ?? false, sessionId);
|
|
169
178
|
if (!prepareResult.success) {
|
|
170
179
|
return { success: false, runId, error: prepareResult.error };
|
|
171
180
|
}
|
|
@@ -303,7 +312,7 @@ export async function runCapture(options) {
|
|
|
303
312
|
message: 'saving captures',
|
|
304
313
|
});
|
|
305
314
|
const provenance = buildRunProvenance(program, schemaVersionOrigin);
|
|
306
|
-
const uploadOutcome = await uploadResults(config, program, runResult, runId, provenance);
|
|
315
|
+
const uploadOutcome = await uploadResults(config, program, runResult, runId, sessionId, provenance);
|
|
307
316
|
if (program.mediaMode === 'video' && runResult.success) {
|
|
308
317
|
await signalVideoComplete(config, program, runResult, uploadOutcome.runId, videoAudioAssets, videoAudioAssetsByLocale);
|
|
309
318
|
}
|
|
@@ -351,7 +360,7 @@ export async function runCapture(options) {
|
|
|
351
360
|
&& (runResult ? !runResult.success : true);
|
|
352
361
|
if (shouldExport) {
|
|
353
362
|
logger.info('[debug-logs] Exporting debug logs to AutoKap…');
|
|
354
|
-
await logCollector.flushTo(runId, program.presetId, config.apiBaseUrl, config.apiKey, options.env);
|
|
363
|
+
await logCollector.flushTo(runId, program.presetId, config.apiBaseUrl, config.apiKey, options.env, runResult?.failureKind);
|
|
355
364
|
}
|
|
356
365
|
logCollector.stop();
|
|
357
366
|
}
|
|
@@ -425,7 +434,7 @@ async function fetchProgram(config, presetId, environmentName) {
|
|
|
425
434
|
}
|
|
426
435
|
return { success: false, error: 'failed to fetch program: retry attempts exhausted' };
|
|
427
436
|
}
|
|
428
|
-
async function prepareVideoSpeechForRun(config, videoId, runId, regenerateTts) {
|
|
437
|
+
async function prepareVideoSpeechForRun(config, videoId, runId, regenerateTts, sessionId) {
|
|
429
438
|
if (regenerateTts) {
|
|
430
439
|
logger.info('[capture] Forcing TTS regeneration — all cached segments will be re-synthesized and billed.');
|
|
431
440
|
}
|
|
@@ -440,7 +449,9 @@ async function prepareVideoSpeechForRun(config, videoId, runId, regenerateTts) {
|
|
|
440
449
|
'Content-Type': 'application/json',
|
|
441
450
|
[CLI_VERSION_HEADER]: APP_VERSION,
|
|
442
451
|
},
|
|
443
|
-
body: JSON.stringify(regenerateTts
|
|
452
|
+
body: JSON.stringify(regenerateTts
|
|
453
|
+
? { videoId, runId, sessionId, regenerateTts: true }
|
|
454
|
+
: { videoId, runId, sessionId }),
|
|
444
455
|
});
|
|
445
456
|
}
|
|
446
457
|
catch (err) {
|
|
@@ -681,7 +692,7 @@ async function postRunStart(config, runId, presetId, variantCount, env) {
|
|
|
681
692
|
logger.warn(`[capture] Run registration error: ${message}`);
|
|
682
693
|
}
|
|
683
694
|
}
|
|
684
|
-
async function uploadResults(config, program, result, runId, provenance) {
|
|
695
|
+
async function uploadResults(config, program, result, runId, sessionId, provenance) {
|
|
685
696
|
const artifactJobs = result.variantResults.flatMap((variant) => {
|
|
686
697
|
const variantSpec = program.variants.find((entry) => entry.id === variant.variantId);
|
|
687
698
|
return variant.artifacts.map((artifact) => ({
|
|
@@ -696,7 +707,7 @@ async function uploadResults(config, program, result, runId, provenance) {
|
|
|
696
707
|
logger.info(`[capture] Uploading ${totalArtifacts} capture artifacts with concurrency ${artifactUploadConcurrency}`);
|
|
697
708
|
}
|
|
698
709
|
await runWithConcurrency(artifactJobs, artifactUploadConcurrency, async (job, index) => {
|
|
699
|
-
await uploadArtifact(config, program, runId, totalArtifacts, index + 1, job, provenance);
|
|
710
|
+
await uploadArtifact(config, program, runId, sessionId, totalArtifacts, index + 1, job, provenance);
|
|
700
711
|
});
|
|
701
712
|
// Strip binary buffers from artifacts before sending. The raw PNG/video
|
|
702
713
|
// buffers were already uploaded via /api/cli/artifacts above, and the
|
|
@@ -857,18 +868,19 @@ function inferVariantLocale(variantId) {
|
|
|
857
868
|
function inferVariantTheme(variantId) {
|
|
858
869
|
return variantId.endsWith('-dark') ? 'dark' : 'light';
|
|
859
870
|
}
|
|
860
|
-
async function uploadArtifact(config, program, runId, totalArtifacts, uploadNumber, job, provenance) {
|
|
871
|
+
async function uploadArtifact(config, program, runId, sessionId, totalArtifacts, uploadNumber, job, provenance) {
|
|
861
872
|
const { artifact, variant, variantSpec } = job;
|
|
862
873
|
const filename = buildArtifactFilename(program.presetId, variant.variantId, artifact);
|
|
863
874
|
const label = artifact.captureName ?? artifact.clipName ?? filename;
|
|
864
875
|
logger.info(`[capture] Exporting capture ${uploadNumber}/${totalArtifacts}: ${label}`);
|
|
865
876
|
if (process.env.AUTOKAP_USE_LEGACY_MULTIPART_UPLOADS === '1') {
|
|
866
|
-
await uploadArtifactMultipart(config, program, runId, job, filename, provenance);
|
|
877
|
+
await uploadArtifactMultipart(config, program, runId, sessionId, job, filename, provenance);
|
|
867
878
|
return;
|
|
868
879
|
}
|
|
869
880
|
const prepared = await prepareDirectArtifactUpload({
|
|
870
881
|
program,
|
|
871
882
|
runId,
|
|
883
|
+
sessionId,
|
|
872
884
|
artifact,
|
|
873
885
|
variant,
|
|
874
886
|
variantSpec,
|
|
@@ -933,7 +945,7 @@ async function uploadArtifact(config, program, runId, totalArtifacts, uploadNumb
|
|
|
933
945
|
throw new Error(`artifact completion failed for ${variant.variantId}: ${await formatServerError(completeResponse, completeUrl)}`);
|
|
934
946
|
}
|
|
935
947
|
}
|
|
936
|
-
async function uploadArtifactMultipart(config, program, runId, job, filename, provenance) {
|
|
948
|
+
async function uploadArtifactMultipart(config, program, runId, sessionId, job, filename, provenance) {
|
|
937
949
|
const { artifact, variant, variantSpec } = job;
|
|
938
950
|
const formData = new FormData();
|
|
939
951
|
formData.append('file', new Blob([new Uint8Array(artifact.buffer)], { type: artifact.mimeType }), filename);
|
|
@@ -947,6 +959,7 @@ async function uploadArtifactMultipart(config, program, runId, job, filename, pr
|
|
|
947
959
|
formData.append('cliVersion', provenance.cliVersion);
|
|
948
960
|
formData.append('programHash', provenance.programHash);
|
|
949
961
|
formData.append('runId', runId);
|
|
962
|
+
formData.append('sessionId', sessionId);
|
|
950
963
|
formData.append('variantId', variant.variantId);
|
|
951
964
|
formData.append('targetId', variantSpec?.targetId ?? variant.variantId);
|
|
952
965
|
formData.append('targetLabel', variantSpec?.targetLabel ?? variantSpec?.deviceFrame ?? variant.variantId);
|
|
@@ -962,6 +975,9 @@ async function uploadArtifactMultipart(config, program, runId, job, filename, pr
|
|
|
962
975
|
if (variantSpec?.deviceFrame) {
|
|
963
976
|
formData.append('deviceFrame', variantSpec.deviceFrame);
|
|
964
977
|
}
|
|
978
|
+
if (variantSpec?.mockupOptions) {
|
|
979
|
+
formData.append('mockupOptions', JSON.stringify(variantSpec.mockupOptions));
|
|
980
|
+
}
|
|
965
981
|
const requestedDeviceScaleFactor = variantSpec?.deviceScaleFactor ?? program.outputScale ?? 2;
|
|
966
982
|
const isFrameCapture = artifact.mediaMode === 'clip' || artifact.mediaMode === 'video';
|
|
967
983
|
const deviceScaleFactor = isFrameCapture && Number.isFinite(requestedDeviceScaleFactor)
|
|
@@ -1015,7 +1031,7 @@ async function uploadArtifactMultipart(config, program, runId, job, filename, pr
|
|
|
1015
1031
|
}
|
|
1016
1032
|
}
|
|
1017
1033
|
async function prepareDirectArtifactUpload(params) {
|
|
1018
|
-
const { program, runId, artifact, variant, variantSpec, provenance } = params;
|
|
1034
|
+
const { program, runId, sessionId, artifact, variant, variantSpec, provenance } = params;
|
|
1019
1035
|
const requestedDeviceScaleFactor = variantSpec?.deviceScaleFactor ?? program.outputScale ?? 2;
|
|
1020
1036
|
const isFrameCapture = artifact.mediaMode === 'clip' || artifact.mediaMode === 'video';
|
|
1021
1037
|
const deviceScaleFactor = isFrameCapture && Number.isFinite(requestedDeviceScaleFactor)
|
|
@@ -1032,6 +1048,7 @@ async function prepareDirectArtifactUpload(params) {
|
|
|
1032
1048
|
cliVersion: provenance.cliVersion,
|
|
1033
1049
|
programHash: provenance.programHash,
|
|
1034
1050
|
runId,
|
|
1051
|
+
sessionId,
|
|
1035
1052
|
variantId: variant.variantId,
|
|
1036
1053
|
targetId: variantSpec?.targetId ?? variant.variantId,
|
|
1037
1054
|
targetLabel: variantSpec?.targetLabel ?? variantSpec?.deviceFrame ?? variant.variantId,
|
|
@@ -1103,22 +1120,24 @@ async function prepareDirectUploadParts(params) {
|
|
|
1103
1120
|
async function prepareScreenshotBufferForDirectUpload(input, metadata, program, variantSpec, tabIcon) {
|
|
1104
1121
|
let output = input;
|
|
1105
1122
|
const artifactPlan = resolveEffectiveCliArtifactPlan(program.artifactPlan, variantSpec?.deviceFrame ?? null);
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
output = await applyDeviceFrame(output, variantSpec.deviceFrame, {
|
|
1123
|
+
// The variant's deviceFrame is the sole gate. Its mockupOptions (orientation, status bar,
|
|
1124
|
+
// safe areas, …) — defined by the user on the preset — drive the frame; the legacy
|
|
1125
|
+
// program-level applyStatusBar only survives as a fallback for old programs without options.
|
|
1126
|
+
if (variantSpec?.deviceFrame) {
|
|
1127
|
+
const mockup = variantSpec.mockupOptions;
|
|
1128
|
+
const frame = resolveVariantFrameOptions(mockup, {
|
|
1114
1129
|
orientation: inferCliOrientation(metadata.viewport ?? null),
|
|
1130
|
+
showStatusBar: artifactPlan?.applyStatusBar,
|
|
1131
|
+
});
|
|
1132
|
+
output = await applyDeviceFrame(output, variantSpec.deviceFrame, {
|
|
1133
|
+
...mockup,
|
|
1134
|
+
...frame,
|
|
1115
1135
|
viewport: metadata.viewport ?? undefined,
|
|
1116
1136
|
colorScheme: metadata.theme,
|
|
1117
1137
|
outputScale: normalizeCliDeviceScaleFactor(metadata.deviceScaleFactor)
|
|
1118
1138
|
?? normalizeCliDeviceScaleFactor(program.outputScale)
|
|
1119
1139
|
?? 2,
|
|
1120
|
-
|
|
1121
|
-
statusBar: localizeStatusBar({}, metadata.lang),
|
|
1140
|
+
statusBar: localizeStatusBar(mockup?.statusBar ?? {}, metadata.lang),
|
|
1122
1141
|
browserBar: buildCliBrowserBar(metadata.captureUrl, metadata.theme, tabIcon, { publicUrl: program.publicUrl, pageTitle: metadata.pageTitle ?? null }),
|
|
1123
1142
|
});
|
|
1124
1143
|
}
|
|
@@ -1181,10 +1200,11 @@ async function prepareClipBuffersForDirectUpload(artifact, metadata) {
|
|
|
1181
1200
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
1182
1201
|
}
|
|
1183
1202
|
}
|
|
1184
|
-
function resolveEffectiveCliArtifactPlan(artifactPlan, deviceFrame) {
|
|
1203
|
+
export function resolveEffectiveCliArtifactPlan(artifactPlan, deviceFrame) {
|
|
1204
|
+
// Device mockups are gated SOLELY by the variant's `deviceFrame` (user-defined, deterministic).
|
|
1205
|
+
// A present deviceFrame always renders its frame; `applyMockup` is a derived value, never a
|
|
1206
|
+
// kill-switch (the legacy `applyMockup: false` toggle is intentionally ignored here).
|
|
1185
1207
|
if (deviceFrame) {
|
|
1186
|
-
if (artifactPlan.applyMockup === false)
|
|
1187
|
-
return artifactPlan;
|
|
1188
1208
|
return artifactPlan.applyMockup ? artifactPlan : { ...artifactPlan, applyMockup: true };
|
|
1189
1209
|
}
|
|
1190
1210
|
if (!artifactPlan.applyMockup && !artifactPlan.applyStatusBar)
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import fs from 'node:fs/promises';
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
@@ -137,6 +138,9 @@ async function runOutdatedPresetsLocally(opts) {
|
|
|
137
138
|
const { runCapture } = await import('./cli-runner.js');
|
|
138
139
|
const failures = [];
|
|
139
140
|
logger.info(`[capture] Running ${data.presets.length} outdated preset(s)`);
|
|
141
|
+
// One session id for the whole invocation so every preset's screenshot/clip/
|
|
142
|
+
// video charges group into a single "CLI capture" billing entry (migration 267).
|
|
143
|
+
const sessionId = randomUUID();
|
|
140
144
|
for (const preset of data.presets) {
|
|
141
145
|
const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
|
|
142
146
|
logger.info(`[capture] Running outdated preset ${label}`);
|
|
@@ -147,6 +151,7 @@ async function runOutdatedPresetsLocally(opts) {
|
|
|
147
151
|
allowUploadFailure: opts.allowUploadFailure,
|
|
148
152
|
dryRun: opts.dry,
|
|
149
153
|
regenerateTts: opts.regenerateTts,
|
|
154
|
+
sessionId,
|
|
150
155
|
});
|
|
151
156
|
if (!result.success) {
|
|
152
157
|
failures.push({
|
|
@@ -397,6 +402,10 @@ program
|
|
|
397
402
|
failedPresets: 0,
|
|
398
403
|
message: `Runner started: ${data.presets.length} preset(s) to capture`,
|
|
399
404
|
});
|
|
405
|
+
// One session id for the whole invocation so every preset's charges group
|
|
406
|
+
// into a single "CLI capture" billing entry (migration 267). For cloud runs
|
|
407
|
+
// child artifact billing is suppressed server-side, so this is a no-op there.
|
|
408
|
+
const sessionId = randomUUID();
|
|
400
409
|
for (const [index, preset] of data.presets.entries()) {
|
|
401
410
|
const presetDisplayName = displayPresetName(preset);
|
|
402
411
|
const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
|
|
@@ -417,6 +426,7 @@ program
|
|
|
417
426
|
headed: opts.headed,
|
|
418
427
|
allowUploadFailure: opts.allowUploadFailure,
|
|
419
428
|
regenerateTts: opts.regenerateTts,
|
|
429
|
+
sessionId,
|
|
420
430
|
// Each preset runs under its own ephemeral runId, which is NOT a
|
|
421
431
|
// capture_runs row in a cloud batch (the parent cloud run owns the row),
|
|
422
432
|
// so the per-preset error-log export would 404. Failure telemetry for
|
|
@@ -457,6 +467,9 @@ program
|
|
|
457
467
|
childRunId,
|
|
458
468
|
status: 'failed',
|
|
459
469
|
errorMessage: error,
|
|
470
|
+
...(result.runResult?.failureKind
|
|
471
|
+
? { failureKind: result.runResult.failureKind }
|
|
472
|
+
: {}),
|
|
460
473
|
message: `Preset failed: ${presetDisplayName}`,
|
|
461
474
|
});
|
|
462
475
|
}
|
|
@@ -1053,7 +1053,7 @@ export declare const VariantSpecSchema: z.ZodObject<{
|
|
|
1053
1053
|
light: "light";
|
|
1054
1054
|
dark: "dark";
|
|
1055
1055
|
}>>;
|
|
1056
|
-
}, z.core.$
|
|
1056
|
+
}, z.core.$strip>>;
|
|
1057
1057
|
}, z.core.$strict>;
|
|
1058
1058
|
export declare const PreconditionSpecSchema: z.ZodObject<{
|
|
1059
1059
|
credentialsId: z.ZodOptional<z.ZodString>;
|
|
@@ -1205,7 +1205,7 @@ export declare const ExecutionProgramSchema: z.ZodObject<{
|
|
|
1205
1205
|
light: "light";
|
|
1206
1206
|
dark: "dark";
|
|
1207
1207
|
}>>;
|
|
1208
|
-
}, z.core.$
|
|
1208
|
+
}, z.core.$strip>>;
|
|
1209
1209
|
}, z.core.$strict>>;
|
|
1210
1210
|
preconditions: z.ZodObject<{
|
|
1211
1211
|
credentialsId: z.ZodOptional<z.ZodString>;
|
package/dist/execution-schema.js
CHANGED
|
@@ -553,7 +553,13 @@ export const VariantSpecSchema = z.object({
|
|
|
553
553
|
radius: z.number(),
|
|
554
554
|
}).strict().optional(),
|
|
555
555
|
colorScheme: z.enum(['light', 'dark']).optional(),
|
|
556
|
-
|
|
556
|
+
// STRIP (not strict): per-variant mockupOptions are reconciled verbatim from the preset's
|
|
557
|
+
// CaptureTarget, whose UI-side MockupOptions carries render-irrelevant keys the engine never
|
|
558
|
+
// consumes (showDock, dockScale, dockMode, userAppIcon, autoBrowserBar, viewport). Strict would
|
|
559
|
+
// make parseProgram THROW on those — stranding every Mac/browser preset at CLI fetch time.
|
|
560
|
+
// Dropping unknown keys post-parse is safe: applyDeviceFrame ignores them, and the full set
|
|
561
|
+
// stays on config.targets for the UI/preview.
|
|
562
|
+
}).optional(),
|
|
557
563
|
}).strict();
|
|
558
564
|
const cookieSchema = z.object({
|
|
559
565
|
name: z.string().min(1),
|
|
@@ -535,9 +535,16 @@ export interface ArtifactSpec {
|
|
|
535
535
|
cursorTheme?: VideoCursorTheme;
|
|
536
536
|
/** Max clip duration in seconds. Clips are trimmed if they exceed this. Default: 8. Ignored when `mediaMode='video'`. */
|
|
537
537
|
maxClipDurationSec?: number;
|
|
538
|
-
/**
|
|
538
|
+
/**
|
|
539
|
+
* @deprecated Device mockups are gated SOLELY by a variant's `deviceFrame` (user-defined,
|
|
540
|
+
* deterministic). The render paths derive mockup application from `deviceFrame` and ignore this
|
|
541
|
+
* flag as a gate. Kept optional for back-compat with stored programs; do not author it.
|
|
542
|
+
*/
|
|
539
543
|
applyMockup?: boolean;
|
|
540
|
-
/**
|
|
544
|
+
/**
|
|
545
|
+
* @deprecated Per-variant `mockupOptions.showStatusBar` drives the status bar now. Retained only
|
|
546
|
+
* as a fallback for legacy programs that lack per-variant `mockupOptions`. Do not author it.
|
|
547
|
+
*/
|
|
541
548
|
applyStatusBar?: boolean;
|
|
542
549
|
}
|
|
543
550
|
export interface ExecutionProgram {
|
|
@@ -647,6 +654,13 @@ export interface OpcodeResult {
|
|
|
647
654
|
/** Error message if failed */
|
|
648
655
|
error?: string;
|
|
649
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Structured failure category, set on top of the free-text `error`. Lets the
|
|
659
|
+
* server surface a specific preset state instead of a generic "failed":
|
|
660
|
+
* `login_failed` = an opcode inside the login window (credential typing → first
|
|
661
|
+
* post-login assertion) failed, so the credentials are likely wrong.
|
|
662
|
+
*/
|
|
663
|
+
export type RunFailureKind = 'login_failed';
|
|
650
664
|
export interface VariantResult {
|
|
651
665
|
variantId: string;
|
|
652
666
|
success: boolean;
|
|
@@ -663,6 +677,8 @@ export interface VariantResult {
|
|
|
663
677
|
*/
|
|
664
678
|
detectedAppVersion?: string | null;
|
|
665
679
|
error?: string;
|
|
680
|
+
/** Set when the failure falls inside the login window — see RunFailureKind. */
|
|
681
|
+
failureKind?: RunFailureKind;
|
|
666
682
|
}
|
|
667
683
|
export interface ArtifactResult {
|
|
668
684
|
mediaMode: MediaMode;
|
|
@@ -814,6 +830,8 @@ export interface RunResult {
|
|
|
814
830
|
*/
|
|
815
831
|
warnings?: string[];
|
|
816
832
|
error?: string;
|
|
833
|
+
/** First non-null variant `failureKind` — see RunFailureKind. */
|
|
834
|
+
failureKind?: RunFailureKind;
|
|
817
835
|
}
|
|
818
836
|
export interface WaitCondition {
|
|
819
837
|
selector: string;
|
package/dist/log-collector.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LogEntry } from './logger.js';
|
|
2
2
|
import type { ProgressEvent } from './opcode-runner.js';
|
|
3
|
+
import type { RunFailureKind } from './execution-types.js';
|
|
3
4
|
export interface OpcodeContext {
|
|
4
5
|
index: number;
|
|
5
6
|
kind: string;
|
|
@@ -24,6 +25,9 @@ export interface ErrorLogsPayload {
|
|
|
24
25
|
entries: ExportedLogEntry[];
|
|
25
26
|
envName?: string;
|
|
26
27
|
endedAt: string;
|
|
28
|
+
/** Structured failure category (e.g. 'login_failed'). Lets the server tag the
|
|
29
|
+
* preset's capture_failed event with a specific kind. */
|
|
30
|
+
failureKind?: RunFailureKind;
|
|
27
31
|
}
|
|
28
32
|
export declare class LogCollector {
|
|
29
33
|
private entries;
|
|
@@ -35,7 +39,7 @@ export declare class LogCollector {
|
|
|
35
39
|
onProgress(event: ProgressEvent): void;
|
|
36
40
|
snapshot(): ExportedLogEntry[];
|
|
37
41
|
size(): number;
|
|
38
|
-
flushTo(runId: string, presetId: string, apiBaseUrl: string, apiKey: string, envName?: string): Promise<{
|
|
42
|
+
flushTo(runId: string, presetId: string, apiBaseUrl: string, apiKey: string, envName?: string, failureKind?: RunFailureKind): Promise<{
|
|
39
43
|
ok: boolean;
|
|
40
44
|
status?: number;
|
|
41
45
|
error?: string;
|
package/dist/log-collector.js
CHANGED
|
@@ -61,7 +61,7 @@ export class LogCollector {
|
|
|
61
61
|
size() {
|
|
62
62
|
return this.entries.length;
|
|
63
63
|
}
|
|
64
|
-
async flushTo(runId, presetId, apiBaseUrl, apiKey, envName) {
|
|
64
|
+
async flushTo(runId, presetId, apiBaseUrl, apiKey, envName, failureKind) {
|
|
65
65
|
if (this.entries.length === 0) {
|
|
66
66
|
return { ok: true, status: 204 };
|
|
67
67
|
}
|
|
@@ -71,6 +71,7 @@ export class LogCollector {
|
|
|
71
71
|
entries: this.entries,
|
|
72
72
|
envName,
|
|
73
73
|
endedAt: new Date().toISOString(),
|
|
74
|
+
...(failureKind ? { failureKind } : {}),
|
|
74
75
|
};
|
|
75
76
|
const controller = new AbortController();
|
|
76
77
|
const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login detection — pure, dependency-light helpers shared by the opcode runner
|
|
3
|
+
* (to classify a failure as login-related) and credential substitution
|
|
4
|
+
* (opcode-actions.ts owns the actual `{{token}}` replacement).
|
|
5
|
+
*
|
|
6
|
+
* A program "logs in" when it types credentials: a TYPE opcode whose text (or a
|
|
7
|
+
* locale override) contains the `{{email}}` / `{{password}}` placeholder. The
|
|
8
|
+
* server resolves these placeholders at capture time from the preset's linked
|
|
9
|
+
* credentials account; the stored program only ever holds the placeholders.
|
|
10
|
+
*
|
|
11
|
+
* This module imports ONLY types so it stays safe to reference from any layer.
|
|
12
|
+
*/
|
|
13
|
+
import type { ExecutionOpcode } from './execution-types.js';
|
|
14
|
+
/** The credential placeholder tokens — the contract between the authored
|
|
15
|
+
* program and the server-side substitution. Single source of truth. */
|
|
16
|
+
export declare const CREDENTIAL_TOKEN_EMAIL = "{{email}}";
|
|
17
|
+
export declare const CREDENTIAL_TOKEN_PASSWORD = "{{password}}";
|
|
18
|
+
export declare const CREDENTIAL_TOKEN_LOGIN_URL = "{{loginUrl}}";
|
|
19
|
+
/**
|
|
20
|
+
* Which credential fields the program actually requires. Lets a caller compare
|
|
21
|
+
* against the linked account's available fields (has_email / has_password)
|
|
22
|
+
* without decrypting anything.
|
|
23
|
+
*/
|
|
24
|
+
export declare function programRequiredCredentialFields(steps: ExecutionOpcode[]): {
|
|
25
|
+
email: boolean;
|
|
26
|
+
password: boolean;
|
|
27
|
+
};
|
|
28
|
+
/** True when the program logs in (types an email or password). */
|
|
29
|
+
export declare function programRequiresLogin(steps: ExecutionOpcode[]): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The contiguous index range that constitutes the login flow:
|
|
32
|
+
* - `start` = first credential-typing opcode,
|
|
33
|
+
* - `end` = first post-login assertion after the last credential opcode —
|
|
34
|
+
* EITHER a standalone assertion opcode (ASSERT_ROUTE /
|
|
35
|
+
* ASSERT_SURFACE / WAIT_FOR) OR, as the generator actually emits,
|
|
36
|
+
* the submit CLICK whose own postcondition asserts the post-login
|
|
37
|
+
* route/element (route_matches / element_visible) — else the last
|
|
38
|
+
* credential opcode itself.
|
|
39
|
+
*
|
|
40
|
+
* Scanning stops at that FIRST asserting opcode, so the window covers the
|
|
41
|
+
* credential typing and the submit (which proves login) but does NOT extend
|
|
42
|
+
* across the post-login navigation. A failure anywhere in `[start, end]` means
|
|
43
|
+
* the login did not go through: a TYPE failing (form gone/changed), the submit
|
|
44
|
+
* failing, or its post-login assertion failing (still on /login). Returns null
|
|
45
|
+
* when the program does not log in.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getLoginWindow(steps: ExecutionOpcode[]): {
|
|
48
|
+
start: number;
|
|
49
|
+
end: number;
|
|
50
|
+
} | null;
|
|
51
|
+
/** True when a failure at `failedIndex` falls inside the login window. */
|
|
52
|
+
export declare function isLoginFailureIndex(steps: ExecutionOpcode[], failedIndex: number): boolean;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login detection — pure, dependency-light helpers shared by the opcode runner
|
|
3
|
+
* (to classify a failure as login-related) and credential substitution
|
|
4
|
+
* (opcode-actions.ts owns the actual `{{token}}` replacement).
|
|
5
|
+
*
|
|
6
|
+
* A program "logs in" when it types credentials: a TYPE opcode whose text (or a
|
|
7
|
+
* locale override) contains the `{{email}}` / `{{password}}` placeholder. The
|
|
8
|
+
* server resolves these placeholders at capture time from the preset's linked
|
|
9
|
+
* credentials account; the stored program only ever holds the placeholders.
|
|
10
|
+
*
|
|
11
|
+
* This module imports ONLY types so it stays safe to reference from any layer.
|
|
12
|
+
*/
|
|
13
|
+
/** The credential placeholder tokens — the contract between the authored
|
|
14
|
+
* program and the server-side substitution. Single source of truth. */
|
|
15
|
+
export const CREDENTIAL_TOKEN_EMAIL = '{{email}}';
|
|
16
|
+
export const CREDENTIAL_TOKEN_PASSWORD = '{{password}}';
|
|
17
|
+
export const CREDENTIAL_TOKEN_LOGIN_URL = '{{loginUrl}}';
|
|
18
|
+
/** Opcode kinds that prove a login advanced past the form: an explicit route /
|
|
19
|
+
* surface assertion, or a wait for a post-login element. The first such opcode
|
|
20
|
+
* after the credential typing closes the "login window". */
|
|
21
|
+
const POSTLOGIN_ASSERTION_KINDS = new Set([
|
|
22
|
+
'ASSERT_ROUTE',
|
|
23
|
+
'ASSERT_SURFACE',
|
|
24
|
+
'WAIT_FOR',
|
|
25
|
+
]);
|
|
26
|
+
/** Postcondition types that assert the login advanced past the form. The
|
|
27
|
+
* canonical generated login flow does NOT emit a standalone assertion opcode —
|
|
28
|
+
* it encodes the post-login check on the submit CLICK's own postcondition
|
|
29
|
+
* (route_matches the post-login route, or element_visible a post-login
|
|
30
|
+
* element). The first opcode after the credentials whose postcondition is one
|
|
31
|
+
* of these closes the window too, so the submit click is inside it. */
|
|
32
|
+
const POSTLOGIN_POSTCONDITION_TYPES = new Set([
|
|
33
|
+
'route_matches',
|
|
34
|
+
'element_visible',
|
|
35
|
+
]);
|
|
36
|
+
/** Every text field of an opcode that may carry a credential placeholder. */
|
|
37
|
+
function credentialTexts(opcode) {
|
|
38
|
+
if (opcode.kind === 'TYPE') {
|
|
39
|
+
const texts = [opcode.text];
|
|
40
|
+
if (opcode.textByLocale)
|
|
41
|
+
texts.push(...Object.values(opcode.textByLocale));
|
|
42
|
+
return texts;
|
|
43
|
+
}
|
|
44
|
+
if (opcode.kind === 'NAVIGATE')
|
|
45
|
+
return [opcode.url];
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
/** True when the opcode types/uses email or password credentials. */
|
|
49
|
+
function isCredentialOpcode(opcode) {
|
|
50
|
+
return credentialTexts(opcode).some((text) => typeof text === 'string' &&
|
|
51
|
+
(text.includes(CREDENTIAL_TOKEN_EMAIL) || text.includes(CREDENTIAL_TOKEN_PASSWORD)));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Which credential fields the program actually requires. Lets a caller compare
|
|
55
|
+
* against the linked account's available fields (has_email / has_password)
|
|
56
|
+
* without decrypting anything.
|
|
57
|
+
*/
|
|
58
|
+
export function programRequiredCredentialFields(steps) {
|
|
59
|
+
let email = false;
|
|
60
|
+
let password = false;
|
|
61
|
+
for (const opcode of steps) {
|
|
62
|
+
for (const text of credentialTexts(opcode)) {
|
|
63
|
+
if (typeof text !== 'string')
|
|
64
|
+
continue;
|
|
65
|
+
if (text.includes(CREDENTIAL_TOKEN_EMAIL))
|
|
66
|
+
email = true;
|
|
67
|
+
if (text.includes(CREDENTIAL_TOKEN_PASSWORD))
|
|
68
|
+
password = true;
|
|
69
|
+
}
|
|
70
|
+
if (email && password)
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
return { email, password };
|
|
74
|
+
}
|
|
75
|
+
/** True when the program logs in (types an email or password). */
|
|
76
|
+
export function programRequiresLogin(steps) {
|
|
77
|
+
const { email, password } = programRequiredCredentialFields(steps);
|
|
78
|
+
return email || password;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The contiguous index range that constitutes the login flow:
|
|
82
|
+
* - `start` = first credential-typing opcode,
|
|
83
|
+
* - `end` = first post-login assertion after the last credential opcode —
|
|
84
|
+
* EITHER a standalone assertion opcode (ASSERT_ROUTE /
|
|
85
|
+
* ASSERT_SURFACE / WAIT_FOR) OR, as the generator actually emits,
|
|
86
|
+
* the submit CLICK whose own postcondition asserts the post-login
|
|
87
|
+
* route/element (route_matches / element_visible) — else the last
|
|
88
|
+
* credential opcode itself.
|
|
89
|
+
*
|
|
90
|
+
* Scanning stops at that FIRST asserting opcode, so the window covers the
|
|
91
|
+
* credential typing and the submit (which proves login) but does NOT extend
|
|
92
|
+
* across the post-login navigation. A failure anywhere in `[start, end]` means
|
|
93
|
+
* the login did not go through: a TYPE failing (form gone/changed), the submit
|
|
94
|
+
* failing, or its post-login assertion failing (still on /login). Returns null
|
|
95
|
+
* when the program does not log in.
|
|
96
|
+
*/
|
|
97
|
+
export function getLoginWindow(steps) {
|
|
98
|
+
let start = -1;
|
|
99
|
+
let lastCred = -1;
|
|
100
|
+
for (let i = 0; i < steps.length; i++) {
|
|
101
|
+
if (isCredentialOpcode(steps[i])) {
|
|
102
|
+
if (start < 0)
|
|
103
|
+
start = i;
|
|
104
|
+
lastCred = i;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (start < 0)
|
|
108
|
+
return null;
|
|
109
|
+
let end = lastCred;
|
|
110
|
+
for (let i = lastCred + 1; i < steps.length; i++) {
|
|
111
|
+
const step = steps[i];
|
|
112
|
+
if (POSTLOGIN_ASSERTION_KINDS.has(step.kind) ||
|
|
113
|
+
(step.postcondition != null &&
|
|
114
|
+
POSTLOGIN_POSTCONDITION_TYPES.has(step.postcondition.type))) {
|
|
115
|
+
end = i;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { start, end };
|
|
120
|
+
}
|
|
121
|
+
/** True when a failure at `failedIndex` falls inside the login window. */
|
|
122
|
+
export function isLoginFailureIndex(steps, failedIndex) {
|
|
123
|
+
const window = getLoginWindow(steps);
|
|
124
|
+
return window !== null && failedIndex >= window.start && failedIndex <= window.end;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=login-detection.js.map
|
package/dist/mockup.d.ts
CHANGED
|
@@ -204,6 +204,19 @@ export interface MockupOptions {
|
|
|
204
204
|
height: number;
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Resolve the two per-variant frame decisions shared by both render paths (CLI direct-upload
|
|
209
|
+
* framing and the cloud legacy-multipart route): a variant's own `mockupOptions` wins, falling
|
|
210
|
+
* back to the viewport-inferred orientation and the deprecated program-level `applyStatusBar`.
|
|
211
|
+
* Pure + exported so the precedence is tested once instead of in two duplicated call sites.
|
|
212
|
+
*/
|
|
213
|
+
export declare function resolveVariantFrameOptions(mockupOptions: MockupOptions | undefined, fallback: {
|
|
214
|
+
orientation?: MockupOrientation;
|
|
215
|
+
showStatusBar?: boolean;
|
|
216
|
+
}): {
|
|
217
|
+
orientation?: MockupOrientation;
|
|
218
|
+
showStatusBar: boolean;
|
|
219
|
+
};
|
|
207
220
|
export interface ResolvedDeviceFrameDescriptor {
|
|
208
221
|
id: string;
|
|
209
222
|
name: string;
|
package/dist/mockup.js
CHANGED
|
@@ -17,6 +17,18 @@ function getSupabaseMockupConfig() {
|
|
|
17
17
|
serviceKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the two per-variant frame decisions shared by both render paths (CLI direct-upload
|
|
22
|
+
* framing and the cloud legacy-multipart route): a variant's own `mockupOptions` wins, falling
|
|
23
|
+
* back to the viewport-inferred orientation and the deprecated program-level `applyStatusBar`.
|
|
24
|
+
* Pure + exported so the precedence is tested once instead of in two duplicated call sites.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveVariantFrameOptions(mockupOptions, fallback) {
|
|
27
|
+
return {
|
|
28
|
+
orientation: mockupOptions?.orientation ?? fallback.orientation,
|
|
29
|
+
showStatusBar: mockupOptions?.showStatusBar ?? fallback.showStatusBar ?? false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
20
32
|
const DEFAULT_MOCKUP_OPTIONS = {
|
|
21
33
|
orientation: 'portrait',
|
|
22
34
|
outputScale: 2,
|
package/dist/opcode-actions.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { VARIANT_PLACEHOLDER } from './execution-types.js';
|
|
8
8
|
import { dismissAllOverlays } from './overlay-engine.js';
|
|
9
|
+
import { CREDENTIAL_TOKEN_EMAIL, CREDENTIAL_TOKEN_PASSWORD, CREDENTIAL_TOKEN_LOGIN_URL, } from './login-detection.js';
|
|
9
10
|
/**
|
|
10
11
|
* Substitute credential placeholders inside opcode text fields.
|
|
11
12
|
* Only the {{email}}, {{password}} and {{loginUrl}} tokens are replaced.
|
|
@@ -16,9 +17,9 @@ export function substituteCredentialPlaceholders(text, credentials) {
|
|
|
16
17
|
return text;
|
|
17
18
|
}
|
|
18
19
|
return text
|
|
19
|
-
.replaceAll(
|
|
20
|
-
.replaceAll(
|
|
21
|
-
.replaceAll(
|
|
20
|
+
.replaceAll(CREDENTIAL_TOKEN_EMAIL, credentials?.email ?? '')
|
|
21
|
+
.replaceAll(CREDENTIAL_TOKEN_PASSWORD, credentials?.password ?? '')
|
|
22
|
+
.replaceAll(CREDENTIAL_TOKEN_LOGIN_URL, credentials?.loginUrl ?? '');
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
24
25
|
* Returns the list of credential placeholders (`{{email}}`, `{{password}}`,
|
|
@@ -32,14 +33,14 @@ export function findUnresolvedCredentialPlaceholders(text, credentials) {
|
|
|
32
33
|
if (typeof text !== 'string' || !text.includes('{{'))
|
|
33
34
|
return [];
|
|
34
35
|
const missing = [];
|
|
35
|
-
if (text.includes(
|
|
36
|
-
missing.push(
|
|
36
|
+
if (text.includes(CREDENTIAL_TOKEN_EMAIL) && !credentials?.email?.trim()) {
|
|
37
|
+
missing.push(CREDENTIAL_TOKEN_EMAIL);
|
|
37
38
|
}
|
|
38
|
-
if (text.includes(
|
|
39
|
-
missing.push(
|
|
39
|
+
if (text.includes(CREDENTIAL_TOKEN_PASSWORD) && !credentials?.password) {
|
|
40
|
+
missing.push(CREDENTIAL_TOKEN_PASSWORD);
|
|
40
41
|
}
|
|
41
|
-
if (text.includes(
|
|
42
|
-
missing.push(
|
|
42
|
+
if (text.includes(CREDENTIAL_TOKEN_LOGIN_URL) && !credentials?.loginUrl?.trim()) {
|
|
43
|
+
missing.push(CREDENTIAL_TOKEN_LOGIN_URL);
|
|
43
44
|
}
|
|
44
45
|
return missing;
|
|
45
46
|
}
|
package/dist/opcode-runner.js
CHANGED
|
@@ -14,6 +14,7 @@ import { smartWaitForStability } from './smart-wait.js';
|
|
|
14
14
|
import { verifyCaptureQuality } from './capture-verification.js';
|
|
15
15
|
import { generateAltText } from './alt-text.js';
|
|
16
16
|
import { executeOpcodeCoreAction } from './opcode-actions.js';
|
|
17
|
+
import { isLoginFailureIndex } from './login-detection.js';
|
|
17
18
|
import { logger } from './logger.js';
|
|
18
19
|
function formatOpcodeDebug(opcode) {
|
|
19
20
|
const fields = [];
|
|
@@ -172,6 +173,7 @@ export async function executeProgram(program, createAdapter, options = {}) {
|
|
|
172
173
|
detectedAppVersion,
|
|
173
174
|
warnings: aggregatedWarnings.length ? aggregatedWarnings : undefined,
|
|
174
175
|
error: aborted ? 'aborted' : (success ? undefined : completedVariantResults.find(v => !v.success)?.error),
|
|
176
|
+
failureKind: success ? undefined : completedVariantResults.find(v => v.failureKind)?.failureKind,
|
|
175
177
|
};
|
|
176
178
|
}
|
|
177
179
|
// ── Variant execution ───────────────────────────────────────────────
|
|
@@ -247,6 +249,12 @@ async function executeVariant(program, variant, createAdapter, recoveryChain, te
|
|
|
247
249
|
durationMs: Date.now() - startTime,
|
|
248
250
|
artifacts,
|
|
249
251
|
error: `opcode ${i} (${opcode.kind}) failed: ${result.error}`,
|
|
252
|
+
// Tag failures inside the login window (credential typing → first
|
|
253
|
+
// post-login assertion) so the server can surface "login failed"
|
|
254
|
+
// instead of a generic capture failure.
|
|
255
|
+
...(isLoginFailureIndex(program.steps, i)
|
|
256
|
+
? { failureKind: 'login_failed' }
|
|
257
|
+
: {}),
|
|
250
258
|
};
|
|
251
259
|
}
|
|
252
260
|
}
|
|
@@ -111,7 +111,7 @@ export declare const SignedExecutionProgramEnvelopeSchema: z.ZodObject<{
|
|
|
111
111
|
light: "light";
|
|
112
112
|
dark: "dark";
|
|
113
113
|
}>>;
|
|
114
|
-
}, z.core.$
|
|
114
|
+
}, z.core.$strip>>;
|
|
115
115
|
}, z.core.$strict>>;
|
|
116
116
|
preconditions: z.ZodObject<{
|
|
117
117
|
credentialsId: z.ZodOptional<z.ZodString>;
|
package/dist/skill-packaging.js
CHANGED
|
@@ -19,6 +19,7 @@ const PRESET_SKILL_SOURCE = {
|
|
|
19
19
|
{ relativePath: 'OPCODE-REFERENCE.md', title: 'Opcode Reference', anchor: 'reference-opcode-reference' },
|
|
20
20
|
{ relativePath: 'references/STANDARDS.md', title: 'Prompt Charter & Quality Standards', anchor: 'reference-prompt-standards' },
|
|
21
21
|
{ relativePath: 'references/mock-data.md', title: 'Mock Data Injection', anchor: 'reference-mock-data-injection' },
|
|
22
|
+
{ relativePath: 'references/video-workflow.md', title: 'Demo Video Workflow', anchor: 'reference-demo-video-workflow' },
|
|
22
23
|
{ relativePath: 'references/examples.md', title: 'Complete Examples', anchor: 'reference-complete-examples' },
|
|
23
24
|
],
|
|
24
25
|
};
|
|
@@ -130,7 +130,7 @@ export declare const VideoIngestPayloadSchema: z.ZodObject<{
|
|
|
130
130
|
light: "light";
|
|
131
131
|
dark: "dark";
|
|
132
132
|
}>>;
|
|
133
|
-
}, z.core.$
|
|
133
|
+
}, z.core.$strip>>;
|
|
134
134
|
}, z.core.$strict>>;
|
|
135
135
|
preconditions: z.ZodObject<{
|
|
136
136
|
credentialsId: z.ZodOptional<z.ZodString>;
|