autokap 1.4.3 → 1.5.1
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 +73 -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 +315 -2
- package/dist/cli.js +122 -66
- package/dist/types.d.ts +1 -1
- package/dist/version-check.d.ts +4 -0
- package/dist/version-check.js +102 -0
- 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({
|
|
@@ -763,10 +773,82 @@ function inferVariantTheme(variantId) {
|
|
|
763
773
|
}
|
|
764
774
|
async function uploadArtifact(config, program, runId, totalArtifacts, uploadNumber, job) {
|
|
765
775
|
const { artifact, variant, variantSpec } = job;
|
|
766
|
-
const formData = new FormData();
|
|
767
776
|
const filename = buildArtifactFilename(program.presetId, variant.variantId, artifact);
|
|
768
777
|
const label = artifact.captureName ?? artifact.clipName ?? filename;
|
|
769
778
|
logger.info(`[capture] Exporting capture ${uploadNumber}/${totalArtifacts}: ${label}`);
|
|
779
|
+
if (process.env.AUTOKAP_USE_LEGACY_MULTIPART_UPLOADS === '1') {
|
|
780
|
+
await uploadArtifactMultipart(config, program, runId, job, filename);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const prepared = await prepareDirectArtifactUpload({
|
|
784
|
+
program,
|
|
785
|
+
runId,
|
|
786
|
+
artifact,
|
|
787
|
+
variant,
|
|
788
|
+
variantSpec,
|
|
789
|
+
});
|
|
790
|
+
const uploadsUrl = `${config.apiBaseUrl}/api/cli/artifacts/uploads`;
|
|
791
|
+
const initResponse = await fetch(uploadsUrl, {
|
|
792
|
+
method: 'POST',
|
|
793
|
+
headers: {
|
|
794
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
795
|
+
'Content-Type': 'application/json',
|
|
796
|
+
[CLI_VERSION_HEADER]: APP_VERSION,
|
|
797
|
+
},
|
|
798
|
+
body: JSON.stringify({ metadata: prepared.metadata }),
|
|
799
|
+
});
|
|
800
|
+
if (!initResponse.ok) {
|
|
801
|
+
throw new Error(`artifact upload init failed for ${variant.variantId}: ${await formatServerError(initResponse, uploadsUrl)}`);
|
|
802
|
+
}
|
|
803
|
+
const initPayload = (await initResponse.json());
|
|
804
|
+
const uploadTargets = new Map(initPayload.uploads.map((target) => [target.id, target]));
|
|
805
|
+
const uploadedObjects = [];
|
|
806
|
+
for (const part of prepared.parts) {
|
|
807
|
+
const target = uploadTargets.get(part.id);
|
|
808
|
+
if (!target) {
|
|
809
|
+
throw new Error(`artifact upload init did not return a target for ${part.id}`);
|
|
810
|
+
}
|
|
811
|
+
const uploadResponse = await fetch(target.signedUrl, {
|
|
812
|
+
method: target.method,
|
|
813
|
+
headers: {
|
|
814
|
+
'content-type': part.contentType,
|
|
815
|
+
'cache-control': 'max-age=3600',
|
|
816
|
+
},
|
|
817
|
+
body: new Uint8Array(part.buffer),
|
|
818
|
+
});
|
|
819
|
+
if (!uploadResponse.ok) {
|
|
820
|
+
throw new Error(`artifact storage upload failed for ${part.id}: ${await formatServerError(uploadResponse, target.signedUrl)}`);
|
|
821
|
+
}
|
|
822
|
+
uploadedObjects.push({
|
|
823
|
+
id: part.id,
|
|
824
|
+
bucket: target.bucket,
|
|
825
|
+
path: target.path,
|
|
826
|
+
contentType: target.contentType,
|
|
827
|
+
sizeBytes: part.buffer.length,
|
|
828
|
+
sha256: sha256Hex(part.buffer),
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
const completeUrl = `${config.apiBaseUrl}/api/cli/artifacts/complete`;
|
|
832
|
+
const completeResponse = await fetch(completeUrl, {
|
|
833
|
+
method: 'POST',
|
|
834
|
+
headers: {
|
|
835
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
836
|
+
'Content-Type': 'application/json',
|
|
837
|
+
[CLI_VERSION_HEADER]: APP_VERSION,
|
|
838
|
+
},
|
|
839
|
+
body: JSON.stringify({
|
|
840
|
+
uploadId: initPayload.uploadId,
|
|
841
|
+
metadata: prepared.metadata,
|
|
842
|
+
objects: uploadedObjects,
|
|
843
|
+
}),
|
|
844
|
+
});
|
|
845
|
+
if (!completeResponse.ok) {
|
|
846
|
+
throw new Error(`artifact completion failed for ${variant.variantId}: ${await formatServerError(completeResponse, completeUrl)}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function uploadArtifactMultipart(config, program, runId, job, filename) {
|
|
850
|
+
const { artifact, variant, variantSpec } = job;
|
|
851
|
+
const formData = new FormData();
|
|
770
852
|
formData.append('file', new Blob([new Uint8Array(artifact.buffer)], { type: artifact.mimeType }), filename);
|
|
771
853
|
formData.append('presetId', program.presetId);
|
|
772
854
|
formData.append('programVersion', String(program.programVersion));
|
|
@@ -836,6 +918,237 @@ async function uploadArtifact(config, program, runId, totalArtifacts, uploadNumb
|
|
|
836
918
|
throw new Error(`artifact upload failed for ${variant.variantId}: ${await formatServerError(response, `${config.apiBaseUrl}/api/cli/artifacts`)}`);
|
|
837
919
|
}
|
|
838
920
|
}
|
|
921
|
+
async function prepareDirectArtifactUpload(params) {
|
|
922
|
+
const { program, runId, artifact, variant, variantSpec } = params;
|
|
923
|
+
const requestedDeviceScaleFactor = variantSpec?.deviceScaleFactor ?? program.outputScale ?? 2;
|
|
924
|
+
const isFrameCapture = artifact.mediaMode === 'clip' || artifact.mediaMode === 'video';
|
|
925
|
+
const deviceScaleFactor = isFrameCapture && Number.isFinite(requestedDeviceScaleFactor)
|
|
926
|
+
? Math.min(Number(requestedDeviceScaleFactor), MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR)
|
|
927
|
+
: requestedDeviceScaleFactor;
|
|
928
|
+
const tabIconSha256 = artifact.tabIconData ? sha256Hex(artifact.tabIconData) : null;
|
|
929
|
+
const metadata = {
|
|
930
|
+
presetId: program.presetId,
|
|
931
|
+
programVersion: program.programVersion,
|
|
932
|
+
compileFingerprint: program.compileFingerprint,
|
|
933
|
+
runId,
|
|
934
|
+
variantId: variant.variantId,
|
|
935
|
+
targetId: variantSpec?.targetId ?? variant.variantId,
|
|
936
|
+
targetLabel: variantSpec?.targetLabel ?? variantSpec?.deviceFrame ?? variant.variantId,
|
|
937
|
+
mediaMode: artifact.mediaMode,
|
|
938
|
+
mimeType: artifact.mimeType,
|
|
939
|
+
captureType: artifact.captureType ?? 'fullpage',
|
|
940
|
+
captureUrl: artifact.captureUrl ?? program.baseUrl,
|
|
941
|
+
lang: variantSpec?.locale ?? 'en',
|
|
942
|
+
theme: variantSpec?.theme ?? 'light',
|
|
943
|
+
deviceFrame: variantSpec?.deviceFrame ?? null,
|
|
944
|
+
deviceScaleFactor: Number.isFinite(deviceScaleFactor) ? Number(deviceScaleFactor) : null,
|
|
945
|
+
viewport: variantSpec?.viewport ?? null,
|
|
946
|
+
altText: artifact.altText ?? null,
|
|
947
|
+
elementSelector: artifact.elementSelector ?? null,
|
|
948
|
+
captureId: artifact.captureId ?? null,
|
|
949
|
+
captureName: artifact.captureName ?? null,
|
|
950
|
+
clipId: artifact.clipId ?? null,
|
|
951
|
+
clipName: artifact.clipName ?? null,
|
|
952
|
+
stepDescription: artifact.stepDescription ?? null,
|
|
953
|
+
stepIndex: typeof artifact.stepIndex === 'number' ? artifact.stepIndex : null,
|
|
954
|
+
durationMs: typeof artifact.durationMs === 'number' ? artifact.durationMs : null,
|
|
955
|
+
trimStartMs: typeof artifact.trimStartMs === 'number' ? artifact.trimStartMs : null,
|
|
956
|
+
artifactPlan: program.artifactPlan,
|
|
957
|
+
tabIconMimeType: artifact.tabIconData ? (artifact.tabIconMimeType ?? 'image/png') : null,
|
|
958
|
+
tabIconSha256,
|
|
959
|
+
};
|
|
960
|
+
const parts = await prepareDirectUploadParts({
|
|
961
|
+
metadata,
|
|
962
|
+
artifact,
|
|
963
|
+
program,
|
|
964
|
+
variantSpec,
|
|
965
|
+
});
|
|
966
|
+
return { metadata, parts };
|
|
967
|
+
}
|
|
968
|
+
async function prepareDirectUploadParts(params) {
|
|
969
|
+
const { metadata, artifact, program, variantSpec } = params;
|
|
970
|
+
if (artifact.mediaMode === 'screenshot') {
|
|
971
|
+
const finalScreenshot = await prepareScreenshotBufferForDirectUpload(artifact.buffer, metadata, program, variantSpec, artifact.tabIconData
|
|
972
|
+
? {
|
|
973
|
+
buffer: artifact.tabIconData,
|
|
974
|
+
mimeType: artifact.tabIconMimeType ?? 'image/png',
|
|
975
|
+
}
|
|
976
|
+
: null);
|
|
977
|
+
const parts = [
|
|
978
|
+
{ id: 'screenshotRaw', buffer: artifact.buffer, contentType: 'image/png' },
|
|
979
|
+
{ id: 'screenshot', buffer: finalScreenshot.buffer, contentType: finalScreenshot.contentType },
|
|
980
|
+
];
|
|
981
|
+
if (artifact.tabIconData) {
|
|
982
|
+
parts.push({
|
|
983
|
+
id: 'tabIcon',
|
|
984
|
+
buffer: artifact.tabIconData,
|
|
985
|
+
contentType: artifact.tabIconMimeType ?? 'image/png',
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
return parts;
|
|
989
|
+
}
|
|
990
|
+
if (artifact.mediaMode === 'clip') {
|
|
991
|
+
return prepareClipBuffersForDirectUpload(artifact, metadata);
|
|
992
|
+
}
|
|
993
|
+
return [
|
|
994
|
+
{
|
|
995
|
+
id: 'videoMp4',
|
|
996
|
+
buffer: artifact.buffer,
|
|
997
|
+
contentType: 'video/mp4',
|
|
998
|
+
},
|
|
999
|
+
];
|
|
1000
|
+
}
|
|
1001
|
+
async function prepareScreenshotBufferForDirectUpload(input, metadata, program, variantSpec, tabIcon) {
|
|
1002
|
+
let output = input;
|
|
1003
|
+
const artifactPlan = resolveEffectiveCliArtifactPlan(program.artifactPlan, variantSpec?.deviceFrame ?? null);
|
|
1004
|
+
if (artifactPlan?.applyStatusBar && !artifactPlan?.applyMockup) {
|
|
1005
|
+
throw new Error('applyStatusBar requires applyMockup with a device frame');
|
|
1006
|
+
}
|
|
1007
|
+
if (artifactPlan?.applyMockup) {
|
|
1008
|
+
if (!variantSpec?.deviceFrame) {
|
|
1009
|
+
throw new Error('applyMockup requires a deviceFrame on the variant');
|
|
1010
|
+
}
|
|
1011
|
+
output = await applyDeviceFrame(output, variantSpec.deviceFrame, {
|
|
1012
|
+
orientation: inferCliOrientation(metadata.viewport ?? null),
|
|
1013
|
+
colorScheme: metadata.theme,
|
|
1014
|
+
outputScale: normalizeCliDeviceScaleFactor(metadata.deviceScaleFactor)
|
|
1015
|
+
?? normalizeCliDeviceScaleFactor(program.outputScale)
|
|
1016
|
+
?? 2,
|
|
1017
|
+
showStatusBar: artifactPlan.applyStatusBar ?? false,
|
|
1018
|
+
statusBar: localizeStatusBar({}, metadata.lang),
|
|
1019
|
+
browserBar: buildCliBrowserBar(metadata.captureUrl, metadata.theme, tabIcon),
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
if (artifactPlan?.format?.screenshotFormat === 'jpeg') {
|
|
1023
|
+
return {
|
|
1024
|
+
buffer: await sharp(output).jpeg({ quality: 90 }).toBuffer(),
|
|
1025
|
+
contentType: 'image/jpeg',
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
return { buffer: output, contentType: 'image/png' };
|
|
1029
|
+
}
|
|
1030
|
+
async function prepareClipBuffersForDirectUpload(artifact, metadata) {
|
|
1031
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'autokap-cli-direct-clip-'));
|
|
1032
|
+
try {
|
|
1033
|
+
const sourceExtension = mimeToExtension(artifact.mimeType);
|
|
1034
|
+
const sourcePath = path.join(tempDir, `source.${sourceExtension}`);
|
|
1035
|
+
await fs.writeFile(sourcePath, artifact.buffer);
|
|
1036
|
+
const clipId = sanitizeArtifactToken(metadata.clipId ?? `${metadata.variantId}-${metadata.stepIndex ?? 0}`);
|
|
1037
|
+
const postResult = await postProcessClipRecording(sourcePath, tempDir, `${clipId}-${sanitizeArtifactToken(metadata.variantId)}`, {
|
|
1038
|
+
format: metadata.artifactPlan?.format && typeof metadata.artifactPlan.format === 'object'
|
|
1039
|
+
? metadata.artifactPlan.format.clipFormat
|
|
1040
|
+
: undefined,
|
|
1041
|
+
maxDurationSec: typeof metadata.artifactPlan?.maxClipDurationSec === 'number'
|
|
1042
|
+
? metadata.artifactPlan.maxClipDurationSec
|
|
1043
|
+
: undefined,
|
|
1044
|
+
gifMaxWidth: metadata.viewport?.width
|
|
1045
|
+
? Math.min(1440, Math.round(metadata.viewport.width))
|
|
1046
|
+
: undefined,
|
|
1047
|
+
trimStartSec: Math.max(0, (metadata.trimStartMs ?? 0) / 1000),
|
|
1048
|
+
mp4Width: resolveClipMp4Size(metadata.viewport ?? null, metadata.deviceScaleFactor ?? null)?.width,
|
|
1049
|
+
mp4Height: resolveClipMp4Size(metadata.viewport ?? null, metadata.deviceScaleFactor ?? null)?.height,
|
|
1050
|
+
skipMp4Encode: artifact.mimeType === 'video/mp4',
|
|
1051
|
+
});
|
|
1052
|
+
metadata.durationMs = postResult.durationMs;
|
|
1053
|
+
const parts = [];
|
|
1054
|
+
if (postResult.gifPath) {
|
|
1055
|
+
parts.push({
|
|
1056
|
+
id: 'clipGif',
|
|
1057
|
+
buffer: await fs.readFile(postResult.gifPath),
|
|
1058
|
+
contentType: 'image/gif',
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
if (postResult.mp4Path) {
|
|
1062
|
+
parts.push({
|
|
1063
|
+
id: 'clipMp4',
|
|
1064
|
+
buffer: await fs.readFile(postResult.mp4Path),
|
|
1065
|
+
contentType: 'video/mp4',
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
if (postResult.thumbnailPath) {
|
|
1069
|
+
parts.push({
|
|
1070
|
+
id: 'clipThumbnail',
|
|
1071
|
+
buffer: await fs.readFile(postResult.thumbnailPath),
|
|
1072
|
+
contentType: 'image/png',
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
return parts;
|
|
1076
|
+
}
|
|
1077
|
+
finally {
|
|
1078
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function resolveEffectiveCliArtifactPlan(artifactPlan, deviceFrame) {
|
|
1082
|
+
if (deviceFrame) {
|
|
1083
|
+
if (artifactPlan.applyMockup === false)
|
|
1084
|
+
return artifactPlan;
|
|
1085
|
+
return artifactPlan.applyMockup ? artifactPlan : { ...artifactPlan, applyMockup: true };
|
|
1086
|
+
}
|
|
1087
|
+
if (!artifactPlan.applyMockup && !artifactPlan.applyStatusBar)
|
|
1088
|
+
return artifactPlan;
|
|
1089
|
+
const { applyMockup: _applyMockup, applyStatusBar: _applyStatusBar, ...rest } = artifactPlan;
|
|
1090
|
+
return rest;
|
|
1091
|
+
}
|
|
1092
|
+
function inferCliOrientation(viewport) {
|
|
1093
|
+
if (!viewport)
|
|
1094
|
+
return undefined;
|
|
1095
|
+
return viewport.width >= viewport.height ? 'landscape' : 'portrait';
|
|
1096
|
+
}
|
|
1097
|
+
function normalizeCliDeviceScaleFactor(value) {
|
|
1098
|
+
if (!Number.isFinite(value))
|
|
1099
|
+
return null;
|
|
1100
|
+
return Math.max(0.5, Math.min(4, Number(value)));
|
|
1101
|
+
}
|
|
1102
|
+
function buildCliBrowserBar(captureUrl, colorScheme, tabIcon) {
|
|
1103
|
+
if (!captureUrl)
|
|
1104
|
+
return undefined;
|
|
1105
|
+
try {
|
|
1106
|
+
const parsed = new URL(captureUrl);
|
|
1107
|
+
return {
|
|
1108
|
+
url: captureUrl,
|
|
1109
|
+
pageTitle: parsed.hostname,
|
|
1110
|
+
colorScheme,
|
|
1111
|
+
tabIconUrl: tabIcon
|
|
1112
|
+
? `data:${tabIcon.mimeType};base64,${tabIcon.buffer.toString('base64')}`
|
|
1113
|
+
: `https://www.google.com/s2/favicons?domain=${parsed.hostname}&sz=64`,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
return {
|
|
1118
|
+
url: captureUrl,
|
|
1119
|
+
pageTitle: captureUrl,
|
|
1120
|
+
colorScheme,
|
|
1121
|
+
...(tabIcon
|
|
1122
|
+
? { tabIconUrl: `data:${tabIcon.mimeType};base64,${tabIcon.buffer.toString('base64')}` }
|
|
1123
|
+
: {}),
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function resolveClipMp4Size(viewport, deviceScaleFactor) {
|
|
1128
|
+
if (!viewport)
|
|
1129
|
+
return null;
|
|
1130
|
+
const scale = normalizeCliDeviceScaleFactor(deviceScaleFactor) ?? 1;
|
|
1131
|
+
return {
|
|
1132
|
+
width: Math.max(2, Math.round(viewport.width * scale)) & ~1,
|
|
1133
|
+
height: Math.max(2, Math.round(viewport.height * scale)) & ~1,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function sha256Hex(buffer) {
|
|
1137
|
+
return createHash('sha256').update(buffer).digest('hex');
|
|
1138
|
+
}
|
|
1139
|
+
function mimeToExtension(mimeType) {
|
|
1140
|
+
if (mimeType.includes('jpeg'))
|
|
1141
|
+
return 'jpg';
|
|
1142
|
+
if (mimeType.includes('png'))
|
|
1143
|
+
return 'png';
|
|
1144
|
+
if (mimeType.includes('gif'))
|
|
1145
|
+
return 'gif';
|
|
1146
|
+
if (mimeType.includes('mp4'))
|
|
1147
|
+
return 'mp4';
|
|
1148
|
+
if (mimeType.includes('webm'))
|
|
1149
|
+
return 'webm';
|
|
1150
|
+
return 'bin';
|
|
1151
|
+
}
|
|
839
1152
|
async function runWithConcurrency(items, concurrency, worker) {
|
|
840
1153
|
if (items.length === 0) {
|
|
841
1154
|
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);
|
package/dist/types.d.ts
CHANGED
|
@@ -583,7 +583,7 @@ export interface ClipOptions {
|
|
|
583
583
|
/** Usage metadata from a single OpenRouter API call */
|
|
584
584
|
export interface StepUsage {
|
|
585
585
|
stepNumber: number;
|
|
586
|
-
stepType: 'agent_iteration' | 'verification' | 'element_capture' | 'video_planning' | 'video_variant_classification' | 'video_step_verification' | 'video_step_fix' | 'assistant_chat' | 'studio_creation' | 'studio_iteration' | 'studio_capture_suggestion' | 'mock_data_generation' | 'page_identity_classification' | 'capture_verification' | 'alt_text_generation' | 'healer_invocation' | 'cron_feedback_classification' | 'tts_generation'
|
|
586
|
+
stepType: 'agent_iteration' | 'verification' | 'element_capture' | 'video_planning' | 'video_variant_classification' | 'video_step_verification' | 'video_step_fix' | 'assistant_chat' | 'studio_creation' | 'studio_iteration' | 'studio_capture_suggestion' | 'mock_data_generation' | 'page_identity_classification' | 'capture_verification' | 'alt_text_generation' | 'healer_invocation' | 'cron_feedback_classification' | 'tts_generation';
|
|
587
587
|
generationId: string | null;
|
|
588
588
|
modelRequested: string;
|
|
589
589
|
modelUsed: string | null;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function fetchLatestVersionFromRegistry(): Promise<string | null>;
|
|
2
|
+
export declare function isNewerVersion(latest: string, current: string): boolean;
|
|
3
|
+
export declare function getCachedOrFetchLatest(): Promise<string | null>;
|
|
4
|
+
export declare function displayNewVersionNoticeIfAvailable(currentVersion: string): Promise<void>;
|