autokap 1.4.3 → 1.5.2
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 +79 -0
- package/dist/cli-contract.js +1 -0
- package/dist/cli-doctor.d.ts +4 -0
- package/dist/cli-doctor.js +302 -0
- package/dist/cli-runner.js +318 -2
- package/dist/cli.js +122 -66
- package/dist/execution-types.d.ts +19 -2
- package/dist/mouse-animation.d.ts +6 -0
- package/dist/mouse-animation.js +8 -0
- package/dist/opcode-actions.d.ts +6 -0
- package/dist/opcode-actions.js +7 -3
- package/dist/opcode-runner.js +4 -0
- package/dist/types.d.ts +1 -1
- package/dist/version-check.d.ts +4 -0
- package/dist/version-check.js +102 -0
- package/dist/web-playwright-local.d.ts +6 -2
- package/dist/web-playwright-local.js +7 -7
- package/package.json +2 -3
- package/dist/crm/email-fallback.d.ts +0 -16
- package/dist/crm/email-fallback.js +0 -217
- package/dist/crm/run-campaign.d.ts +0 -28
- package/dist/crm/run-campaign.js +0 -405
- package/dist/crm/scrape-betalist.d.ts +0 -20
- package/dist/crm/scrape-betalist.js +0 -194
- package/dist/crm/scrape-landing.d.ts +0 -24
- package/dist/crm/scrape-landing.js +0 -240
- package/dist/crm/storage-upload.d.ts +0 -14
- package/dist/crm/storage-upload.js +0 -40
package/dist/cli-runner.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
import fs from 'node:fs/promises';
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
-
import { randomUUID } from 'node:crypto';
|
|
16
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
17
|
+
import sharp from 'sharp';
|
|
17
18
|
import { Browser } from './browser.js';
|
|
18
19
|
import { API_BASE_URL_ENV_VAR, requireConfig } from './cli-config.js';
|
|
19
20
|
import { WebPlaywrightLocal } from './web-playwright-local.js';
|
|
@@ -22,6 +23,9 @@ import { RecoveryChainImpl } from './recovery-chain.js';
|
|
|
22
23
|
import { parseProgram } from './execution-schema.js';
|
|
23
24
|
import { buildCursorOverlayScript } from './cursor-overlay-script.js';
|
|
24
25
|
import { CLI_VERSION_HEADER, } from './cli-contract.js';
|
|
26
|
+
import { postProcessClipRecording } from './clip-postprocess.js';
|
|
27
|
+
import { applyDeviceFrame } from './mockup.js';
|
|
28
|
+
import { localizeStatusBar } from './status-bar-l10n.js';
|
|
25
29
|
import { logger } from './logger.js';
|
|
26
30
|
import { callLLM } from './llm-provider.js';
|
|
27
31
|
import { APP_VERSION } from './version.js';
|
|
@@ -175,6 +179,12 @@ export async function runCapture(options) {
|
|
|
175
179
|
}
|
|
176
180
|
logger.info(`[capture] Running preset "${options.presetId}" — ${program.steps.length} opcodes, ${program.variants.length} variant(s)`);
|
|
177
181
|
logger.info(`[capture] Resolved API origin ${resolvedProgram.security.expectedApiOrigin}; navigation scope: ${resolvedProgram.security.allowedNavigationOrigins.join(', ')}`);
|
|
182
|
+
if (program.mediaMode === 'clip' || program.mediaMode === 'video') {
|
|
183
|
+
const label = program.mediaMode === 'video' ? 'Demo video' : 'Clip';
|
|
184
|
+
logger.warn(`[capture] ${label} fluidity depends on this machine's CPU. For smoother output, ` +
|
|
185
|
+
`re-run this preset via Cloud Recapture (dashboard → Recapture, or ` +
|
|
186
|
+
`\`autokap auto-recapture --project <id> --cloud\`).`);
|
|
187
|
+
}
|
|
178
188
|
const llmConfig = resolveCliLLMConfig(resolvedProgram.security);
|
|
179
189
|
// Step 3: Set up recovery chain
|
|
180
190
|
const recoveryChain = new RecoveryChainImpl({
|
|
@@ -738,6 +748,9 @@ export function buildVideoClipMetadata(videoId, result, program, runId) {
|
|
|
738
748
|
timecodeStartMs: t.timecodeStartMs,
|
|
739
749
|
timecodeEndMs: t.timecodeEndMs,
|
|
740
750
|
bbox: t.bbox ?? null,
|
|
751
|
+
...(t.keystrokeOffsetsMs && t.keystrokeOffsetsMs.length > 0
|
|
752
|
+
? { keystrokeOffsetsMs: t.keystrokeOffsetsMs }
|
|
753
|
+
: {}),
|
|
741
754
|
}));
|
|
742
755
|
clipsByKey.set(`${variantId}:${artifact.clipId}`, {
|
|
743
756
|
variantId,
|
|
@@ -763,10 +776,82 @@ function inferVariantTheme(variantId) {
|
|
|
763
776
|
}
|
|
764
777
|
async function uploadArtifact(config, program, runId, totalArtifacts, uploadNumber, job) {
|
|
765
778
|
const { artifact, variant, variantSpec } = job;
|
|
766
|
-
const formData = new FormData();
|
|
767
779
|
const filename = buildArtifactFilename(program.presetId, variant.variantId, artifact);
|
|
768
780
|
const label = artifact.captureName ?? artifact.clipName ?? filename;
|
|
769
781
|
logger.info(`[capture] Exporting capture ${uploadNumber}/${totalArtifacts}: ${label}`);
|
|
782
|
+
if (process.env.AUTOKAP_USE_LEGACY_MULTIPART_UPLOADS === '1') {
|
|
783
|
+
await uploadArtifactMultipart(config, program, runId, job, filename);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const prepared = await prepareDirectArtifactUpload({
|
|
787
|
+
program,
|
|
788
|
+
runId,
|
|
789
|
+
artifact,
|
|
790
|
+
variant,
|
|
791
|
+
variantSpec,
|
|
792
|
+
});
|
|
793
|
+
const uploadsUrl = `${config.apiBaseUrl}/api/cli/artifacts/uploads`;
|
|
794
|
+
const initResponse = await fetch(uploadsUrl, {
|
|
795
|
+
method: 'POST',
|
|
796
|
+
headers: {
|
|
797
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
798
|
+
'Content-Type': 'application/json',
|
|
799
|
+
[CLI_VERSION_HEADER]: APP_VERSION,
|
|
800
|
+
},
|
|
801
|
+
body: JSON.stringify({ metadata: prepared.metadata }),
|
|
802
|
+
});
|
|
803
|
+
if (!initResponse.ok) {
|
|
804
|
+
throw new Error(`artifact upload init failed for ${variant.variantId}: ${await formatServerError(initResponse, uploadsUrl)}`);
|
|
805
|
+
}
|
|
806
|
+
const initPayload = (await initResponse.json());
|
|
807
|
+
const uploadTargets = new Map(initPayload.uploads.map((target) => [target.id, target]));
|
|
808
|
+
const uploadedObjects = [];
|
|
809
|
+
for (const part of prepared.parts) {
|
|
810
|
+
const target = uploadTargets.get(part.id);
|
|
811
|
+
if (!target) {
|
|
812
|
+
throw new Error(`artifact upload init did not return a target for ${part.id}`);
|
|
813
|
+
}
|
|
814
|
+
const uploadResponse = await fetch(target.signedUrl, {
|
|
815
|
+
method: target.method,
|
|
816
|
+
headers: {
|
|
817
|
+
'content-type': part.contentType,
|
|
818
|
+
'cache-control': 'max-age=3600',
|
|
819
|
+
},
|
|
820
|
+
body: new Uint8Array(part.buffer),
|
|
821
|
+
});
|
|
822
|
+
if (!uploadResponse.ok) {
|
|
823
|
+
throw new Error(`artifact storage upload failed for ${part.id}: ${await formatServerError(uploadResponse, target.signedUrl)}`);
|
|
824
|
+
}
|
|
825
|
+
uploadedObjects.push({
|
|
826
|
+
id: part.id,
|
|
827
|
+
bucket: target.bucket,
|
|
828
|
+
path: target.path,
|
|
829
|
+
contentType: target.contentType,
|
|
830
|
+
sizeBytes: part.buffer.length,
|
|
831
|
+
sha256: sha256Hex(part.buffer),
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
const completeUrl = `${config.apiBaseUrl}/api/cli/artifacts/complete`;
|
|
835
|
+
const completeResponse = await fetch(completeUrl, {
|
|
836
|
+
method: 'POST',
|
|
837
|
+
headers: {
|
|
838
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
839
|
+
'Content-Type': 'application/json',
|
|
840
|
+
[CLI_VERSION_HEADER]: APP_VERSION,
|
|
841
|
+
},
|
|
842
|
+
body: JSON.stringify({
|
|
843
|
+
uploadId: initPayload.uploadId,
|
|
844
|
+
metadata: prepared.metadata,
|
|
845
|
+
objects: uploadedObjects,
|
|
846
|
+
}),
|
|
847
|
+
});
|
|
848
|
+
if (!completeResponse.ok) {
|
|
849
|
+
throw new Error(`artifact completion failed for ${variant.variantId}: ${await formatServerError(completeResponse, completeUrl)}`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function uploadArtifactMultipart(config, program, runId, job, filename) {
|
|
853
|
+
const { artifact, variant, variantSpec } = job;
|
|
854
|
+
const formData = new FormData();
|
|
770
855
|
formData.append('file', new Blob([new Uint8Array(artifact.buffer)], { type: artifact.mimeType }), filename);
|
|
771
856
|
formData.append('presetId', program.presetId);
|
|
772
857
|
formData.append('programVersion', String(program.programVersion));
|
|
@@ -836,6 +921,237 @@ async function uploadArtifact(config, program, runId, totalArtifacts, uploadNumb
|
|
|
836
921
|
throw new Error(`artifact upload failed for ${variant.variantId}: ${await formatServerError(response, `${config.apiBaseUrl}/api/cli/artifacts`)}`);
|
|
837
922
|
}
|
|
838
923
|
}
|
|
924
|
+
async function prepareDirectArtifactUpload(params) {
|
|
925
|
+
const { program, runId, artifact, variant, variantSpec } = params;
|
|
926
|
+
const requestedDeviceScaleFactor = variantSpec?.deviceScaleFactor ?? program.outputScale ?? 2;
|
|
927
|
+
const isFrameCapture = artifact.mediaMode === 'clip' || artifact.mediaMode === 'video';
|
|
928
|
+
const deviceScaleFactor = isFrameCapture && Number.isFinite(requestedDeviceScaleFactor)
|
|
929
|
+
? Math.min(Number(requestedDeviceScaleFactor), MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR)
|
|
930
|
+
: requestedDeviceScaleFactor;
|
|
931
|
+
const tabIconSha256 = artifact.tabIconData ? sha256Hex(artifact.tabIconData) : null;
|
|
932
|
+
const metadata = {
|
|
933
|
+
presetId: program.presetId,
|
|
934
|
+
programVersion: program.programVersion,
|
|
935
|
+
compileFingerprint: program.compileFingerprint,
|
|
936
|
+
runId,
|
|
937
|
+
variantId: variant.variantId,
|
|
938
|
+
targetId: variantSpec?.targetId ?? variant.variantId,
|
|
939
|
+
targetLabel: variantSpec?.targetLabel ?? variantSpec?.deviceFrame ?? variant.variantId,
|
|
940
|
+
mediaMode: artifact.mediaMode,
|
|
941
|
+
mimeType: artifact.mimeType,
|
|
942
|
+
captureType: artifact.captureType ?? 'fullpage',
|
|
943
|
+
captureUrl: artifact.captureUrl ?? program.baseUrl,
|
|
944
|
+
lang: variantSpec?.locale ?? 'en',
|
|
945
|
+
theme: variantSpec?.theme ?? 'light',
|
|
946
|
+
deviceFrame: variantSpec?.deviceFrame ?? null,
|
|
947
|
+
deviceScaleFactor: Number.isFinite(deviceScaleFactor) ? Number(deviceScaleFactor) : null,
|
|
948
|
+
viewport: variantSpec?.viewport ?? null,
|
|
949
|
+
altText: artifact.altText ?? null,
|
|
950
|
+
elementSelector: artifact.elementSelector ?? null,
|
|
951
|
+
captureId: artifact.captureId ?? null,
|
|
952
|
+
captureName: artifact.captureName ?? null,
|
|
953
|
+
clipId: artifact.clipId ?? null,
|
|
954
|
+
clipName: artifact.clipName ?? null,
|
|
955
|
+
stepDescription: artifact.stepDescription ?? null,
|
|
956
|
+
stepIndex: typeof artifact.stepIndex === 'number' ? artifact.stepIndex : null,
|
|
957
|
+
durationMs: typeof artifact.durationMs === 'number' ? artifact.durationMs : null,
|
|
958
|
+
trimStartMs: typeof artifact.trimStartMs === 'number' ? artifact.trimStartMs : null,
|
|
959
|
+
artifactPlan: program.artifactPlan,
|
|
960
|
+
tabIconMimeType: artifact.tabIconData ? (artifact.tabIconMimeType ?? 'image/png') : null,
|
|
961
|
+
tabIconSha256,
|
|
962
|
+
};
|
|
963
|
+
const parts = await prepareDirectUploadParts({
|
|
964
|
+
metadata,
|
|
965
|
+
artifact,
|
|
966
|
+
program,
|
|
967
|
+
variantSpec,
|
|
968
|
+
});
|
|
969
|
+
return { metadata, parts };
|
|
970
|
+
}
|
|
971
|
+
async function prepareDirectUploadParts(params) {
|
|
972
|
+
const { metadata, artifact, program, variantSpec } = params;
|
|
973
|
+
if (artifact.mediaMode === 'screenshot') {
|
|
974
|
+
const finalScreenshot = await prepareScreenshotBufferForDirectUpload(artifact.buffer, metadata, program, variantSpec, artifact.tabIconData
|
|
975
|
+
? {
|
|
976
|
+
buffer: artifact.tabIconData,
|
|
977
|
+
mimeType: artifact.tabIconMimeType ?? 'image/png',
|
|
978
|
+
}
|
|
979
|
+
: null);
|
|
980
|
+
const parts = [
|
|
981
|
+
{ id: 'screenshotRaw', buffer: artifact.buffer, contentType: 'image/png' },
|
|
982
|
+
{ id: 'screenshot', buffer: finalScreenshot.buffer, contentType: finalScreenshot.contentType },
|
|
983
|
+
];
|
|
984
|
+
if (artifact.tabIconData) {
|
|
985
|
+
parts.push({
|
|
986
|
+
id: 'tabIcon',
|
|
987
|
+
buffer: artifact.tabIconData,
|
|
988
|
+
contentType: artifact.tabIconMimeType ?? 'image/png',
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
return parts;
|
|
992
|
+
}
|
|
993
|
+
if (artifact.mediaMode === 'clip') {
|
|
994
|
+
return prepareClipBuffersForDirectUpload(artifact, metadata);
|
|
995
|
+
}
|
|
996
|
+
return [
|
|
997
|
+
{
|
|
998
|
+
id: 'videoMp4',
|
|
999
|
+
buffer: artifact.buffer,
|
|
1000
|
+
contentType: 'video/mp4',
|
|
1001
|
+
},
|
|
1002
|
+
];
|
|
1003
|
+
}
|
|
1004
|
+
async function prepareScreenshotBufferForDirectUpload(input, metadata, program, variantSpec, tabIcon) {
|
|
1005
|
+
let output = input;
|
|
1006
|
+
const artifactPlan = resolveEffectiveCliArtifactPlan(program.artifactPlan, variantSpec?.deviceFrame ?? null);
|
|
1007
|
+
if (artifactPlan?.applyStatusBar && !artifactPlan?.applyMockup) {
|
|
1008
|
+
throw new Error('applyStatusBar requires applyMockup with a device frame');
|
|
1009
|
+
}
|
|
1010
|
+
if (artifactPlan?.applyMockup) {
|
|
1011
|
+
if (!variantSpec?.deviceFrame) {
|
|
1012
|
+
throw new Error('applyMockup requires a deviceFrame on the variant');
|
|
1013
|
+
}
|
|
1014
|
+
output = await applyDeviceFrame(output, variantSpec.deviceFrame, {
|
|
1015
|
+
orientation: inferCliOrientation(metadata.viewport ?? null),
|
|
1016
|
+
colorScheme: metadata.theme,
|
|
1017
|
+
outputScale: normalizeCliDeviceScaleFactor(metadata.deviceScaleFactor)
|
|
1018
|
+
?? normalizeCliDeviceScaleFactor(program.outputScale)
|
|
1019
|
+
?? 2,
|
|
1020
|
+
showStatusBar: artifactPlan.applyStatusBar ?? false,
|
|
1021
|
+
statusBar: localizeStatusBar({}, metadata.lang),
|
|
1022
|
+
browserBar: buildCliBrowserBar(metadata.captureUrl, metadata.theme, tabIcon),
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
if (artifactPlan?.format?.screenshotFormat === 'jpeg') {
|
|
1026
|
+
return {
|
|
1027
|
+
buffer: await sharp(output).jpeg({ quality: 90 }).toBuffer(),
|
|
1028
|
+
contentType: 'image/jpeg',
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
return { buffer: output, contentType: 'image/png' };
|
|
1032
|
+
}
|
|
1033
|
+
async function prepareClipBuffersForDirectUpload(artifact, metadata) {
|
|
1034
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'autokap-cli-direct-clip-'));
|
|
1035
|
+
try {
|
|
1036
|
+
const sourceExtension = mimeToExtension(artifact.mimeType);
|
|
1037
|
+
const sourcePath = path.join(tempDir, `source.${sourceExtension}`);
|
|
1038
|
+
await fs.writeFile(sourcePath, artifact.buffer);
|
|
1039
|
+
const clipId = sanitizeArtifactToken(metadata.clipId ?? `${metadata.variantId}-${metadata.stepIndex ?? 0}`);
|
|
1040
|
+
const postResult = await postProcessClipRecording(sourcePath, tempDir, `${clipId}-${sanitizeArtifactToken(metadata.variantId)}`, {
|
|
1041
|
+
format: metadata.artifactPlan?.format && typeof metadata.artifactPlan.format === 'object'
|
|
1042
|
+
? metadata.artifactPlan.format.clipFormat
|
|
1043
|
+
: undefined,
|
|
1044
|
+
maxDurationSec: typeof metadata.artifactPlan?.maxClipDurationSec === 'number'
|
|
1045
|
+
? metadata.artifactPlan.maxClipDurationSec
|
|
1046
|
+
: undefined,
|
|
1047
|
+
gifMaxWidth: metadata.viewport?.width
|
|
1048
|
+
? Math.min(1440, Math.round(metadata.viewport.width))
|
|
1049
|
+
: undefined,
|
|
1050
|
+
trimStartSec: Math.max(0, (metadata.trimStartMs ?? 0) / 1000),
|
|
1051
|
+
mp4Width: resolveClipMp4Size(metadata.viewport ?? null, metadata.deviceScaleFactor ?? null)?.width,
|
|
1052
|
+
mp4Height: resolveClipMp4Size(metadata.viewport ?? null, metadata.deviceScaleFactor ?? null)?.height,
|
|
1053
|
+
skipMp4Encode: artifact.mimeType === 'video/mp4',
|
|
1054
|
+
});
|
|
1055
|
+
metadata.durationMs = postResult.durationMs;
|
|
1056
|
+
const parts = [];
|
|
1057
|
+
if (postResult.gifPath) {
|
|
1058
|
+
parts.push({
|
|
1059
|
+
id: 'clipGif',
|
|
1060
|
+
buffer: await fs.readFile(postResult.gifPath),
|
|
1061
|
+
contentType: 'image/gif',
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
if (postResult.mp4Path) {
|
|
1065
|
+
parts.push({
|
|
1066
|
+
id: 'clipMp4',
|
|
1067
|
+
buffer: await fs.readFile(postResult.mp4Path),
|
|
1068
|
+
contentType: 'video/mp4',
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
if (postResult.thumbnailPath) {
|
|
1072
|
+
parts.push({
|
|
1073
|
+
id: 'clipThumbnail',
|
|
1074
|
+
buffer: await fs.readFile(postResult.thumbnailPath),
|
|
1075
|
+
contentType: 'image/png',
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
return parts;
|
|
1079
|
+
}
|
|
1080
|
+
finally {
|
|
1081
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
function resolveEffectiveCliArtifactPlan(artifactPlan, deviceFrame) {
|
|
1085
|
+
if (deviceFrame) {
|
|
1086
|
+
if (artifactPlan.applyMockup === false)
|
|
1087
|
+
return artifactPlan;
|
|
1088
|
+
return artifactPlan.applyMockup ? artifactPlan : { ...artifactPlan, applyMockup: true };
|
|
1089
|
+
}
|
|
1090
|
+
if (!artifactPlan.applyMockup && !artifactPlan.applyStatusBar)
|
|
1091
|
+
return artifactPlan;
|
|
1092
|
+
const { applyMockup: _applyMockup, applyStatusBar: _applyStatusBar, ...rest } = artifactPlan;
|
|
1093
|
+
return rest;
|
|
1094
|
+
}
|
|
1095
|
+
function inferCliOrientation(viewport) {
|
|
1096
|
+
if (!viewport)
|
|
1097
|
+
return undefined;
|
|
1098
|
+
return viewport.width >= viewport.height ? 'landscape' : 'portrait';
|
|
1099
|
+
}
|
|
1100
|
+
function normalizeCliDeviceScaleFactor(value) {
|
|
1101
|
+
if (!Number.isFinite(value))
|
|
1102
|
+
return null;
|
|
1103
|
+
return Math.max(0.5, Math.min(4, Number(value)));
|
|
1104
|
+
}
|
|
1105
|
+
function buildCliBrowserBar(captureUrl, colorScheme, tabIcon) {
|
|
1106
|
+
if (!captureUrl)
|
|
1107
|
+
return undefined;
|
|
1108
|
+
try {
|
|
1109
|
+
const parsed = new URL(captureUrl);
|
|
1110
|
+
return {
|
|
1111
|
+
url: captureUrl,
|
|
1112
|
+
pageTitle: parsed.hostname,
|
|
1113
|
+
colorScheme,
|
|
1114
|
+
tabIconUrl: tabIcon
|
|
1115
|
+
? `data:${tabIcon.mimeType};base64,${tabIcon.buffer.toString('base64')}`
|
|
1116
|
+
: `https://www.google.com/s2/favicons?domain=${parsed.hostname}&sz=64`,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
return {
|
|
1121
|
+
url: captureUrl,
|
|
1122
|
+
pageTitle: captureUrl,
|
|
1123
|
+
colorScheme,
|
|
1124
|
+
...(tabIcon
|
|
1125
|
+
? { tabIconUrl: `data:${tabIcon.mimeType};base64,${tabIcon.buffer.toString('base64')}` }
|
|
1126
|
+
: {}),
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
function resolveClipMp4Size(viewport, deviceScaleFactor) {
|
|
1131
|
+
if (!viewport)
|
|
1132
|
+
return null;
|
|
1133
|
+
const scale = normalizeCliDeviceScaleFactor(deviceScaleFactor) ?? 1;
|
|
1134
|
+
return {
|
|
1135
|
+
width: Math.max(2, Math.round(viewport.width * scale)) & ~1,
|
|
1136
|
+
height: Math.max(2, Math.round(viewport.height * scale)) & ~1,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function sha256Hex(buffer) {
|
|
1140
|
+
return createHash('sha256').update(buffer).digest('hex');
|
|
1141
|
+
}
|
|
1142
|
+
function mimeToExtension(mimeType) {
|
|
1143
|
+
if (mimeType.includes('jpeg'))
|
|
1144
|
+
return 'jpg';
|
|
1145
|
+
if (mimeType.includes('png'))
|
|
1146
|
+
return 'png';
|
|
1147
|
+
if (mimeType.includes('gif'))
|
|
1148
|
+
return 'gif';
|
|
1149
|
+
if (mimeType.includes('mp4'))
|
|
1150
|
+
return 'mp4';
|
|
1151
|
+
if (mimeType.includes('webm'))
|
|
1152
|
+
return 'webm';
|
|
1153
|
+
return 'bin';
|
|
1154
|
+
}
|
|
839
1155
|
async function runWithConcurrency(items, concurrency, worker) {
|
|
840
1156
|
if (items.length === 0) {
|
|
841
1157
|
return;
|
package/dist/cli.js
CHANGED
|
@@ -6,8 +6,9 @@ import fs from 'node:fs/promises';
|
|
|
6
6
|
const require = createRequire(import.meta.url);
|
|
7
7
|
const { version } = require('../package.json');
|
|
8
8
|
import { logger } from './logger.js';
|
|
9
|
-
import { writeConfig,
|
|
9
|
+
import { writeConfig, deleteConfig, requireConfig, getConfigPath, DEFAULT_API_BASE_URL, getDefaultApiBaseUrl, getDefaultWsUrl, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, } from './cli-config.js';
|
|
10
10
|
import { renderSkillSingleFile, writeSkillExport } from './skill-packaging.js';
|
|
11
|
+
import { displayNewVersionNoticeIfAvailable } from './version-check.js';
|
|
11
12
|
// ── Program definition ──────────────────────────────────────────────
|
|
12
13
|
export const program = new Command();
|
|
13
14
|
program
|
|
@@ -549,71 +550,6 @@ program
|
|
|
549
550
|
});
|
|
550
551
|
process.exit(0);
|
|
551
552
|
});
|
|
552
|
-
// ── crm-run command ────────────────────────────────────────────────
|
|
553
|
-
program
|
|
554
|
-
.command('crm-run')
|
|
555
|
-
.description('Scrape BetaList launches and feed the CRM (AUT-109 Phase B)')
|
|
556
|
-
.option('--runId <id>', 'CRM run id (defaults to AUTOKAP_RUN_ID env)')
|
|
557
|
-
.option('--lookback-days <n>', 'How far back to look for launches', '1')
|
|
558
|
-
.option('--debug', 'Verbose logging', false)
|
|
559
|
-
.action(async (opts) => {
|
|
560
|
-
// Log immediately on boot — before any other call — so Cloud Run logs
|
|
561
|
-
// show evidence the new command is reachable. If the next line silently
|
|
562
|
-
// crashes (missing dep, bad import) we at least see the boot.
|
|
563
|
-
logger.info(`[crm-run] booted — autokap CLI ${version}`);
|
|
564
|
-
if (opts.debug) {
|
|
565
|
-
const { setDebugEnabled } = await import('./logger.js');
|
|
566
|
-
setDebugEnabled(true);
|
|
567
|
-
logger.info('[crm-run] Debug mode enabled — verbose logging on');
|
|
568
|
-
}
|
|
569
|
-
// Self-kill after 20 min so a hung scraper exits before the backend
|
|
570
|
-
// reconcile cron (25 min) gets to it. Ensures the Cloud Run instance
|
|
571
|
-
// doesn't leak and the run row gets force-failed quickly.
|
|
572
|
-
const SELF_TIMEOUT_MS = 20 * 60 * 1000;
|
|
573
|
-
const selfTimeout = setTimeout(() => {
|
|
574
|
-
logger.error(`[crm-run] Self-kill: scraper exceeded ${SELF_TIMEOUT_MS / 60000}min budget — exiting non-zero`);
|
|
575
|
-
process.exit(1);
|
|
576
|
-
}, SELF_TIMEOUT_MS);
|
|
577
|
-
selfTimeout.unref?.();
|
|
578
|
-
const runToken = process.env.AUTOKAP_RUN_TOKEN?.trim();
|
|
579
|
-
const runId = opts.runId?.trim() || process.env.AUTOKAP_RUN_ID?.trim();
|
|
580
|
-
const config = await readConfig();
|
|
581
|
-
const apiBaseUrl = process.env.AUTOKAP_API_BASE_URL?.trim().replace(/\/+$/, '') || config?.apiBaseUrl;
|
|
582
|
-
logger.info(`[crm-run] env check — runId=${runId ?? '<missing>'} ` +
|
|
583
|
-
`apiBaseUrl=${apiBaseUrl ?? '<missing>'} ` +
|
|
584
|
-
`runToken=${runToken ? `${runToken.slice(0, 12)}…` : '<missing>'}`);
|
|
585
|
-
if (!runToken) {
|
|
586
|
-
fatal('[crm-run] Missing AUTOKAP_RUN_TOKEN');
|
|
587
|
-
}
|
|
588
|
-
if (!runId) {
|
|
589
|
-
fatal('[crm-run] Missing CRM run id. Set AUTOKAP_RUN_ID or pass --runId <id>.');
|
|
590
|
-
}
|
|
591
|
-
if (!apiBaseUrl) {
|
|
592
|
-
fatal('[crm-run] Missing API base URL. Set AUTOKAP_API_BASE_URL or run autokap init.');
|
|
593
|
-
}
|
|
594
|
-
const parsedLookback = Number.parseInt(opts.lookbackDays, 10);
|
|
595
|
-
const lookbackDays = Math.max(1, Math.min(7, Number.isFinite(parsedLookback) ? parsedLookback : 1));
|
|
596
|
-
logger.info(`[crm-run] starting campaign — lookbackDays=${lookbackDays}`);
|
|
597
|
-
try {
|
|
598
|
-
const { runCampaign } = await import('./crm/run-campaign.js');
|
|
599
|
-
const result = await runCampaign({
|
|
600
|
-
runId,
|
|
601
|
-
lookbackDays,
|
|
602
|
-
apiBaseUrl,
|
|
603
|
-
runToken,
|
|
604
|
-
logger,
|
|
605
|
-
});
|
|
606
|
-
clearTimeout(selfTimeout);
|
|
607
|
-
logger.success(`[crm-run] Done — scraped=${result.scraped} inserted=${result.inserted} ` +
|
|
608
|
-
`disqualified=${result.disqualified} skipped=${result.skipped}`);
|
|
609
|
-
process.exit(0);
|
|
610
|
-
}
|
|
611
|
-
catch (error) {
|
|
612
|
-
clearTimeout(selfTimeout);
|
|
613
|
-
logger.error(`[crm-run] Failed: ${error.message}`);
|
|
614
|
-
process.exit(1);
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
553
|
// ── project commands ───────────────────────────────────────────────
|
|
618
554
|
const projectCmd = program
|
|
619
555
|
.command('project')
|
|
@@ -1528,6 +1464,125 @@ proxyCmd
|
|
|
1528
1464
|
}
|
|
1529
1465
|
process.exit(0);
|
|
1530
1466
|
});
|
|
1467
|
+
// ── branding command ───────────────────────────────────────────────
|
|
1468
|
+
const brandingCmd = program
|
|
1469
|
+
.command('branding')
|
|
1470
|
+
.description('Manage project branding and design system');
|
|
1471
|
+
brandingCmd
|
|
1472
|
+
.command('import <file-path>')
|
|
1473
|
+
.description('Import a design.md file as the project design system')
|
|
1474
|
+
.requiredOption('--project <id>', 'Project ID')
|
|
1475
|
+
.option('--dry', 'Validate and extract tokens without persisting', false)
|
|
1476
|
+
.option('--json', 'JSON output for IDE assistants', false)
|
|
1477
|
+
.option('--force-light-only', 'Skip dark-mode AI derivation', false)
|
|
1478
|
+
.action(async (filePath, opts) => {
|
|
1479
|
+
const config = await requireConfig();
|
|
1480
|
+
const resolved = path.resolve(process.cwd(), filePath);
|
|
1481
|
+
let content;
|
|
1482
|
+
try {
|
|
1483
|
+
content = await fs.readFile(resolved, 'utf8');
|
|
1484
|
+
}
|
|
1485
|
+
catch (e) {
|
|
1486
|
+
fatal(`Cannot read file at ${resolved}: ${e.message}`);
|
|
1487
|
+
}
|
|
1488
|
+
if (content.length > 200_000) {
|
|
1489
|
+
fatal(`File too large (${content.length} bytes, max 200000).`);
|
|
1490
|
+
}
|
|
1491
|
+
if (content.trim().length === 0) {
|
|
1492
|
+
fatal(`File at ${resolved} is empty.`);
|
|
1493
|
+
}
|
|
1494
|
+
const result = await requestJson(config, `/api/cli/projects/${opts.project}/design-system/import`, {
|
|
1495
|
+
method: 'POST',
|
|
1496
|
+
headers: authHeaders(config, { 'Content-Type': 'application/json' }),
|
|
1497
|
+
body: JSON.stringify({
|
|
1498
|
+
source_markdown: content,
|
|
1499
|
+
source_path: filePath,
|
|
1500
|
+
options: {
|
|
1501
|
+
dry_run: opts.dry,
|
|
1502
|
+
force_light_only: opts.forceLightOnly,
|
|
1503
|
+
},
|
|
1504
|
+
}),
|
|
1505
|
+
}, 'Failed to import design.md');
|
|
1506
|
+
if (opts.json) {
|
|
1507
|
+
printJson(result);
|
|
1508
|
+
process.exit(0);
|
|
1509
|
+
}
|
|
1510
|
+
const ds = result.design_system;
|
|
1511
|
+
if (ds?.meta?.name) {
|
|
1512
|
+
logger.info(`Design system "${ds.meta.name}" imported for project ${opts.project}.`);
|
|
1513
|
+
}
|
|
1514
|
+
else {
|
|
1515
|
+
logger.info(`Design system imported for project ${opts.project}.`);
|
|
1516
|
+
}
|
|
1517
|
+
const lightColors = ds?.light?.colors ?? {};
|
|
1518
|
+
const colorKeys = Object.keys(lightColors);
|
|
1519
|
+
if (colorKeys.length > 0) {
|
|
1520
|
+
logger.info(` Colors: ${colorKeys.length} (${colorKeys.slice(0, 5).join(', ')}${colorKeys.length > 5 ? '…' : ''})`);
|
|
1521
|
+
}
|
|
1522
|
+
const titleFont = ds?.light?.typography?.title?.fontFamily;
|
|
1523
|
+
if (titleFont) {
|
|
1524
|
+
logger.info(` Title font: ${titleFont}`);
|
|
1525
|
+
}
|
|
1526
|
+
if (ds?.dark?.colors) {
|
|
1527
|
+
logger.info(` Dark mode: derived (${Object.keys(ds.dark.colors).length} colors)`);
|
|
1528
|
+
}
|
|
1529
|
+
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
1530
|
+
logger.info(` Warnings:`);
|
|
1531
|
+
for (const w of result.warnings) {
|
|
1532
|
+
logger.info(` - ${w}`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (opts.dry) {
|
|
1536
|
+
logger.info(` (dry-run: nothing persisted)`);
|
|
1537
|
+
}
|
|
1538
|
+
process.exit(0);
|
|
1539
|
+
});
|
|
1540
|
+
brandingCmd
|
|
1541
|
+
.command('export')
|
|
1542
|
+
.description('Export the project design system as design.md (or JSON)')
|
|
1543
|
+
.requiredOption('--project <id>', 'Project ID')
|
|
1544
|
+
.option('--out <path>', 'Write to file instead of stdout')
|
|
1545
|
+
.option('--format <fmt>', 'Output format: md or json', 'md')
|
|
1546
|
+
.action(async (opts) => {
|
|
1547
|
+
if (opts.format !== 'md' && opts.format !== 'json') {
|
|
1548
|
+
fatal('Invalid --format. Use "md" or "json".');
|
|
1549
|
+
}
|
|
1550
|
+
const config = await requireConfig();
|
|
1551
|
+
const searchParams = opts.format === 'json' ? new URLSearchParams({ format: 'json' }) : undefined;
|
|
1552
|
+
if (opts.format === 'json') {
|
|
1553
|
+
const payload = await requestJson(config, `/api/cli/projects/${opts.project}/design-system`, { headers: authHeaders(config) }, 'Failed to export design system', searchParams);
|
|
1554
|
+
const body = JSON.stringify(payload, null, 2);
|
|
1555
|
+
if (opts.out) {
|
|
1556
|
+
await fs.writeFile(path.resolve(process.cwd(), opts.out), body, 'utf8');
|
|
1557
|
+
logger.info(`Wrote design system JSON to ${opts.out}`);
|
|
1558
|
+
}
|
|
1559
|
+
else {
|
|
1560
|
+
process.stdout.write(`${body}\n`);
|
|
1561
|
+
}
|
|
1562
|
+
process.exit(0);
|
|
1563
|
+
}
|
|
1564
|
+
const result = await requestJson(config, `/api/cli/projects/${opts.project}/design-system`, { headers: authHeaders(config) }, 'Failed to export design system');
|
|
1565
|
+
if (opts.out) {
|
|
1566
|
+
await fs.writeFile(path.resolve(process.cwd(), opts.out), result.content, 'utf8');
|
|
1567
|
+
logger.info(`Wrote design.md to ${opts.out}`);
|
|
1568
|
+
}
|
|
1569
|
+
else {
|
|
1570
|
+
process.stdout.write(result.content);
|
|
1571
|
+
if (!result.content.endsWith('\n'))
|
|
1572
|
+
process.stdout.write('\n');
|
|
1573
|
+
}
|
|
1574
|
+
process.exit(0);
|
|
1575
|
+
});
|
|
1576
|
+
// ── doctor command ──────────────────────────────────────────────────
|
|
1577
|
+
program
|
|
1578
|
+
.command('doctor')
|
|
1579
|
+
.description('Check environment and dependencies (Node, Chromium, ffmpeg, config, skill, version)')
|
|
1580
|
+
.option('--fix', 'Attempt to auto-fix detected issues (Chromium install, skill reinstall)', false)
|
|
1581
|
+
.option('--agent <name>', 'Override agent for skill check: claude, codex, cursor, windsurf, copilot')
|
|
1582
|
+
.action(async (opts) => {
|
|
1583
|
+
const { runDoctor } = await import('./cli-doctor.js');
|
|
1584
|
+
await runDoctor(opts, version);
|
|
1585
|
+
});
|
|
1531
1586
|
// ── Entry point ─────────────────────────────────────────────────────
|
|
1532
1587
|
const resolvedArgv = process.argv[1] && fs.realpath(process.argv[1]).catch(() => process.argv[1]);
|
|
1533
1588
|
const isDirectExecution = resolvedArgv && await resolvedArgv.then(p => {
|
|
@@ -1535,6 +1590,7 @@ const isDirectExecution = resolvedArgv && await resolvedArgv.then(p => {
|
|
|
1535
1590
|
return base === 'cli.js' || base === 'cli.ts';
|
|
1536
1591
|
});
|
|
1537
1592
|
if (isDirectExecution) {
|
|
1593
|
+
await displayNewVersionNoticeIfAvailable(version);
|
|
1538
1594
|
program.parseAsync().catch(async (err) => {
|
|
1539
1595
|
logger.error(err.message);
|
|
1540
1596
|
process.exit(1);
|
|
@@ -693,6 +693,13 @@ export interface OpcodeTiming {
|
|
|
693
693
|
width: number;
|
|
694
694
|
height: number;
|
|
695
695
|
} | null;
|
|
696
|
+
/**
|
|
697
|
+
* For TYPE opcodes captured in clipCursor mode: timestamp (ms relative to the
|
|
698
|
+
* active clip start) of each individual keystroke produced by `humanType`.
|
|
699
|
+
* Drives keyboard SFX per-keystroke in the video compositor. Empty/undefined
|
|
700
|
+
* for non-TYPE opcodes and for typing paths that bypass humanType.
|
|
701
|
+
*/
|
|
702
|
+
keystrokeOffsetsMs?: number[];
|
|
696
703
|
}
|
|
697
704
|
export interface RunResult {
|
|
698
705
|
programId: string;
|
|
@@ -749,13 +756,23 @@ export interface RecordingResult {
|
|
|
749
756
|
mimeType: string;
|
|
750
757
|
trimStartMs?: number;
|
|
751
758
|
}
|
|
759
|
+
export interface TypeOptions {
|
|
760
|
+
/**
|
|
761
|
+
* Called once per keystroke produced by `humanType`, with the absolute
|
|
762
|
+
* wall-clock timestamp (`Date.now()`) of the keystroke. The runner converts
|
|
763
|
+
* those to clip-relative offsets stored on `OpcodeTiming.keystrokeOffsetsMs`
|
|
764
|
+
* so the compositor can fire per-keystroke SFX. Only fires in clipCursor
|
|
765
|
+
* mode (the only path that produces visible per-key animation).
|
|
766
|
+
*/
|
|
767
|
+
onKeystroke?: (timestampMs: number) => void;
|
|
768
|
+
}
|
|
752
769
|
export interface RuntimeAdapter {
|
|
753
770
|
navigate(url: string): Promise<void>;
|
|
754
771
|
getCurrentUrl(): Promise<string>;
|
|
755
772
|
getAKTree(): Promise<AKTree>;
|
|
756
773
|
getPageSignals(): Promise<VideoPageSignals>;
|
|
757
774
|
click(selector: string, options?: ClickOptions): Promise<void>;
|
|
758
|
-
type(selector: string, text: string, clearFirst?: boolean): Promise<void>;
|
|
775
|
+
type(selector: string, text: string, clearFirst?: boolean, opts?: TypeOptions): Promise<void>;
|
|
759
776
|
pressKey(key: string): Promise<void>;
|
|
760
777
|
scroll(direction: 'up' | 'down' | 'left' | 'right', amount?: number): Promise<void>;
|
|
761
778
|
scrollIntoView(selector: string): Promise<void>;
|
|
@@ -808,7 +825,7 @@ export interface RuntimeAdapter {
|
|
|
808
825
|
selector?: string;
|
|
809
826
|
target?: SemanticTarget;
|
|
810
827
|
selectorAlternates?: string[];
|
|
811
|
-
}, text: string, clearFirst?: boolean): Promise<void>;
|
|
828
|
+
}, text: string, clearFirst?: boolean, typeOpts?: TypeOptions): Promise<void>;
|
|
812
829
|
/** Wait for an element by semantic target. */
|
|
813
830
|
waitForTarget?(opts: {
|
|
814
831
|
selector?: string;
|
|
@@ -52,8 +52,14 @@ export declare function animatedHover(page: Page, target: {
|
|
|
52
52
|
/**
|
|
53
53
|
* Type text into the currently focused element at a human-like typing speed.
|
|
54
54
|
* Assumes the field is already focused (via a preceding click).
|
|
55
|
+
*
|
|
56
|
+
* `onKeystroke` fires after each character with the absolute wall-clock
|
|
57
|
+
* timestamp (`Date.now()`) of the keystroke. The video pipeline converts
|
|
58
|
+
* these to clip-relative offsets so keyboard SFX fire in lock-step with the
|
|
59
|
+
* visible typing.
|
|
55
60
|
*/
|
|
56
61
|
export declare function humanType(page: Page, text: string, options?: {
|
|
57
62
|
minDelayMs?: number;
|
|
58
63
|
maxDelayMs?: number;
|
|
64
|
+
onKeystroke?: (timestampMs: number) => void;
|
|
59
65
|
}): Promise<void>;
|
package/dist/mouse-animation.js
CHANGED
|
@@ -132,12 +132,20 @@ export async function animatedHover(page, target, fromCurrent, options = {}) {
|
|
|
132
132
|
/**
|
|
133
133
|
* Type text into the currently focused element at a human-like typing speed.
|
|
134
134
|
* Assumes the field is already focused (via a preceding click).
|
|
135
|
+
*
|
|
136
|
+
* `onKeystroke` fires after each character with the absolute wall-clock
|
|
137
|
+
* timestamp (`Date.now()`) of the keystroke. The video pipeline converts
|
|
138
|
+
* these to clip-relative offsets so keyboard SFX fire in lock-step with the
|
|
139
|
+
* visible typing.
|
|
135
140
|
*/
|
|
136
141
|
export async function humanType(page, text, options = {}) {
|
|
137
142
|
const minDelay = Math.max(0, options.minDelayMs ?? 60);
|
|
138
143
|
const maxDelay = Math.max(minDelay, options.maxDelayMs ?? 140);
|
|
139
144
|
for (const char of text) {
|
|
140
145
|
await page.keyboard.type(char);
|
|
146
|
+
if (options.onKeystroke) {
|
|
147
|
+
options.onKeystroke(Date.now());
|
|
148
|
+
}
|
|
141
149
|
// 60–120 WPM → ~80–130ms between characters (5 chars per word)
|
|
142
150
|
const delay = minDelay + Math.random() * (maxDelay - minDelay);
|
|
143
151
|
if (delay > 0) {
|
package/dist/opcode-actions.d.ts
CHANGED
|
@@ -40,5 +40,11 @@ export declare function findUnresolvedCredentialPlaceholders(text: string, crede
|
|
|
40
40
|
export interface OpcodeActionResult {
|
|
41
41
|
success: boolean;
|
|
42
42
|
error?: string;
|
|
43
|
+
/**
|
|
44
|
+
* For TYPE opcodes: absolute wall-clock timestamps (`Date.now()`) of each
|
|
45
|
+
* keystroke produced by `humanType`. The runner converts these to
|
|
46
|
+
* clip-relative offsets so the video compositor can fire per-keystroke SFX.
|
|
47
|
+
*/
|
|
48
|
+
keystrokeTimestampsMs?: number[];
|
|
43
49
|
}
|
|
44
50
|
export declare function executeOpcodeCoreAction(opcode: ExecutionOpcode, adapter: RuntimeAdapter, context?: OpcodeActionContext): Promise<OpcodeActionResult>;
|