@spark-apps/quickpeek 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +31 -14
- package/dist/mcp-tools.mjs +634 -337
- package/dist/qp.js +413 -188
- package/dist/{scraping-BK8pIJhm.d.mts → scraping-CHN4ClyT.d.mts} +7 -3
- package/dist/web.d.mts +42 -2
- package/dist/web.mjs +110 -13
- package/mcp.mjs +24 -1
- package/package.json +1 -1
package/dist/mcp-tools.mjs
CHANGED
|
@@ -43,7 +43,7 @@ function getErrorMessage(err) {
|
|
|
43
43
|
return err instanceof Error ? err.message : String(err);
|
|
44
44
|
}
|
|
45
45
|
function toTitleCase(text) {
|
|
46
|
-
return text.replace(/\w
|
|
46
|
+
return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
|
|
47
47
|
if (index === 0) {
|
|
48
48
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
49
49
|
}
|
|
@@ -107,6 +107,9 @@ var init_utils = __esm({
|
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
// src/lib/api-client.ts
|
|
110
|
+
function apiKey() {
|
|
111
|
+
return process.env.SPARK_AI_KEY || "";
|
|
112
|
+
}
|
|
110
113
|
async function getOrRefreshDirectToken() {
|
|
111
114
|
const now = Date.now();
|
|
112
115
|
if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
|
|
@@ -115,7 +118,7 @@ async function getOrRefreshDirectToken() {
|
|
|
115
118
|
try {
|
|
116
119
|
const response = await fetch(SPARK_AI_TOKEN, {
|
|
117
120
|
method: "POST",
|
|
118
|
-
headers: { "X-Api-Key":
|
|
121
|
+
headers: { "X-Api-Key": apiKey() }
|
|
119
122
|
});
|
|
120
123
|
if (!response.ok) {
|
|
121
124
|
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
|
|
@@ -226,7 +229,7 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
226
229
|
return lastRateLimitError || { success: false, error: "Request failed after retries" };
|
|
227
230
|
}
|
|
228
231
|
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
229
|
-
if (!
|
|
232
|
+
if (!apiKey()) {
|
|
230
233
|
return {
|
|
231
234
|
success: false,
|
|
232
235
|
error: "AI service is not configured. Please report this issue.",
|
|
@@ -286,7 +289,7 @@ function normalizeUrl(input) {
|
|
|
286
289
|
url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
|
|
287
290
|
return `https://${url}`;
|
|
288
291
|
}
|
|
289
|
-
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL,
|
|
292
|
+
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
|
|
290
293
|
var init_api_client = __esm({
|
|
291
294
|
"src/lib/api-client.ts"() {
|
|
292
295
|
"use strict";
|
|
@@ -297,7 +300,6 @@ var init_api_client = __esm({
|
|
|
297
300
|
RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
|
|
298
301
|
RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
|
|
299
302
|
RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
|
|
300
|
-
API_KEY = process.env.SPARK_AI_KEY || "";
|
|
301
303
|
cachedDirectToken = null;
|
|
302
304
|
cachedRelayToken = null;
|
|
303
305
|
}
|
|
@@ -401,7 +403,7 @@ var VERSION, CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_CAPTIONS;
|
|
|
401
403
|
var init_config = __esm({
|
|
402
404
|
"src/lib/config.ts"() {
|
|
403
405
|
"use strict";
|
|
404
|
-
VERSION = "1.
|
|
406
|
+
VERSION = "1.2.1";
|
|
405
407
|
CONFIG_FILE = "quickpeek.config.json";
|
|
406
408
|
DEFAULT_CONFIG = {
|
|
407
409
|
plan: { maxSteps: 8 },
|
|
@@ -551,7 +553,7 @@ var init_scraping = __esm({
|
|
|
551
553
|
});
|
|
552
554
|
|
|
553
555
|
// src/lib/crawl.ts
|
|
554
|
-
import { chromium } from "playwright";
|
|
556
|
+
import { chromium as chromium2 } from "playwright";
|
|
555
557
|
function getActionForType(type) {
|
|
556
558
|
switch (type) {
|
|
557
559
|
case "button":
|
|
@@ -569,20 +571,29 @@ function getActionForType(type) {
|
|
|
569
571
|
return "type";
|
|
570
572
|
}
|
|
571
573
|
}
|
|
572
|
-
async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
573
|
-
const
|
|
574
|
+
async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
575
|
+
const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
|
|
576
|
+
const cached = crawlCache.get(cacheKey);
|
|
574
577
|
if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
|
|
575
578
|
task2("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
|
|
576
|
-
return { title: cached.title, elements: [...cached.elements] };
|
|
579
|
+
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
577
580
|
}
|
|
578
581
|
const browserTask = task2("Launching browser");
|
|
579
|
-
const browser = await
|
|
580
|
-
const context = await browser.newContext(
|
|
582
|
+
const browser = await chromium2.launch({ headless: true });
|
|
583
|
+
const context = await browser.newContext({
|
|
584
|
+
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
585
|
+
// Crawl at the size we will record at, or the measurements below describe
|
|
586
|
+
// a page nobody will ever see.
|
|
587
|
+
...viewport ? { viewport } : {}
|
|
588
|
+
});
|
|
581
589
|
const page = await context.newPage();
|
|
582
590
|
browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
|
|
583
591
|
const crawlTask = task2("Crawling page");
|
|
584
592
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
585
593
|
const title = await page.title();
|
|
594
|
+
const scrollable = await page.evaluate(
|
|
595
|
+
"Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
|
|
596
|
+
);
|
|
586
597
|
const rawElements = await page.evaluate(`
|
|
587
598
|
(() => {
|
|
588
599
|
const items = [];
|
|
@@ -712,12 +723,12 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
|
712
723
|
});
|
|
713
724
|
await browser.close();
|
|
714
725
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
715
|
-
crawlCache.set(
|
|
726
|
+
crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
|
|
716
727
|
if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
|
|
717
728
|
const oldest = crawlCache.keys().next().value;
|
|
718
729
|
if (oldest !== void 0) crawlCache.delete(oldest);
|
|
719
730
|
}
|
|
720
|
-
return { title, elements };
|
|
731
|
+
return { title, elements, scrollable };
|
|
721
732
|
}
|
|
722
733
|
var noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
|
|
723
734
|
var init_crawl = __esm({
|
|
@@ -1289,7 +1300,7 @@ EXAMPLES:
|
|
|
1289
1300
|
\u2022 File upload: "target": "input[type=\\"file\\"]"
|
|
1290
1301
|
\u2022 Slider: "target": "input[type=\\"range\\"]"`;
|
|
1291
1302
|
}
|
|
1292
|
-
function buildUserPrompt(url, title, elements, langName, description, maxSteps) {
|
|
1303
|
+
function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
|
|
1293
1304
|
const lang = langName ? `
|
|
1294
1305
|
Language: Write ALL captions in ${langName}.` : "";
|
|
1295
1306
|
const purpose = description ? `
|
|
@@ -1327,8 +1338,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
|
|
|
1327
1338
|
${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
1328
1339
|
}
|
|
1329
1340
|
const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
|
|
1341
|
+
const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
|
|
1342
|
+
const scrolling = cannotScroll ? `
|
|
1343
|
+
|
|
1344
|
+
SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
|
|
1345
|
+
const scrollRule = cannotScroll ? "\n\u2022 NEVER use the scroll action: this page does not scroll, so the video would freeze. Use wait and click steps only" : "";
|
|
1330
1346
|
return `URL: ${url}
|
|
1331
|
-
Title: ${title}${purpose}
|
|
1347
|
+
Title: ${title}${purpose}${scrolling}
|
|
1332
1348
|
|
|
1333
1349
|
AVAILABLE ELEMENTS (grouped by action):
|
|
1334
1350
|
${elementSummary}
|
|
@@ -1338,7 +1354,7 @@ REQUIREMENTS:
|
|
|
1338
1354
|
\u2022 Complete ONE user journey: input \u2192 process \u2192 output
|
|
1339
1355
|
\u2022 For CLICK: use simplified text target like "extract frame" or "download"
|
|
1340
1356
|
\u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
|
|
1341
|
-
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
|
|
1357
|
+
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
|
|
1342
1358
|
|
|
1343
1359
|
Return JSON:
|
|
1344
1360
|
{
|
|
@@ -1361,10 +1377,12 @@ TARGET SYNTAX:
|
|
|
1361
1377
|
\u2022 Sliders: input[type="range"]
|
|
1362
1378
|
\u2022 Text inputs: Use selector from list above`;
|
|
1363
1379
|
}
|
|
1380
|
+
var MIN_USEFUL_SCROLL_PX;
|
|
1364
1381
|
var init_prompts = __esm({
|
|
1365
1382
|
"src/lib/prompts.ts"() {
|
|
1366
1383
|
"use strict";
|
|
1367
1384
|
init_utils();
|
|
1385
|
+
MIN_USEFUL_SCROLL_PX = 200;
|
|
1368
1386
|
}
|
|
1369
1387
|
});
|
|
1370
1388
|
|
|
@@ -2246,10 +2264,13 @@ function fileResult(output, data, poster) {
|
|
|
2246
2264
|
}
|
|
2247
2265
|
|
|
2248
2266
|
// src/mcp/tools.ts
|
|
2249
|
-
import { readdirSync as
|
|
2250
|
-
import { join as
|
|
2267
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
2268
|
+
import { join as join23 } from "path";
|
|
2251
2269
|
import { WIDE_VOICES as WIDE_VOICES2 } from "@spark-apps/video-kit";
|
|
2252
2270
|
|
|
2271
|
+
// src/browser/auth-session.ts
|
|
2272
|
+
import { chromium } from "playwright";
|
|
2273
|
+
|
|
2253
2274
|
// src/browser/auth-state.ts
|
|
2254
2275
|
import { existsSync as existsSync2 } from "fs";
|
|
2255
2276
|
import { chmod, mkdir, readdir, unlink, writeFile } from "fs/promises";
|
|
@@ -2271,12 +2292,52 @@ function findAuthState(url) {
|
|
|
2271
2292
|
const path = authStatePath(url);
|
|
2272
2293
|
return path && existsSync2(path) ? path : void 0;
|
|
2273
2294
|
}
|
|
2295
|
+
async function saveAuthState(url, state) {
|
|
2296
|
+
const path = authStatePath(url);
|
|
2297
|
+
if (!path) throw new Error(`Not a valid URL: ${url}`);
|
|
2298
|
+
await mkdir(AUTH_DIR, { recursive: true });
|
|
2299
|
+
await writeFile(path, JSON.stringify(state, null, 2));
|
|
2300
|
+
await chmod(path, 384);
|
|
2301
|
+
return path;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// src/browser/auth-session.ts
|
|
2305
|
+
var pending = null;
|
|
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 };
|
|
2315
|
+
try {
|
|
2316
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
2317
|
+
} catch {
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
async function saveSignIn() {
|
|
2321
|
+
if (!pending) throw new Error("No sign-in window is open. Call start_auth first.");
|
|
2322
|
+
const { context, url } = pending;
|
|
2323
|
+
try {
|
|
2324
|
+
const path = await saveAuthState(url, await context.storageState());
|
|
2325
|
+
return { host: new URL(url).host, path };
|
|
2326
|
+
} finally {
|
|
2327
|
+
await cancelSignIn();
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
async function cancelSignIn() {
|
|
2331
|
+
const open = pending;
|
|
2332
|
+
pending = null;
|
|
2333
|
+
await open?.browser.close().catch(() => {
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2274
2336
|
|
|
2275
|
-
// src/commands/
|
|
2276
|
-
import { existsSync as
|
|
2277
|
-
import {
|
|
2278
|
-
import
|
|
2279
|
-
import chalk14 from "chalk";
|
|
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";
|
|
2280
2341
|
|
|
2281
2342
|
// src/help.ts
|
|
2282
2343
|
init_lib();
|
|
@@ -2341,66 +2402,123 @@ function formatUsageLines(status, tier) {
|
|
|
2341
2402
|
return lines;
|
|
2342
2403
|
}
|
|
2343
2404
|
|
|
2344
|
-
// src/
|
|
2345
|
-
|
|
2346
|
-
|
|
2405
|
+
// src/lib/browser-open.ts
|
|
2406
|
+
import { exec as exec2 } from "child_process";
|
|
2407
|
+
function editorBaseUrl() {
|
|
2408
|
+
return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
|
|
2409
|
+
}
|
|
2410
|
+
function openInBrowser(url) {
|
|
2411
|
+
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
2412
|
+
const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
2413
|
+
return new Promise((resolve10) => {
|
|
2414
|
+
exec2(command, () => resolve10());
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2347
2417
|
|
|
2348
|
-
// src/
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2418
|
+
// src/lib/plan-validation.ts
|
|
2419
|
+
var INVALID_TARGETS = [
|
|
2420
|
+
/\.png$/i,
|
|
2421
|
+
/\.jpg$/i,
|
|
2422
|
+
/\.gif$/i,
|
|
2423
|
+
/\.webp$/i,
|
|
2424
|
+
/\.svg$/i,
|
|
2425
|
+
/sprite_sheet/i,
|
|
2426
|
+
/downloaded/i
|
|
2427
|
+
];
|
|
2428
|
+
var ACTIONS_REQUIRING_TARGET = ["click", "type", "select", "check", "drag"];
|
|
2429
|
+
function isFilePath(value) {
|
|
2430
|
+
if (!value) return false;
|
|
2431
|
+
return /\.\w{2,5}$/.test(value) && (/[/\\]/.test(value) || /^[a-zA-Z]:/.test(value));
|
|
2432
|
+
}
|
|
2433
|
+
function isInvalidTarget(target) {
|
|
2434
|
+
return INVALID_TARGETS.some((pattern) => pattern.test(target));
|
|
2435
|
+
}
|
|
2436
|
+
function validateStep(step) {
|
|
2437
|
+
if (!step?.action) {
|
|
2438
|
+
return "Missing action";
|
|
2439
|
+
}
|
|
2440
|
+
if (step.action === "navigate") {
|
|
2441
|
+
if (!step.target) return "Navigate requires a URL target";
|
|
2442
|
+
if (!step.target.startsWith("http://") && !step.target.startsWith("https://")) {
|
|
2443
|
+
return "Navigate target must be a valid URL";
|
|
2361
2444
|
}
|
|
2362
|
-
} catch {
|
|
2363
2445
|
}
|
|
2446
|
+
if (step.action === "media") {
|
|
2447
|
+
if (!step.mediaFile && !isFilePath(step.value)) {
|
|
2448
|
+
return "Media step requires a file (image or video)";
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
if (step.action === "upload") {
|
|
2452
|
+
if (!step.mediaFile && !isFilePath(step.value)) {
|
|
2453
|
+
return "Upload step requires a file";
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
if (ACTIONS_REQUIRING_TARGET.includes(step.action)) {
|
|
2457
|
+
if (!step.target) return `${step.action} requires a target`;
|
|
2458
|
+
if (isInvalidTarget(step.target)) return `Invalid target pattern: ${step.target}`;
|
|
2459
|
+
}
|
|
2460
|
+
return null;
|
|
2364
2461
|
}
|
|
2365
|
-
async function setLogFilePath(outputDir) {
|
|
2366
|
-
logFilePath = join8(outputDir, LOG_FILE_NAME);
|
|
2367
|
-
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2368
|
-
${"=".repeat(60)}
|
|
2369
2462
|
|
|
2463
|
+
// src/lib/plan-file.ts
|
|
2464
|
+
var MAX_HANDOFF_URL_CHARS = 32e3;
|
|
2465
|
+
function serializePlan(plan) {
|
|
2466
|
+
return `${JSON.stringify(plan, null, 2)}
|
|
2370
2467
|
`;
|
|
2468
|
+
}
|
|
2469
|
+
function parsePlanText(text) {
|
|
2470
|
+
let raw;
|
|
2371
2471
|
try {
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2472
|
+
raw = JSON.parse(text);
|
|
2473
|
+
} catch (e) {
|
|
2474
|
+
return { ok: false, error: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
2375
2475
|
}
|
|
2476
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2477
|
+
return { ok: false, error: "A plan must be a JSON object." };
|
|
2478
|
+
}
|
|
2479
|
+
const candidate = raw;
|
|
2480
|
+
if (typeof candidate.url !== "string" || !candidate.url.trim()) {
|
|
2481
|
+
return { ok: false, error: "A plan needs a `url` to record." };
|
|
2482
|
+
}
|
|
2483
|
+
if (!Array.isArray(candidate.steps) || candidate.steps.length === 0) {
|
|
2484
|
+
return { ok: false, error: "A plan needs at least one step." };
|
|
2485
|
+
}
|
|
2486
|
+
const steps = [];
|
|
2487
|
+
for (const [index, step] of candidate.steps.entries()) {
|
|
2488
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
2489
|
+
return { ok: false, error: `Step ${index + 1} is not an object.` };
|
|
2490
|
+
}
|
|
2491
|
+
const typed = step;
|
|
2492
|
+
if (typeof typed.action !== "string" || !typed.action.trim()) {
|
|
2493
|
+
return { ok: false, error: `Step ${index + 1} has no action.` };
|
|
2494
|
+
}
|
|
2495
|
+
const failure = validateStep(typed);
|
|
2496
|
+
if (failure) return { ok: false, error: `Step ${index + 1}: ${failure}` };
|
|
2497
|
+
steps.push({ ...typed, id: typed.id ?? index + 1 });
|
|
2498
|
+
}
|
|
2499
|
+
return {
|
|
2500
|
+
ok: true,
|
|
2501
|
+
plan: {
|
|
2502
|
+
url: candidate.url,
|
|
2503
|
+
title: candidate.title ?? "",
|
|
2504
|
+
description: candidate.description ?? "",
|
|
2505
|
+
...candidate.lang ? { lang: candidate.lang } : {},
|
|
2506
|
+
steps
|
|
2507
|
+
}
|
|
2508
|
+
};
|
|
2376
2509
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
|
|
2383
|
-
`);
|
|
2510
|
+
function toBase64Url(text) {
|
|
2511
|
+
const bytes = new TextEncoder().encode(text);
|
|
2512
|
+
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
2513
|
+
const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(text, "utf8").toString("base64");
|
|
2514
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2384
2515
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
if (error instanceof Error) {
|
|
2388
|
-
errorMsg = `${error.message}
|
|
2389
|
-
${error.stack}`;
|
|
2390
|
-
} else if (typeof error === "object" && error !== null) {
|
|
2391
|
-
errorMsg = JSON.stringify(error, null, 2);
|
|
2392
|
-
} else {
|
|
2393
|
-
errorMsg = String(error);
|
|
2394
|
-
}
|
|
2395
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
|
|
2396
|
-
${errorMsg}
|
|
2397
|
-
|
|
2398
|
-
`);
|
|
2516
|
+
function encodePlan(plan) {
|
|
2517
|
+
return toBase64Url(serializePlan(plan));
|
|
2399
2518
|
}
|
|
2400
|
-
|
|
2401
|
-
const
|
|
2402
|
-
|
|
2403
|
-
`);
|
|
2519
|
+
function planEditorUrl(baseUrl, plan) {
|
|
2520
|
+
const url = `${baseUrl.replace(/\/$/, "")}/edit#plan=${encodePlan(plan)}`;
|
|
2521
|
+
return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
|
|
2404
2522
|
}
|
|
2405
2523
|
|
|
2406
2524
|
// src/utils/ui.ts
|
|
@@ -2446,7 +2564,7 @@ function promptInput(prompt2) {
|
|
|
2446
2564
|
var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
|
|
2447
2565
|
function formatClickablePath(filePath) {
|
|
2448
2566
|
const absolutePath = resolve4(filePath);
|
|
2449
|
-
if (_isWSL) {
|
|
2567
|
+
if (_isWSL || process.platform !== "win32") {
|
|
2450
2568
|
return absolutePath;
|
|
2451
2569
|
}
|
|
2452
2570
|
let formatted = absolutePath;
|
|
@@ -2462,15 +2580,15 @@ function formatDuration(ms) {
|
|
|
2462
2580
|
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
2463
2581
|
}
|
|
2464
2582
|
|
|
2465
|
-
// src/commands/
|
|
2583
|
+
// src/commands/utils/record-helpers.ts
|
|
2584
|
+
import chalk6 from "chalk";
|
|
2466
2585
|
init_lib();
|
|
2467
|
-
import chalk8 from "chalk";
|
|
2468
2586
|
|
|
2469
2587
|
// src/plan.ts
|
|
2470
|
-
import { mkdir as mkdir5, readFile as readFile3, writeFile as
|
|
2471
|
-
import { join as
|
|
2588
|
+
import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile5 } from "fs/promises";
|
|
2589
|
+
import { join as join8 } from "path";
|
|
2472
2590
|
import chalk5 from "chalk";
|
|
2473
|
-
import { chromium as
|
|
2591
|
+
import { chromium as chromium5 } from "playwright";
|
|
2474
2592
|
|
|
2475
2593
|
// src/ai/client.ts
|
|
2476
2594
|
init_lib();
|
|
@@ -2478,19 +2596,11 @@ import { createInterface as createInterface2 } from "readline";
|
|
|
2478
2596
|
import chalk4 from "chalk";
|
|
2479
2597
|
|
|
2480
2598
|
// src/utils/browser-auth.ts
|
|
2481
|
-
init_SparkStripe_feature_gating();
|
|
2482
2599
|
import { createServer } from "http";
|
|
2483
2600
|
import { URL as URL2 } from "url";
|
|
2484
2601
|
import chalk3 from "chalk";
|
|
2602
|
+
init_SparkStripe_feature_gating();
|
|
2485
2603
|
var TIMEOUT_MS = 12e4;
|
|
2486
|
-
async function openBrowser(url) {
|
|
2487
|
-
const { exec: exec2 } = await import("child_process");
|
|
2488
|
-
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
2489
|
-
const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
2490
|
-
return new Promise((resolve10) => {
|
|
2491
|
-
exec2(command, () => resolve10());
|
|
2492
|
-
});
|
|
2493
|
-
}
|
|
2494
2604
|
async function browserLogin() {
|
|
2495
2605
|
return new Promise((resolve10) => {
|
|
2496
2606
|
let server;
|
|
@@ -2544,7 +2654,7 @@ async function browserLogin() {
|
|
|
2544
2654
|
console.log(chalk3.dim(` ${url}`));
|
|
2545
2655
|
console.log();
|
|
2546
2656
|
console.log(chalk3.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
|
|
2547
|
-
|
|
2657
|
+
void openInBrowser(url);
|
|
2548
2658
|
});
|
|
2549
2659
|
timer = setTimeout(() => {
|
|
2550
2660
|
console.log();
|
|
@@ -2646,13 +2756,13 @@ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "
|
|
|
2646
2756
|
|
|
2647
2757
|
// src/browser/launch.ts
|
|
2648
2758
|
import { mkdir as mkdir4 } from "fs/promises";
|
|
2649
|
-
import { chromium as
|
|
2759
|
+
import { chromium as chromium4 } from "playwright";
|
|
2650
2760
|
|
|
2651
2761
|
// src/browser/wsl.ts
|
|
2652
2762
|
import { execSync, spawn as spawn2 } from "child_process";
|
|
2653
2763
|
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
|
|
2654
2764
|
import { request } from "http";
|
|
2655
|
-
import { chromium as
|
|
2765
|
+
import { chromium as chromium3 } from "playwright";
|
|
2656
2766
|
function isWSL() {
|
|
2657
2767
|
if (process.platform !== "linux") return false;
|
|
2658
2768
|
try {
|
|
@@ -2771,7 +2881,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
|
2771
2881
|
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
2772
2882
|
try {
|
|
2773
2883
|
await waitForChrome(windowsHost, proxyPort);
|
|
2774
|
-
const browser = await
|
|
2884
|
+
const browser = await chromium3.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
2775
2885
|
return {
|
|
2776
2886
|
browser,
|
|
2777
2887
|
cleanup: async () => {
|
|
@@ -2812,7 +2922,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
|
|
|
2812
2922
|
const contexts = chromeSession.browser.contexts();
|
|
2813
2923
|
context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
|
|
2814
2924
|
} catch {
|
|
2815
|
-
context = await
|
|
2925
|
+
context = await chromium4.launchPersistentContext(userDataDir, {
|
|
2816
2926
|
headless: false,
|
|
2817
2927
|
args: persistentArgs,
|
|
2818
2928
|
viewport,
|
|
@@ -2821,7 +2931,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
|
|
|
2821
2931
|
});
|
|
2822
2932
|
}
|
|
2823
2933
|
} else {
|
|
2824
|
-
context = await
|
|
2934
|
+
context = await chromium4.launchPersistentContext(userDataDir, {
|
|
2825
2935
|
headless: false,
|
|
2826
2936
|
args: persistentArgs,
|
|
2827
2937
|
viewport,
|
|
@@ -2830,7 +2940,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo, storageSta
|
|
|
2830
2940
|
});
|
|
2831
2941
|
}
|
|
2832
2942
|
} else {
|
|
2833
|
-
browser = await
|
|
2943
|
+
browser = await chromium4.launch({ headless: true });
|
|
2834
2944
|
context = await browser.newContext({ viewport, ...videoOpts, ...authOpts, acceptDownloads: true });
|
|
2835
2945
|
}
|
|
2836
2946
|
return { context, browser, chromeSession };
|
|
@@ -2848,12 +2958,12 @@ async function cleanupBrowserSession(session) {
|
|
|
2848
2958
|
init_lib();
|
|
2849
2959
|
async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
2850
2960
|
const browserTask = task2("Launching browser with extension");
|
|
2851
|
-
const userDataDir =
|
|
2961
|
+
const userDataDir = join8(workDir, ".browser-data");
|
|
2852
2962
|
await mkdir5(userDataDir, { recursive: true });
|
|
2853
2963
|
let context;
|
|
2854
2964
|
let chromeSession = null;
|
|
2855
2965
|
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
2856
|
-
const launchPersistent = () =>
|
|
2966
|
+
const launchPersistent = () => chromium5.launchPersistentContext(userDataDir, {
|
|
2857
2967
|
headless: false,
|
|
2858
2968
|
args: [
|
|
2859
2969
|
`--disable-extensions-except=${extensionPath}`,
|
|
@@ -2972,12 +3082,12 @@ async function parsePlanJson(content, task2, logError2, userEmail, tier = "free"
|
|
|
2972
3082
|
}
|
|
2973
3083
|
}
|
|
2974
3084
|
}
|
|
2975
|
-
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free") {
|
|
2976
|
-
const { title, elements } = extensionPath && workDir ? await crawlExtension(url, extensionPath, workDir, task2) : await crawlPage(url, task2, findAuthState(url));
|
|
3085
|
+
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
|
|
3086
|
+
const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
|
|
2977
3087
|
const aiTask = task2("Generating plan with AI");
|
|
2978
3088
|
const langName = getLanguageName(lang);
|
|
2979
3089
|
const systemPrompt = buildSystemPrompt(maxSteps);
|
|
2980
|
-
const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps);
|
|
3090
|
+
const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
|
|
2981
3091
|
try {
|
|
2982
3092
|
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
|
|
2983
3093
|
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
@@ -3006,7 +3116,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
|
|
|
3006
3116
|
}
|
|
3007
3117
|
const renumberTask = task2("Validating plan structure");
|
|
3008
3118
|
const originalCount = plan.steps.length;
|
|
3009
|
-
const
|
|
3119
|
+
const INVALID_TARGETS2 = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
|
|
3010
3120
|
const isValidStep = (s) => {
|
|
3011
3121
|
if (!s?.action) return false;
|
|
3012
3122
|
if (s.action === "navigate") {
|
|
@@ -3015,7 +3125,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
|
|
|
3015
3125
|
}
|
|
3016
3126
|
if (["click", "type", "select", "check", "upload", "drag"].includes(s.action)) {
|
|
3017
3127
|
if (!s.target) return false;
|
|
3018
|
-
if (
|
|
3128
|
+
if (INVALID_TARGETS2.some((pattern) => pattern.test(s.target))) return false;
|
|
3019
3129
|
}
|
|
3020
3130
|
if (s.action === "upload") {
|
|
3021
3131
|
if (!s.target?.includes("file") && !s.target?.includes("input")) return false;
|
|
@@ -3032,7 +3142,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
|
|
|
3032
3142
|
renumberTask.fail("No valid steps");
|
|
3033
3143
|
throw new Error("Plan has no valid steps");
|
|
3034
3144
|
}
|
|
3035
|
-
await
|
|
3145
|
+
await writeFile5(planPath, JSON.stringify(plan, null, 2));
|
|
3036
3146
|
const removed = originalCount - plan.steps.length;
|
|
3037
3147
|
renumberTask.succeed(
|
|
3038
3148
|
removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
|
|
@@ -3040,67 +3150,68 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
|
|
|
3040
3150
|
return plan;
|
|
3041
3151
|
}
|
|
3042
3152
|
|
|
3043
|
-
// src/commands/
|
|
3153
|
+
// src/commands/utils/record-helpers.ts
|
|
3044
3154
|
init_config_loader();
|
|
3045
3155
|
|
|
3046
|
-
// src/
|
|
3047
|
-
|
|
3048
|
-
import
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
email = await promptInput("Email: ");
|
|
3062
|
-
}
|
|
3063
|
-
if (!email || !email.includes("@")) {
|
|
3064
|
-
console.log(chalk6.red(" Invalid email. Run qp again to try again."));
|
|
3065
|
-
return null;
|
|
3066
|
-
}
|
|
3067
|
-
const registerTask = task("Sending verification email");
|
|
3068
|
-
const result = await registerFreeUser(email);
|
|
3069
|
-
if (!result.success) {
|
|
3070
|
-
registerTask.fail(result.error || "Registration failed");
|
|
3071
|
-
return null;
|
|
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 {
|
|
3072
3171
|
}
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
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 {
|
|
3076
3183
|
}
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
await
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
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);
|
|
3089
3202
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
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
|
+
`);
|
|
3098
3212
|
}
|
|
3099
3213
|
|
|
3100
3214
|
// src/commands/utils/record-helpers.ts
|
|
3101
|
-
import chalk7 from "chalk";
|
|
3102
|
-
init_lib();
|
|
3103
|
-
init_config_loader();
|
|
3104
3215
|
init_utils();
|
|
3105
3216
|
var FREE_VIDEO_LIMIT = 5;
|
|
3106
3217
|
var INDENT2 = " ";
|
|
@@ -3124,41 +3235,41 @@ function displayRateLimitInfo() {
|
|
|
3124
3235
|
const percentage = Math.round(limits.used / limits.limit * 100);
|
|
3125
3236
|
if (percentage >= 90) {
|
|
3126
3237
|
console.log();
|
|
3127
|
-
console.log(
|
|
3238
|
+
console.log(chalk6.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
|
|
3128
3239
|
if (limits.tier === "free") {
|
|
3129
|
-
console.log(
|
|
3240
|
+
console.log(chalk6.dim(` Upgrade: ${getUpgradeUrl()}`));
|
|
3130
3241
|
}
|
|
3131
3242
|
} else if (percentage >= 50) {
|
|
3132
|
-
console.log(
|
|
3243
|
+
console.log(chalk6.dim(` ${remaining}/${limits.limit} requests remaining`));
|
|
3133
3244
|
}
|
|
3134
3245
|
}
|
|
3135
3246
|
async function displayUsageStatus(userEmail, userTier, _localVideoCount = 0) {
|
|
3136
3247
|
const tier = userTier === "pro" ? "pro" : "free";
|
|
3137
|
-
const tierLabel = tier === "pro" ?
|
|
3138
|
-
console.log(
|
|
3139
|
-
console.log(
|
|
3248
|
+
const tierLabel = tier === "pro" ? chalk6.green("Pro") : "Free";
|
|
3249
|
+
console.log(chalk6.cyan(` \u2714 ${tierLabel} plan`));
|
|
3250
|
+
console.log(chalk6.dim(` ${userEmail}`));
|
|
3140
3251
|
if (userEmail && userEmail !== "anonymous") {
|
|
3141
3252
|
try {
|
|
3142
3253
|
const status = await getStatusCached(userEmail);
|
|
3143
3254
|
for (const line of formatUsageLines(status, tier)) {
|
|
3144
|
-
console.log(
|
|
3255
|
+
console.log(chalk6.dim(` ${line}`));
|
|
3145
3256
|
}
|
|
3146
3257
|
} catch {
|
|
3147
3258
|
}
|
|
3148
3259
|
}
|
|
3149
3260
|
if (userTier === "free") {
|
|
3150
3261
|
const upgradeUrl = getUpgradeUrl(userEmail !== "anonymous" ? userEmail : void 0);
|
|
3151
|
-
console.log(
|
|
3262
|
+
console.log(chalk6.dim(` Upgrade: ${upgradeUrl}`));
|
|
3152
3263
|
}
|
|
3153
3264
|
}
|
|
3154
3265
|
function displayPlanSummary(plan) {
|
|
3155
3266
|
console.log();
|
|
3156
|
-
console.log(
|
|
3157
|
-
console.log(
|
|
3267
|
+
console.log(chalk6.dim(` ${plan.title || "Untitled"}`));
|
|
3268
|
+
console.log(chalk6.dim(` ${plan.steps?.length || 0} steps`));
|
|
3158
3269
|
for (const step of plan.steps || []) {
|
|
3159
3270
|
const text = step.caption || step.action || "";
|
|
3160
3271
|
const caption = text.length > 50 ? `${text.slice(0, 47)}...` : text;
|
|
3161
|
-
console.log(
|
|
3272
|
+
console.log(chalk6.dim(` ${step.id}. ${caption}`));
|
|
3162
3273
|
}
|
|
3163
3274
|
}
|
|
3164
3275
|
function getOutputDirFromUrl(url) {
|
|
@@ -3205,6 +3316,86 @@ function getOutputDirFromUrl(url) {
|
|
|
3205
3316
|
}
|
|
3206
3317
|
}
|
|
3207
3318
|
|
|
3319
|
+
// src/commands/edit-cmd.ts
|
|
3320
|
+
var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
|
|
3321
|
+
function findPlanFile(dir, lang) {
|
|
3322
|
+
if (lang) {
|
|
3323
|
+
const exact = join10(dir, `plan-${lang}.json`);
|
|
3324
|
+
return existsSync7(exact) ? exact : null;
|
|
3325
|
+
}
|
|
3326
|
+
if (!existsSync7(dir)) return null;
|
|
3327
|
+
const plans = readdirSync(dir).filter((f) => PLAN_FILE.test(f));
|
|
3328
|
+
const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
|
|
3329
|
+
return preferred ? join10(dir, preferred) : null;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
// src/commands/record-cmd.ts
|
|
3333
|
+
import { existsSync as existsSync18 } from "fs";
|
|
3334
|
+
import { mkdir as mkdir8 } from "fs/promises";
|
|
3335
|
+
import { isAbsolute as isAbsolute5, join as join21, resolve as resolve9 } from "path";
|
|
3336
|
+
import chalk15 from "chalk";
|
|
3337
|
+
init_lib();
|
|
3338
|
+
init_config_loader();
|
|
3339
|
+
|
|
3340
|
+
// src/commands/phases/auth-phase.ts
|
|
3341
|
+
init_lib();
|
|
3342
|
+
import chalk9 from "chalk";
|
|
3343
|
+
init_config_loader();
|
|
3344
|
+
|
|
3345
|
+
// src/commands/utils/auth-helpers.ts
|
|
3346
|
+
init_lib();
|
|
3347
|
+
import chalk8 from "chalk";
|
|
3348
|
+
async function handleRegistrationGate() {
|
|
3349
|
+
console.log();
|
|
3350
|
+
console.log(chalk8.yellow(" \u{1F389} You've made 5 free videos!"));
|
|
3351
|
+
console.log(chalk8.white(" Sign in to continue creating videos."));
|
|
3352
|
+
console.log();
|
|
3353
|
+
console.log(chalk8.dim(" Benefits of signing in:"));
|
|
3354
|
+
console.log(chalk8.dim(" \u2022 20 more videos per month"));
|
|
3355
|
+
console.log(chalk8.dim(" \u2022 Keep your video history"));
|
|
3356
|
+
console.log(chalk8.dim(" \u2022 Upgrade to Pro anytime"));
|
|
3357
|
+
let email = await browserLogin();
|
|
3358
|
+
if (!email) {
|
|
3359
|
+
console.log();
|
|
3360
|
+
email = await promptInput("Email: ");
|
|
3361
|
+
}
|
|
3362
|
+
if (!email || !email.includes("@")) {
|
|
3363
|
+
console.log(chalk8.red(" Invalid email. Run qp again to try again."));
|
|
3364
|
+
return null;
|
|
3365
|
+
}
|
|
3366
|
+
const registerTask = task("Sending verification email");
|
|
3367
|
+
const result = await registerFreeUser(email);
|
|
3368
|
+
if (!result.success) {
|
|
3369
|
+
registerTask.fail(result.error || "Registration failed");
|
|
3370
|
+
return null;
|
|
3371
|
+
}
|
|
3372
|
+
if (result.alreadyVerified) {
|
|
3373
|
+
registerTask.succeed("Already verified!");
|
|
3374
|
+
return { email, verified: true };
|
|
3375
|
+
}
|
|
3376
|
+
registerTask.succeed("Check your email!");
|
|
3377
|
+
console.log();
|
|
3378
|
+
console.log(chalk8.cyan(" \u{1F4E7} Verification email sent to:"));
|
|
3379
|
+
console.log(chalk8.white(` ${email}`));
|
|
3380
|
+
console.log();
|
|
3381
|
+
console.log(chalk8.dim(" Click the link in the email, then press Enter to continue..."));
|
|
3382
|
+
await promptInput("");
|
|
3383
|
+
const verifyTask = task("Checking verification");
|
|
3384
|
+
const verified = await isVerified(email);
|
|
3385
|
+
if (verified) {
|
|
3386
|
+
verifyTask.succeed("Email verified!");
|
|
3387
|
+
return { email, verified: true };
|
|
3388
|
+
}
|
|
3389
|
+
verifyTask.fail("Not verified yet");
|
|
3390
|
+
console.log();
|
|
3391
|
+
console.log(chalk8.yellow(" Email not verified. Check your inbox and spam folder."));
|
|
3392
|
+
console.log(chalk8.dim(" Run qp again after clicking the verification link."));
|
|
3393
|
+
console.log();
|
|
3394
|
+
console.log(chalk8.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
|
|
3395
|
+
console.log();
|
|
3396
|
+
return { email, verified: false };
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3208
3399
|
// src/commands/phases/auth-phase.ts
|
|
3209
3400
|
async function runAuthPhase(input) {
|
|
3210
3401
|
let { userEmail, userTier, localVideoCount } = input;
|
|
@@ -3228,7 +3419,7 @@ async function runAuthPhase(input) {
|
|
|
3228
3419
|
} else {
|
|
3229
3420
|
verifyTask.fail("Email not verified");
|
|
3230
3421
|
console.log();
|
|
3231
|
-
console.log(
|
|
3422
|
+
console.log(chalk9.yellow(" Check your inbox for the verification email."));
|
|
3232
3423
|
console.log();
|
|
3233
3424
|
const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
|
|
3234
3425
|
if (input2.toLowerCase() === "exit") {
|
|
@@ -3253,8 +3444,8 @@ async function runAuthPhase(input) {
|
|
|
3253
3444
|
registerTask.succeed("Check your email!");
|
|
3254
3445
|
await saveUserData({ email: newEmail, verified: false });
|
|
3255
3446
|
console.log();
|
|
3256
|
-
console.log(
|
|
3257
|
-
console.log(
|
|
3447
|
+
console.log(chalk9.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
|
|
3448
|
+
console.log(chalk9.dim(" Click the link in the email, then run qp again."));
|
|
3258
3449
|
console.log();
|
|
3259
3450
|
gracefulExit();
|
|
3260
3451
|
return null;
|
|
@@ -3270,7 +3461,7 @@ async function runAuthPhase(input) {
|
|
|
3270
3461
|
} else {
|
|
3271
3462
|
recheckTask.fail("Still not verified");
|
|
3272
3463
|
console.log();
|
|
3273
|
-
console.log(
|
|
3464
|
+
console.log(chalk9.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
|
|
3274
3465
|
console.log();
|
|
3275
3466
|
gracefulExit();
|
|
3276
3467
|
return null;
|
|
@@ -3297,8 +3488,8 @@ async function runAuthPhase(input) {
|
|
|
3297
3488
|
} else {
|
|
3298
3489
|
const remaining = FREE_VIDEO_LIMIT - localVideoCount;
|
|
3299
3490
|
console.log();
|
|
3300
|
-
console.log(
|
|
3301
|
-
console.log(
|
|
3491
|
+
console.log(chalk9.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
|
|
3492
|
+
console.log(chalk9.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
|
|
3302
3493
|
userEmail = await setupUserEmail();
|
|
3303
3494
|
let registeredVerified = false;
|
|
3304
3495
|
try {
|
|
@@ -3317,10 +3508,10 @@ async function runAuthPhase(input) {
|
|
|
3317
3508
|
}
|
|
3318
3509
|
|
|
3319
3510
|
// src/commands/phases/plan-phase.ts
|
|
3320
|
-
import { existsSync as
|
|
3511
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "fs";
|
|
3321
3512
|
import { readFile as readFile4 } from "fs/promises";
|
|
3322
|
-
import { basename, join as
|
|
3323
|
-
import
|
|
3513
|
+
import { basename, join as join11 } from "path";
|
|
3514
|
+
import chalk10 from "chalk";
|
|
3324
3515
|
|
|
3325
3516
|
// src/ai/fix.ts
|
|
3326
3517
|
init_lib();
|
|
@@ -3473,12 +3664,22 @@ Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
|
|
|
3473
3664
|
init_config_loader();
|
|
3474
3665
|
var INDENT3 = " ";
|
|
3475
3666
|
async function runPlanPhase(params) {
|
|
3476
|
-
const {
|
|
3667
|
+
const {
|
|
3668
|
+
url,
|
|
3669
|
+
config,
|
|
3670
|
+
userEmail,
|
|
3671
|
+
description,
|
|
3672
|
+
planPath,
|
|
3673
|
+
legacyPlanPath,
|
|
3674
|
+
targetLang,
|
|
3675
|
+
userTier = "free",
|
|
3676
|
+
fresh = false
|
|
3677
|
+
} = params;
|
|
3477
3678
|
const maxSteps = config.plan?.maxSteps ?? 8;
|
|
3478
3679
|
const extensionPath = config.browser?.extension;
|
|
3479
3680
|
let existingPlan = null;
|
|
3480
3681
|
let existingPlanPath = null;
|
|
3481
|
-
if (
|
|
3682
|
+
if (existsSync8(planPath)) {
|
|
3482
3683
|
try {
|
|
3483
3684
|
const parsed = JSON.parse(await readFile4(planPath, "utf-8"));
|
|
3484
3685
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
@@ -3490,10 +3691,10 @@ async function runPlanPhase(params) {
|
|
|
3490
3691
|
}
|
|
3491
3692
|
if (!existingPlan) {
|
|
3492
3693
|
try {
|
|
3493
|
-
const files =
|
|
3694
|
+
const files = readdirSync2(config.output.dir);
|
|
3494
3695
|
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== basename(planPath));
|
|
3495
3696
|
for (const file of planFiles) {
|
|
3496
|
-
const filePath =
|
|
3697
|
+
const filePath = join11(config.output.dir, file);
|
|
3497
3698
|
try {
|
|
3498
3699
|
const parsed = JSON.parse(await readFile4(filePath, "utf-8"));
|
|
3499
3700
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
@@ -3507,7 +3708,7 @@ async function runPlanPhase(params) {
|
|
|
3507
3708
|
} catch {
|
|
3508
3709
|
}
|
|
3509
3710
|
}
|
|
3510
|
-
if (!existingPlan &&
|
|
3711
|
+
if (!existingPlan && existsSync8(legacyPlanPath)) {
|
|
3511
3712
|
try {
|
|
3512
3713
|
const parsed = JSON.parse(await readFile4(legacyPlanPath, "utf-8"));
|
|
3513
3714
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
@@ -3533,25 +3734,25 @@ async function runPlanPhase(params) {
|
|
|
3533
3734
|
translateTask.fail(failMsg);
|
|
3534
3735
|
}
|
|
3535
3736
|
};
|
|
3536
|
-
if (existingPlan && existingPlanPath) {
|
|
3737
|
+
if (existingPlan && existingPlanPath && !fresh) {
|
|
3537
3738
|
console.log();
|
|
3538
|
-
console.log(
|
|
3539
|
-
console.log(
|
|
3739
|
+
console.log(chalk10.dim(` Found existing plan: `) + chalk10.cyan(formatClickablePath(existingPlanPath)));
|
|
3740
|
+
console.log(chalk10.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
|
|
3540
3741
|
if (!urlMatches) {
|
|
3541
|
-
console.log(
|
|
3542
|
-
console.log(
|
|
3543
|
-
console.log(
|
|
3742
|
+
console.log(chalk10.yellow(` \u26A0 Plan URL differs from argument:`));
|
|
3743
|
+
console.log(chalk10.yellow(` Plan: ${existingPlan.url}`));
|
|
3744
|
+
console.log(chalk10.yellow(` Arg: ${url}`));
|
|
3544
3745
|
}
|
|
3545
3746
|
const existingLang = existingPlan.lang || "en";
|
|
3546
3747
|
if (existingLang !== targetLang) {
|
|
3547
|
-
console.log(
|
|
3748
|
+
console.log(chalk10.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
|
|
3548
3749
|
}
|
|
3549
3750
|
const reuse = await confirm(`${INDENT3}Use existing plan?`);
|
|
3550
3751
|
if (reuse) {
|
|
3551
3752
|
plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
|
|
3552
3753
|
if (existingPlanPath !== planPath) {
|
|
3553
3754
|
await savePlan(plan, planPath);
|
|
3554
|
-
console.log(
|
|
3755
|
+
console.log(chalk10.dim(` Migrated to: ${formatClickablePath(planPath)}`));
|
|
3555
3756
|
}
|
|
3556
3757
|
if (existingLang !== targetLang) {
|
|
3557
3758
|
const translate = await confirm(`${INDENT3}Translate to ${targetLang}?`);
|
|
@@ -3569,7 +3770,8 @@ async function runPlanPhase(params) {
|
|
|
3569
3770
|
userEmail,
|
|
3570
3771
|
extensionPath,
|
|
3571
3772
|
config.output.dir,
|
|
3572
|
-
userTier
|
|
3773
|
+
userTier,
|
|
3774
|
+
{ width: config.video.width, height: config.video.height }
|
|
3573
3775
|
);
|
|
3574
3776
|
displayRateLimitInfo();
|
|
3575
3777
|
if (urlMatches) {
|
|
@@ -3586,7 +3788,7 @@ async function runPlanPhase(params) {
|
|
|
3586
3788
|
uploadIdx++;
|
|
3587
3789
|
}
|
|
3588
3790
|
}
|
|
3589
|
-
console.log(
|
|
3791
|
+
console.log(chalk10.dim(` Preserved upload paths from previous plan`));
|
|
3590
3792
|
}
|
|
3591
3793
|
await savePlan(plan, planPath);
|
|
3592
3794
|
plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
|
|
@@ -3602,7 +3804,9 @@ async function runPlanPhase(params) {
|
|
|
3602
3804
|
logError,
|
|
3603
3805
|
userEmail,
|
|
3604
3806
|
extensionPath,
|
|
3605
|
-
config.output.dir
|
|
3807
|
+
config.output.dir,
|
|
3808
|
+
void 0,
|
|
3809
|
+
{ width: config.video.width, height: config.video.height }
|
|
3606
3810
|
);
|
|
3607
3811
|
displayRateLimitInfo();
|
|
3608
3812
|
await savePlan(plan, planPath);
|
|
@@ -3610,8 +3814,8 @@ async function runPlanPhase(params) {
|
|
|
3610
3814
|
}
|
|
3611
3815
|
displayPlanSummary(plan);
|
|
3612
3816
|
console.log();
|
|
3613
|
-
console.log(
|
|
3614
|
-
console.log(
|
|
3817
|
+
console.log(chalk10.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
3818
|
+
console.log(chalk10.dim(` Enter a prompt to refine, or press Enter to continue.`));
|
|
3615
3819
|
const MAX_REFINEMENTS = 10;
|
|
3616
3820
|
let refinementCount = 0;
|
|
3617
3821
|
while (true) {
|
|
@@ -3629,15 +3833,15 @@ async function runPlanPhase(params) {
|
|
|
3629
3833
|
refineTask.succeed("Plan refined");
|
|
3630
3834
|
displayRateLimitInfo();
|
|
3631
3835
|
if (plan.steps.length > maxSteps) {
|
|
3632
|
-
console.log(
|
|
3836
|
+
console.log(chalk10.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
|
|
3633
3837
|
}
|
|
3634
3838
|
displayPlanSummary(plan);
|
|
3635
3839
|
console.log();
|
|
3636
|
-
console.log(
|
|
3840
|
+
console.log(chalk10.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
3637
3841
|
if (refinementCount >= MAX_REFINEMENTS) {
|
|
3638
3842
|
console.log();
|
|
3639
|
-
console.log(
|
|
3640
|
-
console.log(
|
|
3843
|
+
console.log(chalk10.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
|
|
3844
|
+
console.log(chalk10.dim(` Further refinements may incur additional costs.`));
|
|
3641
3845
|
const continueRefining = await confirm(`${INDENT3}Continue refining?`);
|
|
3642
3846
|
if (!continueRefining) break;
|
|
3643
3847
|
}
|
|
@@ -3649,12 +3853,12 @@ async function runPlanPhase(params) {
|
|
|
3649
3853
|
const planLang = plan.lang || "en";
|
|
3650
3854
|
if (planLang !== targetLang) {
|
|
3651
3855
|
let useExistingTranslation = false;
|
|
3652
|
-
if (
|
|
3856
|
+
if (existsSync8(planPath) && existingPlanPath !== planPath) {
|
|
3653
3857
|
try {
|
|
3654
3858
|
const translatedPlan = JSON.parse(await readFile4(planPath, "utf-8"));
|
|
3655
3859
|
if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
|
|
3656
3860
|
console.log();
|
|
3657
|
-
console.log(
|
|
3861
|
+
console.log(chalk10.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
|
|
3658
3862
|
const useTranslated = await confirm(`${INDENT3}Use ${targetLang} translation?`);
|
|
3659
3863
|
if (useTranslated) {
|
|
3660
3864
|
plan = translatedPlan;
|
|
@@ -3666,7 +3870,7 @@ async function runPlanPhase(params) {
|
|
|
3666
3870
|
}
|
|
3667
3871
|
if (!useExistingTranslation) {
|
|
3668
3872
|
console.log();
|
|
3669
|
-
console.log(
|
|
3873
|
+
console.log(chalk10.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
|
|
3670
3874
|
const translate = await confirm(`${INDENT3}Translate captions to ${targetLang}?`);
|
|
3671
3875
|
if (translate) await doTranslate("Translation failed, using original");
|
|
3672
3876
|
}
|
|
@@ -3676,32 +3880,32 @@ async function runPlanPhase(params) {
|
|
|
3676
3880
|
|
|
3677
3881
|
// src/commands/phases/validation-phase.ts
|
|
3678
3882
|
import { readFile as readFile7 } from "fs/promises";
|
|
3679
|
-
import
|
|
3883
|
+
import chalk12 from "chalk";
|
|
3680
3884
|
|
|
3681
3885
|
// src/record.ts
|
|
3682
|
-
import { existsSync as
|
|
3886
|
+
import { existsSync as existsSync13 } from "fs";
|
|
3683
3887
|
import { mkdir as mkdir6 } from "fs/promises";
|
|
3684
|
-
import { join as
|
|
3685
|
-
import
|
|
3888
|
+
import { join as join16 } from "path";
|
|
3889
|
+
import chalk11 from "chalk";
|
|
3686
3890
|
|
|
3687
3891
|
// src/browser/actions.ts
|
|
3688
|
-
import { existsSync as
|
|
3892
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3689
3893
|
import { readFile as readFile5 } from "fs/promises";
|
|
3690
3894
|
import { isAbsolute as isAbsolute2, resolve as resolve5 } from "path";
|
|
3691
3895
|
|
|
3692
3896
|
// src/browser/effects.ts
|
|
3693
3897
|
init_lib();
|
|
3694
|
-
import { existsSync as
|
|
3695
|
-
import { join as
|
|
3898
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
|
|
3899
|
+
import { join as join12 } from "path";
|
|
3696
3900
|
function loadHighlightCSS() {
|
|
3697
3901
|
const candidates = [
|
|
3698
|
-
|
|
3902
|
+
join12(__dirname, "highlight.css"),
|
|
3699
3903
|
// dist/ build
|
|
3700
|
-
|
|
3904
|
+
join12(__dirname, "..", "highlight.css")
|
|
3701
3905
|
// src/browser/ → src/
|
|
3702
3906
|
];
|
|
3703
3907
|
for (const p of candidates) {
|
|
3704
|
-
if (
|
|
3908
|
+
if (existsSync9(p)) return readFileSync4(p, "utf-8");
|
|
3705
3909
|
}
|
|
3706
3910
|
return "";
|
|
3707
3911
|
}
|
|
@@ -4025,7 +4229,14 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
4025
4229
|
}
|
|
4026
4230
|
} else {
|
|
4027
4231
|
const amount = direction === "up" ? -PAGE_SCROLL_PX : PAGE_SCROLL_PX;
|
|
4028
|
-
await page.evaluate((y) =>
|
|
4232
|
+
const moved = await page.evaluate((y) => {
|
|
4233
|
+
const before = window.scrollY;
|
|
4234
|
+
window.scrollBy(0, y);
|
|
4235
|
+
return window.scrollY - before;
|
|
4236
|
+
}, amount);
|
|
4237
|
+
if (moved === 0) {
|
|
4238
|
+
throw new Error("scroll moved nothing: the page has no scroll headroom at this viewport");
|
|
4239
|
+
}
|
|
4029
4240
|
}
|
|
4030
4241
|
break;
|
|
4031
4242
|
}
|
|
@@ -4043,7 +4254,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
4043
4254
|
if (!isPlaceholder) {
|
|
4044
4255
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
4045
4256
|
const filePath = isAbsolute2(normalizedPath) ? normalizedPath : resolve5(baseDir, normalizedPath);
|
|
4046
|
-
if (
|
|
4257
|
+
if (existsSync10(filePath)) {
|
|
4047
4258
|
await fileInput.setInputFiles(filePath);
|
|
4048
4259
|
await page.waitForTimeout(2e3);
|
|
4049
4260
|
break;
|
|
@@ -4160,7 +4371,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
4160
4371
|
break;
|
|
4161
4372
|
case "image": {
|
|
4162
4373
|
const imagePath = step.value ? isAbsolute2(step.value) ? step.value : resolve5(baseDir, step.value) : null;
|
|
4163
|
-
if (imagePath &&
|
|
4374
|
+
if (imagePath && existsSync10(imagePath)) {
|
|
4164
4375
|
const imgStyle = step.target === "stretch" ? `width: ${config.video.width}px; height: ${config.video.height}px; object-fit: cover;` : "max-width: 90%; max-height: 90%;";
|
|
4165
4376
|
if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
|
|
4166
4377
|
}
|
|
@@ -4190,7 +4401,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
4190
4401
|
await pendingDownload;
|
|
4191
4402
|
pendingDownload = null;
|
|
4192
4403
|
}
|
|
4193
|
-
if (lastDownloadPath &&
|
|
4404
|
+
if (lastDownloadPath && existsSync10(lastDownloadPath)) {
|
|
4194
4405
|
const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
|
|
4195
4406
|
if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
|
|
4196
4407
|
}
|
|
@@ -4235,22 +4446,22 @@ init_lib();
|
|
|
4235
4446
|
|
|
4236
4447
|
// src/browser/dry-run.ts
|
|
4237
4448
|
init_utils();
|
|
4238
|
-
import { join as
|
|
4449
|
+
import { join as join14 } from "path";
|
|
4239
4450
|
|
|
4240
4451
|
// src/browser/step-validator.ts
|
|
4241
4452
|
init_utils();
|
|
4242
|
-
import { existsSync as
|
|
4243
|
-
import { basename as basename2, isAbsolute as isAbsolute3, join as
|
|
4453
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3 } from "fs";
|
|
4454
|
+
import { basename as basename2, isAbsolute as isAbsolute3, join as join13, resolve as resolve6 } from "path";
|
|
4244
4455
|
function findMediaFile(filename, baseDir) {
|
|
4245
4456
|
const name = basename2(filename);
|
|
4246
|
-
const searchDirs = [baseDir,
|
|
4457
|
+
const searchDirs = [baseDir, join13(baseDir, "res"), join13(baseDir, "assets"), join13(baseDir, "media")];
|
|
4247
4458
|
for (const dir of searchDirs) {
|
|
4248
|
-
if (!
|
|
4459
|
+
if (!existsSync11(dir)) continue;
|
|
4249
4460
|
try {
|
|
4250
|
-
const files =
|
|
4251
|
-
if (files.includes(name)) return
|
|
4461
|
+
const files = readdirSync3(dir);
|
|
4462
|
+
if (files.includes(name)) return join13(dir, name);
|
|
4252
4463
|
const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
|
|
4253
|
-
if (match) return
|
|
4464
|
+
if (match) return join13(dir, match);
|
|
4254
4465
|
} catch {
|
|
4255
4466
|
}
|
|
4256
4467
|
}
|
|
@@ -4323,7 +4534,7 @@ function resolveMediaStep(step, baseDir, label, startTime) {
|
|
|
4323
4534
|
return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
|
|
4324
4535
|
}
|
|
4325
4536
|
const specifiedPath = isAbsolute3(step.value) ? step.value : resolve6(baseDir, step.value);
|
|
4326
|
-
if (
|
|
4537
|
+
if (existsSync11(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
|
|
4327
4538
|
const foundPath = findMediaFile(step.value, baseDir);
|
|
4328
4539
|
if (foundPath) {
|
|
4329
4540
|
step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -4445,7 +4656,7 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
|
|
|
4445
4656
|
if (!isPlaceholder) {
|
|
4446
4657
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
4447
4658
|
let filePath = isAbsolute3(normalizedPath) ? normalizedPath : resolve6(baseDir, normalizedPath);
|
|
4448
|
-
if (!
|
|
4659
|
+
if (!existsSync11(filePath)) {
|
|
4449
4660
|
const foundPath = findMediaFile(rawPath, baseDir);
|
|
4450
4661
|
if (foundPath) {
|
|
4451
4662
|
const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -4544,7 +4755,7 @@ async function getPageElements(page) {
|
|
|
4544
4755
|
});
|
|
4545
4756
|
}
|
|
4546
4757
|
async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart, onStepEnd) {
|
|
4547
|
-
const userDataDir =
|
|
4758
|
+
const userDataDir = join14(baseDir, ".browser-data");
|
|
4548
4759
|
const { context, browser, chromeSession } = await launchBrowserContext(
|
|
4549
4760
|
config,
|
|
4550
4761
|
userDataDir,
|
|
@@ -4609,17 +4820,17 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
|
|
|
4609
4820
|
}
|
|
4610
4821
|
|
|
4611
4822
|
// src/browser/meta.ts
|
|
4612
|
-
import { existsSync as
|
|
4823
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4613
4824
|
import { readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
|
|
4614
|
-
import { join as
|
|
4825
|
+
import { join as join15 } from "path";
|
|
4615
4826
|
var RECORDING_FILE = "recording.mp4";
|
|
4616
4827
|
var RECORDING_META_FILE = "recording.meta.json";
|
|
4617
4828
|
function getRecordingPath(outputDir) {
|
|
4618
|
-
return
|
|
4829
|
+
return join15(outputDir, RECORDING_FILE);
|
|
4619
4830
|
}
|
|
4620
4831
|
async function loadRecordingMeta(outputDir) {
|
|
4621
|
-
const metaPath =
|
|
4622
|
-
if (!
|
|
4832
|
+
const metaPath = join15(outputDir, RECORDING_META_FILE);
|
|
4833
|
+
if (!existsSync12(metaPath)) return null;
|
|
4623
4834
|
try {
|
|
4624
4835
|
const content = await readFile6(metaPath, "utf-8");
|
|
4625
4836
|
return JSON.parse(content);
|
|
@@ -4628,7 +4839,7 @@ async function loadRecordingMeta(outputDir) {
|
|
|
4628
4839
|
}
|
|
4629
4840
|
}
|
|
4630
4841
|
async function saveRecordingMeta(outputDir, meta) {
|
|
4631
|
-
const metaPath =
|
|
4842
|
+
const metaPath = join15(outputDir, RECORDING_META_FILE);
|
|
4632
4843
|
await writeFile7(metaPath, JSON.stringify(meta, null, 2));
|
|
4633
4844
|
}
|
|
4634
4845
|
function canReuseRecording(plan, config, stepDurations, existingMeta) {
|
|
@@ -4698,14 +4909,14 @@ function formatStepLabel(step) {
|
|
|
4698
4909
|
return `${caption.slice(0, 47)}...`;
|
|
4699
4910
|
}
|
|
4700
4911
|
async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
|
|
4701
|
-
const videoDir =
|
|
4912
|
+
const videoDir = join16(workDir, "video");
|
|
4702
4913
|
await mkdir6(videoDir, { recursive: true });
|
|
4703
4914
|
const getStepDuration = (stepNum) => {
|
|
4704
4915
|
return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
|
|
4705
4916
|
};
|
|
4706
4917
|
const browserTask = task2("Starting browser recording");
|
|
4707
4918
|
const recordingStartTime = Date.now();
|
|
4708
|
-
const userDataDir =
|
|
4919
|
+
const userDataDir = join16(workDir, "browser-data");
|
|
4709
4920
|
const recordVideo = { dir: videoDir, size: { width: config.video.width, height: config.video.height } };
|
|
4710
4921
|
const authState = findAuthState(plan.url);
|
|
4711
4922
|
const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
|
|
@@ -4806,9 +5017,9 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4806
5017
|
actualStepDurations.push(actualDuration);
|
|
4807
5018
|
}
|
|
4808
5019
|
if (failures.length > 0) {
|
|
4809
|
-
console.log(
|
|
5020
|
+
console.log(chalk11.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
|
|
4810
5021
|
for (const f of failures) {
|
|
4811
|
-
console.log(
|
|
5022
|
+
console.log(chalk11.yellow(` - ${f}`));
|
|
4812
5023
|
}
|
|
4813
5024
|
}
|
|
4814
5025
|
const saveTask = task2("Saving video");
|
|
@@ -4817,7 +5028,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4817
5028
|
await page.close();
|
|
4818
5029
|
const videoPath = await video.path();
|
|
4819
5030
|
await cleanupBrowserSession({ context, browser, chromeSession });
|
|
4820
|
-
if (!videoPath || !
|
|
5031
|
+
if (!videoPath || !existsSync13(videoPath)) {
|
|
4821
5032
|
throw new Error("Video file was not created");
|
|
4822
5033
|
}
|
|
4823
5034
|
saveTask.succeed("Video saved");
|
|
@@ -4876,21 +5087,21 @@ async function runValidationPhase(params) {
|
|
|
4876
5087
|
const stepNum = result.stepIndex + 1;
|
|
4877
5088
|
const step = plan.steps[result.stepIndex];
|
|
4878
5089
|
console.log();
|
|
4879
|
-
console.log(
|
|
5090
|
+
console.log(chalk12.red(` \u2717 Step ${stepNum}: ${step.action}`));
|
|
4880
5091
|
const caption = step.caption || "";
|
|
4881
|
-
if (caption) console.log(
|
|
4882
|
-
console.log(
|
|
4883
|
-
if (step.value) console.log(
|
|
4884
|
-
console.log(
|
|
5092
|
+
if (caption) console.log(chalk12.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
|
|
5093
|
+
console.log(chalk12.dim(` Target: ${step.target || "none"}`));
|
|
5094
|
+
if (step.value) console.log(chalk12.dim(` Value: ${step.value}`));
|
|
5095
|
+
console.log(chalk12.dim(` Error: ${result.error}`));
|
|
4885
5096
|
if (result.suggestion) {
|
|
4886
|
-
console.log(
|
|
5097
|
+
console.log(chalk12.yellow(` Hint: ${result.suggestion}`));
|
|
4887
5098
|
}
|
|
4888
5099
|
console.log();
|
|
4889
5100
|
const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
|
|
4890
5101
|
if (isUploadMissingFile) {
|
|
4891
5102
|
const filePath = await promptInput('Enter file path to upload (or "skip"): ');
|
|
4892
5103
|
if (filePath.toLowerCase() === "skip") {
|
|
4893
|
-
console.log(
|
|
5104
|
+
console.log(chalk12.yellow(` Skipping step ${stepNum}`));
|
|
4894
5105
|
continue;
|
|
4895
5106
|
}
|
|
4896
5107
|
if (filePath) {
|
|
@@ -4899,13 +5110,13 @@ async function runValidationPhase(params) {
|
|
|
4899
5110
|
const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
4900
5111
|
const newResult2 = revalidation2.results[result.stepIndex];
|
|
4901
5112
|
if (newResult2?.success) {
|
|
4902
|
-
console.log(
|
|
5113
|
+
console.log(chalk12.green(` \u2713 Step ${stepNum} now passes`));
|
|
4903
5114
|
validationResult = revalidation2;
|
|
4904
5115
|
continue;
|
|
4905
5116
|
}
|
|
4906
5117
|
result.error = newResult2?.error;
|
|
4907
5118
|
result.suggestion = newResult2?.suggestion;
|
|
4908
|
-
console.log(
|
|
5119
|
+
console.log(chalk12.red(` Still failing: ${result.error}`));
|
|
4909
5120
|
}
|
|
4910
5121
|
}
|
|
4911
5122
|
const fixTask = task(`Fixing step ${stepNum} with AI`);
|
|
@@ -4927,8 +5138,8 @@ async function runValidationPhase(params) {
|
|
|
4927
5138
|
await savePlan(plan, planPath);
|
|
4928
5139
|
fixTask.succeed("AI suggested a fix");
|
|
4929
5140
|
const fixedStep = plan.steps[result.stepIndex];
|
|
4930
|
-
console.log(
|
|
4931
|
-
if (fixedStep.value) console.log(
|
|
5141
|
+
console.log(chalk12.dim(` New target: ${fixedStep.target || "none"}`));
|
|
5142
|
+
if (fixedStep.value) console.log(chalk12.dim(` New value: ${fixedStep.value}`));
|
|
4932
5143
|
} catch {
|
|
4933
5144
|
fixTask.fail("AI could not fix automatically");
|
|
4934
5145
|
}
|
|
@@ -4944,11 +5155,11 @@ async function runValidationPhase(params) {
|
|
|
4944
5155
|
result.error = newResult?.error;
|
|
4945
5156
|
result.suggestion = newResult?.suggestion;
|
|
4946
5157
|
console.log();
|
|
4947
|
-
console.log(
|
|
5158
|
+
console.log(chalk12.yellow(` Edit step ${stepNum} in: `) + chalk12.cyan(formatClickablePath(planPath)));
|
|
4948
5159
|
console.log();
|
|
4949
5160
|
const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
|
|
4950
5161
|
if (choice.toLowerCase() === "skip") {
|
|
4951
|
-
console.log(
|
|
5162
|
+
console.log(chalk12.yellow(` Skipping step ${stepNum} - may fail during recording`));
|
|
4952
5163
|
continue;
|
|
4953
5164
|
}
|
|
4954
5165
|
plan = JSON.parse(await readFile7(planPath, "utf-8"));
|
|
@@ -4976,17 +5187,17 @@ async function runValidationPhase(params) {
|
|
|
4976
5187
|
|
|
4977
5188
|
// src/commands/phases/video-phase.ts
|
|
4978
5189
|
init_lib();
|
|
4979
|
-
import { existsSync as
|
|
5190
|
+
import { existsSync as existsSync15 } from "fs";
|
|
4980
5191
|
import { copyFile } from "fs/promises";
|
|
4981
|
-
import { join as
|
|
5192
|
+
import { join as join18, resolve as resolve8 } from "path";
|
|
4982
5193
|
|
|
4983
5194
|
// src/utils/video-pipeline.ts
|
|
4984
5195
|
init_lib();
|
|
4985
5196
|
import { chunkWordsByCount, assTimeFromMs as formatASSTime } from "@spark-apps/video-kit";
|
|
4986
|
-
import { existsSync as
|
|
5197
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4987
5198
|
import { mkdir as mkdir7, writeFile as writeFile8 } from "fs/promises";
|
|
4988
|
-
import { isAbsolute as isAbsolute4, join as
|
|
4989
|
-
import
|
|
5199
|
+
import { isAbsolute as isAbsolute4, join as join17, resolve as resolve7 } from "path";
|
|
5200
|
+
import chalk13 from "chalk";
|
|
4990
5201
|
var INDENT4 = " ";
|
|
4991
5202
|
async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
4992
5203
|
const regularColor = hexToAss(captions.regular.color);
|
|
@@ -5033,7 +5244,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
5033
5244
|
}
|
|
5034
5245
|
currentTime += audio.duration;
|
|
5035
5246
|
}
|
|
5036
|
-
const assPath =
|
|
5247
|
+
const assPath = join17(workDir, "subtitles.ass");
|
|
5037
5248
|
await writeFile8(assPath, ass);
|
|
5038
5249
|
return assPath;
|
|
5039
5250
|
}
|
|
@@ -5048,7 +5259,7 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
5048
5259
|
const ext = step.value.split(".").pop()?.toLowerCase() || "";
|
|
5049
5260
|
if (["mp4", "webm", "mov"].includes(ext)) {
|
|
5050
5261
|
const mediaPath = isAbsolute4(step.value) ? step.value : resolve7(baseDir, step.value);
|
|
5051
|
-
if (
|
|
5262
|
+
if (existsSync14(mediaPath)) {
|
|
5052
5263
|
const partial = parsePartial(step.partial);
|
|
5053
5264
|
overlays.push({
|
|
5054
5265
|
path: mediaPath,
|
|
@@ -5067,11 +5278,11 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
5067
5278
|
}
|
|
5068
5279
|
async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, _stepSync) {
|
|
5069
5280
|
header("Compose");
|
|
5070
|
-
const configPath =
|
|
5071
|
-
console.log(`${INDENT4}${
|
|
5281
|
+
const configPath = join17(process.cwd(), CONFIG_FILE);
|
|
5282
|
+
console.log(`${INDENT4}${chalk13.dim(`Config: ${formatClickablePath(configPath)}`)}`);
|
|
5072
5283
|
const proceed = await confirm(`${INDENT4}Review config or continue?`);
|
|
5073
5284
|
if (!proceed) {
|
|
5074
|
-
console.log(
|
|
5285
|
+
console.log(chalk13.yellow(`
|
|
5075
5286
|
Edit ${CONFIG_FILE} and run again.
|
|
5076
5287
|
`));
|
|
5077
5288
|
process.exit(0);
|
|
@@ -5079,11 +5290,11 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
|
|
|
5079
5290
|
const composeTask = task("Building final video");
|
|
5080
5291
|
await mkdir7(config.output.dir, { recursive: true });
|
|
5081
5292
|
const suffix = config.lang ? `-${config.lang}` : "";
|
|
5082
|
-
const finalPath =
|
|
5293
|
+
const finalPath = join17(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
5083
5294
|
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
5084
|
-
const hasVoiceover = audioPath &&
|
|
5295
|
+
const hasVoiceover = audioPath && existsSync14(audioPath);
|
|
5085
5296
|
const musicPath = config.music?.path ? isAbsolute4(config.music.path) ? config.music.path : resolve7(config.output.dir, config.music.path) : "";
|
|
5086
|
-
const hasMusic = musicPath &&
|
|
5297
|
+
const hasMusic = musicPath && existsSync14(musicPath);
|
|
5087
5298
|
try {
|
|
5088
5299
|
await composeVideo({
|
|
5089
5300
|
videoPath,
|
|
@@ -5122,7 +5333,7 @@ async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
|
|
|
5122
5333
|
if (duration <= limitSecs * 1e3) return finalPath;
|
|
5123
5334
|
const speedFactor = Math.ceil(duration / (limitSecs * 1e3) * 10) / 10;
|
|
5124
5335
|
console.log();
|
|
5125
|
-
console.log(
|
|
5336
|
+
console.log(chalk13.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
|
|
5126
5337
|
const speedUp = await confirm(` Speed up ${speedFactor}x to fit under ${limitSecs}s?`);
|
|
5127
5338
|
if (!speedUp) return finalPath;
|
|
5128
5339
|
const speedTask = task(`Speeding up ${speedFactor}x`);
|
|
@@ -5163,7 +5374,7 @@ async function runVideoPhase(params) {
|
|
|
5163
5374
|
mediaDuration = (trimEnd - trimStart2) * 1e3;
|
|
5164
5375
|
} else {
|
|
5165
5376
|
const mediaPath = resolve8(s.value);
|
|
5166
|
-
if (
|
|
5377
|
+
if (existsSync15(mediaPath)) {
|
|
5167
5378
|
try {
|
|
5168
5379
|
const fullDuration = await getMediaDuration(mediaPath);
|
|
5169
5380
|
const start = (trimStart2 ?? 0) * 1e3;
|
|
@@ -5175,7 +5386,7 @@ async function runVideoPhase(params) {
|
|
|
5175
5386
|
}
|
|
5176
5387
|
} else {
|
|
5177
5388
|
const mediaPath = resolve8(s.value);
|
|
5178
|
-
if (
|
|
5389
|
+
if (existsSync15(mediaPath)) {
|
|
5179
5390
|
try {
|
|
5180
5391
|
mediaDuration = await getMediaDuration(mediaPath);
|
|
5181
5392
|
} catch {
|
|
@@ -5188,7 +5399,7 @@ async function runVideoPhase(params) {
|
|
|
5188
5399
|
})
|
|
5189
5400
|
);
|
|
5190
5401
|
const recordingPath = getRecordingPath(config.output.dir);
|
|
5191
|
-
const hasExistingRecording = existingRecordingMeta &&
|
|
5402
|
+
const hasExistingRecording = existingRecordingMeta && existsSync15(recordingPath);
|
|
5192
5403
|
let videoPath = recordingPath;
|
|
5193
5404
|
const firstStep = plan.steps[0];
|
|
5194
5405
|
const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
|
|
@@ -5228,7 +5439,7 @@ async function runVideoPhase(params) {
|
|
|
5228
5439
|
if (videoOverlays.length > 0) {
|
|
5229
5440
|
const spliceTask = task("Splicing videos into recording");
|
|
5230
5441
|
try {
|
|
5231
|
-
const splicedPath =
|
|
5442
|
+
const splicedPath = join18(workDir, "spliced.mp4");
|
|
5232
5443
|
await spliceVideos({
|
|
5233
5444
|
inputPath: videoPath,
|
|
5234
5445
|
outputPath: splicedPath,
|
|
@@ -5248,7 +5459,7 @@ async function runVideoPhase(params) {
|
|
|
5248
5459
|
if (stepTransitions.length > 0) {
|
|
5249
5460
|
const transitionTask = task("Applying transitions");
|
|
5250
5461
|
try {
|
|
5251
|
-
const transitionedPath =
|
|
5462
|
+
const transitionedPath = join18(workDir, "transitioned.mp4");
|
|
5252
5463
|
const videoDuration = alignedStepDurations.reduce((a, b) => a + b, 0) / 1e3;
|
|
5253
5464
|
await applyTransitions({
|
|
5254
5465
|
inputVideo: videoPath,
|
|
@@ -5269,15 +5480,15 @@ async function runVideoPhase(params) {
|
|
|
5269
5480
|
let mainContentDuration;
|
|
5270
5481
|
if (userTier === "free") {
|
|
5271
5482
|
const scriptDir = getScriptDir();
|
|
5272
|
-
let outroPath =
|
|
5273
|
-
if (!
|
|
5274
|
-
outroPath =
|
|
5483
|
+
let outroPath = join18(scriptDir, "assets", "qp.wm");
|
|
5484
|
+
if (!existsSync15(outroPath)) {
|
|
5485
|
+
outroPath = join18(scriptDir, "..", "dist", "assets", "qp.wm");
|
|
5275
5486
|
}
|
|
5276
|
-
if (
|
|
5487
|
+
if (existsSync15(outroPath)) {
|
|
5277
5488
|
try {
|
|
5278
5489
|
const mainDuration = await getMediaDuration(videoPath);
|
|
5279
5490
|
mainContentDuration = mainDuration / 1e3 - trimStart;
|
|
5280
|
-
const combinedPath =
|
|
5491
|
+
const combinedPath = join18(workDir, "combined-with-outro.mp4");
|
|
5281
5492
|
await concatVideoWithOutro({
|
|
5282
5493
|
inputPath: videoPath,
|
|
5283
5494
|
outroPath,
|
|
@@ -5325,16 +5536,16 @@ async function runVideoPhase(params) {
|
|
|
5325
5536
|
|
|
5326
5537
|
// src/commands/phases/voiceover-phase.ts
|
|
5327
5538
|
init_lib();
|
|
5328
|
-
import { existsSync as
|
|
5329
|
-
import { join as
|
|
5330
|
-
import
|
|
5539
|
+
import { existsSync as existsSync16 } from "fs";
|
|
5540
|
+
import { join as join19 } from "path";
|
|
5541
|
+
import chalk14 from "chalk";
|
|
5331
5542
|
var INDENT6 = " ";
|
|
5332
5543
|
async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
5333
5544
|
if (fresh) {
|
|
5334
5545
|
const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
|
|
5335
5546
|
for (const file of filesToDelete) {
|
|
5336
|
-
const filePath =
|
|
5337
|
-
if (
|
|
5547
|
+
const filePath = join19(config.output.dir, file);
|
|
5548
|
+
if (existsSync16(filePath)) {
|
|
5338
5549
|
const { unlink: unlink3 } = await import("fs/promises");
|
|
5339
5550
|
await unlink3(filePath).catch(() => {
|
|
5340
5551
|
});
|
|
@@ -5344,8 +5555,8 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5344
5555
|
}
|
|
5345
5556
|
const voice = resolveVoice(config);
|
|
5346
5557
|
const existingMeta = await loadVoiceoverMeta(config.output.dir);
|
|
5347
|
-
const voiceoverFile =
|
|
5348
|
-
const hasExistingVoiceover = existingMeta &&
|
|
5558
|
+
const voiceoverFile = join19(config.output.dir, "voiceover.wav");
|
|
5559
|
+
const hasExistingVoiceover = existingMeta && existsSync16(voiceoverFile);
|
|
5349
5560
|
const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
|
|
5350
5561
|
let voiceoverPath;
|
|
5351
5562
|
let stepAudio;
|
|
@@ -5368,9 +5579,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5368
5579
|
const reuseCheck = canReuseVoiceover(plan.steps, voice, existingMeta);
|
|
5369
5580
|
if (reuseCheck.canReuse) {
|
|
5370
5581
|
console.log();
|
|
5371
|
-
console.log(
|
|
5582
|
+
console.log(chalk14.dim(` Found existing voiceover in ${config.output.dir}`));
|
|
5372
5583
|
if (reuseCheck.reason) {
|
|
5373
|
-
console.log(
|
|
5584
|
+
console.log(chalk14.dim(` ${reuseCheck.reason}`));
|
|
5374
5585
|
}
|
|
5375
5586
|
const reuse = await confirm(`${INDENT6}Reuse existing voiceover?`);
|
|
5376
5587
|
if (reuse) {
|
|
@@ -5402,31 +5613,31 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5402
5613
|
}
|
|
5403
5614
|
|
|
5404
5615
|
// src/commands/utils/chrome-extension.ts
|
|
5405
|
-
import { existsSync as
|
|
5616
|
+
import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "fs";
|
|
5406
5617
|
import { homedir as homedir3 } from "os";
|
|
5407
|
-
import { join as
|
|
5618
|
+
import { join as join20 } from "path";
|
|
5408
5619
|
function resolveChromeExtension(extensionId) {
|
|
5409
5620
|
const platform = process.platform;
|
|
5410
5621
|
const possibleDirs = [];
|
|
5411
5622
|
const getChromeExtDirs = (userDataPath) => {
|
|
5412
5623
|
const dirs = [];
|
|
5413
5624
|
try {
|
|
5414
|
-
const profiles =
|
|
5625
|
+
const profiles = readdirSync4(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
|
|
5415
5626
|
for (const profile of profiles) {
|
|
5416
|
-
dirs.push(
|
|
5627
|
+
dirs.push(join20(userDataPath, profile, "Extensions"));
|
|
5417
5628
|
}
|
|
5418
5629
|
} catch {
|
|
5419
|
-
dirs.push(
|
|
5630
|
+
dirs.push(join20(userDataPath, "Default", "Extensions"));
|
|
5420
5631
|
}
|
|
5421
5632
|
return dirs;
|
|
5422
5633
|
};
|
|
5423
5634
|
if (platform === "win32") {
|
|
5424
|
-
possibleDirs.push(...getChromeExtDirs(
|
|
5635
|
+
possibleDirs.push(...getChromeExtDirs(join20(homedir3(), "AppData", "Local", "Google", "Chrome", "User Data")));
|
|
5425
5636
|
} else if (platform === "darwin") {
|
|
5426
|
-
possibleDirs.push(...getChromeExtDirs(
|
|
5637
|
+
possibleDirs.push(...getChromeExtDirs(join20(homedir3(), "Library", "Application Support", "Google", "Chrome")));
|
|
5427
5638
|
} else {
|
|
5428
5639
|
try {
|
|
5429
|
-
const windowsUsers =
|
|
5640
|
+
const windowsUsers = readdirSync4("/mnt/c/Users").filter(
|
|
5430
5641
|
(u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
|
|
5431
5642
|
);
|
|
5432
5643
|
for (const user of windowsUsers) {
|
|
@@ -5434,22 +5645,22 @@ function resolveChromeExtension(extensionId) {
|
|
|
5434
5645
|
}
|
|
5435
5646
|
} catch {
|
|
5436
5647
|
}
|
|
5437
|
-
possibleDirs.push(...getChromeExtDirs(
|
|
5648
|
+
possibleDirs.push(...getChromeExtDirs(join20(homedir3(), ".config", "google-chrome")));
|
|
5438
5649
|
}
|
|
5439
5650
|
for (const chromeExtDir of possibleDirs) {
|
|
5440
|
-
const extDir =
|
|
5441
|
-
if (!
|
|
5651
|
+
const extDir = join20(chromeExtDir, extensionId);
|
|
5652
|
+
if (!existsSync17(extDir)) continue;
|
|
5442
5653
|
try {
|
|
5443
|
-
const versions =
|
|
5654
|
+
const versions = readdirSync4(extDir).filter((f) => !f.startsWith("."));
|
|
5444
5655
|
if (versions.length === 0) continue;
|
|
5445
5656
|
const latestVersion = versions.sort().pop();
|
|
5446
|
-
const extPath =
|
|
5657
|
+
const extPath = join20(extDir, latestVersion);
|
|
5447
5658
|
let name = extensionId;
|
|
5448
5659
|
let popupPath = "popup.html";
|
|
5449
|
-
const manifestPath =
|
|
5450
|
-
if (
|
|
5660
|
+
const manifestPath = join20(extPath, "manifest.json");
|
|
5661
|
+
if (existsSync17(manifestPath)) {
|
|
5451
5662
|
try {
|
|
5452
|
-
const manifest = JSON.parse(
|
|
5663
|
+
const manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
5453
5664
|
if (manifest.name && !manifest.name.startsWith("__MSG_")) {
|
|
5454
5665
|
name = manifest.name;
|
|
5455
5666
|
}
|
|
@@ -5480,16 +5691,16 @@ async function recordCommand(url, options = {}) {
|
|
|
5480
5691
|
const extensionId = url.slice(7);
|
|
5481
5692
|
chromeExtension = resolveChromeExtension(extensionId);
|
|
5482
5693
|
if (!chromeExtension) {
|
|
5483
|
-
console.log(
|
|
5694
|
+
console.log(chalk15.red(`
|
|
5484
5695
|
\u2717 Chrome extension not found: ${extensionId}`));
|
|
5485
|
-
console.log(
|
|
5696
|
+
console.log(chalk15.dim(` Make sure the extension is installed in Chrome
|
|
5486
5697
|
`));
|
|
5487
5698
|
process.exit(1);
|
|
5488
5699
|
}
|
|
5489
5700
|
config.browser = { extension: chromeExtension.path };
|
|
5490
5701
|
url = chromeExtension.popupUrl;
|
|
5491
|
-
console.log(
|
|
5492
|
-
console.log(
|
|
5702
|
+
console.log(chalk15.dim(` Extension: ${chromeExtension.name}`));
|
|
5703
|
+
console.log(chalk15.dim(` Path: ${chromeExtension.path}`));
|
|
5493
5704
|
}
|
|
5494
5705
|
config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
|
|
5495
5706
|
const userData = await loadUserData();
|
|
@@ -5502,20 +5713,20 @@ async function recordCommand(url, options = {}) {
|
|
|
5502
5713
|
if (!authResult) return;
|
|
5503
5714
|
const { userEmail, userTier, localVideoCount } = authResult;
|
|
5504
5715
|
if (configCreated) {
|
|
5505
|
-
const configPath =
|
|
5716
|
+
const configPath = join21(process.cwd(), CONFIG_FILE);
|
|
5506
5717
|
console.log();
|
|
5507
|
-
console.log(
|
|
5508
|
-
console.log(
|
|
5718
|
+
console.log(chalk15.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
5719
|
+
console.log(chalk15.dim(` ${configPath}`));
|
|
5509
5720
|
console.log();
|
|
5510
|
-
console.log(
|
|
5511
|
-
console.log(
|
|
5512
|
-
console.log(
|
|
5513
|
-
console.log(
|
|
5514
|
-
console.log(
|
|
5721
|
+
console.log(chalk15.cyan(" Review the config file to customize:"));
|
|
5722
|
+
console.log(chalk15.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
|
|
5723
|
+
console.log(chalk15.dim(" \u2022 video: Resolution, quality, effects"));
|
|
5724
|
+
console.log(chalk15.dim(" \u2022 music: Background music path and volume"));
|
|
5725
|
+
console.log(chalk15.dim(" \u2022 output: Output directory"));
|
|
5515
5726
|
console.log();
|
|
5516
5727
|
const proceed = await confirm(`${INDENT2}Proceed with default settings?`);
|
|
5517
5728
|
if (!proceed) {
|
|
5518
|
-
console.log(
|
|
5729
|
+
console.log(chalk15.yellow(`
|
|
5519
5730
|
Edit ${CONFIG_FILE} and run again.
|
|
5520
5731
|
`));
|
|
5521
5732
|
process.exit(0);
|
|
@@ -5524,16 +5735,16 @@ async function recordCommand(url, options = {}) {
|
|
|
5524
5735
|
if (langOverride) config.lang = langOverride;
|
|
5525
5736
|
if (config.music?.path) {
|
|
5526
5737
|
const musicPath = isAbsolute5(config.music.path) ? config.music.path : resolve9(config.output.dir, config.music.path);
|
|
5527
|
-
if (!
|
|
5738
|
+
if (!existsSync18(musicPath)) {
|
|
5528
5739
|
console.log();
|
|
5529
|
-
console.log(
|
|
5530
|
-
console.log(
|
|
5740
|
+
console.log(chalk15.yellow(` \u26A0 Music file not found: ${config.music.path}`));
|
|
5741
|
+
console.log(chalk15.dim(` Config: ${join21(process.cwd(), CONFIG_FILE)}`));
|
|
5531
5742
|
console.log();
|
|
5532
5743
|
const proceed = await confirm(`${INDENT2}Continue without background music?`);
|
|
5533
5744
|
if (!proceed) {
|
|
5534
|
-
console.log(
|
|
5745
|
+
console.log(chalk15.yellow(`
|
|
5535
5746
|
Edit music.path in config and run again.`));
|
|
5536
|
-
console.log(
|
|
5747
|
+
console.log(chalk15.dim(` ${join21(process.cwd(), CONFIG_FILE)}
|
|
5537
5748
|
`));
|
|
5538
5749
|
process.exit(0);
|
|
5539
5750
|
}
|
|
@@ -5543,8 +5754,8 @@ async function recordCommand(url, options = {}) {
|
|
|
5543
5754
|
const captions = await loadCaptions();
|
|
5544
5755
|
const workDir = await prepareWorkDir();
|
|
5545
5756
|
const targetLang = config.lang || "en";
|
|
5546
|
-
const planPath =
|
|
5547
|
-
const legacyPlanPath =
|
|
5757
|
+
const planPath = join21(config.output.dir, `plan-${targetLang}.json`);
|
|
5758
|
+
const legacyPlanPath = join21(config.output.dir, "plan.json");
|
|
5548
5759
|
const isExtensionUrl = url.startsWith("chrome-extension://");
|
|
5549
5760
|
await mkdir8(config.output.dir, { recursive: true });
|
|
5550
5761
|
await setLogFilePath(config.output.dir);
|
|
@@ -5557,7 +5768,8 @@ async function recordCommand(url, options = {}) {
|
|
|
5557
5768
|
...description !== void 0 && { description },
|
|
5558
5769
|
planPath,
|
|
5559
5770
|
legacyPlanPath,
|
|
5560
|
-
targetLang
|
|
5771
|
+
targetLang,
|
|
5772
|
+
...fresh !== void 0 && { fresh }
|
|
5561
5773
|
});
|
|
5562
5774
|
const validatedPlan = await runValidationPhase({
|
|
5563
5775
|
plan,
|
|
@@ -5582,29 +5794,29 @@ async function recordCommand(url, options = {}) {
|
|
|
5582
5794
|
await saveUserData({ localVideoCount: newCount });
|
|
5583
5795
|
const remaining = FREE_VIDEO_LIMIT - newCount;
|
|
5584
5796
|
if (remaining > 0) {
|
|
5585
|
-
console.log(
|
|
5797
|
+
console.log(chalk15.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
|
|
5586
5798
|
}
|
|
5587
5799
|
}
|
|
5588
5800
|
console.log();
|
|
5589
|
-
console.log(
|
|
5801
|
+
console.log(chalk15.bold.green(" \u2713 Done!"));
|
|
5590
5802
|
console.log();
|
|
5591
|
-
console.log(
|
|
5803
|
+
console.log(chalk15.cyan(` ${formatClickablePath(finalPath)}`));
|
|
5592
5804
|
console.log();
|
|
5593
5805
|
return finalPath;
|
|
5594
5806
|
} catch (err) {
|
|
5595
5807
|
await logError("Record Command", err);
|
|
5596
5808
|
if (rethrow) throw err;
|
|
5597
5809
|
console.log();
|
|
5598
|
-
console.log(
|
|
5599
|
-
console.log(
|
|
5810
|
+
console.log(chalk15.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
|
|
5811
|
+
console.log(chalk15.dim(` See ${logFilePath} for details`));
|
|
5600
5812
|
process.exit(1);
|
|
5601
5813
|
}
|
|
5602
5814
|
}
|
|
5603
5815
|
|
|
5604
5816
|
// src/commands/script-cmd.ts
|
|
5605
5817
|
import { readFile as readFile8 } from "fs/promises";
|
|
5606
|
-
import { join as
|
|
5607
|
-
import
|
|
5818
|
+
import { join as join22 } from "path";
|
|
5819
|
+
import chalk16 from "chalk";
|
|
5608
5820
|
|
|
5609
5821
|
// src/ai/narrate.ts
|
|
5610
5822
|
init_lib();
|
|
@@ -5659,7 +5871,7 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
5659
5871
|
const raw = await readFile8(scriptPath, "utf-8");
|
|
5660
5872
|
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
5661
5873
|
if (lines.length === 0) {
|
|
5662
|
-
console.log(
|
|
5874
|
+
console.log(chalk16.red(" Script file is empty or has no non-comment lines"));
|
|
5663
5875
|
process.exit(1);
|
|
5664
5876
|
}
|
|
5665
5877
|
info(`${lines.length} captions from script`);
|
|
@@ -5671,7 +5883,7 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
5671
5883
|
const planTask = task("Mapping script to UI actions");
|
|
5672
5884
|
const appTitle = new URL(url).hostname.replace(/^www\./, "");
|
|
5673
5885
|
const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
|
|
5674
|
-
const planPath =
|
|
5886
|
+
const planPath = join22(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
5675
5887
|
await savePlan(plan, planPath);
|
|
5676
5888
|
planTask.succeed(`${plan.steps.length} steps planned`);
|
|
5677
5889
|
header("Voiceover");
|
|
@@ -5691,12 +5903,23 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
5691
5903
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
5692
5904
|
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, "free");
|
|
5693
5905
|
console.log();
|
|
5694
|
-
console.log(
|
|
5695
|
-
console.log(
|
|
5906
|
+
console.log(chalk16.bold.green(" Done!"));
|
|
5907
|
+
console.log(chalk16.cyan(` ${finalPath}`));
|
|
5696
5908
|
}
|
|
5697
5909
|
|
|
5698
5910
|
// src/mcp/tools.ts
|
|
5699
5911
|
init_SparkStripe_feature_gating();
|
|
5912
|
+
|
|
5913
|
+
// src/mcp/elicit.ts
|
|
5914
|
+
var elicit = null;
|
|
5915
|
+
function setElicit(fn) {
|
|
5916
|
+
elicit = fn;
|
|
5917
|
+
}
|
|
5918
|
+
async function askUser(message) {
|
|
5919
|
+
return elicit ? elicit(message) : null;
|
|
5920
|
+
}
|
|
5921
|
+
|
|
5922
|
+
// src/mcp/tools.ts
|
|
5700
5923
|
var URL_PROPERTY = {
|
|
5701
5924
|
url: {
|
|
5702
5925
|
type: "string",
|
|
@@ -5712,7 +5935,7 @@ var LANG_PROPERTY = {
|
|
|
5712
5935
|
var QUICKPEEK_TOOLS = [
|
|
5713
5936
|
{
|
|
5714
5937
|
name: "generate_demo",
|
|
5715
|
-
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. Pages behind a login can be demoed
|
|
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. Pages behind a login can be demoed once a session is saved for the host, by start_auth or by the user running `qp auth <url>`; it is picked up automatically. The result says whether this recording was signed in.",
|
|
5716
5939
|
inputSchema: {
|
|
5717
5940
|
type: "object",
|
|
5718
5941
|
properties: {
|
|
@@ -5750,6 +5973,29 @@ var QUICKPEEK_TOOLS = [
|
|
|
5750
5973
|
required: ["url", "script_path"]
|
|
5751
5974
|
}
|
|
5752
5975
|
},
|
|
5976
|
+
{
|
|
5977
|
+
name: "open_in_editor",
|
|
5978
|
+
description: "Open a recorded demo's plan in the browser editor, where the user can reword captions, reorder steps or drop them. The plan travels in the URL fragment, so nothing is uploaded; saving in the editor writes back over the same file and the next recording picks it up. Records nothing itself.",
|
|
5979
|
+
inputSchema: {
|
|
5980
|
+
type: "object",
|
|
5981
|
+
properties: { ...URL_PROPERTY, ...LANG_PROPERTY },
|
|
5982
|
+
required: ["url"]
|
|
5983
|
+
}
|
|
5984
|
+
},
|
|
5985
|
+
{
|
|
5986
|
+
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, so a demo can cover pages behind the login. 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
|
+
inputSchema: {
|
|
5989
|
+
type: "object",
|
|
5990
|
+
properties: { ...URL_PROPERTY },
|
|
5991
|
+
required: ["url"]
|
|
5992
|
+
}
|
|
5993
|
+
},
|
|
5994
|
+
{
|
|
5995
|
+
name: "finish_auth",
|
|
5996
|
+
description: "Save the session from the browser window start_auth opened, and close it. Call this once the user says they have signed in.",
|
|
5997
|
+
inputSchema: { type: "object", properties: {} }
|
|
5998
|
+
},
|
|
5753
5999
|
{
|
|
5754
6000
|
name: "list_demos",
|
|
5755
6001
|
description: "List demo videos already produced for a URL, newest first, with size and modified time. Cheap: reads the output directory, records nothing.",
|
|
@@ -5803,7 +6049,7 @@ function authStatus(url) {
|
|
|
5803
6049
|
return {
|
|
5804
6050
|
signedIn: false,
|
|
5805
6051
|
host,
|
|
5806
|
-
note: `No saved session for ${host}, so only pages reachable while logged out were recorded. To cover pages behind the login,
|
|
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.`
|
|
5807
6053
|
};
|
|
5808
6054
|
}
|
|
5809
6055
|
async function handleGenerateDemoFromScript(args) {
|
|
@@ -5817,9 +6063,56 @@ async function handleGenerateDemoFromScript(args) {
|
|
|
5817
6063
|
return fileResult(newest, { name: newest.split("/").pop(), url_recorded: url, scriptPath });
|
|
5818
6064
|
});
|
|
5819
6065
|
}
|
|
6066
|
+
async function handleOpenInEditor(args) {
|
|
6067
|
+
const url = resolveUrl(args.url);
|
|
6068
|
+
return withSilencedStdout(async () => {
|
|
6069
|
+
const dir = getOutputDirFromUrl(url);
|
|
6070
|
+
const planPath = findPlanFile(dir, args.lang);
|
|
6071
|
+
if (!planPath) {
|
|
6072
|
+
return errorContent(`No plan found in ${dir}. Record the demo first with generate_demo.`);
|
|
6073
|
+
}
|
|
6074
|
+
const parsed = parsePlanText(readFileSync6(planPath, "utf8"));
|
|
6075
|
+
if (!parsed.ok) return errorContent(`${planPath} cannot be opened: ${parsed.error}`);
|
|
6076
|
+
const base = editorBaseUrl();
|
|
6077
|
+
const editorUrl = planEditorUrl(base, parsed.plan);
|
|
6078
|
+
await openInBrowser(editorUrl ?? `${base}/edit`);
|
|
6079
|
+
return jsonContent({
|
|
6080
|
+
opened: true,
|
|
6081
|
+
plan_path: planPath,
|
|
6082
|
+
steps: parsed.plan.steps.length,
|
|
6083
|
+
url: editorUrl ?? `${base}/edit`,
|
|
6084
|
+
next_steps: editorUrl ? ["The editor opens with the plan loaded.", "Saving there writes back over plan_path; then re-record."] : [`This plan is too long for a link. Load ${planPath} in the editor by hand.`]
|
|
6085
|
+
});
|
|
6086
|
+
});
|
|
6087
|
+
}
|
|
6088
|
+
async function handleStartAuth(args) {
|
|
6089
|
+
const url = resolveUrl(args.url);
|
|
6090
|
+
return withSilencedStdout(async () => {
|
|
6091
|
+
await openSignIn(url);
|
|
6092
|
+
const confirmed = await askUser(`A browser window is open on ${url}. Sign in there, then confirm.`);
|
|
6093
|
+
if (confirmed === null) {
|
|
6094
|
+
return jsonContent({
|
|
6095
|
+
status: "awaiting_sign_in",
|
|
6096
|
+
url,
|
|
6097
|
+
next: "A browser window is open. Call finish_auth once the user says they are signed in."
|
|
6098
|
+
});
|
|
6099
|
+
}
|
|
6100
|
+
if (!confirmed) {
|
|
6101
|
+
await cancelSignIn();
|
|
6102
|
+
return jsonContent({ status: "cancelled", url, note: "Nothing was saved." });
|
|
6103
|
+
}
|
|
6104
|
+
return jsonContent({ status: "saved", ...await saveSignIn() });
|
|
6105
|
+
});
|
|
6106
|
+
}
|
|
6107
|
+
async function handleFinishAuth() {
|
|
6108
|
+
return withSilencedStdout(async () => {
|
|
6109
|
+
if (!pendingSignIn()) return errorContent("No sign-in window is open. Call start_auth first.");
|
|
6110
|
+
return jsonContent({ status: "saved", ...await saveSignIn() });
|
|
6111
|
+
});
|
|
6112
|
+
}
|
|
5820
6113
|
function newestVideo(dir) {
|
|
5821
6114
|
try {
|
|
5822
|
-
const videos =
|
|
6115
|
+
const videos = readdirSync5(dir).filter((f) => f.toLowerCase().endsWith(".mp4")).map((f) => join23(dir, f)).map((p) => ({ path: p, mtime: statSync2(p).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
5823
6116
|
return videos[0]?.path ?? null;
|
|
5824
6117
|
} catch {
|
|
5825
6118
|
return null;
|
|
@@ -5831,8 +6124,8 @@ async function handleListDemos(args) {
|
|
|
5831
6124
|
return withSilencedStdout(async () => {
|
|
5832
6125
|
let entries = [];
|
|
5833
6126
|
try {
|
|
5834
|
-
entries =
|
|
5835
|
-
const s = statSync2(
|
|
6127
|
+
entries = readdirSync5(dir).filter((f) => f.toLowerCase().endsWith(".mp4")).map((f) => {
|
|
6128
|
+
const s = statSync2(join23(dir, f));
|
|
5836
6129
|
return { name: f, bytes: s.size, modified: new Date(s.mtimeMs).toISOString() };
|
|
5837
6130
|
}).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
5838
6131
|
} catch {
|
|
@@ -5878,6 +6171,9 @@ async function handleCheckAccount() {
|
|
|
5878
6171
|
var QUICKPEEK_HANDLERS = {
|
|
5879
6172
|
generate_demo: handleGenerateDemo,
|
|
5880
6173
|
generate_demo_from_script: handleGenerateDemoFromScript,
|
|
6174
|
+
open_in_editor: handleOpenInEditor,
|
|
6175
|
+
start_auth: handleStartAuth,
|
|
6176
|
+
finish_auth: handleFinishAuth,
|
|
5881
6177
|
list_demos: handleListDemos,
|
|
5882
6178
|
get_demo_config: handleGetDemoConfig,
|
|
5883
6179
|
list_voices: handleListVoices,
|
|
@@ -5905,5 +6201,6 @@ export {
|
|
|
5905
6201
|
TOOLS,
|
|
5906
6202
|
TOOL_HANDLERS,
|
|
5907
6203
|
errorContent,
|
|
5908
|
-
jsonContent
|
|
6204
|
+
jsonContent,
|
|
6205
|
+
setElicit
|
|
5909
6206
|
};
|