@spark-apps/quickpeek 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -22
- package/dist/assets/music/manifest.json +1 -1
- package/dist/index.d.mts +49 -10
- package/dist/index.mjs +215 -143
- package/dist/mcp-tools.mjs +968 -431
- package/dist/qp.js +1014 -473
- package/dist/{voices-CPpnWn39.d.mts → voices-5AFt1GDp.d.mts} +80 -4
- package/dist/web.d.mts +2 -2
- package/dist/web.mjs +154 -20
- package/package.json +9 -2
|
@@ -3,7 +3,7 @@ import { Page } from 'playwright';
|
|
|
3
3
|
/**
|
|
4
4
|
* Configuration types and defaults for QuickPeek
|
|
5
5
|
*/
|
|
6
|
-
declare const VERSION = "1.2.
|
|
6
|
+
declare const VERSION = "1.2.6";
|
|
7
7
|
declare const CONFIG_FILE = "quickpeek.config.json";
|
|
8
8
|
type UserTier = 'free' | 'pro' | 'elite';
|
|
9
9
|
/** The one dimension knob users see: wide (16:9) or short (9:16). */
|
|
@@ -63,6 +63,12 @@ interface Config {
|
|
|
63
63
|
contrast: number;
|
|
64
64
|
tempo: number;
|
|
65
65
|
stepDelay: number;
|
|
66
|
+
watermark?: {
|
|
67
|
+
name: string;
|
|
68
|
+
iconPath?: string;
|
|
69
|
+
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
|
70
|
+
opacity?: number;
|
|
71
|
+
};
|
|
66
72
|
outro?: string;
|
|
67
73
|
outroCard?: {
|
|
68
74
|
/** The app the video is about - the card's headline. */
|
|
@@ -71,6 +77,8 @@ interface Config {
|
|
|
71
77
|
tagline?: string;
|
|
72
78
|
domain?: string;
|
|
73
79
|
iconPath?: string;
|
|
80
|
+
/** Play the mark rather than show a still of it. See OutroSubject. */
|
|
81
|
+
animatedMark?: 'vidlet';
|
|
74
82
|
};
|
|
75
83
|
/** Tools that made the video, shown small in the corner. */
|
|
76
84
|
credits?: Array<{
|
|
@@ -123,10 +131,25 @@ interface Config {
|
|
|
123
131
|
/**
|
|
124
132
|
* Which bundled CC0 bed to score with when no `path` is given.
|
|
125
133
|
*
|
|
126
|
-
* '
|
|
134
|
+
* 'corporate' | 'calm' | 'lofi' - see assets/music/manifest.json. An unknown
|
|
127
135
|
* mood falls back to the default rather than dropping the music.
|
|
128
136
|
*/
|
|
129
137
|
mood?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Hold the music back until the closing card.
|
|
140
|
+
*
|
|
141
|
+
* A track written FOR the product is not a bed: ducked under a narrator
|
|
142
|
+
* for a minute it is inaudible, and it is the thing people are meant to
|
|
143
|
+
* hear. With this on, the demo plays dry and the song fades in exactly as
|
|
144
|
+
* the closing card arrives, with the rest of the video silent underneath
|
|
145
|
+
* the voice.
|
|
146
|
+
*
|
|
147
|
+
* ON by default. Music under narration was the single most disliked thing
|
|
148
|
+
* about the output: ducked it is mud, unducked it fights the voice, and
|
|
149
|
+
* either way it makes a 17 second demo sound like a stock video. Set
|
|
150
|
+
* false in quickpeek.config.json to score the whole video.
|
|
151
|
+
*/
|
|
152
|
+
outroOnly?: boolean;
|
|
130
153
|
};
|
|
131
154
|
timing: {
|
|
132
155
|
minStepDuration: number;
|
|
@@ -257,6 +280,8 @@ interface AIError {
|
|
|
257
280
|
host?: string;
|
|
258
281
|
/** Raw response body (truncated), for the error log. */
|
|
259
282
|
detail?: string;
|
|
283
|
+
/** The call succeeded but its output was not a usable plan. */
|
|
284
|
+
isParseError?: boolean;
|
|
260
285
|
}
|
|
261
286
|
type AIResult = AIResponse | AIError;
|
|
262
287
|
/** Called before each backoff so the CLI can explain a long pause. */
|
|
@@ -299,6 +324,11 @@ declare const SERVER_BACKOFF_MS: number[];
|
|
|
299
324
|
* 16 Aug 2026). Only safe where a human is waiting on a spinner.
|
|
300
325
|
*/
|
|
301
326
|
declare const CLI_BACKOFF_MS: number[];
|
|
327
|
+
declare const RELAY_BASE: string;
|
|
328
|
+
/**
|
|
329
|
+
* Relay mode: exchange the signed-in email for a short-lived relay token (cached)
|
|
330
|
+
*/
|
|
331
|
+
declare function getOrRefreshRelayToken(email: string): Promise<string>;
|
|
302
332
|
/**
|
|
303
333
|
* Direct Spark AI call using the runtime SPARK_AI_KEY (dev + server-side use).
|
|
304
334
|
*/
|
|
@@ -310,7 +340,17 @@ declare function callAI(systemPrompt: string, userPrompt: string, tier?: UserTie
|
|
|
310
340
|
*/
|
|
311
341
|
declare function callAIViaRelay(systemPrompt: string, userPrompt: string, email: string, tier?: UserTier, opts?: ChatOptions): Promise<AIResult>;
|
|
312
342
|
/**
|
|
313
|
-
* Parse AI-generated plan JSON from response text
|
|
343
|
+
* Parse AI-generated plan JSON from response text.
|
|
344
|
+
*
|
|
345
|
+
* Delegates to parseAIJson, the same lenient path the CLI's plan step uses.
|
|
346
|
+
* This used to do its own `\{[\s\S]*\}` extraction, which fails on exactly the
|
|
347
|
+
* two things a model does under a token ceiling: stopping mid-array (the
|
|
348
|
+
* greedy match then ends at an inner brace and JSON.parse throws), and
|
|
349
|
+
* spending the whole budget on prose so no braces are emitted at all — the
|
|
350
|
+
* "No valid JSON found in AI response" the web wizard kept showing, where
|
|
351
|
+
* hitting Retry worked only because the next sample happened to be terser.
|
|
352
|
+
* The repair path recovers every complete step from a truncated plan, so a
|
|
353
|
+
* clipped response yields a usable plan instead of an error and a re-crawl.
|
|
314
354
|
*/
|
|
315
355
|
declare function parseAIPlanResponse(content: string): {
|
|
316
356
|
success: true;
|
|
@@ -329,6 +369,36 @@ declare function parseAIPlanResponse(content: string): {
|
|
|
329
369
|
success: false;
|
|
330
370
|
error: string;
|
|
331
371
|
};
|
|
372
|
+
/**
|
|
373
|
+
* Ask for a plan and parse it, resampling once if the answer is unusable.
|
|
374
|
+
*
|
|
375
|
+
* parseAIJson recovers a truncated plan, but it cannot recover the other
|
|
376
|
+
* failure mode: a model that spends its whole completion budget reasoning in
|
|
377
|
+
* prose and is cut off before writing a single brace. That is sampling luck,
|
|
378
|
+
* not a broken request — which is why the wizard's Retry button "fixed" it,
|
|
379
|
+
* and why the fix is to resample here rather than to hand the user a red box.
|
|
380
|
+
* Retrying in the route is strictly cheaper than the user retrying: their
|
|
381
|
+
* Retry re-runs the page crawl as well.
|
|
382
|
+
*
|
|
383
|
+
* Exactly one extra attempt. A model that produces prose twice is a prompt or
|
|
384
|
+
* backend problem, and a third round trip only spends the caller's serverless
|
|
385
|
+
* budget on the same answer.
|
|
386
|
+
*/
|
|
387
|
+
declare function callAIForPlan(systemPrompt: string, userPrompt: string, tier?: UserTier, opts?: ChatOptions): Promise<{
|
|
388
|
+
success: true;
|
|
389
|
+
plan: {
|
|
390
|
+
title?: string;
|
|
391
|
+
description?: string;
|
|
392
|
+
steps: Array<{
|
|
393
|
+
id?: number;
|
|
394
|
+
action?: string;
|
|
395
|
+
caption?: string;
|
|
396
|
+
target?: string;
|
|
397
|
+
value?: string;
|
|
398
|
+
}>;
|
|
399
|
+
};
|
|
400
|
+
limits: RateLimitInfo;
|
|
401
|
+
} | AIError>;
|
|
332
402
|
/**
|
|
333
403
|
* Normalize a URL by adding https:// and stripping www prefix
|
|
334
404
|
*/
|
|
@@ -373,6 +443,12 @@ interface Step {
|
|
|
373
443
|
highlight?: HighlightMode;
|
|
374
444
|
mediaFile?: string;
|
|
375
445
|
mediaType?: 'image' | 'video';
|
|
446
|
+
/**
|
|
447
|
+
* A small JPEG data URL of the file `value` names, attached only when a plan
|
|
448
|
+
* is handed to the browser editor, which cannot read a path on disk. Never
|
|
449
|
+
* written to a plan file; see plan-thumbs.ts.
|
|
450
|
+
*/
|
|
451
|
+
thumb?: string;
|
|
376
452
|
/**
|
|
377
453
|
* Speak this step's caption but do not burn it.
|
|
378
454
|
*
|
|
@@ -610,4 +686,4 @@ declare function hasVoice(lang: string): boolean;
|
|
|
610
686
|
*/
|
|
611
687
|
declare function voiceFor(lang: string, gender?: VoiceGender): string | null;
|
|
612
688
|
|
|
613
|
-
export {
|
|
689
|
+
export { getOrRefreshRelayToken as $, type AIError as A, type VideoSize as B, type CaptionStyle as C, DEFAULT_CAPTIONS as D, EXCLUDED_LINK_PATTERNS as E, type VoiceGender as F, applyProfile as G, type HighlightMode as H, INTERACTIVE_SELECTORS as I, ariaLabelSelector as J, buildSystemPrompt as K, buildUserPrompt as L, callAI as M, NO_CLIP_OUTRO as N, callAIForPlan as O, PLAN_MAX_TOKENS as P, callAIViaRelay as Q, RATE_FACTOR as R, SERVER_BACKOFF_MS as S, type Tier as T, type UserTier as U, type VoiceRate as V, crawlPage as W, deStock as X, defaultCaptionsFor as Y, getErrorMessage as Z, getLanguageName as _, type CaptionPosition as a, getStatus as a0, getStatusCached as a1, getTierByEmail as a2, gotoSettled as a3, hasTrialRemaining as a4, hasVoice as a5, idSelector as a6, internationalVoiceFor as a7, isCanceling as a8, isExcludedLink as a9, isPaid as aa, isVerified as ab, normalizeUrl as ac, parseAIPlanResponse as ad, parsePartial as ae, pricingUrl as af, rgbaToHex as ag, runPooled as ah, shouldSkipLink as ai, showPaywall as aj, stripLongDashes as ak, toTitleCase as al, voiceFor as am, waitForPlaceholdersGone as an, getTier as ao, type Config as b, type AIResponse as c, type AIResult as d, CLI_BACKOFF_MS as e, CONFIG_FILE as f, type CaptionPreset as g, type CaptionWordStyle as h, type ChatOptions as i, DEFAULT_CONFIG as j, type ElementInfo as k, type ElementType as l, INTERNATIONAL_VOICE as m, type Plan as n, RELAY_BASE as o, type RateLimitInfo as p, type RetryNotice as q, SIZE_PRESETS as r, type SparkStatus as s, type SparkSubscription as t, type SparkTrial as u, type Step as v, type SuggestedAction as w, VERSION as x, VIDEO_PROFILES as y, type VideoProfileName as z };
|
package/dist/web.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as Plan } from './voices-
|
|
2
|
-
export { A as AIError, c as AIResponse, d as AIResult, i as ChatOptions, E as EXCLUDED_LINK_PATTERNS, k as ElementInfo, l as ElementType, H as HighlightMode, I as INTERACTIVE_SELECTORS, m as INTERNATIONAL_VOICE, P as PLAN_MAX_TOKENS,
|
|
1
|
+
import { n as Plan } from './voices-5AFt1GDp.mjs';
|
|
2
|
+
export { A as AIError, c as AIResponse, d as AIResult, i as ChatOptions, E as EXCLUDED_LINK_PATTERNS, k as ElementInfo, l as ElementType, H as HighlightMode, I as INTERACTIVE_SELECTORS, m as INTERNATIONAL_VOICE, P as PLAN_MAX_TOKENS, p as RateLimitInfo, s as SparkStatus, t as SparkSubscription, u as SparkTrial, v as Step, w as SuggestedAction, T as Tier, K as buildSystemPrompt, L as buildUserPrompt, M as callAI, O as callAIForPlan, W as crawlPage, X as deStock, Z as getErrorMessage, _ as getLanguageName, a0 as getStatus, a1 as getStatusCached, ao as getTier, a2 as getTierByEmail, a4 as hasTrialRemaining, a5 as hasVoice, a7 as internationalVoiceFor, a8 as isCanceling, a9 as isExcludedLink, aa as isPaid, ab as isVerified, ac as normalizeUrl, ad as parseAIPlanResponse, ae as parsePartial, af as pricingUrl, ag as rgbaToHex, ah as runPooled, ai as shouldSkipLink, aj as showPaywall, ak as stripLongDashes, al as toTitleCase, am as voiceFor } from './voices-5AFt1GDp.mjs';
|
|
3
3
|
export { MALE_VOICES, MULTILINGUAL_VOICES, WIDE_VOICES, hexToAss as hexToASS, windowsDrivePathToWsl as windowsPathToWSL } from '@spark-apps/video-kit';
|
|
4
4
|
import 'playwright';
|
|
5
5
|
|
package/dist/web.mjs
CHANGED
|
@@ -1,3 +1,105 @@
|
|
|
1
|
+
// src/lib/json-repair.ts
|
|
2
|
+
function repairUnicodeEscapes(content) {
|
|
3
|
+
return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
|
|
4
|
+
const code = Number.parseInt(hex, 16);
|
|
5
|
+
return code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
6
|
+
}).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
|
|
7
|
+
}
|
|
8
|
+
var MAX_SALVAGE_ATTEMPTS = 500;
|
|
9
|
+
var CLOSERS = { "{": "}", "[": "]" };
|
|
10
|
+
function salvageTruncatedJson(content) {
|
|
11
|
+
const stack = [];
|
|
12
|
+
const cuts = [];
|
|
13
|
+
let inString = false;
|
|
14
|
+
let escaped = false;
|
|
15
|
+
const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
|
|
16
|
+
for (let i = 0; i < content.length; i++) {
|
|
17
|
+
const ch = content[i];
|
|
18
|
+
if (inString) {
|
|
19
|
+
if (escaped) escaped = false;
|
|
20
|
+
else if (ch === "\\") escaped = true;
|
|
21
|
+
else if (ch === '"') {
|
|
22
|
+
inString = false;
|
|
23
|
+
record(i + 1, false);
|
|
24
|
+
}
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (ch === '"') {
|
|
28
|
+
inString = true;
|
|
29
|
+
} else if (ch === "{" || ch === "[") {
|
|
30
|
+
stack.push(ch);
|
|
31
|
+
} else if (ch === "}" || ch === "]") {
|
|
32
|
+
stack.pop();
|
|
33
|
+
record(i + 1, true);
|
|
34
|
+
} else if (ch === ",") {
|
|
35
|
+
record(i, content[i - 1] === "}" || content[i - 1] === "]");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const tryCut = (cut) => {
|
|
39
|
+
const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
|
|
40
|
+
if (head.endsWith(":")) return null;
|
|
41
|
+
let closed = head;
|
|
42
|
+
for (let d = cut.open.length - 1; d >= 0; d--) {
|
|
43
|
+
closed += CLOSERS[cut.open[d]];
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
JSON.parse(closed);
|
|
47
|
+
return closed;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
|
|
53
|
+
for (const containerOnly of [true, false]) {
|
|
54
|
+
for (let n = 0; n < attempts; n++) {
|
|
55
|
+
const cut = cuts[cuts.length - 1 - n];
|
|
56
|
+
if (containerOnly && !cut.closesContainer) continue;
|
|
57
|
+
const salvaged = tryCut(cut);
|
|
58
|
+
if (salvaged !== null) return salvaged;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function extractJsonCandidates(content) {
|
|
64
|
+
let text = content.trim();
|
|
65
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
|
|
66
|
+
if (fence?.[1]?.trim()) text = fence[1].trim();
|
|
67
|
+
const start = text.search(/[{[]/);
|
|
68
|
+
if (start === -1) return [];
|
|
69
|
+
const closer = text[start] === "{" ? "}" : "]";
|
|
70
|
+
const end = text.lastIndexOf(closer);
|
|
71
|
+
const candidates = [text.slice(start)];
|
|
72
|
+
if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
|
|
73
|
+
return candidates;
|
|
74
|
+
}
|
|
75
|
+
function parseAIJson(content) {
|
|
76
|
+
const candidates = extractJsonCandidates(content);
|
|
77
|
+
if (candidates.length === 0) return null;
|
|
78
|
+
for (const text of candidates) {
|
|
79
|
+
try {
|
|
80
|
+
return { value: JSON.parse(text), repaired: false };
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const text of candidates) {
|
|
85
|
+
try {
|
|
86
|
+
return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const text of candidates) {
|
|
91
|
+
for (const variant of [repairUnicodeEscapes(text), text]) {
|
|
92
|
+
const salvaged = salvageTruncatedJson(variant);
|
|
93
|
+
if (salvaged === null) continue;
|
|
94
|
+
try {
|
|
95
|
+
return { value: JSON.parse(salvaged), repaired: true };
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
1
103
|
// src/lib/utils.ts
|
|
2
104
|
import { hexToAss } from "@spark-apps/video-kit";
|
|
3
105
|
import { windowsDrivePathToWsl } from "@spark-apps/video-kit";
|
|
@@ -276,24 +378,26 @@ async function callAI(systemPrompt, userPrompt, tier = "free", opts = {}) {
|
|
|
276
378
|
}
|
|
277
379
|
}
|
|
278
380
|
function parseAIPlanResponse(content) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
381
|
+
const parsed = parseAIJson(content);
|
|
382
|
+
if (!parsed) {
|
|
383
|
+
return { success: false, error: "No valid JSON found in AI response" };
|
|
384
|
+
}
|
|
385
|
+
if (!Array.isArray(parsed.value.steps) || parsed.value.steps.length === 0) {
|
|
386
|
+
return { success: false, error: "AI response has no steps" };
|
|
387
|
+
}
|
|
388
|
+
return { success: true, plan: { ...parsed.value, steps: parsed.value.steps } };
|
|
389
|
+
}
|
|
390
|
+
async function callAIForPlan(systemPrompt, userPrompt, tier = "free", opts = {}) {
|
|
391
|
+
let lastParseError = "No valid JSON found in AI response";
|
|
392
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
393
|
+
const aiResult = await callAI(systemPrompt, userPrompt, tier, opts);
|
|
394
|
+
if (!aiResult.success) return aiResult;
|
|
395
|
+
const parsed = parseAIPlanResponse(aiResult.content);
|
|
396
|
+
if (parsed.success) return { success: true, plan: parsed.plan, limits: aiResult.limits };
|
|
397
|
+
lastParseError = parsed.error;
|
|
398
|
+
console.error(`Plan parse failed (attempt ${attempt}/2):`, parsed.error);
|
|
296
399
|
}
|
|
400
|
+
return { success: false, error: lastParseError, isParseError: true };
|
|
297
401
|
}
|
|
298
402
|
function normalizeUrl(input) {
|
|
299
403
|
let url = input.trim();
|
|
@@ -301,8 +405,24 @@ function normalizeUrl(input) {
|
|
|
301
405
|
return `https://${url}`;
|
|
302
406
|
}
|
|
303
407
|
|
|
304
|
-
// src/
|
|
408
|
+
// src/browser/headless.ts
|
|
305
409
|
import { chromium } from "playwright";
|
|
410
|
+
var NO_BROWSER_MESSAGE = "QuickPeek needs a browser to record and could not find one.\n Run this once, then run QuickPeek again:\n npx playwright install chromium";
|
|
411
|
+
async function launchHeadlessChromium(options = {}) {
|
|
412
|
+
const opts = { ...options, headless: true };
|
|
413
|
+
try {
|
|
414
|
+
return await chromium.launch(opts);
|
|
415
|
+
} catch (bundledError) {
|
|
416
|
+
try {
|
|
417
|
+
return await chromium.launch({ ...opts, channel: "chrome" });
|
|
418
|
+
} catch {
|
|
419
|
+
const detail = bundledError instanceof Error ? bundledError.message : String(bundledError);
|
|
420
|
+
throw new Error(`${NO_BROWSER_MESSAGE}
|
|
421
|
+
|
|
422
|
+
Playwright reported: ${detail}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
306
426
|
|
|
307
427
|
// src/lib/scraping.ts
|
|
308
428
|
var EXCLUDED_LINK_PATTERNS = [
|
|
@@ -423,6 +543,7 @@ async function waitForDomStable(page, cap = SETTLE_CAP_MS) {
|
|
|
423
543
|
var PLACEHOLDER_POLL_MS = 250;
|
|
424
544
|
var PLACEHOLDER_CAP_MS = 8e3;
|
|
425
545
|
var PLACEHOLDER_STATIC_POLLS = 4;
|
|
546
|
+
var PLACEHOLDER_CLEAR_POLLS = 3;
|
|
426
547
|
var PLACEHOLDER_SELECTOR = [
|
|
427
548
|
'[aria-busy="true"]',
|
|
428
549
|
'[role="progressbar"]',
|
|
@@ -435,6 +556,7 @@ async function waitForPlaceholdersGone(page, cap = PLACEHOLDER_CAP_MS) {
|
|
|
435
556
|
const deadline = Date.now() + cap;
|
|
436
557
|
let previous = -1;
|
|
437
558
|
let unchanged = 0;
|
|
559
|
+
let clear = 0;
|
|
438
560
|
while (Date.now() < deadline) {
|
|
439
561
|
const count = await page.evaluate((selector) => {
|
|
440
562
|
let visible = 0;
|
|
@@ -444,7 +566,12 @@ async function waitForPlaceholdersGone(page, cap = PLACEHOLDER_CAP_MS) {
|
|
|
444
566
|
}
|
|
445
567
|
return visible;
|
|
446
568
|
}, PLACEHOLDER_SELECTOR).catch(() => 0);
|
|
447
|
-
if (count === 0)
|
|
569
|
+
if (count === 0) {
|
|
570
|
+
if (++clear >= PLACEHOLDER_CLEAR_POLLS) return;
|
|
571
|
+
await page.waitForTimeout(PLACEHOLDER_POLL_MS);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
clear = 0;
|
|
448
575
|
if (count === previous) {
|
|
449
576
|
if (++unchanged >= PLACEHOLDER_STATIC_POLLS) return;
|
|
450
577
|
} else {
|
|
@@ -530,7 +657,13 @@ async function crawlPage(url, task = noopTask, storageStatePath, viewport) {
|
|
|
530
657
|
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
531
658
|
}
|
|
532
659
|
const browserTask = task("Launching browser");
|
|
533
|
-
|
|
660
|
+
let browser;
|
|
661
|
+
try {
|
|
662
|
+
browser = await launchHeadlessChromium();
|
|
663
|
+
} catch (err) {
|
|
664
|
+
browserTask.succeed("Could not launch a browser");
|
|
665
|
+
throw err;
|
|
666
|
+
}
|
|
534
667
|
try {
|
|
535
668
|
const context = await browser.newContext({
|
|
536
669
|
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
@@ -1307,6 +1440,7 @@ export {
|
|
|
1307
1440
|
buildSystemPrompt,
|
|
1308
1441
|
buildUserPrompt,
|
|
1309
1442
|
callAI,
|
|
1443
|
+
callAIForPlan,
|
|
1310
1444
|
collectPreservedValues,
|
|
1311
1445
|
crawlPage,
|
|
1312
1446
|
deStock,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spark-apps/quickpeek",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "Auto-generate demo videos for your web app with AI",
|
|
5
5
|
"mcpName": "co.quickpeek/quickpeek",
|
|
6
6
|
"main": "dist/qp.js",
|
|
@@ -48,8 +48,15 @@
|
|
|
48
48
|
"voiceover",
|
|
49
49
|
"automation",
|
|
50
50
|
"cli",
|
|
51
|
-
"documentation"
|
|
51
|
+
"documentation",
|
|
52
|
+
"mcp",
|
|
53
|
+
"model-context-protocol"
|
|
52
54
|
],
|
|
55
|
+
"homepage": "https://quickpeek.co",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://quickpeek.co/docs",
|
|
58
|
+
"email": "support@quickpeek.co"
|
|
59
|
+
},
|
|
53
60
|
"license": "MIT",
|
|
54
61
|
"dependencies": {
|
|
55
62
|
"@modelcontextprotocol/sdk": "^1.29.0",
|