@spark-apps/quickpeek 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -204
- package/dist/highlight.css +15 -15
- package/dist/index.d.mts +79 -3
- package/dist/index.mjs +254 -52
- package/dist/mcp-tools.mjs +1295 -603
- package/dist/qp.js +1272 -729
- package/dist/{scraping-CHN4ClyT.d.mts → scraping-C334VxX5.d.mts} +45 -4
- package/dist/web.d.mts +2 -2
- package/dist/web.mjs +64 -24
- package/mcp.mjs +5 -1
- package/package.json +1 -1
package/dist/mcp-tools.mjs
CHANGED
|
@@ -110,6 +110,20 @@ var init_utils = __esm({
|
|
|
110
110
|
function apiKey() {
|
|
111
111
|
return process.env.SPARK_AI_KEY || "";
|
|
112
112
|
}
|
|
113
|
+
function hostOf2(url) {
|
|
114
|
+
try {
|
|
115
|
+
return new URL(url).host;
|
|
116
|
+
} catch {
|
|
117
|
+
return "AI service";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function firstLine(text) {
|
|
121
|
+
return truncate((text ?? "").trim().split("\n")[0] ?? "", 200);
|
|
122
|
+
}
|
|
123
|
+
function truncate(text, max) {
|
|
124
|
+
const trimmed = (text ?? "").trim();
|
|
125
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}\u2026` : trimmed;
|
|
126
|
+
}
|
|
113
127
|
async function getOrRefreshDirectToken() {
|
|
114
128
|
const now = Date.now();
|
|
115
129
|
if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
|
|
@@ -161,8 +175,10 @@ async function getOrRefreshRelayToken(email) {
|
|
|
161
175
|
throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
|
|
162
176
|
}
|
|
163
177
|
}
|
|
164
|
-
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
|
|
165
|
-
|
|
178
|
+
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier, opts = {}) {
|
|
179
|
+
const { onRetry, backoffMs = SERVER_BACKOFF_MS } = opts;
|
|
180
|
+
let lastRetriableError = null;
|
|
181
|
+
const host = hostOf2(chatUrl);
|
|
166
182
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
167
183
|
try {
|
|
168
184
|
const response = await fetch(chatUrl, {
|
|
@@ -178,36 +194,53 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
178
194
|
]
|
|
179
195
|
})
|
|
180
196
|
});
|
|
197
|
+
const bodyText = await response.text().catch(() => "");
|
|
181
198
|
let data = {};
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
199
|
+
try {
|
|
200
|
+
data = bodyText ? JSON.parse(bodyText) : {};
|
|
201
|
+
} catch {
|
|
185
202
|
}
|
|
186
203
|
if (!response.ok) {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
204
|
+
const upstream = data.error || firstLine(bodyText) || response.statusText || "no error body";
|
|
205
|
+
const error = `${host} ${response.status}: ${upstream}`;
|
|
206
|
+
const detail = truncate(bodyText, 800);
|
|
207
|
+
const isRetriable = response.status === 429 || response.status >= 500;
|
|
208
|
+
if (isRetriable && attempt < 3) {
|
|
209
|
+
const retryAfter2 = response.headers.get("Retry-After");
|
|
210
|
+
const delayMs = retryAfter2 ? parseInt(retryAfter2, 10) * 1e3 : response.status >= 500 ? backoffMs[attempt - 1] ?? backoffMs[backoffMs.length - 1] ?? 2e3 : 1e3 * 2 ** (attempt - 1);
|
|
211
|
+
onRetry?.({ attempt, status: response.status, delayMs, host });
|
|
212
|
+
lastRetriableError = {
|
|
192
213
|
success: false,
|
|
193
214
|
error,
|
|
194
|
-
isRateLimit:
|
|
195
|
-
|
|
215
|
+
isRateLimit: response.status === 429,
|
|
216
|
+
status: response.status,
|
|
217
|
+
host,
|
|
218
|
+
...detail ? { detail } : {},
|
|
219
|
+
...retryAfter2 ? { retryAfterSeconds: parseInt(retryAfter2, 10) } : {}
|
|
196
220
|
};
|
|
197
221
|
await new Promise((resolve10) => setTimeout(resolve10, Math.min(delayMs, 3e4)));
|
|
198
222
|
continue;
|
|
199
223
|
}
|
|
224
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
225
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : Number.NaN;
|
|
200
226
|
return {
|
|
201
227
|
success: false,
|
|
202
228
|
error,
|
|
203
229
|
isRateLimit: response.status === 429,
|
|
204
|
-
isAuthError: response.status === 401 || response.status === 403
|
|
230
|
+
isAuthError: response.status === 401 || response.status === 403,
|
|
231
|
+
status: response.status,
|
|
232
|
+
host,
|
|
233
|
+
...detail ? { detail } : {},
|
|
234
|
+
...Number.isFinite(retryAfterSeconds) ? { retryAfterSeconds } : {}
|
|
205
235
|
};
|
|
206
236
|
}
|
|
207
237
|
if (!data.success || !data.response) {
|
|
208
238
|
return {
|
|
209
239
|
success: false,
|
|
210
|
-
error: data.error || "
|
|
240
|
+
error: `${host} 200 but no content: ${data.error || firstLine(bodyText) || "empty response"}`,
|
|
241
|
+
status: response.status,
|
|
242
|
+
host,
|
|
243
|
+
...bodyText ? { detail: truncate(bodyText, 800) } : {}
|
|
211
244
|
};
|
|
212
245
|
}
|
|
213
246
|
return {
|
|
@@ -221,14 +254,16 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
221
254
|
}
|
|
222
255
|
};
|
|
223
256
|
} catch (err) {
|
|
224
|
-
if (attempt === 3)
|
|
257
|
+
if (attempt === 3) {
|
|
258
|
+
return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
|
|
259
|
+
}
|
|
225
260
|
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
226
261
|
await new Promise((resolve10) => setTimeout(resolve10, delayMs));
|
|
227
262
|
}
|
|
228
263
|
}
|
|
229
|
-
return
|
|
264
|
+
return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
|
|
230
265
|
}
|
|
231
|
-
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
266
|
+
async function callAI(systemPrompt, userPrompt, tier = "free", opts = {}) {
|
|
232
267
|
if (!apiKey()) {
|
|
233
268
|
return {
|
|
234
269
|
success: false,
|
|
@@ -238,12 +273,12 @@ async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
|
238
273
|
}
|
|
239
274
|
try {
|
|
240
275
|
const token = await getOrRefreshDirectToken();
|
|
241
|
-
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
|
|
276
|
+
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier, opts);
|
|
242
277
|
} catch (err) {
|
|
243
278
|
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
244
279
|
}
|
|
245
280
|
}
|
|
246
|
-
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
281
|
+
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", opts = {}) {
|
|
247
282
|
if (!email) {
|
|
248
283
|
return {
|
|
249
284
|
success: false,
|
|
@@ -253,11 +288,11 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
|
253
288
|
}
|
|
254
289
|
try {
|
|
255
290
|
let token = await getOrRefreshRelayToken(email);
|
|
256
|
-
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
291
|
+
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
257
292
|
if (!result.success && result.isAuthError) {
|
|
258
293
|
cachedRelayToken = null;
|
|
259
294
|
token = await getOrRefreshRelayToken(email);
|
|
260
|
-
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
295
|
+
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
261
296
|
}
|
|
262
297
|
return result;
|
|
263
298
|
} catch (err) {
|
|
@@ -289,11 +324,13 @@ function normalizeUrl(input) {
|
|
|
289
324
|
url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
|
|
290
325
|
return `https://${url}`;
|
|
291
326
|
}
|
|
292
|
-
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
|
|
327
|
+
var SERVER_BACKOFF_MS, CLI_BACKOFF_MS, SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
|
|
293
328
|
var init_api_client = __esm({
|
|
294
329
|
"src/lib/api-client.ts"() {
|
|
295
330
|
"use strict";
|
|
296
331
|
init_utils();
|
|
332
|
+
SERVER_BACKOFF_MS = [2e3, 4e3];
|
|
333
|
+
CLI_BACKOFF_MS = [1e4, 3e4];
|
|
297
334
|
SPARK_AI_BASE = "https://sparkbrain.app";
|
|
298
335
|
SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
|
|
299
336
|
SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
@@ -324,7 +361,7 @@ async function getStatusCached(email) {
|
|
|
324
361
|
function getTier(d) {
|
|
325
362
|
if (!d.subscription || !d.registered) return "free";
|
|
326
363
|
if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
|
|
327
|
-
return TIERS[d.subscription.price_id] ?? "
|
|
364
|
+
return TIERS[d.subscription.price_id] ?? "pro";
|
|
328
365
|
}
|
|
329
366
|
async function getTierByEmail(email) {
|
|
330
367
|
try {
|
|
@@ -341,17 +378,18 @@ async function isVerified(email) {
|
|
|
341
378
|
}
|
|
342
379
|
}
|
|
343
380
|
function pricingUrl(opts) {
|
|
344
|
-
const u = new URL(`${BASE}
|
|
381
|
+
const u = new URL(`${BASE}/${OWNER_SLUG}/${APP_ID}`);
|
|
345
382
|
if (opts?.email) u.searchParams.set("email", opts.email);
|
|
346
383
|
if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
|
|
347
384
|
return u.toString();
|
|
348
385
|
}
|
|
349
|
-
var APP_ID, BASE, TIERS, RANK, FEATURES, getStatus, _cache, isPaid, hasAccess, hasTrialRemaining, showPaywall, isCanceling;
|
|
386
|
+
var APP_ID, OWNER_SLUG, BASE, TIERS, RANK, FEATURES, getStatus, _cache, isPaid, hasAccess, hasTrialRemaining, showPaywall, isCanceling;
|
|
350
387
|
var init_SparkStripe_feature_gating = __esm({
|
|
351
388
|
"src/lib/SparkStripe-feature-gating.ts"() {
|
|
352
389
|
"use strict";
|
|
353
390
|
APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
354
|
-
|
|
391
|
+
OWNER_SLUG = process.env.SPARK_OWNER_SLUG ?? "spark";
|
|
392
|
+
BASE = "https://sparkpay.dev";
|
|
355
393
|
TIERS = {
|
|
356
394
|
price_1TADnPEf9opkuWid9NtqOYON: "pro",
|
|
357
395
|
price_1TADnPEf9opkuWidgbvvX9ma: "pro",
|
|
@@ -403,7 +441,7 @@ var VERSION, CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_CAPTIONS;
|
|
|
403
441
|
var init_config = __esm({
|
|
404
442
|
"src/lib/config.ts"() {
|
|
405
443
|
"use strict";
|
|
406
|
-
VERSION = "1.2.
|
|
444
|
+
VERSION = "1.2.3";
|
|
407
445
|
CONFIG_FILE = "quickpeek.config.json";
|
|
408
446
|
DEFAULT_CONFIG = {
|
|
409
447
|
plan: { maxSteps: 8 },
|
|
@@ -553,7 +591,10 @@ var init_scraping = __esm({
|
|
|
553
591
|
});
|
|
554
592
|
|
|
555
593
|
// src/lib/crawl.ts
|
|
556
|
-
import { chromium as
|
|
594
|
+
import { chromium as chromium3 } from "playwright";
|
|
595
|
+
function idSelector(id) {
|
|
596
|
+
return /^[A-Za-z_-][\w-]*$/.test(id) ? `#${id}` : `[id="${id.replace(/(["\\])/g, "\\$1")}"]`;
|
|
597
|
+
}
|
|
557
598
|
function getActionForType(type) {
|
|
558
599
|
switch (type) {
|
|
559
600
|
case "button":
|
|
@@ -579,7 +620,7 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
|
579
620
|
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
580
621
|
}
|
|
581
622
|
const browserTask = task2("Launching browser");
|
|
582
|
-
const browser = await
|
|
623
|
+
const browser = await chromium3.launch({ headless: true });
|
|
583
624
|
const context = await browser.newContext({
|
|
584
625
|
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
585
626
|
// Crawl at the size we will record at, or the measurements below describe
|
|
@@ -683,9 +724,9 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
|
683
724
|
} else if (el.isFile || el.type === "file") {
|
|
684
725
|
selector = 'input[type="file"]';
|
|
685
726
|
} else if (el.type === "range") {
|
|
686
|
-
selector = el.id ?
|
|
727
|
+
selector = el.id ? idSelector(el.id) : 'input[type="range"]';
|
|
687
728
|
} else if (el.type === "checkbox") {
|
|
688
|
-
selector = el.id ?
|
|
729
|
+
selector = el.id ? idSelector(el.id) : 'input[type="checkbox"]';
|
|
689
730
|
} else if (el.type === "button") {
|
|
690
731
|
if (btnTexts[el.text] === 1) {
|
|
691
732
|
selector = `text="${el.text}"`;
|
|
@@ -695,7 +736,7 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
|
695
736
|
} else if (el.type === "link") {
|
|
696
737
|
selector = `text="${el.text}"`;
|
|
697
738
|
} else if (el.id) {
|
|
698
|
-
selector =
|
|
739
|
+
selector = idSelector(el.id);
|
|
699
740
|
} else if (el.name) {
|
|
700
741
|
selector = `[name="${el.name}"]`;
|
|
701
742
|
} else if (el.placeholder) {
|
|
@@ -782,7 +823,7 @@ var init_watermark = __esm({
|
|
|
782
823
|
});
|
|
783
824
|
|
|
784
825
|
// src/lib/ffmpeg/spawn.ts
|
|
785
|
-
import { spawn } from "child_process";
|
|
826
|
+
import { spawn as spawn2 } from "child_process";
|
|
786
827
|
function toWSLPath(path) {
|
|
787
828
|
if (!isWindowsRuntime) return path;
|
|
788
829
|
return windowsDrivePathToWsl(path) ?? path.replace(/\\/g, "/");
|
|
@@ -796,9 +837,9 @@ function convertArgToWSL(arg2) {
|
|
|
796
837
|
function spawnFFmpeg(cmd2, args) {
|
|
797
838
|
if (isWindowsRuntime) {
|
|
798
839
|
const wslArgs = args.map(convertArgToWSL);
|
|
799
|
-
return
|
|
840
|
+
return spawn2("wsl", ["-e", cmd2, ...wslArgs]);
|
|
800
841
|
}
|
|
801
|
-
return
|
|
842
|
+
return spawn2(cmd2, args);
|
|
802
843
|
}
|
|
803
844
|
function runFFmpeg(args, errorLabel) {
|
|
804
845
|
return new Promise((resolve10, reject) => {
|
|
@@ -997,8 +1038,39 @@ function getMediaDuration(filePath) {
|
|
|
997
1038
|
proc.on("error", reject);
|
|
998
1039
|
});
|
|
999
1040
|
}
|
|
1041
|
+
function getAudioCodec(filePath) {
|
|
1042
|
+
return new Promise((resolve10) => {
|
|
1043
|
+
const args = [
|
|
1044
|
+
"-v",
|
|
1045
|
+
"error",
|
|
1046
|
+
"-select_streams",
|
|
1047
|
+
"a:0",
|
|
1048
|
+
"-show_entries",
|
|
1049
|
+
"stream=codec_name",
|
|
1050
|
+
"-of",
|
|
1051
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
1052
|
+
filePath
|
|
1053
|
+
];
|
|
1054
|
+
const proc = spawnFFmpeg("ffprobe", args);
|
|
1055
|
+
let stdout = "";
|
|
1056
|
+
proc.stdout.on("data", (data) => {
|
|
1057
|
+
stdout += data;
|
|
1058
|
+
});
|
|
1059
|
+
proc.on("close", (code) => resolve10(code === 0 ? stdout.trim() : ""));
|
|
1060
|
+
proc.on("error", () => resolve10(""));
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
async function isHonestWav(filePath) {
|
|
1064
|
+
const codec = await getAudioCodec(filePath);
|
|
1065
|
+
return codec.startsWith("pcm_");
|
|
1066
|
+
}
|
|
1067
|
+
function buildConcatArgs(listPath, output) {
|
|
1068
|
+
const isWav = output.toLowerCase().endsWith(".wav");
|
|
1069
|
+
const codec = isWav ? ["-c:a", "pcm_s16le"] : ["-c", "copy"];
|
|
1070
|
+
return ["-y", "-f", "concat", "-safe", "0", "-i", listPath, ...codec, output];
|
|
1071
|
+
}
|
|
1000
1072
|
function concatMedia(listPath, output) {
|
|
1001
|
-
return runFFmpeg(
|
|
1073
|
+
return runFFmpeg(buildConcatArgs(listPath, output), "ffmpeg concat failed");
|
|
1002
1074
|
}
|
|
1003
1075
|
function generateSilence(outputPath, durationMs) {
|
|
1004
1076
|
return runFFmpeg(
|
|
@@ -1247,6 +1319,114 @@ var init_ffmpeg2 = __esm({
|
|
|
1247
1319
|
}
|
|
1248
1320
|
});
|
|
1249
1321
|
|
|
1322
|
+
// src/lib/json-repair.ts
|
|
1323
|
+
function repairUnicodeEscapes(content) {
|
|
1324
|
+
return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
|
|
1325
|
+
const code = Number.parseInt(hex, 16);
|
|
1326
|
+
return code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
1327
|
+
}).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
|
|
1328
|
+
}
|
|
1329
|
+
function salvageTruncatedJson(content) {
|
|
1330
|
+
const stack = [];
|
|
1331
|
+
const cuts = [];
|
|
1332
|
+
let inString = false;
|
|
1333
|
+
let escaped = false;
|
|
1334
|
+
const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
|
|
1335
|
+
for (let i = 0; i < content.length; i++) {
|
|
1336
|
+
const ch = content[i];
|
|
1337
|
+
if (inString) {
|
|
1338
|
+
if (escaped) escaped = false;
|
|
1339
|
+
else if (ch === "\\") escaped = true;
|
|
1340
|
+
else if (ch === '"') {
|
|
1341
|
+
inString = false;
|
|
1342
|
+
record(i + 1, false);
|
|
1343
|
+
}
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
if (ch === '"') {
|
|
1347
|
+
inString = true;
|
|
1348
|
+
} else if (ch === "{" || ch === "[") {
|
|
1349
|
+
stack.push(ch);
|
|
1350
|
+
} else if (ch === "}" || ch === "]") {
|
|
1351
|
+
stack.pop();
|
|
1352
|
+
record(i + 1, true);
|
|
1353
|
+
} else if (ch === ",") {
|
|
1354
|
+
record(i, content[i - 1] === "}" || content[i - 1] === "]");
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
const tryCut = (cut) => {
|
|
1358
|
+
const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
|
|
1359
|
+
if (head.endsWith(":")) return null;
|
|
1360
|
+
let closed = head;
|
|
1361
|
+
for (let d = cut.open.length - 1; d >= 0; d--) {
|
|
1362
|
+
closed += CLOSERS[cut.open[d]];
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
JSON.parse(closed);
|
|
1366
|
+
return closed;
|
|
1367
|
+
} catch {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
|
|
1372
|
+
for (const containerOnly of [true, false]) {
|
|
1373
|
+
for (let n = 0; n < attempts; n++) {
|
|
1374
|
+
const cut = cuts[cuts.length - 1 - n];
|
|
1375
|
+
if (containerOnly && !cut.closesContainer) continue;
|
|
1376
|
+
const salvaged = tryCut(cut);
|
|
1377
|
+
if (salvaged !== null) return salvaged;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
function extractJsonCandidates(content) {
|
|
1383
|
+
let text = content.trim();
|
|
1384
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
|
|
1385
|
+
if (fence?.[1]?.trim()) text = fence[1].trim();
|
|
1386
|
+
const start = text.search(/[{[]/);
|
|
1387
|
+
if (start === -1) return [];
|
|
1388
|
+
const closer = text[start] === "{" ? "}" : "]";
|
|
1389
|
+
const end = text.lastIndexOf(closer);
|
|
1390
|
+
const candidates = [text.slice(start)];
|
|
1391
|
+
if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
|
|
1392
|
+
return candidates;
|
|
1393
|
+
}
|
|
1394
|
+
function parseAIJson(content) {
|
|
1395
|
+
const candidates = extractJsonCandidates(content);
|
|
1396
|
+
if (candidates.length === 0) return null;
|
|
1397
|
+
for (const text of candidates) {
|
|
1398
|
+
try {
|
|
1399
|
+
return { value: JSON.parse(text), repaired: false };
|
|
1400
|
+
} catch {
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
for (const text of candidates) {
|
|
1404
|
+
try {
|
|
1405
|
+
return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
for (const text of candidates) {
|
|
1410
|
+
for (const variant of [repairUnicodeEscapes(text), text]) {
|
|
1411
|
+
const salvaged = salvageTruncatedJson(variant);
|
|
1412
|
+
if (salvaged === null) continue;
|
|
1413
|
+
try {
|
|
1414
|
+
return { value: JSON.parse(salvaged), repaired: true };
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
var MAX_SALVAGE_ATTEMPTS, CLOSERS;
|
|
1422
|
+
var init_json_repair = __esm({
|
|
1423
|
+
"src/lib/json-repair.ts"() {
|
|
1424
|
+
"use strict";
|
|
1425
|
+
MAX_SALVAGE_ATTEMPTS = 500;
|
|
1426
|
+
CLOSERS = { "{": "}", "[": "]" };
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1250
1430
|
// src/lib/prompts.ts
|
|
1251
1431
|
function buildSystemPrompt(maxSteps) {
|
|
1252
1432
|
let scope;
|
|
@@ -1448,7 +1628,7 @@ var init_styles = __esm({
|
|
|
1448
1628
|
async function registerFreeUser(email) {
|
|
1449
1629
|
const APP_ID2 = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
1450
1630
|
try {
|
|
1451
|
-
const response = await fetch(`https://
|
|
1631
|
+
const response = await fetch(`https://sparkpay.dev/api/register-free`, {
|
|
1452
1632
|
method: "POST",
|
|
1453
1633
|
headers: { "Content-Type": "application/json" },
|
|
1454
1634
|
body: JSON.stringify({ email, app_id: APP_ID2, app_name: "QuickPeek" })
|
|
@@ -1483,7 +1663,7 @@ var init_subscription_client = __esm({
|
|
|
1483
1663
|
});
|
|
1484
1664
|
|
|
1485
1665
|
// src/lib/tts/helpers.ts
|
|
1486
|
-
import { existsSync as
|
|
1666
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1487
1667
|
import { readFile, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
|
|
1488
1668
|
import { isAbsolute, join as join2, resolve as resolve2 } from "path";
|
|
1489
1669
|
function toWSLPathForTTS(path) {
|
|
@@ -1518,7 +1698,7 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1518
1698
|
return 0;
|
|
1519
1699
|
}
|
|
1520
1700
|
const mediaPath = isAbsolute(step.value) ? step.value : resolve2(baseDir, step.value);
|
|
1521
|
-
if (!
|
|
1701
|
+
if (!existsSync4(mediaPath)) {
|
|
1522
1702
|
return 0;
|
|
1523
1703
|
}
|
|
1524
1704
|
try {
|
|
@@ -1540,7 +1720,7 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1540
1720
|
async function loadVoiceoverMeta(outputDir) {
|
|
1541
1721
|
const metaPath = join2(outputDir, "voiceover.json");
|
|
1542
1722
|
try {
|
|
1543
|
-
if (!
|
|
1723
|
+
if (!existsSync4(metaPath)) return null;
|
|
1544
1724
|
const content = await readFile(metaPath, "utf-8");
|
|
1545
1725
|
return JSON.parse(content);
|
|
1546
1726
|
} catch {
|
|
@@ -1579,7 +1759,7 @@ __export(timing_exports, {
|
|
|
1579
1759
|
alignVoiceoverToRecording: () => alignVoiceoverToRecording,
|
|
1580
1760
|
estimateWordTimings: () => estimateWordTimings
|
|
1581
1761
|
});
|
|
1582
|
-
import { existsSync as
|
|
1762
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1583
1763
|
import { join as join3 } from "path";
|
|
1584
1764
|
function estimateWordTimings(caption, duration) {
|
|
1585
1765
|
const words = caption.split(/\s+/).filter((w) => w.length > 0);
|
|
@@ -1616,9 +1796,9 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1616
1796
|
const audioPath = join3(audioDir, `step-${stepNum}.mp3`);
|
|
1617
1797
|
const originalPaddingPath = join3(audioDir, `step-${stepNum}-padding.mp3`);
|
|
1618
1798
|
const silencePath = join3(audioDir, `step-${stepNum}-silence.mp3`);
|
|
1619
|
-
if (
|
|
1799
|
+
if (existsSync5(audioPath)) {
|
|
1620
1800
|
segments.push(audioPath);
|
|
1621
|
-
if (
|
|
1801
|
+
if (existsSync5(originalPaddingPath)) {
|
|
1622
1802
|
segments.push(originalPaddingPath);
|
|
1623
1803
|
}
|
|
1624
1804
|
if (recordingDuration > voiceDuration) {
|
|
@@ -1626,7 +1806,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1626
1806
|
await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
|
|
1627
1807
|
segments.push(alignPaddingPath);
|
|
1628
1808
|
}
|
|
1629
|
-
} else if (
|
|
1809
|
+
} else if (existsSync5(silencePath)) {
|
|
1630
1810
|
if (targetDuration !== voiceDuration) {
|
|
1631
1811
|
await generateSilence(silencePath, targetDuration);
|
|
1632
1812
|
}
|
|
@@ -1714,10 +1894,25 @@ var init_voices = __esm({
|
|
|
1714
1894
|
|
|
1715
1895
|
// src/lib/tts/synthesize.ts
|
|
1716
1896
|
import { exec } from "child_process";
|
|
1717
|
-
import {
|
|
1897
|
+
import { createWriteStream } from "fs";
|
|
1898
|
+
import { stat, unlink as unlink3, writeFile as writeFile3 } from "fs/promises";
|
|
1718
1899
|
import { join as join5, resolve as resolve3 } from "path";
|
|
1900
|
+
import { pipeline } from "stream/promises";
|
|
1719
1901
|
import { promisify } from "util";
|
|
1720
1902
|
async function synthesizeSpeech(text, outputPath, voice) {
|
|
1903
|
+
let pythonMsg = "";
|
|
1904
|
+
for (let attempt = 0; attempt < PYTHON_ATTEMPTS; attempt++) {
|
|
1905
|
+
try {
|
|
1906
|
+
await synthesizeWithPython(text, outputPath, voice);
|
|
1907
|
+
return;
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
pythonMsg = err instanceof Error ? err.message : String(err);
|
|
1910
|
+
if (attempt < PYTHON_ATTEMPTS - 1) await new Promise((r) => setTimeout(r, 500));
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
await synthesizeWithNode(text, outputPath, voice, pythonMsg);
|
|
1914
|
+
}
|
|
1915
|
+
async function synthesizeWithPython(text, outputPath, voice) {
|
|
1721
1916
|
const tempScriptPath = join5(resolve3(outputPath, ".."), `tts-${Date.now()}.py`);
|
|
1722
1917
|
const wslScriptPath = isWindows ? toWSLPathForTTS(resolve3(tempScriptPath)) : tempScriptPath;
|
|
1723
1918
|
const wslOutputPath = isWindows ? toWSLPathForTTS(resolve3(outputPath)) : outputPath;
|
|
@@ -1738,42 +1933,45 @@ asyncio.run(main())
|
|
|
1738
1933
|
await writeFile3(tempScriptPath, pythonScript);
|
|
1739
1934
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
1740
1935
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1936
|
+
} finally {
|
|
1937
|
+
await unlink3(tempScriptPath).catch(() => {
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
async function synthesizeWithNode(text, outputPath, voice, pythonMsg, maxRetries = 2) {
|
|
1942
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1943
|
+
try {
|
|
1944
|
+
const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
|
|
1945
|
+
const tts = new MsEdgeTTS();
|
|
1946
|
+
await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
|
|
1947
|
+
const { audioStream } = tts.toStream(text);
|
|
1948
|
+
await pipeline(audioStream, createWriteStream(outputPath));
|
|
1949
|
+
const { size } = await stat(outputPath);
|
|
1950
|
+
if (size === 0) throw new Error("No audio data received");
|
|
1951
|
+
return;
|
|
1952
|
+
} catch (nodeErr) {
|
|
1953
|
+
await unlink3(outputPath).catch(() => {
|
|
1954
|
+
});
|
|
1955
|
+
if (attempt === maxRetries) {
|
|
1956
|
+
const nodeMsg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
|
|
1957
|
+
throw new Error(`TTS failed (Python: ${pythonMsg.slice(0, 100)}, Node: ${nodeMsg.slice(0, 100)})`);
|
|
1762
1958
|
}
|
|
1959
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1763
1960
|
}
|
|
1764
1961
|
}
|
|
1765
1962
|
}
|
|
1766
|
-
var execAsync;
|
|
1963
|
+
var execAsync, PYTHON_ATTEMPTS;
|
|
1767
1964
|
var init_synthesize = __esm({
|
|
1768
1965
|
"src/lib/tts/synthesize.ts"() {
|
|
1769
1966
|
"use strict";
|
|
1770
1967
|
init_helpers();
|
|
1771
1968
|
execAsync = promisify(exec);
|
|
1969
|
+
PYTHON_ATTEMPTS = 2;
|
|
1772
1970
|
}
|
|
1773
1971
|
});
|
|
1774
1972
|
|
|
1775
1973
|
// src/lib/tts/generate.ts
|
|
1776
|
-
import { mkdir as
|
|
1974
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
1777
1975
|
import { join as join6 } from "path";
|
|
1778
1976
|
function estimateDurationFromText(text, wordsPerMinute = 150) {
|
|
1779
1977
|
const words = text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
@@ -1807,7 +2005,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1807
2005
|
return { voiceoverPath: "", stepAudio: stepAudio2 };
|
|
1808
2006
|
}
|
|
1809
2007
|
const audioDir = join6(workDir, "audio");
|
|
1810
|
-
await
|
|
2008
|
+
await mkdir3(audioDir, { recursive: true });
|
|
1811
2009
|
const segments = [];
|
|
1812
2010
|
const stepAudio = /* @__PURE__ */ new Map();
|
|
1813
2011
|
const ttsTask = task2("Generating voiceover");
|
|
@@ -1858,7 +2056,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1858
2056
|
return { voiceoverPath: "", stepAudio };
|
|
1859
2057
|
}
|
|
1860
2058
|
const concatTask = task2("Combining audio");
|
|
1861
|
-
await
|
|
2059
|
+
await mkdir3(outputDir, { recursive: true });
|
|
1862
2060
|
const voiceoverPath = join6(outputDir, "voiceover.wav");
|
|
1863
2061
|
await concatenateAudio(segments, voiceoverPath, workDir);
|
|
1864
2062
|
concatTask.succeed("Done");
|
|
@@ -1920,6 +2118,7 @@ var init_types3 = __esm({
|
|
|
1920
2118
|
// src/lib/index.ts
|
|
1921
2119
|
var lib_exports = {};
|
|
1922
2120
|
__export(lib_exports, {
|
|
2121
|
+
CLI_BACKOFF_MS: () => CLI_BACKOFF_MS,
|
|
1923
2122
|
CONFIG_FILE: () => CONFIG_FILE,
|
|
1924
2123
|
CURSOR_CSS: () => CURSOR_CSS,
|
|
1925
2124
|
CURSOR_PNG: () => CURSOR_PNG,
|
|
@@ -1930,9 +2129,11 @@ __export(lib_exports, {
|
|
|
1930
2129
|
FEATURES: () => FEATURES,
|
|
1931
2130
|
INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
|
|
1932
2131
|
LANGUAGE_VOICES: () => WIDE_VOICES,
|
|
2132
|
+
SERVER_BACKOFF_MS: () => SERVER_BACKOFF_MS,
|
|
1933
2133
|
VERSION: () => VERSION,
|
|
1934
2134
|
alignVoiceoverToRecording: () => alignVoiceoverToRecording,
|
|
1935
2135
|
applyTransitions: () => applyTransitions,
|
|
2136
|
+
buildConcatArgs: () => buildConcatArgs,
|
|
1936
2137
|
buildKeepSegments: () => buildKeepSegments,
|
|
1937
2138
|
buildStepTransitions: () => buildStepTransitions,
|
|
1938
2139
|
buildSystemPrompt: () => buildSystemPrompt,
|
|
@@ -1946,9 +2147,11 @@ __export(lib_exports, {
|
|
|
1946
2147
|
concatVideoWithOutro: () => concatVideoWithOutro,
|
|
1947
2148
|
crawlPage: () => crawlPage,
|
|
1948
2149
|
estimateWordTimings: () => estimateWordTimings,
|
|
2150
|
+
extractJsonCandidates: () => extractJsonCandidates,
|
|
1949
2151
|
generateASSHeader: () => generateASSHeader,
|
|
1950
2152
|
generateSilence: () => generateSilence,
|
|
1951
2153
|
generateVoiceover: () => generateVoiceover,
|
|
2154
|
+
getAudioCodec: () => getAudioCodec,
|
|
1952
2155
|
getErrorMessage: () => getErrorMessage,
|
|
1953
2156
|
getLanguageName: () => getLanguageName,
|
|
1954
2157
|
getMediaDuration: () => getMediaDuration,
|
|
@@ -1959,20 +2162,25 @@ __export(lib_exports, {
|
|
|
1959
2162
|
hasAccess: () => hasAccess,
|
|
1960
2163
|
hasTrialRemaining: () => hasTrialRemaining,
|
|
1961
2164
|
hexToASS: () => hexToAss,
|
|
2165
|
+
idSelector: () => idSelector,
|
|
1962
2166
|
isCanceling: () => isCanceling,
|
|
1963
2167
|
isExcludedLink: () => isExcludedLink,
|
|
2168
|
+
isHonestWav: () => isHonestWav,
|
|
1964
2169
|
isPaid: () => isPaid,
|
|
1965
2170
|
isVerified: () => isVerified,
|
|
1966
2171
|
loadExistingVoiceover: () => loadExistingVoiceover,
|
|
1967
2172
|
loadVoiceoverMeta: () => loadVoiceoverMeta,
|
|
1968
2173
|
mapTimestampsAfterCompression: () => mapTimestampsAfterCompression,
|
|
1969
2174
|
normalizeUrl: () => normalizeUrl,
|
|
2175
|
+
parseAIJson: () => parseAIJson,
|
|
1970
2176
|
parseAIPlanResponse: () => parseAIPlanResponse,
|
|
1971
2177
|
parsePartial: () => parsePartial,
|
|
1972
2178
|
pricingUrl: () => pricingUrl,
|
|
1973
2179
|
registerFreeUser: () => registerFreeUser,
|
|
2180
|
+
repairUnicodeEscapes: () => repairUnicodeEscapes,
|
|
1974
2181
|
resolveVoice: () => resolveVoice,
|
|
1975
2182
|
rgbaToHex: () => rgbaToHex,
|
|
2183
|
+
salvageTruncatedJson: () => salvageTruncatedJson,
|
|
1976
2184
|
shouldSkipLink: () => shouldSkipLink,
|
|
1977
2185
|
shouldWatermark: () => shouldWatermark,
|
|
1978
2186
|
showPaywall: () => showPaywall,
|
|
@@ -1991,6 +2199,7 @@ var init_lib = __esm({
|
|
|
1991
2199
|
init_crawl();
|
|
1992
2200
|
init_cursor();
|
|
1993
2201
|
init_ffmpeg2();
|
|
2202
|
+
init_json_repair();
|
|
1994
2203
|
init_prompts();
|
|
1995
2204
|
init_SparkStripe_feature_gating();
|
|
1996
2205
|
init_scraping();
|
|
@@ -2014,13 +2223,13 @@ __export(config_loader_exports, {
|
|
|
2014
2223
|
savePlan: () => savePlan,
|
|
2015
2224
|
saveUserData: () => saveUserData
|
|
2016
2225
|
});
|
|
2017
|
-
import { existsSync as
|
|
2018
|
-
import { mkdir as
|
|
2226
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2227
|
+
import { mkdir as mkdir4, readFile as readFile2, rm, writeFile as writeFile4 } from "fs/promises";
|
|
2019
2228
|
import { homedir as homedir2 } from "os";
|
|
2020
2229
|
import { dirname, join as join7 } from "path";
|
|
2021
2230
|
async function loadConfig(_url, langOverride) {
|
|
2022
2231
|
const configPath = join7(process.cwd(), CONFIG_FILE);
|
|
2023
|
-
if (
|
|
2232
|
+
if (existsSync6(configPath)) {
|
|
2024
2233
|
try {
|
|
2025
2234
|
const content = await readFile2(configPath, "utf-8");
|
|
2026
2235
|
const userConfig = JSON.parse(content);
|
|
@@ -2070,15 +2279,15 @@ async function loadConfig(_url, langOverride) {
|
|
|
2070
2279
|
}
|
|
2071
2280
|
async function prepareWorkDir() {
|
|
2072
2281
|
const workDir = join7(process.cwd(), WORK_DIR);
|
|
2073
|
-
if (
|
|
2282
|
+
if (existsSync6(workDir)) {
|
|
2074
2283
|
await rm(workDir, { recursive: true });
|
|
2075
2284
|
}
|
|
2076
|
-
await
|
|
2285
|
+
await mkdir4(workDir, { recursive: true });
|
|
2077
2286
|
return workDir;
|
|
2078
2287
|
}
|
|
2079
2288
|
async function loadUserData() {
|
|
2080
2289
|
try {
|
|
2081
|
-
if (
|
|
2290
|
+
if (existsSync6(USER_DATA_FILE)) {
|
|
2082
2291
|
return JSON.parse(await readFile2(USER_DATA_FILE, "utf-8"));
|
|
2083
2292
|
}
|
|
2084
2293
|
} catch {
|
|
@@ -2086,8 +2295,8 @@ async function loadUserData() {
|
|
|
2086
2295
|
return {};
|
|
2087
2296
|
}
|
|
2088
2297
|
async function saveUserData(userData) {
|
|
2089
|
-
if (!
|
|
2090
|
-
await
|
|
2298
|
+
if (!existsSync6(USER_DATA_DIR)) {
|
|
2299
|
+
await mkdir4(USER_DATA_DIR, { recursive: true });
|
|
2091
2300
|
}
|
|
2092
2301
|
const existing = await loadUserData();
|
|
2093
2302
|
const merged = { ...existing, ...userData };
|
|
@@ -2140,7 +2349,7 @@ function parseCaptionCSS(css) {
|
|
|
2140
2349
|
}
|
|
2141
2350
|
async function loadCaptions() {
|
|
2142
2351
|
const captionsPath = join7(process.cwd(), "captions.css");
|
|
2143
|
-
if (!
|
|
2352
|
+
if (!existsSync6(captionsPath)) {
|
|
2144
2353
|
await writeFile4(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
2145
2354
|
return DEFAULT_CAPTIONS;
|
|
2146
2355
|
}
|
|
@@ -2156,7 +2365,7 @@ async function savePlan(plan, planPath) {
|
|
|
2156
2365
|
...plan,
|
|
2157
2366
|
steps: plan.steps.map(({ duration, ...step }) => step)
|
|
2158
2367
|
};
|
|
2159
|
-
await
|
|
2368
|
+
await mkdir4(dirname(planPath), { recursive: true });
|
|
2160
2369
|
await writeFile4(planPath, JSON.stringify(cleanPlan, null, 2));
|
|
2161
2370
|
}
|
|
2162
2371
|
var WORK_DIR, USER_DATA_DIR, USER_DATA_FILE, DEFAULT_CAPTIONS_CSS;
|
|
@@ -2264,19 +2473,16 @@ function fileResult(output, data, poster) {
|
|
|
2264
2473
|
}
|
|
2265
2474
|
|
|
2266
2475
|
// src/mcp/tools.ts
|
|
2267
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
2476
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
2268
2477
|
import { join as join23 } from "path";
|
|
2269
2478
|
import { WIDE_VOICES as WIDE_VOICES2 } from "@spark-apps/video-kit";
|
|
2270
2479
|
|
|
2271
|
-
// src/browser/auth-session.ts
|
|
2272
|
-
import { chromium } from "playwright";
|
|
2273
|
-
|
|
2274
2480
|
// src/browser/auth-state.ts
|
|
2275
|
-
import { existsSync as existsSync2 } from "fs";
|
|
2481
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
2276
2482
|
import { chmod, mkdir, readdir, unlink, writeFile } from "fs/promises";
|
|
2277
2483
|
import { homedir } from "os";
|
|
2278
2484
|
import { join } from "path";
|
|
2279
|
-
var AUTH_DIR = join(homedir(), ".quickpeek", "auth");
|
|
2485
|
+
var AUTH_DIR = process.env.QUICKPEEK_AUTH_DIR || join(homedir(), ".quickpeek", "auth");
|
|
2280
2486
|
function hostOf(url) {
|
|
2281
2487
|
try {
|
|
2282
2488
|
return new URL(url).host;
|
|
@@ -2288,136 +2494,445 @@ function authStatePath(url) {
|
|
|
2288
2494
|
const host = hostOf(url);
|
|
2289
2495
|
return host ? join(AUTH_DIR, `${host}.json`) : null;
|
|
2290
2496
|
}
|
|
2497
|
+
function authMetaPath(url) {
|
|
2498
|
+
const host = hostOf(url);
|
|
2499
|
+
return host ? join(AUTH_DIR, `${host}.meta.json`) : null;
|
|
2500
|
+
}
|
|
2291
2501
|
function findAuthState(url) {
|
|
2292
2502
|
const path = authStatePath(url);
|
|
2293
2503
|
return path && existsSync2(path) ? path : void 0;
|
|
2294
2504
|
}
|
|
2295
|
-
async function saveAuthState(url, state) {
|
|
2505
|
+
async function saveAuthState(url, state, startUrl) {
|
|
2296
2506
|
const path = authStatePath(url);
|
|
2297
2507
|
if (!path) throw new Error(`Not a valid URL: ${url}`);
|
|
2298
2508
|
await mkdir(AUTH_DIR, { recursive: true });
|
|
2299
2509
|
await writeFile(path, JSON.stringify(state, null, 2));
|
|
2300
2510
|
await chmod(path, 384);
|
|
2511
|
+
if (startUrl) {
|
|
2512
|
+
await writeFile(authMetaPath(url), JSON.stringify({ startUrl, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
2513
|
+
}
|
|
2301
2514
|
return path;
|
|
2302
2515
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
function pendingSignIn() {
|
|
2307
|
-
return pending?.url ?? null;
|
|
2308
|
-
}
|
|
2309
|
-
async function openSignIn(url) {
|
|
2310
|
-
await cancelSignIn();
|
|
2311
|
-
const browser = await chromium.launch({ headless: false });
|
|
2312
|
-
const context = await browser.newContext();
|
|
2313
|
-
const page = await context.newPage();
|
|
2314
|
-
pending = { browser, context, url };
|
|
2516
|
+
function findStartUrl(url) {
|
|
2517
|
+
const path = authMetaPath(url);
|
|
2518
|
+
if (!path || !existsSync2(path)) return void 0;
|
|
2315
2519
|
try {
|
|
2316
|
-
|
|
2520
|
+
const startUrl = JSON.parse(readFileSync2(path, "utf8")).startUrl;
|
|
2521
|
+
return typeof startUrl === "string" && startUrl.startsWith("http") ? startUrl : void 0;
|
|
2317
2522
|
} catch {
|
|
2523
|
+
return void 0;
|
|
2318
2524
|
}
|
|
2319
2525
|
}
|
|
2320
|
-
|
|
2321
|
-
if (!pending) throw new Error("No sign-in window is open. Call start_auth first.");
|
|
2322
|
-
const { context, url } = pending;
|
|
2526
|
+
function resolveStartUrl(url) {
|
|
2323
2527
|
try {
|
|
2324
|
-
const
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2528
|
+
const parsed = new URL(url);
|
|
2529
|
+
const bareOrigin = (parsed.pathname === "/" || parsed.pathname === "") && !parsed.search && !parsed.hash;
|
|
2530
|
+
if (!bareOrigin || !findAuthState(url)) return url;
|
|
2531
|
+
return findStartUrl(url) ?? url;
|
|
2532
|
+
} catch {
|
|
2533
|
+
return url;
|
|
2328
2534
|
}
|
|
2329
2535
|
}
|
|
2330
|
-
async function cancelSignIn() {
|
|
2331
|
-
const open = pending;
|
|
2332
|
-
pending = null;
|
|
2333
|
-
await open?.browser.close().catch(() => {
|
|
2334
|
-
});
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
// src/commands/edit-cmd.ts
|
|
2338
|
-
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
2339
|
-
import { join as join10 } from "path";
|
|
2340
|
-
import chalk7 from "chalk";
|
|
2341
2536
|
|
|
2342
|
-
// src/
|
|
2343
|
-
|
|
2344
|
-
import
|
|
2537
|
+
// src/browser/launch.ts
|
|
2538
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
2539
|
+
import { chromium as chromium2 } from "playwright";
|
|
2345
2540
|
|
|
2346
|
-
// src/
|
|
2347
|
-
import
|
|
2348
|
-
import
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
font: resolved.font,
|
|
2360
|
-
horizontalLayout: resolved.horizontalLayout
|
|
2361
|
-
});
|
|
2362
|
-
const colored = gradient(resolved.gradientColors)(ascii);
|
|
2363
|
-
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
2541
|
+
// src/browser/wsl.ts
|
|
2542
|
+
import { execSync, spawn } from "child_process";
|
|
2543
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
2544
|
+
import { request } from "http";
|
|
2545
|
+
import { chromium } from "playwright";
|
|
2546
|
+
function isWSL() {
|
|
2547
|
+
if (process.platform !== "linux") return false;
|
|
2548
|
+
try {
|
|
2549
|
+
const r = execSync("uname -r", { encoding: "utf-8" }).toLowerCase();
|
|
2550
|
+
return r.includes("microsoft") || r.includes("wsl");
|
|
2551
|
+
} catch {
|
|
2552
|
+
return false;
|
|
2553
|
+
}
|
|
2364
2554
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
init_config_loader();
|
|
2368
|
-
var cmd = chalk.cyan;
|
|
2369
|
-
var arg = chalk.green;
|
|
2370
|
-
var opt = chalk.magenta;
|
|
2371
|
-
var desc = chalk.white;
|
|
2372
|
-
var ex = chalk.dim;
|
|
2373
|
-
function showLogo() {
|
|
2374
|
-
const logo = renderLogo();
|
|
2375
|
-
console.log(logo);
|
|
2376
|
-
console.log(chalk.dim(` v${VERSION} - Auto-generate demo videos with AI`));
|
|
2377
|
-
console.log(chalk.italic.dim(" Because Tutorial Videos are Overrated!\n"));
|
|
2555
|
+
function tryEnableWSLInterop() {
|
|
2556
|
+
return existsSync3("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
2378
2557
|
}
|
|
2379
|
-
function
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
if (
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
const end = status.subscription.current_period_end;
|
|
2392
|
-
if (status.subscription.is_lifetime) {
|
|
2393
|
-
lines.push("Lifetime access");
|
|
2394
|
-
} else if (isCanceling(status)) {
|
|
2395
|
-
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2396
|
-
lines.push(`Cancels: ${date}`);
|
|
2397
|
-
} else if (end) {
|
|
2398
|
-
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2399
|
-
lines.push(`Renews: ${date}`);
|
|
2400
|
-
}
|
|
2558
|
+
function wslToWindowsPath(wslPath) {
|
|
2559
|
+
if (!wslPath) return wslPath;
|
|
2560
|
+
const m = wslPath.match(/^\/mnt\/([a-z])\/(.*)/);
|
|
2561
|
+
if (!m) return wslPath;
|
|
2562
|
+
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
2563
|
+
}
|
|
2564
|
+
function findExe(whereExeQuery, ...hardcoded) {
|
|
2565
|
+
for (const p of hardcoded) if (existsSync3(p)) return p;
|
|
2566
|
+
try {
|
|
2567
|
+
const line = execSync(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
2568
|
+
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
2569
|
+
} catch {
|
|
2401
2570
|
}
|
|
2402
|
-
return
|
|
2571
|
+
return null;
|
|
2403
2572
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2573
|
+
var findWindowsChrome = () => findExe(
|
|
2574
|
+
"chrome.exe",
|
|
2575
|
+
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
2576
|
+
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
|
|
2577
|
+
);
|
|
2578
|
+
var findWindowsNode = () => findExe("node.exe");
|
|
2579
|
+
function getWindowsHostIP() {
|
|
2580
|
+
try {
|
|
2581
|
+
const m = execSync("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
|
|
2582
|
+
/default via (\d+\.\d+\.\d+\.\d+)/
|
|
2583
|
+
);
|
|
2584
|
+
if (m?.[1]) return m[1];
|
|
2585
|
+
} catch {
|
|
2586
|
+
}
|
|
2587
|
+
try {
|
|
2588
|
+
const m = readFileSync3("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
2589
|
+
if (m?.[1]) return m[1];
|
|
2590
|
+
} catch {
|
|
2591
|
+
}
|
|
2592
|
+
return "127.0.0.1";
|
|
2409
2593
|
}
|
|
2410
|
-
function
|
|
2411
|
-
const
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2594
|
+
function startRelay(nodePath, windowsHost, targetPort) {
|
|
2595
|
+
const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
|
|
2596
|
+
return new Promise((resolve10, reject) => {
|
|
2597
|
+
const proc = spawn(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
2598
|
+
const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
|
|
2599
|
+
let buf = "";
|
|
2600
|
+
proc.stdout.on("data", (d) => {
|
|
2601
|
+
buf += d.toString();
|
|
2602
|
+
const m = buf.match(/PORT:(\d+)/);
|
|
2603
|
+
if (m) {
|
|
2604
|
+
clearTimeout(timer);
|
|
2605
|
+
resolve10({ proc, port: +m[1] });
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
proc.on("error", (e) => {
|
|
2609
|
+
clearTimeout(timer);
|
|
2610
|
+
reject(e);
|
|
2611
|
+
});
|
|
2415
2612
|
});
|
|
2416
2613
|
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2614
|
+
function waitForChrome(host, port) {
|
|
2615
|
+
return new Promise((resolve10, reject) => {
|
|
2616
|
+
const deadline = Date.now() + 2e4;
|
|
2617
|
+
const poll = () => {
|
|
2618
|
+
if (Date.now() > deadline) {
|
|
2619
|
+
reject(new Error("Chrome debug port did not open within 20 s"));
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
const req = request({ host, port, path: "/json/version", method: "GET" }, (res) => {
|
|
2623
|
+
res.resume();
|
|
2624
|
+
resolve10();
|
|
2625
|
+
});
|
|
2626
|
+
req.on("error", () => setTimeout(poll, 600));
|
|
2627
|
+
req.setTimeout(1e3, () => {
|
|
2628
|
+
req.destroy();
|
|
2629
|
+
setTimeout(poll, 600);
|
|
2630
|
+
});
|
|
2631
|
+
req.end();
|
|
2632
|
+
};
|
|
2633
|
+
poll();
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
2637
|
+
const debugPort = options.debugPort ?? 9222;
|
|
2638
|
+
const chromePath = findWindowsChrome();
|
|
2639
|
+
if (!chromePath) throw new Error("Windows Chrome not found. Install Google Chrome for Windows.");
|
|
2640
|
+
const nodePath = findWindowsNode();
|
|
2641
|
+
if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
|
|
2642
|
+
const winUserData = wslToWindowsPath(userDataDir);
|
|
2643
|
+
const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
|
|
2644
|
+
const chromeProc = spawn(
|
|
2645
|
+
chromePath,
|
|
2646
|
+
[
|
|
2647
|
+
`--remote-debugging-port=${debugPort}`,
|
|
2648
|
+
`--user-data-dir=${winUserData}`,
|
|
2649
|
+
...winExtPath ? [`--disable-extensions-except=${winExtPath}`, `--load-extension=${winExtPath}`] : [],
|
|
2650
|
+
"--no-first-run",
|
|
2651
|
+
"--no-default-browser-check",
|
|
2652
|
+
"--disable-background-networking",
|
|
2653
|
+
"--disable-sync",
|
|
2654
|
+
"--disable-features=TranslateUI",
|
|
2655
|
+
options.startMaximized ? "--start-maximized" : "--start-minimized"
|
|
2656
|
+
],
|
|
2657
|
+
{ stdio: "ignore", detached: true }
|
|
2658
|
+
);
|
|
2659
|
+
chromeProc.unref();
|
|
2660
|
+
const windowsHost = getWindowsHostIP();
|
|
2661
|
+
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
2662
|
+
try {
|
|
2663
|
+
await waitForChrome(windowsHost, proxyPort);
|
|
2664
|
+
const browser = await chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
2665
|
+
return {
|
|
2666
|
+
browser,
|
|
2667
|
+
cleanup: async () => {
|
|
2668
|
+
try {
|
|
2669
|
+
await browser.close();
|
|
2670
|
+
} catch {
|
|
2671
|
+
}
|
|
2672
|
+
relay.kill();
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
} catch (err) {
|
|
2676
|
+
relay.kill();
|
|
2677
|
+
throw err;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// src/browser/launch.ts
|
|
2682
|
+
function recordingViewport(config) {
|
|
2683
|
+
const zoom = config.video.zoom && config.video.zoom > 0 ? config.video.zoom : 1;
|
|
2684
|
+
return {
|
|
2685
|
+
width: Math.round(config.video.width / zoom),
|
|
2686
|
+
height: Math.round(config.video.height / zoom)
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
|
|
2690
|
+
const extensionPath = config.browser?.extension;
|
|
2691
|
+
const zoom = config.video.zoom && config.video.zoom > 0 ? config.video.zoom : 1;
|
|
2692
|
+
const viewport = recordingViewport(config);
|
|
2693
|
+
const scaleOpts = zoom === 1 ? {} : { deviceScaleFactor: zoom };
|
|
2694
|
+
const videoOpts = recordVideo ? { recordVideo } : {};
|
|
2695
|
+
const cspOpts = { bypassCSP: true };
|
|
2696
|
+
const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
2697
|
+
let context;
|
|
2698
|
+
let browser = null;
|
|
2699
|
+
let chromeSession = null;
|
|
2700
|
+
if (extensionPath) {
|
|
2701
|
+
await mkdir2(userDataDir, { recursive: true });
|
|
2702
|
+
const persistentArgs = [
|
|
2703
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
2704
|
+
`--load-extension=${extensionPath}`,
|
|
2705
|
+
"--window-position=-32000,-32000",
|
|
2706
|
+
"--window-size=1,1"
|
|
2707
|
+
];
|
|
2708
|
+
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
2709
|
+
if (useWindowsChrome) {
|
|
2710
|
+
try {
|
|
2711
|
+
chromeSession = await launchWindowsChrome(extensionPath, userDataDir, { viewport, ...scaleOpts, ...videoOpts });
|
|
2712
|
+
const contexts = chromeSession.browser.contexts();
|
|
2713
|
+
context = contexts[0] || await chromeSession.browser.newContext({
|
|
2714
|
+
viewport,
|
|
2715
|
+
...scaleOpts,
|
|
2716
|
+
...videoOpts,
|
|
2717
|
+
...cspOpts,
|
|
2718
|
+
acceptDownloads: true
|
|
2719
|
+
});
|
|
2720
|
+
} catch {
|
|
2721
|
+
context = await chromium2.launchPersistentContext(userDataDir, {
|
|
2722
|
+
headless: false,
|
|
2723
|
+
args: persistentArgs,
|
|
2724
|
+
viewport,
|
|
2725
|
+
...scaleOpts,
|
|
2726
|
+
...videoOpts,
|
|
2727
|
+
...cspOpts,
|
|
2728
|
+
acceptDownloads: true
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
} else {
|
|
2732
|
+
context = await chromium2.launchPersistentContext(userDataDir, {
|
|
2733
|
+
headless: false,
|
|
2734
|
+
args: persistentArgs,
|
|
2735
|
+
viewport,
|
|
2736
|
+
...scaleOpts,
|
|
2737
|
+
...videoOpts,
|
|
2738
|
+
...cspOpts,
|
|
2739
|
+
acceptDownloads: true
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
} else {
|
|
2743
|
+
if (process.env.QP_HEADED === "1") {
|
|
2744
|
+
const headedOpts = {
|
|
2745
|
+
headless: false,
|
|
2746
|
+
args: ["--disable-blink-features=AutomationControlled"],
|
|
2747
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
2748
|
+
};
|
|
2749
|
+
try {
|
|
2750
|
+
browser = await chromium2.launch({ ...headedOpts, channel: "chrome" });
|
|
2751
|
+
} catch {
|
|
2752
|
+
browser = await chromium2.launch(headedOpts);
|
|
2753
|
+
}
|
|
2754
|
+
} else {
|
|
2755
|
+
browser = await chromium2.launch({ headless: true });
|
|
2756
|
+
}
|
|
2757
|
+
context = await browser.newContext({
|
|
2758
|
+
viewport,
|
|
2759
|
+
...scaleOpts,
|
|
2760
|
+
...videoOpts,
|
|
2761
|
+
...authOpts,
|
|
2762
|
+
...cspOpts,
|
|
2763
|
+
acceptDownloads: true
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
await hideDevOverlays(context);
|
|
2767
|
+
return { context, browser, chromeSession };
|
|
2768
|
+
}
|
|
2769
|
+
async function hideDevOverlays(context) {
|
|
2770
|
+
await context.addInitScript(() => {
|
|
2771
|
+
const css = "nextjs-portal,vite-error-overlay,vite-plugin-checker-error-overlay{display:none!important;pointer-events:none!important}";
|
|
2772
|
+
const add = () => {
|
|
2773
|
+
const style = document.createElement("style");
|
|
2774
|
+
style.textContent = css;
|
|
2775
|
+
document.head?.appendChild(style);
|
|
2776
|
+
};
|
|
2777
|
+
if (document.head) add();
|
|
2778
|
+
else document.addEventListener("DOMContentLoaded", add, { once: true });
|
|
2779
|
+
}).catch(() => {
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
async function launchSignInBrowser() {
|
|
2783
|
+
const opts = {
|
|
2784
|
+
headless: false,
|
|
2785
|
+
args: ["--disable-blink-features=AutomationControlled"],
|
|
2786
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
2787
|
+
};
|
|
2788
|
+
try {
|
|
2789
|
+
return await chromium2.launch({ ...opts, channel: "chrome" });
|
|
2790
|
+
} catch {
|
|
2791
|
+
return await chromium2.launch(opts);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
async function cleanupBrowserSession(session) {
|
|
2795
|
+
if (session.chromeSession) {
|
|
2796
|
+
await session.chromeSession.cleanup();
|
|
2797
|
+
} else {
|
|
2798
|
+
await session.context.close();
|
|
2799
|
+
if (session.browser) await session.browser.close();
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// src/browser/auth-session.ts
|
|
2804
|
+
var pending = null;
|
|
2805
|
+
function pendingSignIn() {
|
|
2806
|
+
return pending?.url ?? null;
|
|
2807
|
+
}
|
|
2808
|
+
async function openSignIn(url) {
|
|
2809
|
+
await cancelSignIn();
|
|
2810
|
+
const browser = await launchSignInBrowser();
|
|
2811
|
+
const context = await browser.newContext();
|
|
2812
|
+
const page = await context.newPage();
|
|
2813
|
+
pending = { browser, context, url };
|
|
2814
|
+
try {
|
|
2815
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
2816
|
+
} catch {
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
function sameSite(a, b) {
|
|
2820
|
+
if (a === b) return true;
|
|
2821
|
+
const tail = (h) => h.split(".").slice(-2).join(".");
|
|
2822
|
+
return a.includes(".") && b.includes(".") && tail(a) === tail(b);
|
|
2823
|
+
}
|
|
2824
|
+
function captureStartUrl(context, targetUrl) {
|
|
2825
|
+
const targetHost = new URL(targetUrl).host;
|
|
2826
|
+
const pages = context.pages();
|
|
2827
|
+
for (let i = pages.length - 1; i >= 0; i--) {
|
|
2828
|
+
const pageUrl = pages[i].url();
|
|
2829
|
+
if (!pageUrl.startsWith("http")) continue;
|
|
2830
|
+
if (sameSite(new URL(pageUrl).host, targetHost)) return pageUrl;
|
|
2831
|
+
}
|
|
2832
|
+
return void 0;
|
|
2833
|
+
}
|
|
2834
|
+
async function saveSignIn() {
|
|
2835
|
+
if (!pending) throw new Error("No sign-in window is open. Call start_auth first.");
|
|
2836
|
+
const { context, url } = pending;
|
|
2837
|
+
try {
|
|
2838
|
+
const startUrl = captureStartUrl(context, url);
|
|
2839
|
+
const path = await saveAuthState(url, await context.storageState(), startUrl);
|
|
2840
|
+
return { host: new URL(url).host, path, ...startUrl ? { startUrl } : {} };
|
|
2841
|
+
} finally {
|
|
2842
|
+
await cancelSignIn();
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
async function cancelSignIn() {
|
|
2846
|
+
const open = pending;
|
|
2847
|
+
pending = null;
|
|
2848
|
+
await open?.browser.close().catch(() => {
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// src/commands/edit-cmd.ts
|
|
2853
|
+
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4 } from "fs";
|
|
2854
|
+
import { join as join10 } from "path";
|
|
2855
|
+
import chalk7 from "chalk";
|
|
2856
|
+
|
|
2857
|
+
// src/help.ts
|
|
2858
|
+
init_lib();
|
|
2859
|
+
import chalk from "chalk";
|
|
2860
|
+
|
|
2861
|
+
// src/logo.ts
|
|
2862
|
+
import figlet from "figlet";
|
|
2863
|
+
import gradient from "gradient-string";
|
|
2864
|
+
var DEFAULT_LOGO_CONFIG = {
|
|
2865
|
+
text: "QuickPeek",
|
|
2866
|
+
font: "Slant",
|
|
2867
|
+
horizontalLayout: "default",
|
|
2868
|
+
gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
|
|
2869
|
+
suffix: " \u{1F440}"
|
|
2870
|
+
};
|
|
2871
|
+
function renderLogo(config = {}) {
|
|
2872
|
+
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
2873
|
+
const ascii = figlet.textSync(resolved.text, {
|
|
2874
|
+
font: resolved.font,
|
|
2875
|
+
horizontalLayout: resolved.horizontalLayout
|
|
2876
|
+
});
|
|
2877
|
+
const colored = gradient(resolved.gradientColors)(ascii);
|
|
2878
|
+
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// src/help.ts
|
|
2882
|
+
init_config_loader();
|
|
2883
|
+
var cmd = chalk.cyan;
|
|
2884
|
+
var arg = chalk.green;
|
|
2885
|
+
var opt = chalk.magenta;
|
|
2886
|
+
var desc = chalk.white;
|
|
2887
|
+
var ex = chalk.dim;
|
|
2888
|
+
function showLogo() {
|
|
2889
|
+
const logo = renderLogo();
|
|
2890
|
+
console.log(logo);
|
|
2891
|
+
console.log(chalk.dim(` v${VERSION} - Auto-generate demo videos with AI`));
|
|
2892
|
+
console.log(chalk.italic.dim(" Because Tutorial Videos are Overrated!\n"));
|
|
2893
|
+
}
|
|
2894
|
+
function formatUsageLines(status, tier) {
|
|
2895
|
+
const lines = [];
|
|
2896
|
+
lines.push(`Videos generated: ${status.usage_count}`);
|
|
2897
|
+
if (tier !== "pro" && status.trial) {
|
|
2898
|
+
const { uses_remaining, max_uses, days_remaining, is_expired } = status.trial;
|
|
2899
|
+
const used = max_uses - uses_remaining;
|
|
2900
|
+
lines.push(`Trial: ${used}/${max_uses} uses${is_expired ? " (expired)" : ` (${uses_remaining} remaining)`}`);
|
|
2901
|
+
if (!is_expired && days_remaining > 0) {
|
|
2902
|
+
lines.push(`Trial ends in ${days_remaining} day${days_remaining === 1 ? "" : "s"}`);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
if (tier === "pro" && status.subscription) {
|
|
2906
|
+
const end = status.subscription.current_period_end;
|
|
2907
|
+
if (status.subscription.is_lifetime) {
|
|
2908
|
+
lines.push("Lifetime access");
|
|
2909
|
+
} else if (isCanceling(status)) {
|
|
2910
|
+
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2911
|
+
lines.push(`Cancels: ${date}`);
|
|
2912
|
+
} else if (end) {
|
|
2913
|
+
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2914
|
+
lines.push(`Renews: ${date}`);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
return lines;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// src/lib/browser-open.ts
|
|
2921
|
+
import { exec as exec2 } from "child_process";
|
|
2922
|
+
function editorBaseUrl() {
|
|
2923
|
+
return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
|
|
2924
|
+
}
|
|
2925
|
+
function openInBrowser(url) {
|
|
2926
|
+
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
2927
|
+
const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
2928
|
+
return new Promise((resolve10) => {
|
|
2929
|
+
exec2(command, () => resolve10());
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// src/lib/plan-validation.ts
|
|
2934
|
+
var INVALID_TARGETS = [
|
|
2935
|
+
/\.png$/i,
|
|
2421
2936
|
/\.jpg$/i,
|
|
2422
2937
|
/\.gif$/i,
|
|
2423
2938
|
/\.webp$/i,
|
|
@@ -2531,6 +3046,9 @@ var autoYes = false;
|
|
|
2531
3046
|
function setAutoYes(value) {
|
|
2532
3047
|
autoYes = value;
|
|
2533
3048
|
}
|
|
3049
|
+
function isAutoYes() {
|
|
3050
|
+
return autoYes;
|
|
3051
|
+
}
|
|
2534
3052
|
function task(text) {
|
|
2535
3053
|
return ora({ text, prefixText: INDENT, spinner: "dots" }).start();
|
|
2536
3054
|
}
|
|
@@ -2585,10 +3103,10 @@ import chalk6 from "chalk";
|
|
|
2585
3103
|
init_lib();
|
|
2586
3104
|
|
|
2587
3105
|
// src/plan.ts
|
|
2588
|
-
import { mkdir as
|
|
2589
|
-
import { join as
|
|
3106
|
+
import { mkdir as mkdir6, readFile as readFile3, writeFile as writeFile6 } from "fs/promises";
|
|
3107
|
+
import { join as join9 } from "path";
|
|
2590
3108
|
import chalk5 from "chalk";
|
|
2591
|
-
import { chromium as
|
|
3109
|
+
import { chromium as chromium4 } from "playwright";
|
|
2592
3110
|
|
|
2593
3111
|
// src/ai/client.ts
|
|
2594
3112
|
init_lib();
|
|
@@ -2664,8 +3182,133 @@ async function browserLogin() {
|
|
|
2664
3182
|
});
|
|
2665
3183
|
}
|
|
2666
3184
|
|
|
3185
|
+
// src/utils/logging.ts
|
|
3186
|
+
import { appendFile, mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
3187
|
+
import { dirname as dirname2, join as join8 } from "path";
|
|
3188
|
+
var LOG_FILE_NAME = "quickpeek.log";
|
|
3189
|
+
var logFileInitialized = false;
|
|
3190
|
+
var logFilePath = LOG_FILE_NAME;
|
|
3191
|
+
async function writeLogEntry(entry) {
|
|
3192
|
+
try {
|
|
3193
|
+
if (!logFileInitialized) {
|
|
3194
|
+
await writeFile5(logFilePath, entry);
|
|
3195
|
+
logFileInitialized = true;
|
|
3196
|
+
} else {
|
|
3197
|
+
await appendFile(logFilePath, entry);
|
|
3198
|
+
}
|
|
3199
|
+
} catch {
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
async function setLogFilePath(outputDir) {
|
|
3203
|
+
logFilePath = join8(outputDir, LOG_FILE_NAME);
|
|
3204
|
+
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3205
|
+
${"=".repeat(60)}
|
|
3206
|
+
|
|
3207
|
+
`;
|
|
3208
|
+
try {
|
|
3209
|
+
await mkdir5(dirname2(logFilePath), { recursive: true });
|
|
3210
|
+
await writeFile5(logFilePath, header2);
|
|
3211
|
+
logFileInitialized = true;
|
|
3212
|
+
} catch {
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
async function logInfo(context, message) {
|
|
3216
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
|
|
3217
|
+
`);
|
|
3218
|
+
}
|
|
3219
|
+
async function logDebug(context, message) {
|
|
3220
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
|
|
3221
|
+
`);
|
|
3222
|
+
}
|
|
3223
|
+
async function logError(context, error) {
|
|
3224
|
+
let errorMsg;
|
|
3225
|
+
if (error instanceof Error) {
|
|
3226
|
+
errorMsg = `${error.message}
|
|
3227
|
+
${error.stack}`;
|
|
3228
|
+
} else if (typeof error === "object" && error !== null) {
|
|
3229
|
+
errorMsg = JSON.stringify(error, null, 2);
|
|
3230
|
+
} else {
|
|
3231
|
+
errorMsg = String(error);
|
|
3232
|
+
}
|
|
3233
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
|
|
3234
|
+
${errorMsg}
|
|
3235
|
+
|
|
3236
|
+
`);
|
|
3237
|
+
}
|
|
3238
|
+
async function logStep(stepNum, action, details) {
|
|
3239
|
+
const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
|
|
3240
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
|
|
3241
|
+
`);
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
// src/ai/mcp-context.ts
|
|
3245
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
3246
|
+
var DelegatedGenerationError = class extends Error {
|
|
3247
|
+
tool;
|
|
3248
|
+
step;
|
|
3249
|
+
/** The system instructions the call was about to send. */
|
|
3250
|
+
system;
|
|
3251
|
+
/** The user-side prompt. */
|
|
3252
|
+
prompt;
|
|
3253
|
+
constructor(tool, step, system, prompt2) {
|
|
3254
|
+
super(
|
|
3255
|
+
`Generation for "${tool}" (${step}) belongs to the calling model, not Groq. The tool should hand back this prompt and accept the result as a parameter.`
|
|
3256
|
+
);
|
|
3257
|
+
this.name = "DelegatedGenerationError";
|
|
3258
|
+
this.tool = tool;
|
|
3259
|
+
this.step = step;
|
|
3260
|
+
this.system = system;
|
|
3261
|
+
this.prompt = prompt2;
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
var mcpCall = new AsyncLocalStorage();
|
|
3265
|
+
function runInMcpTool(tool, fn, answers = {}) {
|
|
3266
|
+
return mcpCall.run({ tool, answers, pending: [] }, fn);
|
|
3267
|
+
}
|
|
3268
|
+
function currentMcpTool() {
|
|
3269
|
+
return mcpCall.getStore()?.tool ?? null;
|
|
3270
|
+
}
|
|
3271
|
+
function suppliedAnswer(step) {
|
|
3272
|
+
return mcpCall.getStore()?.answers?.[step];
|
|
3273
|
+
}
|
|
3274
|
+
function recordDelegation(e) {
|
|
3275
|
+
const store = mcpCall.getStore();
|
|
3276
|
+
if (store && !store.pending.some((p) => p.step === e.step)) store.pending.push(e);
|
|
3277
|
+
}
|
|
3278
|
+
function pendingDelegations() {
|
|
3279
|
+
return mcpCall.getStore()?.pending ?? [];
|
|
3280
|
+
}
|
|
3281
|
+
|
|
2667
3282
|
// src/ai/client.ts
|
|
2668
3283
|
var lastRateLimitInfo = null;
|
|
3284
|
+
function wrap(text, width) {
|
|
3285
|
+
const lines = [];
|
|
3286
|
+
for (const paragraph of text.split("\n")) {
|
|
3287
|
+
let current = "";
|
|
3288
|
+
for (const word of paragraph.split(/\s+/).filter(Boolean)) {
|
|
3289
|
+
if (current && current.length + word.length + 1 > width) {
|
|
3290
|
+
lines.push(current);
|
|
3291
|
+
current = word;
|
|
3292
|
+
} else {
|
|
3293
|
+
current = current ? `${current} ${word}` : word;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
lines.push(current);
|
|
3297
|
+
}
|
|
3298
|
+
return lines.filter((l) => l.length > 0);
|
|
3299
|
+
}
|
|
3300
|
+
function diagnose(status, host) {
|
|
3301
|
+
const where = host ?? "the AI service";
|
|
3302
|
+
if (status === void 0) return `Could not reach ${where}. Check your network, then retry.`;
|
|
3303
|
+
if (status === 401 || status === 403) return `${where} rejected the credentials. Run qp login and retry.`;
|
|
3304
|
+
if (status === 413) return `The prompt was too large for ${where}. Shorten the script or the page.`;
|
|
3305
|
+
if (status === 429) return `${where} is rate limiting. Wait for the quota to reset, then retry.`;
|
|
3306
|
+
if (status === 502 || status === 503 || status === 504) {
|
|
3307
|
+
return `${where} could not reach the AI backend (upstream outage). Retry in a few minutes.`;
|
|
3308
|
+
}
|
|
3309
|
+
if (status >= 500) return `${where} failed internally. Retry in a few minutes; if it persists, report this.`;
|
|
3310
|
+
return `${where} rejected the request. See the body above.`;
|
|
3311
|
+
}
|
|
2669
3312
|
function getLastRateLimitInfo() {
|
|
2670
3313
|
return lastRateLimitInfo;
|
|
2671
3314
|
}
|
|
@@ -2713,257 +3356,123 @@ async function setupUserEmail() {
|
|
|
2713
3356
|
console.log();
|
|
2714
3357
|
return email;
|
|
2715
3358
|
}
|
|
2716
|
-
async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free") {
|
|
2717
|
-
const
|
|
3359
|
+
async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free", step = "plan") {
|
|
3360
|
+
const mcpTool = currentMcpTool();
|
|
3361
|
+
if (mcpTool) {
|
|
3362
|
+
const answered = suppliedAnswer(step);
|
|
3363
|
+
if (typeof answered === "string") return answered;
|
|
3364
|
+
const refusal = new DelegatedGenerationError(mcpTool, step, systemPrompt, userPrompt);
|
|
3365
|
+
recordDelegation(refusal);
|
|
3366
|
+
throw refusal;
|
|
3367
|
+
}
|
|
3368
|
+
const onRetry = ({ attempt, status, delayMs, host }) => {
|
|
3369
|
+
console.log(
|
|
3370
|
+
chalk4.dim(
|
|
3371
|
+
`
|
|
3372
|
+
${host} returned ${status}; retrying in ${Math.round(delayMs / 1e3)}s (attempt ${attempt + 1} of 3)`
|
|
3373
|
+
)
|
|
3374
|
+
);
|
|
3375
|
+
};
|
|
3376
|
+
const chatOpts = { onRetry, backoffMs: CLI_BACKOFF_MS };
|
|
3377
|
+
const result = process.env.SPARK_AI_KEY ? await callAI(systemPrompt, userPrompt, tier, chatOpts) : await callAIViaRelay(systemPrompt, userPrompt, userEmail ?? "", tier, chatOpts);
|
|
2718
3378
|
if (!result.success) {
|
|
2719
|
-
await logError2("AI API", new Error(result.error
|
|
3379
|
+
await logError2("AI API", new Error(result.detail ? `${result.error}
|
|
3380
|
+
Body: ${result.detail}` : result.error));
|
|
2720
3381
|
if (result.isRateLimit) {
|
|
2721
3382
|
console.log();
|
|
2722
3383
|
console.log(chalk4.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
|
|
2723
3384
|
console.log();
|
|
2724
|
-
console.log(chalk4.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
|
|
2725
|
-
console.log(chalk4.dim(" Options:"));
|
|
2726
|
-
if (result.retryAfterSeconds) {
|
|
2727
|
-
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
2728
|
-
console.log(chalk4.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
|
|
2729
|
-
} else {
|
|
2730
|
-
console.log(chalk4.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
|
|
2731
|
-
}
|
|
2732
|
-
console.log(chalk4.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
|
|
2733
|
-
console.log(chalk4.dim(" \u2022 Upgrade to a higher tier for more requests"));
|
|
2734
|
-
console.log();
|
|
2735
|
-
process.exit(1);
|
|
2736
|
-
}
|
|
2737
|
-
if (result.isAuthError) {
|
|
2738
|
-
console.log();
|
|
2739
|
-
console.log(chalk4.red(" \u26A0 Authentication Error"));
|
|
2740
|
-
console.log();
|
|
2741
3385
|
console.log(chalk4.dim(` ${result.error}`));
|
|
2742
|
-
console.log();
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
const firstLine = result.error.split("\n")[0] ?? result.error;
|
|
2749
|
-
console.log(chalk4.dim(` ${firstLine.slice(0, 80)}`));
|
|
2750
|
-
console.log();
|
|
2751
|
-
process.exit(1);
|
|
2752
|
-
}
|
|
2753
|
-
lastRateLimitInfo = result.limits;
|
|
2754
|
-
return result.content;
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
// src/browser/launch.ts
|
|
2758
|
-
import { mkdir as mkdir4 } from "fs/promises";
|
|
2759
|
-
import { chromium as chromium4 } from "playwright";
|
|
2760
|
-
|
|
2761
|
-
// src/browser/wsl.ts
|
|
2762
|
-
import { execSync, spawn as spawn2 } from "child_process";
|
|
2763
|
-
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
|
|
2764
|
-
import { request } from "http";
|
|
2765
|
-
import { chromium as chromium3 } from "playwright";
|
|
2766
|
-
function isWSL() {
|
|
2767
|
-
if (process.platform !== "linux") return false;
|
|
2768
|
-
try {
|
|
2769
|
-
const r = execSync("uname -r", { encoding: "utf-8" }).toLowerCase();
|
|
2770
|
-
return r.includes("microsoft") || r.includes("wsl");
|
|
2771
|
-
} catch {
|
|
2772
|
-
return false;
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
function tryEnableWSLInterop() {
|
|
2776
|
-
return existsSync6("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
2777
|
-
}
|
|
2778
|
-
function wslToWindowsPath(wslPath) {
|
|
2779
|
-
if (!wslPath) return wslPath;
|
|
2780
|
-
const m = wslPath.match(/^\/mnt\/([a-z])\/(.*)/);
|
|
2781
|
-
if (!m) return wslPath;
|
|
2782
|
-
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
2783
|
-
}
|
|
2784
|
-
function findExe(whereExeQuery, ...hardcoded) {
|
|
2785
|
-
for (const p of hardcoded) if (existsSync6(p)) return p;
|
|
2786
|
-
try {
|
|
2787
|
-
const line = execSync(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
2788
|
-
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
2789
|
-
} catch {
|
|
2790
|
-
}
|
|
2791
|
-
return null;
|
|
2792
|
-
}
|
|
2793
|
-
var findWindowsChrome = () => findExe(
|
|
2794
|
-
"chrome.exe",
|
|
2795
|
-
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
2796
|
-
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
|
|
2797
|
-
);
|
|
2798
|
-
var findWindowsNode = () => findExe("node.exe");
|
|
2799
|
-
function getWindowsHostIP() {
|
|
2800
|
-
try {
|
|
2801
|
-
const m = execSync("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
|
|
2802
|
-
/default via (\d+\.\d+\.\d+\.\d+)/
|
|
2803
|
-
);
|
|
2804
|
-
if (m?.[1]) return m[1];
|
|
2805
|
-
} catch {
|
|
2806
|
-
}
|
|
2807
|
-
try {
|
|
2808
|
-
const m = readFileSync2("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
2809
|
-
if (m?.[1]) return m[1];
|
|
2810
|
-
} catch {
|
|
2811
|
-
}
|
|
2812
|
-
return "127.0.0.1";
|
|
2813
|
-
}
|
|
2814
|
-
function startRelay(nodePath, windowsHost, targetPort) {
|
|
2815
|
-
const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
|
|
2816
|
-
return new Promise((resolve10, reject) => {
|
|
2817
|
-
const proc = spawn2(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
2818
|
-
const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
|
|
2819
|
-
let buf = "";
|
|
2820
|
-
proc.stdout.on("data", (d) => {
|
|
2821
|
-
buf += d.toString();
|
|
2822
|
-
const m = buf.match(/PORT:(\d+)/);
|
|
2823
|
-
if (m) {
|
|
2824
|
-
clearTimeout(timer);
|
|
2825
|
-
resolve10({ proc, port: +m[1] });
|
|
2826
|
-
}
|
|
2827
|
-
});
|
|
2828
|
-
proc.on("error", (e) => {
|
|
2829
|
-
clearTimeout(timer);
|
|
2830
|
-
reject(e);
|
|
2831
|
-
});
|
|
2832
|
-
});
|
|
2833
|
-
}
|
|
2834
|
-
function waitForChrome(host, port) {
|
|
2835
|
-
return new Promise((resolve10, reject) => {
|
|
2836
|
-
const deadline = Date.now() + 2e4;
|
|
2837
|
-
const poll = () => {
|
|
2838
|
-
if (Date.now() > deadline) {
|
|
2839
|
-
reject(new Error("Chrome debug port did not open within 20 s"));
|
|
2840
|
-
return;
|
|
2841
|
-
}
|
|
2842
|
-
const req = request({ host, port, path: "/json/version", method: "GET" }, (res) => {
|
|
2843
|
-
res.resume();
|
|
2844
|
-
resolve10();
|
|
2845
|
-
});
|
|
2846
|
-
req.on("error", () => setTimeout(poll, 600));
|
|
2847
|
-
req.setTimeout(1e3, () => {
|
|
2848
|
-
req.destroy();
|
|
2849
|
-
setTimeout(poll, 600);
|
|
2850
|
-
});
|
|
2851
|
-
req.end();
|
|
2852
|
-
};
|
|
2853
|
-
poll();
|
|
2854
|
-
});
|
|
2855
|
-
}
|
|
2856
|
-
async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
2857
|
-
const debugPort = options.debugPort ?? 9222;
|
|
2858
|
-
const chromePath = findWindowsChrome();
|
|
2859
|
-
if (!chromePath) throw new Error("Windows Chrome not found. Install Google Chrome for Windows.");
|
|
2860
|
-
const nodePath = findWindowsNode();
|
|
2861
|
-
if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
|
|
2862
|
-
const winUserData = wslToWindowsPath(userDataDir);
|
|
2863
|
-
const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
|
|
2864
|
-
const chromeProc = spawn2(
|
|
2865
|
-
chromePath,
|
|
2866
|
-
[
|
|
2867
|
-
`--remote-debugging-port=${debugPort}`,
|
|
2868
|
-
`--user-data-dir=${winUserData}`,
|
|
2869
|
-
...winExtPath ? [`--disable-extensions-except=${winExtPath}`, `--load-extension=${winExtPath}`] : [],
|
|
2870
|
-
"--no-first-run",
|
|
2871
|
-
"--no-default-browser-check",
|
|
2872
|
-
"--disable-background-networking",
|
|
2873
|
-
"--disable-sync",
|
|
2874
|
-
"--disable-features=TranslateUI",
|
|
2875
|
-
options.startMaximized ? "--start-maximized" : "--start-minimized"
|
|
2876
|
-
],
|
|
2877
|
-
{ stdio: "ignore", detached: true }
|
|
2878
|
-
);
|
|
2879
|
-
chromeProc.unref();
|
|
2880
|
-
const windowsHost = getWindowsHostIP();
|
|
2881
|
-
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
2882
|
-
try {
|
|
2883
|
-
await waitForChrome(windowsHost, proxyPort);
|
|
2884
|
-
const browser = await chromium3.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
2885
|
-
return {
|
|
2886
|
-
browser,
|
|
2887
|
-
cleanup: async () => {
|
|
2888
|
-
try {
|
|
2889
|
-
await browser.close();
|
|
2890
|
-
} catch {
|
|
2891
|
-
}
|
|
2892
|
-
relay.kill();
|
|
3386
|
+
console.log(chalk4.dim(" Options:"));
|
|
3387
|
+
if (result.retryAfterSeconds) {
|
|
3388
|
+
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
3389
|
+
console.log(chalk4.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
|
|
3390
|
+
} else {
|
|
3391
|
+
console.log(chalk4.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
|
|
2893
3392
|
}
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
const videoOpts = recordVideo ? { recordVideo } : {};
|
|
2906
|
-
const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
2907
|
-
let context;
|
|
2908
|
-
let browser = null;
|
|
2909
|
-
let chromeSession = null;
|
|
2910
|
-
if (extensionPath) {
|
|
2911
|
-
await mkdir4(userDataDir, { recursive: true });
|
|
2912
|
-
const persistentArgs = [
|
|
2913
|
-
`--disable-extensions-except=${extensionPath}`,
|
|
2914
|
-
`--load-extension=${extensionPath}`,
|
|
2915
|
-
"--window-position=-32000,-32000",
|
|
2916
|
-
"--window-size=1,1"
|
|
2917
|
-
];
|
|
2918
|
-
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
2919
|
-
if (useWindowsChrome) {
|
|
2920
|
-
try {
|
|
2921
|
-
chromeSession = await launchWindowsChrome(extensionPath, userDataDir, { viewport, ...videoOpts });
|
|
2922
|
-
const contexts = chromeSession.browser.contexts();
|
|
2923
|
-
context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
|
|
2924
|
-
} catch {
|
|
2925
|
-
context = await chromium4.launchPersistentContext(userDataDir, {
|
|
2926
|
-
headless: false,
|
|
2927
|
-
args: persistentArgs,
|
|
2928
|
-
viewport,
|
|
2929
|
-
...videoOpts,
|
|
2930
|
-
acceptDownloads: true
|
|
2931
|
-
});
|
|
3393
|
+
console.log(chalk4.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
|
|
3394
|
+
console.log(chalk4.dim(" \u2022 Upgrade to a higher tier for more requests"));
|
|
3395
|
+
console.log();
|
|
3396
|
+
process.exit(1);
|
|
3397
|
+
}
|
|
3398
|
+
if (result.isAuthError) {
|
|
3399
|
+
console.log();
|
|
3400
|
+
console.log(chalk4.red(` \u26A0 Authentication Error${result.status ? ` (HTTP ${result.status})` : ""}`));
|
|
3401
|
+
console.log();
|
|
3402
|
+
for (const line of wrap(result.error, 76)) {
|
|
3403
|
+
console.log(chalk4.dim(` ${line}`));
|
|
2932
3404
|
}
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
viewport,
|
|
2938
|
-
...videoOpts,
|
|
2939
|
-
acceptDownloads: true
|
|
2940
|
-
});
|
|
3405
|
+
console.log();
|
|
3406
|
+
console.log(chalk4.dim(` ${diagnose(result.status, result.host)}`));
|
|
3407
|
+
console.log();
|
|
3408
|
+
process.exit(1);
|
|
2941
3409
|
}
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
3410
|
+
console.log();
|
|
3411
|
+
console.log(chalk4.red(` \u26A0 AI Error${result.status ? ` (HTTP ${result.status})` : ""}`));
|
|
3412
|
+
console.log();
|
|
3413
|
+
for (const line of wrap(result.error, 76)) {
|
|
3414
|
+
console.log(chalk4.dim(` ${line}`));
|
|
3415
|
+
}
|
|
3416
|
+
if (result.detail && !result.error.includes(result.detail)) {
|
|
3417
|
+
console.log();
|
|
3418
|
+
for (const line of wrap(result.detail, 76).slice(0, 6)) {
|
|
3419
|
+
console.log(chalk4.dim(` ${line}`));
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
console.log();
|
|
3423
|
+
console.log(chalk4.dim(` ${diagnose(result.status, result.host)}`));
|
|
3424
|
+
console.log(chalk4.dim(` Full response body: ${logFilePath}`));
|
|
3425
|
+
console.log();
|
|
3426
|
+
process.exit(1);
|
|
2945
3427
|
}
|
|
2946
|
-
|
|
3428
|
+
lastRateLimitInfo = result.limits;
|
|
3429
|
+
return result.content;
|
|
2947
3430
|
}
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3431
|
+
|
|
3432
|
+
// src/ai/json-plan.ts
|
|
3433
|
+
init_lib();
|
|
3434
|
+
init_utils();
|
|
3435
|
+
var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
|
|
3436
|
+
var PLAN_SHAPE = `The JSON should have: url, title, description, steps array.
|
|
3437
|
+
Each step has: id (number), action, caption, and optionally target, value.`;
|
|
3438
|
+
var jsonFixUserPrompt = (content, shape) => `Fix this malformed JSON. Return ONLY valid JSON, no explanation.
|
|
3439
|
+
${shape}
|
|
3440
|
+
|
|
3441
|
+
Malformed content:
|
|
3442
|
+
${content}`;
|
|
3443
|
+
var noopTask2 = () => ({ succeed: () => {
|
|
3444
|
+
}, fail: () => {
|
|
3445
|
+
} });
|
|
3446
|
+
async function parseAIJsonWithRepair(content, opts) {
|
|
3447
|
+
const { logError: logError2, userEmail, tier = "free", shape = PLAN_SHAPE, context = "JSON Fix" } = opts;
|
|
3448
|
+
const direct = parseAIJson(content);
|
|
3449
|
+
if (direct) return direct.value;
|
|
3450
|
+
const makeTask = opts.task ?? noopTask2;
|
|
3451
|
+
const fixTask = makeTask("Fixing malformed JSON");
|
|
3452
|
+
try {
|
|
3453
|
+
const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content, shape), logError2, userEmail, tier);
|
|
3454
|
+
const repaired = parseAIJson(fixed);
|
|
3455
|
+
if (!repaired) throw new Error("No usable JSON in fix");
|
|
3456
|
+
fixTask.succeed("JSON fixed by AI");
|
|
3457
|
+
return repaired.value;
|
|
3458
|
+
} catch (err) {
|
|
3459
|
+
await logError2(context, err);
|
|
3460
|
+
fixTask.fail("Could not fix JSON");
|
|
3461
|
+
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
2954
3462
|
}
|
|
2955
3463
|
}
|
|
2956
3464
|
|
|
2957
3465
|
// src/plan.ts
|
|
2958
3466
|
init_lib();
|
|
3467
|
+
init_json_repair();
|
|
2959
3468
|
async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
2960
3469
|
const browserTask = task2("Launching browser with extension");
|
|
2961
|
-
const userDataDir =
|
|
2962
|
-
await
|
|
3470
|
+
const userDataDir = join9(workDir, ".browser-data");
|
|
3471
|
+
await mkdir6(userDataDir, { recursive: true });
|
|
2963
3472
|
let context;
|
|
2964
3473
|
let chromeSession = null;
|
|
2965
3474
|
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
2966
|
-
const launchPersistent = () =>
|
|
3475
|
+
const launchPersistent = () => chromium4.launchPersistentContext(userDataDir, {
|
|
2967
3476
|
headless: false,
|
|
2968
3477
|
args: [
|
|
2969
3478
|
`--disable-extensions-except=${extensionPath}`,
|
|
@@ -3044,7 +3553,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
3044
3553
|
else if (el.type === "select" || el.type === "select-one") action = "select";
|
|
3045
3554
|
else if (el.type === "checkbox") action = "check";
|
|
3046
3555
|
else if (["text", "email", "password", "number", "search", "url", "tel"].includes(el.type)) action = "type";
|
|
3047
|
-
const selector = el.id ?
|
|
3556
|
+
const selector = el.id ? idSelector(el.id) : el.name ? `[name="${el.name}"]` : `text="${el.text}"`;
|
|
3048
3557
|
return {
|
|
3049
3558
|
type: el.type === "button" || el.type === "link" ? el.type : "input",
|
|
3050
3559
|
text: el.text,
|
|
@@ -3056,31 +3565,8 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
3056
3565
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
3057
3566
|
return { title, elements };
|
|
3058
3567
|
}
|
|
3059
|
-
var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
|
|
3060
|
-
var jsonFixUserPrompt = (content) => `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
|
|
3061
|
-
The JSON should have: url, title, description, steps array.
|
|
3062
|
-
Each step has: id (number), action, caption, and optionally target, value.
|
|
3063
|
-
|
|
3064
|
-
Malformed content:
|
|
3065
|
-
${content}`;
|
|
3066
3568
|
async function parsePlanJson(content, task2, logError2, userEmail, tier = "free") {
|
|
3067
|
-
|
|
3068
|
-
return JSON.parse(content);
|
|
3069
|
-
} catch {
|
|
3070
|
-
const fixTask = task2("Fixing malformed JSON");
|
|
3071
|
-
try {
|
|
3072
|
-
const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content), logError2, userEmail, tier);
|
|
3073
|
-
const jsonMatch = fixed.match(/\{[\s\S]*\}/);
|
|
3074
|
-
if (!jsonMatch) throw new Error("No JSON in fix");
|
|
3075
|
-
const plan = JSON.parse(jsonMatch[0]);
|
|
3076
|
-
fixTask.succeed("JSON fixed by AI");
|
|
3077
|
-
return plan;
|
|
3078
|
-
} catch (err) {
|
|
3079
|
-
await logError2("JSON Fix", err);
|
|
3080
|
-
fixTask.fail("Could not fix JSON");
|
|
3081
|
-
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3569
|
+
return parseAIJsonWithRepair(content, { logError: logError2, task: task2, userEmail, tier });
|
|
3084
3570
|
}
|
|
3085
3571
|
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
|
|
3086
3572
|
const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
|
|
@@ -3089,10 +3575,9 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
3089
3575
|
const systemPrompt = buildSystemPrompt(maxSteps);
|
|
3090
3576
|
const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
|
|
3091
3577
|
try {
|
|
3092
|
-
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
|
|
3093
|
-
const
|
|
3094
|
-
if (!
|
|
3095
|
-
const plan = await parsePlanJson(jsonMatch[0], task2, logError2, userEmail, tier);
|
|
3578
|
+
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier, "plan");
|
|
3579
|
+
const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
|
|
3580
|
+
if (!Array.isArray(plan.steps)) throw new Error("AI response has no steps array");
|
|
3096
3581
|
if (lang !== void 0) plan.lang = lang;
|
|
3097
3582
|
plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
|
|
3098
3583
|
...s,
|
|
@@ -3142,7 +3627,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
|
|
|
3142
3627
|
renumberTask.fail("No valid steps");
|
|
3143
3628
|
throw new Error("Plan has no valid steps");
|
|
3144
3629
|
}
|
|
3145
|
-
await
|
|
3630
|
+
await writeFile6(planPath, JSON.stringify(plan, null, 2));
|
|
3146
3631
|
const removed = originalCount - plan.steps.length;
|
|
3147
3632
|
renumberTask.succeed(
|
|
3148
3633
|
removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
|
|
@@ -3152,66 +3637,6 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
|
|
|
3152
3637
|
|
|
3153
3638
|
// src/commands/utils/record-helpers.ts
|
|
3154
3639
|
init_config_loader();
|
|
3155
|
-
|
|
3156
|
-
// src/utils/logging.ts
|
|
3157
|
-
import { appendFile, writeFile as writeFile6 } from "fs/promises";
|
|
3158
|
-
import { join as join9 } from "path";
|
|
3159
|
-
var LOG_FILE_NAME = "quickpeek.log";
|
|
3160
|
-
var logFileInitialized = false;
|
|
3161
|
-
var logFilePath = LOG_FILE_NAME;
|
|
3162
|
-
async function writeLogEntry(entry) {
|
|
3163
|
-
try {
|
|
3164
|
-
if (!logFileInitialized) {
|
|
3165
|
-
await writeFile6(logFilePath, entry);
|
|
3166
|
-
logFileInitialized = true;
|
|
3167
|
-
} else {
|
|
3168
|
-
await appendFile(logFilePath, entry);
|
|
3169
|
-
}
|
|
3170
|
-
} catch {
|
|
3171
|
-
}
|
|
3172
|
-
}
|
|
3173
|
-
async function setLogFilePath(outputDir) {
|
|
3174
|
-
logFilePath = join9(outputDir, LOG_FILE_NAME);
|
|
3175
|
-
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3176
|
-
${"=".repeat(60)}
|
|
3177
|
-
|
|
3178
|
-
`;
|
|
3179
|
-
try {
|
|
3180
|
-
await writeFile6(logFilePath, header2);
|
|
3181
|
-
logFileInitialized = true;
|
|
3182
|
-
} catch {
|
|
3183
|
-
}
|
|
3184
|
-
}
|
|
3185
|
-
async function logInfo(context, message) {
|
|
3186
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
|
|
3187
|
-
`);
|
|
3188
|
-
}
|
|
3189
|
-
async function logDebug(context, message) {
|
|
3190
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
|
|
3191
|
-
`);
|
|
3192
|
-
}
|
|
3193
|
-
async function logError(context, error) {
|
|
3194
|
-
let errorMsg;
|
|
3195
|
-
if (error instanceof Error) {
|
|
3196
|
-
errorMsg = `${error.message}
|
|
3197
|
-
${error.stack}`;
|
|
3198
|
-
} else if (typeof error === "object" && error !== null) {
|
|
3199
|
-
errorMsg = JSON.stringify(error, null, 2);
|
|
3200
|
-
} else {
|
|
3201
|
-
errorMsg = String(error);
|
|
3202
|
-
}
|
|
3203
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
|
|
3204
|
-
${errorMsg}
|
|
3205
|
-
|
|
3206
|
-
`);
|
|
3207
|
-
}
|
|
3208
|
-
async function logStep(stepNum, action, details) {
|
|
3209
|
-
const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
|
|
3210
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
|
|
3211
|
-
`);
|
|
3212
|
-
}
|
|
3213
|
-
|
|
3214
|
-
// src/commands/utils/record-helpers.ts
|
|
3215
3640
|
init_utils();
|
|
3216
3641
|
var FREE_VIDEO_LIMIT = 5;
|
|
3217
3642
|
var INDENT2 = " ";
|
|
@@ -3331,7 +3756,7 @@ function findPlanFile(dir, lang) {
|
|
|
3331
3756
|
|
|
3332
3757
|
// src/commands/record-cmd.ts
|
|
3333
3758
|
import { existsSync as existsSync18 } from "fs";
|
|
3334
|
-
import { mkdir as
|
|
3759
|
+
import { mkdir as mkdir9 } from "fs/promises";
|
|
3335
3760
|
import { isAbsolute as isAbsolute5, join as join21, resolve as resolve9 } from "path";
|
|
3336
3761
|
import chalk15 from "chalk";
|
|
3337
3762
|
init_lib();
|
|
@@ -3516,10 +3941,10 @@ import chalk10 from "chalk";
|
|
|
3516
3941
|
// src/ai/fix.ts
|
|
3517
3942
|
init_lib();
|
|
3518
3943
|
async function callGroqAndFinalize(systemPrompt, userMessage, plan, logError2, userEmail, tier = "free") {
|
|
3519
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
3520
|
-
const
|
|
3521
|
-
if (!
|
|
3522
|
-
return finalizePlan(
|
|
3944
|
+
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "fix");
|
|
3945
|
+
const result = await parseAIJsonWithRepair(content, { logError: logError2, userEmail, tier, context: "Plan Fix" });
|
|
3946
|
+
if (!Array.isArray(result.steps)) throw new Error("AI response has no steps array");
|
|
3947
|
+
return finalizePlan(result, plan);
|
|
3523
3948
|
}
|
|
3524
3949
|
function finalizePlan(result, original) {
|
|
3525
3950
|
result.url = original.url;
|
|
@@ -3643,10 +4068,15 @@ async function translatePlan(plan, targetLang, logError2, userEmail, tier = "fre
|
|
|
3643
4068
|
${JSON.stringify(toTranslate)}
|
|
3644
4069
|
|
|
3645
4070
|
Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
|
|
3646
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
3647
|
-
const
|
|
3648
|
-
|
|
3649
|
-
|
|
4071
|
+
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "fix");
|
|
4072
|
+
const translated = await parseAIJsonWithRepair(content, {
|
|
4073
|
+
logError: logError2,
|
|
4074
|
+
userEmail,
|
|
4075
|
+
tier,
|
|
4076
|
+
shape: "The JSON should have: description (string), captions (array of strings).",
|
|
4077
|
+
context: "Translate Plan"
|
|
4078
|
+
});
|
|
4079
|
+
if (!Array.isArray(translated.captions)) throw new Error("Translation response has no captions array");
|
|
3650
4080
|
const translatedPlan = JSON.parse(JSON.stringify(plan));
|
|
3651
4081
|
translatedPlan.title = plan.title;
|
|
3652
4082
|
translatedPlan.description = translated.description || plan.description;
|
|
@@ -3720,6 +4150,11 @@ async function runPlanPhase(params) {
|
|
|
3720
4150
|
}
|
|
3721
4151
|
const normalizeUrl2 = (u) => u.replace(/\/+$/, "").toLowerCase();
|
|
3722
4152
|
const urlMatches = existingPlan && normalizeUrl2(existingPlan.url) === normalizeUrl2(url);
|
|
4153
|
+
if (existingPlan && !urlMatches && isAutoYes()) {
|
|
4154
|
+
console.log();
|
|
4155
|
+
console.log(chalk10.dim(` Existing plan is for ${existingPlan.url}, not ${url} \u2014 regenerating.`));
|
|
4156
|
+
existingPlan = null;
|
|
4157
|
+
}
|
|
3723
4158
|
let plan;
|
|
3724
4159
|
const doTranslate = async (failMsg) => {
|
|
3725
4160
|
const translateTask = task(`Translating to ${targetLang}`);
|
|
@@ -3884,7 +4319,7 @@ import chalk12 from "chalk";
|
|
|
3884
4319
|
|
|
3885
4320
|
// src/record.ts
|
|
3886
4321
|
import { existsSync as existsSync13 } from "fs";
|
|
3887
|
-
import { mkdir as
|
|
4322
|
+
import { mkdir as mkdir7 } from "fs/promises";
|
|
3888
4323
|
import { join as join16 } from "path";
|
|
3889
4324
|
import chalk11 from "chalk";
|
|
3890
4325
|
|
|
@@ -3895,7 +4330,7 @@ import { isAbsolute as isAbsolute2, resolve as resolve5 } from "path";
|
|
|
3895
4330
|
|
|
3896
4331
|
// src/browser/effects.ts
|
|
3897
4332
|
init_lib();
|
|
3898
|
-
import { existsSync as existsSync9, readFileSync as
|
|
4333
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
3899
4334
|
import { join as join12 } from "path";
|
|
3900
4335
|
function loadHighlightCSS() {
|
|
3901
4336
|
const candidates = [
|
|
@@ -3905,7 +4340,7 @@ function loadHighlightCSS() {
|
|
|
3905
4340
|
// src/browser/ → src/
|
|
3906
4341
|
];
|
|
3907
4342
|
for (const p of candidates) {
|
|
3908
|
-
if (existsSync9(p)) return
|
|
4343
|
+
if (existsSync9(p)) return readFileSync5(p, "utf-8");
|
|
3909
4344
|
}
|
|
3910
4345
|
return "";
|
|
3911
4346
|
}
|
|
@@ -4105,6 +4540,10 @@ async function resolveElement(page, target) {
|
|
|
4105
4540
|
await locator.waitFor({ state: "visible", timeout });
|
|
4106
4541
|
return await locator.elementHandle({ timeout: 1e3 });
|
|
4107
4542
|
}
|
|
4543
|
+
if (target.startsWith("#") && !/^#[A-Za-z_-][\w-]*$/.test(target)) {
|
|
4544
|
+
const attr = `[id="${target.slice(1).replace(/(["\\])/g, "\\$1")}"]`;
|
|
4545
|
+
return await page.waitForSelector(attr, { state: "visible", timeout });
|
|
4546
|
+
}
|
|
4108
4547
|
return await page.waitForSelector(target, { state: "visible", timeout });
|
|
4109
4548
|
} catch {
|
|
4110
4549
|
return null;
|
|
@@ -4910,14 +5349,14 @@ function formatStepLabel(step) {
|
|
|
4910
5349
|
}
|
|
4911
5350
|
async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
|
|
4912
5351
|
const videoDir = join16(workDir, "video");
|
|
4913
|
-
await
|
|
5352
|
+
await mkdir7(videoDir, { recursive: true });
|
|
4914
5353
|
const getStepDuration = (stepNum) => {
|
|
4915
5354
|
return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
|
|
4916
5355
|
};
|
|
4917
5356
|
const browserTask = task2("Starting browser recording");
|
|
4918
5357
|
const recordingStartTime = Date.now();
|
|
4919
5358
|
const userDataDir = join16(workDir, "browser-data");
|
|
4920
|
-
const recordVideo = { dir: videoDir, size:
|
|
5359
|
+
const recordVideo = { dir: videoDir, size: recordingViewport(config) };
|
|
4921
5360
|
const authState = findAuthState(plan.url);
|
|
4922
5361
|
const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
|
|
4923
5362
|
const page = context.pages()[0] || await context.newPage();
|
|
@@ -4938,6 +5377,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4938
5377
|
let onMediaPage = firstStepIsMedia;
|
|
4939
5378
|
const failures = [];
|
|
4940
5379
|
const actualStepDurations = [];
|
|
5380
|
+
let pageClosedEarly = false;
|
|
4941
5381
|
for (let i = 0; i < plan.steps.length; i++) {
|
|
4942
5382
|
const step = plan.steps[i];
|
|
4943
5383
|
const stepNum = i + 1;
|
|
@@ -4964,6 +5404,12 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4964
5404
|
let targetEl = null;
|
|
4965
5405
|
if (step.target) {
|
|
4966
5406
|
targetEl = await resolveElement(page, step.target);
|
|
5407
|
+
if (!targetEl) {
|
|
5408
|
+
console.log(
|
|
5409
|
+
`
|
|
5410
|
+
\u26A0 target "${step.target}" not found on page \u2014 step ${stepNum} recorded without the action`
|
|
5411
|
+
);
|
|
5412
|
+
}
|
|
4967
5413
|
}
|
|
4968
5414
|
if (step.zoom && step.zoom !== 1 && targetEl && !isExtensionPage) {
|
|
4969
5415
|
const preZoomBox = await targetEl.boundingBox() ?? void 0;
|
|
@@ -5000,11 +5446,19 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
5000
5446
|
}
|
|
5001
5447
|
const stepElapsed = Date.now() - stepStartTime;
|
|
5002
5448
|
const waitTime = Math.max(0, stepDuration - stepElapsed);
|
|
5003
|
-
if (waitTime > 0) await page.waitForTimeout(waitTime);
|
|
5004
5449
|
const stepDelay = config.video.stepDelay ?? 500;
|
|
5005
5450
|
const isLastStep = i === plan.steps.length - 1;
|
|
5006
|
-
|
|
5007
|
-
await page.waitForTimeout(
|
|
5451
|
+
try {
|
|
5452
|
+
if (waitTime > 0) await page.waitForTimeout(waitTime);
|
|
5453
|
+
if (!isLastStep && stepDelay > 0) await page.waitForTimeout(stepDelay);
|
|
5454
|
+
} catch (err) {
|
|
5455
|
+
if (page.isClosed()) {
|
|
5456
|
+
console.log(chalk11.yellow("\n \u26A0 Browser page closed early \u2014 saving the recording captured so far"));
|
|
5457
|
+
pageClosedEarly = true;
|
|
5458
|
+
actualStepDurations.push(Math.max(stepElapsed, stepDuration));
|
|
5459
|
+
break;
|
|
5460
|
+
}
|
|
5461
|
+
throw err;
|
|
5008
5462
|
}
|
|
5009
5463
|
if (!isExtensionPage) {
|
|
5010
5464
|
const shouldHighlight = step.highlight !== void 0 ? step.highlight : config.video.highlight;
|
|
@@ -5022,10 +5476,11 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
5022
5476
|
console.log(chalk11.yellow(` - ${f}`));
|
|
5023
5477
|
}
|
|
5024
5478
|
}
|
|
5479
|
+
while (actualStepDurations.length < plan.steps.length) actualStepDurations.push(0);
|
|
5025
5480
|
const saveTask = task2("Saving video");
|
|
5026
5481
|
const video = page.video();
|
|
5027
5482
|
if (!video) throw new Error("No video recorder available");
|
|
5028
|
-
await page.close();
|
|
5483
|
+
if (!pageClosedEarly) await page.close();
|
|
5029
5484
|
const videoPath = await video.path();
|
|
5030
5485
|
await cleanupBrowserSession({ context, browser, chromeSession });
|
|
5031
5486
|
if (!videoPath || !existsSync13(videoPath)) {
|
|
@@ -5195,7 +5650,7 @@ import { join as join18, resolve as resolve8 } from "path";
|
|
|
5195
5650
|
init_lib();
|
|
5196
5651
|
import { chunkWordsByCount, assTimeFromMs as formatASSTime } from "@spark-apps/video-kit";
|
|
5197
5652
|
import { existsSync as existsSync14 } from "fs";
|
|
5198
|
-
import { mkdir as
|
|
5653
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
5199
5654
|
import { isAbsolute as isAbsolute4, join as join17, resolve as resolve7 } from "path";
|
|
5200
5655
|
import chalk13 from "chalk";
|
|
5201
5656
|
var INDENT4 = " ";
|
|
@@ -5288,7 +5743,7 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
|
|
|
5288
5743
|
process.exit(0);
|
|
5289
5744
|
}
|
|
5290
5745
|
const composeTask = task("Building final video");
|
|
5291
|
-
await
|
|
5746
|
+
await mkdir8(config.output.dir, { recursive: true });
|
|
5292
5747
|
const suffix = config.lang ? `-${config.lang}` : "";
|
|
5293
5748
|
const finalPath = join17(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
5294
5749
|
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
@@ -5356,6 +5811,21 @@ async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
|
|
|
5356
5811
|
|
|
5357
5812
|
// src/commands/phases/video-phase.ts
|
|
5358
5813
|
var INDENT5 = " ";
|
|
5814
|
+
function resolveOutro(config, userTier) {
|
|
5815
|
+
if (userTier !== "free") {
|
|
5816
|
+
const custom = config.video.outro?.trim();
|
|
5817
|
+
if (!custom) return void 0;
|
|
5818
|
+
const path = resolve8(custom);
|
|
5819
|
+
if (existsSync15(path)) return path;
|
|
5820
|
+
info(`${INDENT5}\u26A0 outro not found: ${path}`);
|
|
5821
|
+
return void 0;
|
|
5822
|
+
}
|
|
5823
|
+
const scriptDir = getScriptDir();
|
|
5824
|
+
const bundled = join18(scriptDir, "assets", "qp.wm");
|
|
5825
|
+
if (existsSync15(bundled)) return bundled;
|
|
5826
|
+
const fallback = join18(scriptDir, "..", "dist", "assets", "qp.wm");
|
|
5827
|
+
return existsSync15(fallback) ? fallback : void 0;
|
|
5828
|
+
}
|
|
5359
5829
|
async function runVideoPhase(params) {
|
|
5360
5830
|
let { stepAudio } = params;
|
|
5361
5831
|
const { plan, config, voiceoverPath, workDir, captions, userTier } = params;
|
|
@@ -5460,7 +5930,7 @@ async function runVideoPhase(params) {
|
|
|
5460
5930
|
const transitionTask = task("Applying transitions");
|
|
5461
5931
|
try {
|
|
5462
5932
|
const transitionedPath = join18(workDir, "transitioned.mp4");
|
|
5463
|
-
const videoDuration =
|
|
5933
|
+
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
5464
5934
|
await applyTransitions({
|
|
5465
5935
|
inputVideo: videoPath,
|
|
5466
5936
|
outputVideo: transitionedPath,
|
|
@@ -5478,30 +5948,24 @@ async function runVideoPhase(params) {
|
|
|
5478
5948
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
5479
5949
|
let videoToCompose = videoPath;
|
|
5480
5950
|
let mainContentDuration;
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
});
|
|
5500
|
-
videoToCompose = combinedPath;
|
|
5501
|
-
} catch (err) {
|
|
5502
|
-
await logError("Concat Outro", err);
|
|
5503
|
-
mainContentDuration = void 0;
|
|
5504
|
-
}
|
|
5951
|
+
const outroPath = resolveOutro(config, userTier);
|
|
5952
|
+
if (outroPath) {
|
|
5953
|
+
try {
|
|
5954
|
+
const mainDuration = await getMediaDuration(videoPath);
|
|
5955
|
+
mainContentDuration = mainDuration / 1e3 - trimStart;
|
|
5956
|
+
const combinedPath = join18(workDir, "combined-with-outro.mp4");
|
|
5957
|
+
await concatVideoWithOutro({
|
|
5958
|
+
inputPath: videoPath,
|
|
5959
|
+
outroPath,
|
|
5960
|
+
outputPath: combinedPath,
|
|
5961
|
+
width: config.video.width,
|
|
5962
|
+
height: config.video.height,
|
|
5963
|
+
preset: config.video.preset
|
|
5964
|
+
});
|
|
5965
|
+
videoToCompose = combinedPath;
|
|
5966
|
+
} catch (err) {
|
|
5967
|
+
await logError("Concat Outro", err);
|
|
5968
|
+
mainContentDuration = void 0;
|
|
5505
5969
|
}
|
|
5506
5970
|
}
|
|
5507
5971
|
let finalPath = await composeAndFinish(
|
|
@@ -5546,8 +6010,8 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5546
6010
|
for (const file of filesToDelete) {
|
|
5547
6011
|
const filePath = join19(config.output.dir, file);
|
|
5548
6012
|
if (existsSync16(filePath)) {
|
|
5549
|
-
const { unlink:
|
|
5550
|
-
await
|
|
6013
|
+
const { unlink: unlink4 } = await import("fs/promises");
|
|
6014
|
+
await unlink4(filePath).catch(() => {
|
|
5551
6015
|
});
|
|
5552
6016
|
}
|
|
5553
6017
|
}
|
|
@@ -5556,7 +6020,12 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5556
6020
|
const voice = resolveVoice(config);
|
|
5557
6021
|
const existingMeta = await loadVoiceoverMeta(config.output.dir);
|
|
5558
6022
|
const voiceoverFile = join19(config.output.dir, "voiceover.wav");
|
|
5559
|
-
const
|
|
6023
|
+
const voiceoverOnDisk = existingMeta && existsSync16(voiceoverFile);
|
|
6024
|
+
const staleContainer = voiceoverOnDisk ? !await isHonestWav(voiceoverFile) : false;
|
|
6025
|
+
if (staleContainer) {
|
|
6026
|
+
info("Existing voiceover has a stale container, regenerating");
|
|
6027
|
+
}
|
|
6028
|
+
const hasExistingVoiceover = voiceoverOnDisk && !staleContainer;
|
|
5560
6029
|
const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
|
|
5561
6030
|
let voiceoverPath;
|
|
5562
6031
|
let stepAudio;
|
|
@@ -5613,7 +6082,7 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5613
6082
|
}
|
|
5614
6083
|
|
|
5615
6084
|
// src/commands/utils/chrome-extension.ts
|
|
5616
|
-
import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as
|
|
6085
|
+
import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
|
|
5617
6086
|
import { homedir as homedir3 } from "os";
|
|
5618
6087
|
import { join as join20 } from "path";
|
|
5619
6088
|
function resolveChromeExtension(extensionId) {
|
|
@@ -5660,7 +6129,7 @@ function resolveChromeExtension(extensionId) {
|
|
|
5660
6129
|
const manifestPath = join20(extPath, "manifest.json");
|
|
5661
6130
|
if (existsSync17(manifestPath)) {
|
|
5662
6131
|
try {
|
|
5663
|
-
const manifest = JSON.parse(
|
|
6132
|
+
const manifest = JSON.parse(readFileSync6(manifestPath, "utf-8"));
|
|
5664
6133
|
if (manifest.name && !manifest.name.startsWith("__MSG_")) {
|
|
5665
6134
|
name = manifest.name;
|
|
5666
6135
|
}
|
|
@@ -5702,6 +6171,13 @@ async function recordCommand(url, options = {}) {
|
|
|
5702
6171
|
console.log(chalk15.dim(` Extension: ${chromeExtension.name}`));
|
|
5703
6172
|
console.log(chalk15.dim(` Path: ${chromeExtension.path}`));
|
|
5704
6173
|
}
|
|
6174
|
+
if (!chromeExtension) {
|
|
6175
|
+
const startUrl = resolveStartUrl(url);
|
|
6176
|
+
if (startUrl !== url) {
|
|
6177
|
+
console.log(chalk15.dim(` Signed-in start page: ${startUrl}`));
|
|
6178
|
+
url = startUrl;
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
5705
6181
|
config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
|
|
5706
6182
|
const userData = await loadUserData();
|
|
5707
6183
|
const authResult = await runAuthPhase({
|
|
@@ -5757,7 +6233,7 @@ async function recordCommand(url, options = {}) {
|
|
|
5757
6233
|
const planPath = join21(config.output.dir, `plan-${targetLang}.json`);
|
|
5758
6234
|
const legacyPlanPath = join21(config.output.dir, "plan.json");
|
|
5759
6235
|
const isExtensionUrl = url.startsWith("chrome-extension://");
|
|
5760
|
-
await
|
|
6236
|
+
await mkdir9(config.output.dir, { recursive: true });
|
|
5761
6237
|
await setLogFilePath(config.output.dir);
|
|
5762
6238
|
try {
|
|
5763
6239
|
const plan = await runPlanPhase({
|
|
@@ -5820,23 +6296,32 @@ import chalk16 from "chalk";
|
|
|
5820
6296
|
|
|
5821
6297
|
// src/ai/narrate.ts
|
|
5822
6298
|
init_lib();
|
|
5823
|
-
|
|
6299
|
+
var SCRIPT_ACTIONS = /* @__PURE__ */ new Set(["click", "type", "scroll", "wait", "select", "check", "navigate"]);
|
|
6300
|
+
var NEEDS_TARGET = /* @__PURE__ */ new Set(["click", "type", "select", "check", "navigate"]);
|
|
6301
|
+
function isUsableStep(step) {
|
|
6302
|
+
if (!step?.action || !SCRIPT_ACTIONS.has(step.action)) return false;
|
|
6303
|
+
if (NEEDS_TARGET.has(step.action) && !step.target) return false;
|
|
6304
|
+
if (step.action === "navigate" && !/^https?:\/\//.test(step.target ?? "")) return false;
|
|
6305
|
+
return true;
|
|
6306
|
+
}
|
|
6307
|
+
async function generatePlanFromScript(captions, url, title, pageElementsText, lang, logError2, userEmail, tier = "free", task2) {
|
|
5824
6308
|
const systemPrompt = `You map narration sentences to web UI actions.
|
|
5825
|
-
Given page elements and a script, return
|
|
6309
|
+
Given page elements and a numbered script, return one action per script line.
|
|
5826
6310
|
|
|
5827
6311
|
RULES:
|
|
5828
|
-
- Return ONLY valid JSON
|
|
5829
|
-
-
|
|
6312
|
+
- Return ONLY valid JSON, no explanation, no captions
|
|
6313
|
+
- Return exactly one entry per script line, in order, with "line" set to that line's number
|
|
5830
6314
|
- Use page elements to pick realistic actions/targets
|
|
5831
|
-
- If a
|
|
6315
|
+
- If a line doesn't match a specific UI action, use { "action": "wait" }
|
|
6316
|
+
- Keep every target and value short
|
|
5832
6317
|
|
|
5833
6318
|
ACTION FORMATS:
|
|
5834
|
-
\u2022 click: { "action": "click", "target": "button text" }
|
|
5835
|
-
\u2022 type: { "action": "type", "target": "input selector", "value": "text
|
|
5836
|
-
\u2022 scroll: { "action": "scroll", "value": "up|down" }
|
|
5837
|
-
\u2022 wait: { "action": "wait" }
|
|
6319
|
+
\u2022 click: { "line": 1, "action": "click", "target": "button text" }
|
|
6320
|
+
\u2022 type: { "line": 2, "action": "type", "target": "input selector", "value": "text to type" }
|
|
6321
|
+
\u2022 scroll: { "line": 3, "action": "scroll", "value": "up|down" }
|
|
6322
|
+
\u2022 wait: { "line": 4, "action": "wait" }
|
|
5838
6323
|
|
|
5839
|
-
OUTPUT:
|
|
6324
|
+
OUTPUT: {"description":"one short sentence","steps":[ ... ]}`;
|
|
5840
6325
|
const scriptLines = captions.map((c, i) => `${i + 1}. "${c}"`).join("\n");
|
|
5841
6326
|
const userMessage = `URL: ${url}
|
|
5842
6327
|
App: ${title}
|
|
@@ -5844,29 +6329,67 @@ App: ${title}
|
|
|
5844
6329
|
Available page elements:
|
|
5845
6330
|
${pageElementsText}
|
|
5846
6331
|
|
|
5847
|
-
Narration script (map each line to a UI action
|
|
6332
|
+
Narration script (map each line to a UI action):
|
|
5848
6333
|
${scriptLines}
|
|
5849
6334
|
|
|
5850
|
-
Return
|
|
5851
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
5852
|
-
const
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
6335
|
+
Return JSON with a "description" and a "steps" array of exactly ${captions.length} actions.`;
|
|
6336
|
+
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "narration");
|
|
6337
|
+
const parsed = await parseAIJsonWithRepair(content, {
|
|
6338
|
+
logError: logError2,
|
|
6339
|
+
task: task2,
|
|
6340
|
+
userEmail,
|
|
6341
|
+
tier,
|
|
6342
|
+
shape: "The JSON should have: description (string), steps (array of {line, action, target, value}).",
|
|
6343
|
+
context: "Script Plan"
|
|
6344
|
+
});
|
|
6345
|
+
const raw = Array.isArray(parsed?.steps) ? parsed.steps : [];
|
|
6346
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
6347
|
+
raw.forEach((step, i) => {
|
|
6348
|
+
if (!step || typeof step !== "object") return;
|
|
6349
|
+
const line = typeof step.line === "number" && step.line >= 1 ? step.line : i + 1;
|
|
6350
|
+
if (!byLine.has(line)) byLine.set(line, step);
|
|
6351
|
+
});
|
|
6352
|
+
const steps = captions.map((caption, i) => {
|
|
6353
|
+
const mapped = byLine.get(i + 1);
|
|
6354
|
+
if (!isUsableStep(mapped)) {
|
|
6355
|
+
return { id: i + 1, action: "wait", caption };
|
|
6356
|
+
}
|
|
6357
|
+
return {
|
|
6358
|
+
id: i + 1,
|
|
6359
|
+
action: mapped.action,
|
|
6360
|
+
caption,
|
|
6361
|
+
...mapped.target ? { target: mapped.target } : {},
|
|
6362
|
+
...mapped.value ? { value: mapped.value } : {}
|
|
6363
|
+
};
|
|
6364
|
+
});
|
|
6365
|
+
return {
|
|
6366
|
+
url,
|
|
6367
|
+
title,
|
|
6368
|
+
description: typeof parsed?.description === "string" ? parsed.description : title,
|
|
6369
|
+
...lang !== void 0 ? { lang } : {},
|
|
6370
|
+
steps
|
|
6371
|
+
};
|
|
5863
6372
|
}
|
|
5864
6373
|
|
|
5865
6374
|
// src/commands/script-cmd.ts
|
|
5866
6375
|
init_lib();
|
|
5867
6376
|
init_config_loader();
|
|
6377
|
+
async function reusablePlan(planPath, lines) {
|
|
6378
|
+
try {
|
|
6379
|
+
const plan = JSON.parse(await readFile8(planPath, "utf-8"));
|
|
6380
|
+
if (!Array.isArray(plan.steps)) return null;
|
|
6381
|
+
if (plan.steps.length < lines.length) return null;
|
|
6382
|
+
const matches = lines.every((line, i) => plan.steps[i]?.caption === line);
|
|
6383
|
+
return matches ? plan : null;
|
|
6384
|
+
} catch {
|
|
6385
|
+
return null;
|
|
6386
|
+
}
|
|
6387
|
+
}
|
|
5868
6388
|
async function scriptCommand(url, scriptPath, lang) {
|
|
6389
|
+
url = resolveStartUrl(url);
|
|
5869
6390
|
const { config, workDir, captions } = await initCommandEnv(url, lang);
|
|
6391
|
+
const userData = await loadUserData();
|
|
6392
|
+
const userTier = userData.email ? await getTierByEmail(userData.email) : "free";
|
|
5870
6393
|
header("Script");
|
|
5871
6394
|
const raw = await readFile8(scriptPath, "utf-8");
|
|
5872
6395
|
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
@@ -5875,17 +6398,39 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
5875
6398
|
process.exit(1);
|
|
5876
6399
|
}
|
|
5877
6400
|
info(`${lines.length} captions from script`);
|
|
6401
|
+
const maxSteps = config.plan?.maxSteps ?? 8;
|
|
6402
|
+
if (lines.length > maxSteps) {
|
|
6403
|
+
console.log(
|
|
6404
|
+
chalk16.yellow(` \u26A0 ${lines.length} script lines exceeds plan.maxSteps (${maxSteps}) \u2014 recording all of them.`)
|
|
6405
|
+
);
|
|
6406
|
+
console.log(chalk16.dim(` Raise plan.maxSteps in quickpeek.config.json to silence this, or split the script.`));
|
|
6407
|
+
}
|
|
5878
6408
|
header("Analyze");
|
|
5879
6409
|
const crawlTask = task("Crawling page for UI elements");
|
|
5880
|
-
const { elements } = await crawlPage(url, () => crawlTask);
|
|
6410
|
+
const { elements } = await crawlPage(url, () => crawlTask, findAuthState(url));
|
|
5881
6411
|
const elementsText = elements.slice(0, 30).map((e) => ` ${e.action}: ${e.text} (${e.selector})`).join("\n");
|
|
5882
6412
|
header("Plan");
|
|
5883
6413
|
const planTask = task("Mapping script to UI actions");
|
|
5884
6414
|
const appTitle = new URL(url).hostname.replace(/^www\./, "");
|
|
5885
|
-
const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
|
|
5886
6415
|
const planPath = join22(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
5887
|
-
await
|
|
5888
|
-
|
|
6416
|
+
let plan = await reusablePlan(planPath, lines);
|
|
6417
|
+
if (plan) {
|
|
6418
|
+
planTask.succeed(`${plan.steps.length} steps reused from existing plan`);
|
|
6419
|
+
} else {
|
|
6420
|
+
plan = await generatePlanFromScript(
|
|
6421
|
+
lines,
|
|
6422
|
+
url,
|
|
6423
|
+
appTitle,
|
|
6424
|
+
elementsText,
|
|
6425
|
+
lang,
|
|
6426
|
+
logError,
|
|
6427
|
+
userData.email,
|
|
6428
|
+
userTier,
|
|
6429
|
+
() => planTask
|
|
6430
|
+
);
|
|
6431
|
+
await savePlan(plan, planPath);
|
|
6432
|
+
planTask.succeed(`${plan.steps.length} steps planned`);
|
|
6433
|
+
}
|
|
5889
6434
|
header("Voiceover");
|
|
5890
6435
|
const { voiceoverPath, stepAudio } = await generateVoiceover(
|
|
5891
6436
|
plan.steps,
|
|
@@ -5901,7 +6446,7 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
5901
6446
|
const { videoPath } = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
|
|
5902
6447
|
header("Compose");
|
|
5903
6448
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
5904
|
-
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config,
|
|
6449
|
+
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, userTier);
|
|
5905
6450
|
console.log();
|
|
5906
6451
|
console.log(chalk16.bold.green(" Done!"));
|
|
5907
6452
|
console.log(chalk16.cyan(` ${finalPath}`));
|
|
@@ -5910,11 +6455,102 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
5910
6455
|
// src/mcp/tools.ts
|
|
5911
6456
|
init_SparkStripe_feature_gating();
|
|
5912
6457
|
|
|
6458
|
+
// src/mcp/delegate.ts
|
|
6459
|
+
init_SparkStripe_feature_gating();
|
|
6460
|
+
var STEPS = {
|
|
6461
|
+
plan: {
|
|
6462
|
+
field: "plan",
|
|
6463
|
+
shape: "the demo plan, as the JSON object the instructions describe"
|
|
6464
|
+
},
|
|
6465
|
+
fix: {
|
|
6466
|
+
field: "fix",
|
|
6467
|
+
shape: "the corrected steps, as the JSON object the instructions describe"
|
|
6468
|
+
},
|
|
6469
|
+
narration: {
|
|
6470
|
+
field: "narration",
|
|
6471
|
+
shape: "the narration, as the JSON object the instructions describe"
|
|
6472
|
+
}
|
|
6473
|
+
};
|
|
6474
|
+
function textResult(text) {
|
|
6475
|
+
return { content: [{ type: "text", text }] };
|
|
6476
|
+
}
|
|
6477
|
+
function section(e, index) {
|
|
6478
|
+
const { field, shape } = STEPS[e.step];
|
|
6479
|
+
return [`### ${index}. ${field}`, ``, e.system, ``, e.prompt, ``, `Answer as \`${field}\`: ${shape}`].join("\n");
|
|
6480
|
+
}
|
|
6481
|
+
function delegatedBrief(pending2) {
|
|
6482
|
+
const tool = pending2[0]?.tool ?? "this tool";
|
|
6483
|
+
const fields = pending2.map((e) => STEPS[e.step].field);
|
|
6484
|
+
return textResult(
|
|
6485
|
+
[
|
|
6486
|
+
`\`${tool}\` needs ${pending2.length === 1 ? "a piece of" : `${pending2.length} pieces of`} writing,`,
|
|
6487
|
+
`and you are a stronger model than the one this CLI would relay to, so it is yours to`,
|
|
6488
|
+
`do. QuickPeek never generates on your behalf.`,
|
|
6489
|
+
``,
|
|
6490
|
+
...pending2.map((e, i) => `${section(e, i + 1)}
|
|
6491
|
+
`),
|
|
6492
|
+
`## Next`,
|
|
6493
|
+
``,
|
|
6494
|
+
`Call \`${tool}\` again with the same arguments plus \`ai\`:`,
|
|
6495
|
+
``,
|
|
6496
|
+
"```json",
|
|
6497
|
+
`{ "ai": { ${fields.map((f) => `"${f}": ...`).join(", ")} } }`,
|
|
6498
|
+
"```",
|
|
6499
|
+
``,
|
|
6500
|
+
`Each value is used verbatim and no further writing is asked for.`
|
|
6501
|
+
].join("\n")
|
|
6502
|
+
);
|
|
6503
|
+
}
|
|
6504
|
+
async function callerTier() {
|
|
6505
|
+
const { loadUserData: loadUserData2 } = await Promise.resolve().then(() => (init_config_loader(), config_loader_exports));
|
|
6506
|
+
const userData = await loadUserData2();
|
|
6507
|
+
if (!userData.email) return "free";
|
|
6508
|
+
try {
|
|
6509
|
+
return await getTierByEmail(userData.email);
|
|
6510
|
+
} catch {
|
|
6511
|
+
return "free";
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6514
|
+
async function withDelegation(tool, args, fn) {
|
|
6515
|
+
const answers = args.ai ?? {};
|
|
6516
|
+
const tier = await callerTier();
|
|
6517
|
+
try {
|
|
6518
|
+
const result = await runInMcpTool(tool, fn, answers);
|
|
6519
|
+
const pending2 = pendingDelegations();
|
|
6520
|
+
return pending2.length ? delegatedBrief(pending2) : withTierNote(result, tier);
|
|
6521
|
+
} catch (e) {
|
|
6522
|
+
const pending2 = pendingDelegations();
|
|
6523
|
+
if (pending2.length) return delegatedBrief(pending2);
|
|
6524
|
+
if (e instanceof DelegatedGenerationError) return delegatedBrief([e]);
|
|
6525
|
+
throw e;
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6528
|
+
function withTierNote(result, tier) {
|
|
6529
|
+
if (tier !== "free") return result;
|
|
6530
|
+
return {
|
|
6531
|
+
...result,
|
|
6532
|
+
content: [
|
|
6533
|
+
...result.content,
|
|
6534
|
+
{
|
|
6535
|
+
type: "text",
|
|
6536
|
+
text: `Free tier: this demo carries a watermark and the free video limit applies. Upgrade at ${pricingUrl()}`
|
|
6537
|
+
}
|
|
6538
|
+
]
|
|
6539
|
+
};
|
|
6540
|
+
}
|
|
6541
|
+
|
|
5913
6542
|
// src/mcp/elicit.ts
|
|
5914
6543
|
var elicit = null;
|
|
6544
|
+
var canPrompt = null;
|
|
5915
6545
|
function setElicit(fn) {
|
|
5916
6546
|
elicit = fn;
|
|
5917
6547
|
}
|
|
6548
|
+
function setCanElicit(fn) {
|
|
6549
|
+
canPrompt = fn;
|
|
6550
|
+
}
|
|
6551
|
+
function canElicit() {
|
|
6552
|
+
return canPrompt ? canPrompt() : elicit !== null;
|
|
6553
|
+
}
|
|
5918
6554
|
async function askUser(message) {
|
|
5919
6555
|
return elicit ? elicit(message) : null;
|
|
5920
6556
|
}
|
|
@@ -5932,15 +6568,27 @@ var LANG_PROPERTY = {
|
|
|
5932
6568
|
description: 'ISO 639-1 language for narration and captions (e.g. "en", "fr"). Default from config.'
|
|
5933
6569
|
}
|
|
5934
6570
|
};
|
|
6571
|
+
var SKIP_AUTH_PROPERTY = {
|
|
6572
|
+
skip_auth: {
|
|
6573
|
+
type: "boolean",
|
|
6574
|
+
description: "Record without signing in. By default, a URL with no saved session prompts the user to sign in first so the demo covers the app, not the logged-out landing page. Pass true only for public sites with nothing behind a login, or when a logged-out demo is intended."
|
|
6575
|
+
}
|
|
6576
|
+
};
|
|
5935
6577
|
var QUICKPEEK_TOOLS = [
|
|
5936
6578
|
{
|
|
5937
6579
|
name: "generate_demo",
|
|
5938
|
-
description: "Record a narrated demo video of a web app, end to end: an AI plans the walkthrough, Playwright drives a real browser, and the result is composed with voiceover and burned karaoke captions. This is the headline tool. It takes minutes, not seconds, because a real browser session is recorded. Returns the finished MP4 path and a file:// url.
|
|
6580
|
+
description: "Record a narrated demo video of a web app, end to end: an AI plans the walkthrough, Playwright drives a real browser, and the result is composed with voiceover and burned karaoke captions. This is the headline tool. It takes minutes, not seconds, because a real browser session is recorded. Returns the finished MP4 path and a file:// url. If the host has no saved session, the user is first prompted to sign in (a visible browser opens; QuickPeek never sees the password) so the demo starts inside the app, usually the dashboard, instead of the logged-out landing page. Pass skip_auth for public sites. The result says whether this recording was signed in.",
|
|
5939
6581
|
inputSchema: {
|
|
5940
6582
|
type: "object",
|
|
5941
6583
|
properties: {
|
|
6584
|
+
ai: {
|
|
6585
|
+
type: "object",
|
|
6586
|
+
description: `Writing you produced from this tool's brief, keyed by the fields it asked for (e.g. {"plan": {...}}). Each value is used verbatim and no AI call is made for it. Omit on the first call: the tool will tell you what it needs. This server never generates text itself - you are the model it delegates to.`,
|
|
6587
|
+
additionalProperties: true
|
|
6588
|
+
},
|
|
5942
6589
|
...URL_PROPERTY,
|
|
5943
6590
|
...LANG_PROPERTY,
|
|
6591
|
+
...SKIP_AUTH_PROPERTY,
|
|
5944
6592
|
description: {
|
|
5945
6593
|
type: "string",
|
|
5946
6594
|
description: "What the demo should focus on, in your words. Steers the AI plan. Without it, the planner infers the story from the page itself."
|
|
@@ -5959,16 +6607,22 @@ var QUICKPEEK_TOOLS = [
|
|
|
5959
6607
|
},
|
|
5960
6608
|
{
|
|
5961
6609
|
name: "generate_demo_from_script",
|
|
5962
|
-
description: "Record a demo that follows a script you supply, one caption per line, instead of an AI-planned walkthrough. The AI still maps each line to a UI action. Use when you know exactly what the video should say and in what order.",
|
|
6610
|
+
description: "Record a demo that follows a script you supply, one caption per line, instead of an AI-planned walkthrough. The AI still maps each line to a UI action. Use when you know exactly what the video should say and in what order. Prompts for sign-in like generate_demo when the host has no saved session.",
|
|
5963
6611
|
inputSchema: {
|
|
5964
6612
|
type: "object",
|
|
5965
6613
|
properties: {
|
|
6614
|
+
ai: {
|
|
6615
|
+
type: "object",
|
|
6616
|
+
description: `Writing you produced from this tool's brief, keyed by the fields it asked for (e.g. {"plan": {...}}). Each value is used verbatim and no AI call is made for it. Omit on the first call: the tool will tell you what it needs. This server never generates text itself - you are the model it delegates to.`,
|
|
6617
|
+
additionalProperties: true
|
|
6618
|
+
},
|
|
5966
6619
|
...URL_PROPERTY,
|
|
5967
6620
|
script_path: {
|
|
5968
6621
|
type: "string",
|
|
5969
6622
|
description: "Path to a plain text file with one sentence or caption per line."
|
|
5970
6623
|
},
|
|
5971
|
-
...LANG_PROPERTY
|
|
6624
|
+
...LANG_PROPERTY,
|
|
6625
|
+
...SKIP_AUTH_PROPERTY
|
|
5972
6626
|
},
|
|
5973
6627
|
required: ["url", "script_path"]
|
|
5974
6628
|
}
|
|
@@ -5984,7 +6638,7 @@ var QUICKPEEK_TOOLS = [
|
|
|
5984
6638
|
},
|
|
5985
6639
|
{
|
|
5986
6640
|
name: "start_auth",
|
|
5987
|
-
description: "Open a visible browser on a site so the user can sign in, once, by hand. Everything recorded afterwards starts from that session,
|
|
6641
|
+
description: "Open a visible browser on a site so the user can sign in, once, by hand. Everything recorded afterwards starts from that session, and demos of the bare URL begin on the page the user leaves open when the session is saved \u2014 tell them to finish on their dashboard. QuickPeek never sees the password: only the resulting cookies are saved. On clients that support prompting, this saves the session as soon as the user confirms; otherwise call finish_auth when they say they are done.",
|
|
5988
6642
|
inputSchema: {
|
|
5989
6643
|
type: "object",
|
|
5990
6644
|
properties: { ...URL_PROPERTY },
|
|
@@ -6025,9 +6679,38 @@ var QUICKPEEK_TOOLS = [
|
|
|
6025
6679
|
inputSchema: { type: "object", properties: {} }
|
|
6026
6680
|
}
|
|
6027
6681
|
];
|
|
6682
|
+
async function ensureSignedIn(url, skipAuth) {
|
|
6683
|
+
if (!url.startsWith("http") || skipAuth || findAuthState(url)) return null;
|
|
6684
|
+
const host = new URL(url).host;
|
|
6685
|
+
const authRequired = () => ({
|
|
6686
|
+
...jsonContent({
|
|
6687
|
+
status: "auth_required",
|
|
6688
|
+
host,
|
|
6689
|
+
message: `No saved sign-in for ${host}, so the demo would only show logged-out pages. Ask the user how to proceed: to demo the app signed in, call start_auth with this url, then finish_auth once they confirm, and retry. For a public site with no login, retry with skip_auth: true.`
|
|
6690
|
+
}),
|
|
6691
|
+
isError: true
|
|
6692
|
+
});
|
|
6693
|
+
if (!canElicit()) return authRequired();
|
|
6694
|
+
await openSignIn(url);
|
|
6695
|
+
const confirmed = await askUser(
|
|
6696
|
+
`No saved sign-in for ${host}. A browser window is open: sign in there and leave it on the page the demo should start from (usually your dashboard), then confirm. Decline to record logged out.`
|
|
6697
|
+
);
|
|
6698
|
+
if (confirmed === null) {
|
|
6699
|
+
await cancelSignIn();
|
|
6700
|
+
return authRequired();
|
|
6701
|
+
}
|
|
6702
|
+
if (confirmed) {
|
|
6703
|
+
await saveSignIn();
|
|
6704
|
+
return null;
|
|
6705
|
+
}
|
|
6706
|
+
await cancelSignIn();
|
|
6707
|
+
return null;
|
|
6708
|
+
}
|
|
6028
6709
|
async function handleGenerateDemo(args) {
|
|
6029
6710
|
const url = resolveUrl(args.url);
|
|
6030
6711
|
return withSilencedStdout(async () => {
|
|
6712
|
+
const authGate = await ensureSignedIn(url, args.skip_auth);
|
|
6713
|
+
if (authGate) return authGate;
|
|
6031
6714
|
const output = await recordCommand(url, {
|
|
6032
6715
|
langOverride: args.lang,
|
|
6033
6716
|
description: args.description,
|
|
@@ -6039,7 +6722,11 @@ async function handleGenerateDemo(args) {
|
|
|
6039
6722
|
rethrow: true
|
|
6040
6723
|
});
|
|
6041
6724
|
if (!output) return errorContent("Recording finished without producing a video.");
|
|
6042
|
-
return fileResult(output, {
|
|
6725
|
+
return fileResult(output, {
|
|
6726
|
+
name: output.split("/").pop(),
|
|
6727
|
+
url_recorded: resolveStartUrl(url),
|
|
6728
|
+
auth: authStatus(url)
|
|
6729
|
+
});
|
|
6043
6730
|
});
|
|
6044
6731
|
}
|
|
6045
6732
|
function authStatus(url) {
|
|
@@ -6049,18 +6736,20 @@ function authStatus(url) {
|
|
|
6049
6736
|
return {
|
|
6050
6737
|
signedIn: false,
|
|
6051
6738
|
host,
|
|
6052
|
-
note: `No saved session for ${host}, so only pages reachable while logged out were recorded. To cover pages behind the login, call start_auth with this url (or have the user run \`qp auth ${url}\`), then re-run with fresh: true.`
|
|
6739
|
+
note: `No saved session for ${host}, so only pages reachable while logged out were recorded. To cover pages behind the login, call start_auth with this url (or have the user run \`qp auth ${url}\`), then re-run with fresh: true and without skip_auth.`
|
|
6053
6740
|
};
|
|
6054
6741
|
}
|
|
6055
6742
|
async function handleGenerateDemoFromScript(args) {
|
|
6056
6743
|
const url = resolveUrl(args.url);
|
|
6057
6744
|
const scriptPath = resolveInputPath(args.script_path);
|
|
6058
6745
|
return withSilencedStdout(async () => {
|
|
6746
|
+
const authGate = await ensureSignedIn(url, args.skip_auth);
|
|
6747
|
+
if (authGate) return authGate;
|
|
6059
6748
|
await scriptCommand(url, scriptPath, args.lang);
|
|
6060
6749
|
const dir = getOutputDirFromUrl(url);
|
|
6061
6750
|
const newest = newestVideo(dir);
|
|
6062
6751
|
if (!newest) return errorContent(`No video found in ${dir} after running the script.`);
|
|
6063
|
-
return fileResult(newest, { name: newest.split("/").pop(), url_recorded: url, scriptPath });
|
|
6752
|
+
return fileResult(newest, { name: newest.split("/").pop(), url_recorded: resolveStartUrl(url), scriptPath });
|
|
6064
6753
|
});
|
|
6065
6754
|
}
|
|
6066
6755
|
async function handleOpenInEditor(args) {
|
|
@@ -6071,7 +6760,7 @@ async function handleOpenInEditor(args) {
|
|
|
6071
6760
|
if (!planPath) {
|
|
6072
6761
|
return errorContent(`No plan found in ${dir}. Record the demo first with generate_demo.`);
|
|
6073
6762
|
}
|
|
6074
|
-
const parsed = parsePlanText(
|
|
6763
|
+
const parsed = parsePlanText(readFileSync7(planPath, "utf8"));
|
|
6075
6764
|
if (!parsed.ok) return errorContent(`${planPath} cannot be opened: ${parsed.error}`);
|
|
6076
6765
|
const base = editorBaseUrl();
|
|
6077
6766
|
const editorUrl = planEditorUrl(base, parsed.plan);
|
|
@@ -6089,7 +6778,9 @@ async function handleStartAuth(args) {
|
|
|
6089
6778
|
const url = resolveUrl(args.url);
|
|
6090
6779
|
return withSilencedStdout(async () => {
|
|
6091
6780
|
await openSignIn(url);
|
|
6092
|
-
const confirmed = await askUser(
|
|
6781
|
+
const confirmed = await askUser(
|
|
6782
|
+
`A browser window is open on ${url}. Sign in there, leave it on the page demos should start from (usually your dashboard), then confirm.`
|
|
6783
|
+
);
|
|
6093
6784
|
if (confirmed === null) {
|
|
6094
6785
|
return jsonContent({
|
|
6095
6786
|
status: "awaiting_sign_in",
|
|
@@ -6169,8 +6860,8 @@ async function handleCheckAccount() {
|
|
|
6169
6860
|
});
|
|
6170
6861
|
}
|
|
6171
6862
|
var QUICKPEEK_HANDLERS = {
|
|
6172
|
-
generate_demo: handleGenerateDemo,
|
|
6173
|
-
generate_demo_from_script: handleGenerateDemoFromScript,
|
|
6863
|
+
generate_demo: (args) => withDelegation("generate_demo", args, () => handleGenerateDemo(args)),
|
|
6864
|
+
generate_demo_from_script: (args) => withDelegation("generate_demo_from_script", args, () => handleGenerateDemoFromScript(args)),
|
|
6174
6865
|
open_in_editor: handleOpenInEditor,
|
|
6175
6866
|
start_auth: handleStartAuth,
|
|
6176
6867
|
finish_auth: handleFinishAuth,
|
|
@@ -6202,5 +6893,6 @@ export {
|
|
|
6202
6893
|
TOOL_HANDLERS,
|
|
6203
6894
|
errorContent,
|
|
6204
6895
|
jsonContent,
|
|
6896
|
+
setCanElicit,
|
|
6205
6897
|
setElicit
|
|
6206
6898
|
};
|