@spark-apps/quickpeek 1.2.5 → 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 CHANGED
@@ -1,19 +1,15 @@
1
- [license-link]: ./LICENSE
2
- [privacy-link]: ./PRIVACY.md
3
- [discord-link]: https://discord.gg/mAjHWE3mSp
4
1
  [coffee-link]: https://buymeacoffee.com/spark88
5
2
 
6
3
  <div align="center">
7
4
 
8
- <img src=imgs/logo.png>
5
+ <img src="https://quickpeek.co/logo.png" alt="QuickPeek" width="220">
9
6
 
10
7
  **Demo videos for your web app with AI**
11
8
 
12
9
  *Because Tutorial Videos are Overrated!*
13
10
 
14
11
  [![npm](https://img.shields.io/npm/v/@spark-apps/quickpeek?style=flat-square&logo=npm&logoColor=white&color=crimson)](https://www.npmjs.com/package/@spark-apps/quickpeek)
15
- [![MIT](https://img.shields.io/badge/License-MIT-blueviolet?style=flat-square)][license-link]
16
- [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?style=flat-square&logo=discord&logoColor=white)][discord-link]
12
+ ![MIT](https://img.shields.io/badge/License-MIT-blueviolet?style=flat-square)
17
13
 
18
14
  [![Listed on Sellular](https://sellular.online/badge/quickpeek.svg)](https://sellular.online/badge/quickpeek/go)
19
15
 
@@ -46,8 +42,7 @@ AI planner runs on our servers and is metered per account; over
46
42
  writing. A free video of 20 seconds or under comes out clean; a longer free video
47
43
  carries a QuickPeek watermark. The hosted web recorder at quickpeek.co is
48
44
  deliberately different: there, free videos are capped at 20 seconds and are
49
- always watermarked. [docs/plans-and-pricing.md](docs/plans-and-pricing.md) sets
50
- the two side by side.
45
+ always watermarked. https://quickpeek.co/pricing sets the two side by side.
51
46
 
52
47
  ## Features
53
48
 
@@ -139,7 +134,7 @@ Config is auto-generated on first run. Edit `quickpeek.config.json` to customize
139
134
  | `pt` | Portuguese | `hi` | Hindi |
140
135
  | `it` | Italian | `ru` | Russian |
141
136
 
142
- See `src/voices.ts` for all 60+ supported languages.
137
+ The full list comes back from the `list_voices` tool on the MCP server.
143
138
 
144
139
  </details>
145
140
 
@@ -148,8 +143,6 @@ See `src/voices.ts` for all 60+ supported languages.
148
143
 
149
144
  Edit `captions.css` (auto-generated) to customize:
150
145
 
151
- ![Classic Yellow](imgs/caption-classic.png)
152
-
153
146
  ```css
154
147
  .caption {
155
148
  font-family: Arial, sans-serif;
@@ -200,7 +193,7 @@ QuickPeek ships an MCP server (`quickpeek-mcp`) so an AI agent can record a demo
200
193
  directly, with no shelling out to the CLI.
201
194
 
202
195
  ```bash
203
- claude mcp add quickpeek -- npx -y --package=@spark-apps/quickpeek quickpeek-mcp
196
+ claude mcp add quickpeek -- npx -y --ignore-scripts --package=@spark-apps/quickpeek quickpeek-mcp
204
197
  ```
205
198
 
206
199
  Or add it by hand, for any MCP client:
@@ -210,7 +203,7 @@ Or add it by hand, for any MCP client:
210
203
  "mcpServers": {
211
204
  "quickpeek": {
212
205
  "command": "npx",
213
- "args": ["-y", "--package=@spark-apps/quickpeek", "quickpeek-mcp"]
206
+ "args": ["-y", "--ignore-scripts", "--package=@spark-apps/quickpeek", "quickpeek-mcp"]
214
207
  }
215
208
  }
216
209
  }
@@ -244,7 +237,7 @@ needs no account at all for the first 5 videos, where the CLI asks for an email.
244
237
  ☕ **Devs run on coffee** - [Buy me one?][coffee-link]
245
238
  💰 **Crypto tips welcome** - [Tip in crypto](https://tip.md/muammar-yacoob)
246
239
  🎬 **See it work** - [Watch a demo](https://www.youtube.com/shorts/HaibrNvJknY)
247
- 🐛 **Found a bug or want a feature?** <img src="https://img.icons8.com/color/20/discord--v2.png" alt="Discord" width="20" height="20" style="vertical-align: middle;"> [Join Discord][discord-link] - the repo is private, so Discord is the way in.
240
+ 🐛 **Found a bug or want a feature?** Email [support@quickpeek.co](mailto:support@quickpeek.co) - the repo is private, so email is the way in.
248
241
 
249
242
  <div align="center">
250
243
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { U as UserTier, C as CaptionStyle, a as CaptionPosition, b as Config, V as VoiceRate } from './voices-ufga0oOn.mjs';
2
- export { A as AIError, c as AIResponse, d as AIResult, e as CLI_BACKOFF_MS, f as CONFIG_FILE, g as CaptionPreset, h as CaptionWordStyle, i as ChatOptions, D as DEFAULT_CAPTIONS, j as DEFAULT_CONFIG, E as EXCLUDED_LINK_PATTERNS, k as ElementInfo, l as ElementType, H as HighlightMode, I as INTERACTIVE_SELECTORS, m as INTERNATIONAL_VOICE, N as NO_CLIP_OUTRO, P as PLAN_MAX_TOKENS, n as Plan, R as RATE_FACTOR, o as RELAY_BASE, p as RateLimitInfo, q as RetryNotice, S as SERVER_BACKOFF_MS, r as SIZE_PRESETS, s as SparkStatus, t as SparkSubscription, u as SparkTrial, v as Step, w as SuggestedAction, T as Tier, x as VERSION, y as VIDEO_PROFILES, z as VideoProfileName, B as VideoSize, F as VoiceGender, G as applyProfile, J as ariaLabelSelector, K as buildSystemPrompt, L as buildUserPrompt, M as callAI, O as callAIViaRelay, Q as crawlPage, W as deStock, X as defaultCaptionsFor, Y as getErrorMessage, Z as getLanguageName, _ as getOrRefreshRelayToken, $ as getStatus, a0 as getStatusCached, a1 as getTierByEmail, a2 as gotoSettled, a3 as hasTrialRemaining, a4 as hasVoice, a5 as idSelector, a6 as internationalVoiceFor, a7 as isCanceling, a8 as isExcludedLink, a9 as isPaid, aa as isVerified, ab as normalizeUrl, ac as parseAIPlanResponse, ad as parsePartial, ae as pricingUrl, af as rgbaToHex, ag as runPooled, ah as shouldSkipLink, ai as showPaywall, aj as stripLongDashes, ak as toTitleCase, al as voiceFor, am as waitForPlaceholdersGone } from './voices-ufga0oOn.mjs';
1
+ import { U as UserTier, C as CaptionStyle, a as CaptionPosition, b as Config, V as VoiceRate } from './voices-5AFt1GDp.mjs';
2
+ export { A as AIError, c as AIResponse, d as AIResult, e as CLI_BACKOFF_MS, f as CONFIG_FILE, g as CaptionPreset, h as CaptionWordStyle, i as ChatOptions, D as DEFAULT_CAPTIONS, j as DEFAULT_CONFIG, E as EXCLUDED_LINK_PATTERNS, k as ElementInfo, l as ElementType, H as HighlightMode, I as INTERACTIVE_SELECTORS, m as INTERNATIONAL_VOICE, N as NO_CLIP_OUTRO, P as PLAN_MAX_TOKENS, n as Plan, R as RATE_FACTOR, o as RELAY_BASE, p as RateLimitInfo, q as RetryNotice, S as SERVER_BACKOFF_MS, r as SIZE_PRESETS, s as SparkStatus, t as SparkSubscription, u as SparkTrial, v as Step, w as SuggestedAction, T as Tier, x as VERSION, y as VIDEO_PROFILES, z as VideoProfileName, B as VideoSize, F as VoiceGender, G as applyProfile, J as ariaLabelSelector, K as buildSystemPrompt, L as buildUserPrompt, M as callAI, O as callAIForPlan, Q as callAIViaRelay, W as crawlPage, X as deStock, Y as defaultCaptionsFor, Z as getErrorMessage, _ as getLanguageName, $ as getOrRefreshRelayToken, a0 as getStatus, a1 as getStatusCached, a2 as getTierByEmail, a3 as gotoSettled, a4 as hasTrialRemaining, a5 as hasVoice, a6 as idSelector, 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, an as waitForPlaceholdersGone } 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/index.mjs CHANGED
@@ -1863,6 +1863,108 @@ var init_timing = __esm({
1863
1863
  }
1864
1864
  });
1865
1865
 
1866
+ // src/lib/json-repair.ts
1867
+ function repairUnicodeEscapes(content) {
1868
+ return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
1869
+ const code = Number.parseInt(hex, 16);
1870
+ return code <= 1114111 ? String.fromCodePoint(code) : match;
1871
+ }).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
1872
+ }
1873
+ var MAX_SALVAGE_ATTEMPTS = 500;
1874
+ var CLOSERS = { "{": "}", "[": "]" };
1875
+ function salvageTruncatedJson(content) {
1876
+ const stack = [];
1877
+ const cuts = [];
1878
+ let inString = false;
1879
+ let escaped = false;
1880
+ const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
1881
+ for (let i = 0; i < content.length; i++) {
1882
+ const ch = content[i];
1883
+ if (inString) {
1884
+ if (escaped) escaped = false;
1885
+ else if (ch === "\\") escaped = true;
1886
+ else if (ch === '"') {
1887
+ inString = false;
1888
+ record(i + 1, false);
1889
+ }
1890
+ continue;
1891
+ }
1892
+ if (ch === '"') {
1893
+ inString = true;
1894
+ } else if (ch === "{" || ch === "[") {
1895
+ stack.push(ch);
1896
+ } else if (ch === "}" || ch === "]") {
1897
+ stack.pop();
1898
+ record(i + 1, true);
1899
+ } else if (ch === ",") {
1900
+ record(i, content[i - 1] === "}" || content[i - 1] === "]");
1901
+ }
1902
+ }
1903
+ const tryCut = (cut) => {
1904
+ const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
1905
+ if (head.endsWith(":")) return null;
1906
+ let closed = head;
1907
+ for (let d = cut.open.length - 1; d >= 0; d--) {
1908
+ closed += CLOSERS[cut.open[d]];
1909
+ }
1910
+ try {
1911
+ JSON.parse(closed);
1912
+ return closed;
1913
+ } catch {
1914
+ return null;
1915
+ }
1916
+ };
1917
+ const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
1918
+ for (const containerOnly of [true, false]) {
1919
+ for (let n = 0; n < attempts; n++) {
1920
+ const cut = cuts[cuts.length - 1 - n];
1921
+ if (containerOnly && !cut.closesContainer) continue;
1922
+ const salvaged = tryCut(cut);
1923
+ if (salvaged !== null) return salvaged;
1924
+ }
1925
+ }
1926
+ return null;
1927
+ }
1928
+ function extractJsonCandidates(content) {
1929
+ let text = content.trim();
1930
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
1931
+ if (fence?.[1]?.trim()) text = fence[1].trim();
1932
+ const start = text.search(/[{[]/);
1933
+ if (start === -1) return [];
1934
+ const closer = text[start] === "{" ? "}" : "]";
1935
+ const end = text.lastIndexOf(closer);
1936
+ const candidates = [text.slice(start)];
1937
+ if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
1938
+ return candidates;
1939
+ }
1940
+ function parseAIJson(content) {
1941
+ const candidates = extractJsonCandidates(content);
1942
+ if (candidates.length === 0) return null;
1943
+ for (const text of candidates) {
1944
+ try {
1945
+ return { value: JSON.parse(text), repaired: false };
1946
+ } catch {
1947
+ }
1948
+ }
1949
+ for (const text of candidates) {
1950
+ try {
1951
+ return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
1952
+ } catch {
1953
+ }
1954
+ }
1955
+ for (const text of candidates) {
1956
+ for (const variant of [repairUnicodeEscapes(text), text]) {
1957
+ const salvaged = salvageTruncatedJson(variant);
1958
+ if (salvaged === null) continue;
1959
+ try {
1960
+ return { value: JSON.parse(salvaged), repaired: true };
1961
+ } catch {
1962
+ }
1963
+ }
1964
+ }
1965
+ return null;
1966
+ }
1967
+
1866
1968
  // src/lib/api-client.ts
1867
1969
  init_utils();
1868
1970
  var PLAN_MAX_TOKENS = 1500;
@@ -2073,24 +2175,26 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", op
2073
2175
  }
2074
2176
  }
2075
2177
  function parseAIPlanResponse(content) {
2076
- try {
2077
- let processedContent = content;
2078
- const codeBlockMatch = processedContent.match(/```(?:json)?\s*([\s\S]*?)```/);
2079
- if (codeBlockMatch?.[1]) {
2080
- processedContent = codeBlockMatch[1].trim();
2081
- }
2082
- const jsonMatch = processedContent.match(/\{[\s\S]*\}/);
2083
- if (!jsonMatch) {
2084
- return { success: false, error: "No valid JSON found in AI response" };
2085
- }
2086
- const parsed = JSON.parse(jsonMatch[0]);
2087
- return { success: true, plan: parsed };
2088
- } catch (err) {
2089
- return {
2090
- success: false,
2091
- error: `Failed to parse AI response: ${err instanceof Error ? err.message : String(err)}`
2092
- };
2178
+ const parsed = parseAIJson(content);
2179
+ if (!parsed) {
2180
+ return { success: false, error: "No valid JSON found in AI response" };
2181
+ }
2182
+ if (!Array.isArray(parsed.value.steps) || parsed.value.steps.length === 0) {
2183
+ return { success: false, error: "AI response has no steps" };
2093
2184
  }
2185
+ return { success: true, plan: { ...parsed.value, steps: parsed.value.steps } };
2186
+ }
2187
+ async function callAIForPlan(systemPrompt, userPrompt, tier = "free", opts = {}) {
2188
+ let lastParseError = "No valid JSON found in AI response";
2189
+ for (let attempt = 1; attempt <= 2; attempt++) {
2190
+ const aiResult = await callAI(systemPrompt, userPrompt, tier, opts);
2191
+ if (!aiResult.success) return aiResult;
2192
+ const parsed = parseAIPlanResponse(aiResult.content);
2193
+ if (parsed.success) return { success: true, plan: parsed.plan, limits: aiResult.limits };
2194
+ lastParseError = parsed.error;
2195
+ console.error(`Plan parse failed (attempt ${attempt}/2):`, parsed.error);
2196
+ }
2197
+ return { success: false, error: lastParseError, isParseError: true };
2094
2198
  }
2095
2199
  function normalizeUrl(input) {
2096
2200
  let url = input.trim();
@@ -2174,7 +2278,7 @@ function getUpgradeUrl(userEmail) {
2174
2278
  }
2175
2279
 
2176
2280
  // src/lib/config.ts
2177
- var VERSION = "1.2.5";
2281
+ var VERSION = "1.2.6";
2178
2282
  var CONFIG_FILE = "quickpeek.config.json";
2179
2283
  var SIZE_PRESETS = {
2180
2284
  wide: { width: 1920, height: 1080, zoom: 1 },
@@ -2311,8 +2415,24 @@ function defaultCaptionsFor(width, height) {
2311
2415
  };
2312
2416
  }
2313
2417
 
2314
- // src/lib/crawl.ts
2418
+ // src/browser/headless.ts
2315
2419
  import { chromium } from "playwright";
2420
+ 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";
2421
+ async function launchHeadlessChromium(options = {}) {
2422
+ const opts = { ...options, headless: true };
2423
+ try {
2424
+ return await chromium.launch(opts);
2425
+ } catch (bundledError) {
2426
+ try {
2427
+ return await chromium.launch({ ...opts, channel: "chrome" });
2428
+ } catch {
2429
+ const detail = bundledError instanceof Error ? bundledError.message : String(bundledError);
2430
+ throw new Error(`${NO_BROWSER_MESSAGE}
2431
+
2432
+ Playwright reported: ${detail}`);
2433
+ }
2434
+ }
2435
+ }
2316
2436
 
2317
2437
  // src/lib/scraping.ts
2318
2438
  var EXCLUDED_LINK_PATTERNS = [
@@ -2550,7 +2670,13 @@ async function crawlPage(url, task = noopTask, storageStatePath, viewport) {
2550
2670
  return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
2551
2671
  }
2552
2672
  const browserTask = task("Launching browser");
2553
- const browser = await chromium.launch({ headless: true });
2673
+ let browser;
2674
+ try {
2675
+ browser = await launchHeadlessChromium();
2676
+ } catch (err) {
2677
+ browserTask.succeed("Could not launch a browser");
2678
+ throw err;
2679
+ }
2554
2680
  try {
2555
2681
  const context = await browser.newContext({
2556
2682
  ...storageStatePath ? { storageState: storageStatePath } : {},
@@ -2733,108 +2859,6 @@ var CURSOR_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAEsBAMAAAB0
2733
2859
  // src/lib/index.ts
2734
2860
  init_ffmpeg2();
2735
2861
 
2736
- // src/lib/json-repair.ts
2737
- function repairUnicodeEscapes(content) {
2738
- return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
2739
- const code = Number.parseInt(hex, 16);
2740
- return code <= 1114111 ? String.fromCodePoint(code) : match;
2741
- }).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
2742
- }
2743
- var MAX_SALVAGE_ATTEMPTS = 500;
2744
- var CLOSERS = { "{": "}", "[": "]" };
2745
- function salvageTruncatedJson(content) {
2746
- const stack = [];
2747
- const cuts = [];
2748
- let inString = false;
2749
- let escaped = false;
2750
- const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
2751
- for (let i = 0; i < content.length; i++) {
2752
- const ch = content[i];
2753
- if (inString) {
2754
- if (escaped) escaped = false;
2755
- else if (ch === "\\") escaped = true;
2756
- else if (ch === '"') {
2757
- inString = false;
2758
- record(i + 1, false);
2759
- }
2760
- continue;
2761
- }
2762
- if (ch === '"') {
2763
- inString = true;
2764
- } else if (ch === "{" || ch === "[") {
2765
- stack.push(ch);
2766
- } else if (ch === "}" || ch === "]") {
2767
- stack.pop();
2768
- record(i + 1, true);
2769
- } else if (ch === ",") {
2770
- record(i, content[i - 1] === "}" || content[i - 1] === "]");
2771
- }
2772
- }
2773
- const tryCut = (cut) => {
2774
- const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
2775
- if (head.endsWith(":")) return null;
2776
- let closed = head;
2777
- for (let d = cut.open.length - 1; d >= 0; d--) {
2778
- closed += CLOSERS[cut.open[d]];
2779
- }
2780
- try {
2781
- JSON.parse(closed);
2782
- return closed;
2783
- } catch {
2784
- return null;
2785
- }
2786
- };
2787
- const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
2788
- for (const containerOnly of [true, false]) {
2789
- for (let n = 0; n < attempts; n++) {
2790
- const cut = cuts[cuts.length - 1 - n];
2791
- if (containerOnly && !cut.closesContainer) continue;
2792
- const salvaged = tryCut(cut);
2793
- if (salvaged !== null) return salvaged;
2794
- }
2795
- }
2796
- return null;
2797
- }
2798
- function extractJsonCandidates(content) {
2799
- let text = content.trim();
2800
- const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
2801
- if (fence?.[1]?.trim()) text = fence[1].trim();
2802
- const start = text.search(/[{[]/);
2803
- if (start === -1) return [];
2804
- const closer = text[start] === "{" ? "}" : "]";
2805
- const end = text.lastIndexOf(closer);
2806
- const candidates = [text.slice(start)];
2807
- if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
2808
- return candidates;
2809
- }
2810
- function parseAIJson(content) {
2811
- const candidates = extractJsonCandidates(content);
2812
- if (candidates.length === 0) return null;
2813
- for (const text of candidates) {
2814
- try {
2815
- return { value: JSON.parse(text), repaired: false };
2816
- } catch {
2817
- }
2818
- }
2819
- for (const text of candidates) {
2820
- try {
2821
- return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
2822
- } catch {
2823
- }
2824
- }
2825
- for (const text of candidates) {
2826
- for (const variant of [repairUnicodeEscapes(text), text]) {
2827
- const salvaged = salvageTruncatedJson(variant);
2828
- if (salvaged === null) continue;
2829
- try {
2830
- return { value: JSON.parse(salvaged), repaired: true };
2831
- } catch {
2832
- }
2833
- }
2834
- }
2835
- return null;
2836
- }
2837
-
2838
2862
  // src/lib/prompts.ts
2839
2863
  init_utils();
2840
2864
  function buildSystemPrompt(maxSteps, profile) {
@@ -3925,6 +3949,7 @@ export {
3925
3949
  buildUserPrompt,
3926
3950
  buildWatermarkFilters,
3927
3951
  callAI,
3952
+ callAIForPlan,
3928
3953
  callAIViaRelay,
3929
3954
  canReuseVoiceover,
3930
3955
  captionCanvas,