@spark-apps/quickpeek 1.1.3 → 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/web.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  // src/lib/utils.ts
2
+ import { hexToAss } from "@spark-apps/video-kit";
3
+ import { windowsDrivePathToWsl } from "@spark-apps/video-kit";
2
4
  var LANGUAGE_NAMES = {
3
5
  ar: "Arabic",
4
6
  es: "Spanish",
@@ -17,14 +19,6 @@ function getLanguageName(lang) {
17
19
  if (!lang || lang === "en") return null;
18
20
  return LANGUAGE_NAMES[lang] || lang.toUpperCase();
19
21
  }
20
- function hexToASS(hex, alpha = 0) {
21
- const clean = hex.replace("#", "");
22
- const r = clean.slice(0, 2);
23
- const g = clean.slice(2, 4);
24
- const b = clean.slice(4, 6);
25
- const a = alpha.toString(16).padStart(2, "0").toUpperCase();
26
- return `&H${a}${b}${g}${r}`.toUpperCase();
27
- }
28
22
  function rgbaToHex(color) {
29
23
  if (color.startsWith("#")) return color;
30
24
  const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
@@ -66,12 +60,6 @@ var LOWERCASE_WORDS = /* @__PURE__ */ new Set([
66
60
  "under",
67
61
  "than"
68
62
  ]);
69
- function windowsPathToWSL(path) {
70
- const driveMatch = /^([A-Z]):[/\\]/i.exec(path);
71
- if (!driveMatch) return null;
72
- const drive = driveMatch[1].toLowerCase();
73
- return `/mnt/${drive}/${path.slice(3).replace(/\\/g, "/")}`;
74
- }
75
63
  function parsePartial(partial) {
76
64
  if (!partial) return {};
77
65
  const parts = partial.split("-");
@@ -87,7 +75,7 @@ function getErrorMessage(err) {
87
75
  return err instanceof Error ? err.message : String(err);
88
76
  }
89
77
  function toTitleCase(text) {
90
- return text.replace(/\w+/g, (word, index) => {
78
+ return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
91
79
  if (index === 0) {
92
80
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
93
81
  }
@@ -102,23 +90,31 @@ function toTitleCase(text) {
102
90
  var SPARK_AI_BASE = "https://sparkbrain.app";
103
91
  var SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
104
92
  var SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
105
- var API_KEY = process.env.SPARK_AI_KEY || "";
106
- var cachedToken = null;
107
- async function getOrRefreshToken() {
93
+ var RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
94
+ var RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
95
+ var RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
96
+ function apiKey() {
97
+ return process.env.SPARK_AI_KEY || "";
98
+ }
99
+ var cachedDirectToken = null;
100
+ async function getOrRefreshDirectToken() {
108
101
  const now = Date.now();
109
- if (cachedToken && cachedToken.expiresAt > now + 3e5) {
110
- return cachedToken.token;
102
+ if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
103
+ return cachedDirectToken.token;
111
104
  }
112
105
  try {
113
106
  const response = await fetch(SPARK_AI_TOKEN, {
114
107
  method: "POST",
115
- headers: { "X-Api-Key": API_KEY }
108
+ headers: { "X-Api-Key": apiKey() }
116
109
  });
110
+ if (!response.ok) {
111
+ throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
112
+ }
117
113
  const data = await response.json();
118
- if (!response.ok || !data.success || !data.token) {
119
- throw new Error(data.error || `Token exchange failed: ${response.status}`);
114
+ if (!data.success || !data.token) {
115
+ throw new Error(data.error || `Token exchange failed: unexpected response`);
120
116
  }
121
- cachedToken = {
117
+ cachedDirectToken = {
122
118
  token: data.token,
123
119
  expiresAt: data.expiresAt || now + 36e5
124
120
  };
@@ -127,77 +123,84 @@ async function getOrRefreshToken() {
127
123
  throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
128
124
  }
129
125
  }
130
- async function callAI(systemPrompt, userPrompt, tier = "free") {
131
- if (!API_KEY) {
132
- return {
133
- success: false,
134
- error: "AI service is not configured. Please report this issue.",
135
- isAuthError: true
136
- };
137
- }
138
- try {
139
- const token = await getOrRefreshToken();
140
- let lastRateLimitError = null;
141
- for (let attempt = 1; attempt <= 3; attempt++) {
142
- try {
143
- const response = await fetch(SPARK_AI_CHAT, {
144
- method: "POST",
145
- headers: {
146
- "Content-Type": "application/json",
147
- Authorization: `Bearer ${token}`
148
- },
149
- body: JSON.stringify({
150
- messages: [
151
- { role: "system", content: systemPrompt },
152
- { role: "user", content: userPrompt }
153
- ]
154
- })
155
- });
156
- const data = await response.json();
157
- if (!response.ok) {
158
- const error = data.error || `Spark AI error: ${response.status}`;
159
- if (response.status === 429 && attempt < 3) {
160
- const retryAfter = response.headers.get("Retry-After");
161
- const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * Math.pow(2, attempt - 1);
162
- lastRateLimitError = {
163
- success: false,
164
- error,
165
- isRateLimit: true,
166
- ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
167
- };
168
- await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, 3e4)));
169
- continue;
170
- }
171
- return {
126
+ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
127
+ let lastRateLimitError = null;
128
+ for (let attempt = 1; attempt <= 3; attempt++) {
129
+ try {
130
+ const response = await fetch(chatUrl, {
131
+ method: "POST",
132
+ headers: {
133
+ "Content-Type": "application/json",
134
+ Authorization: `Bearer ${bearerToken}`
135
+ },
136
+ body: JSON.stringify({
137
+ messages: [
138
+ { role: "system", content: systemPrompt },
139
+ { role: "user", content: userPrompt }
140
+ ]
141
+ })
142
+ });
143
+ let data = {};
144
+ const contentType = response.headers.get("content-type") ?? "";
145
+ if (contentType.includes("application/json")) {
146
+ data = await response.json();
147
+ }
148
+ if (!response.ok) {
149
+ const error = data.error || `Spark AI error: ${response.status} ${response.statusText}`;
150
+ if (response.status === 429 && attempt < 3) {
151
+ const retryAfter = response.headers.get("Retry-After");
152
+ const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** (attempt - 1);
153
+ lastRateLimitError = {
172
154
  success: false,
173
155
  error,
174
- isRateLimit: response.status === 429,
175
- isAuthError: response.status === 401 || response.status === 403
176
- };
177
- }
178
- if (!data.success || !data.response) {
179
- return {
180
- success: false,
181
- error: data.error || "No response from Spark AI"
156
+ isRateLimit: true,
157
+ ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
182
158
  };
159
+ await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, 3e4)));
160
+ continue;
183
161
  }
184
162
  return {
185
- success: true,
186
- content: data.response,
187
- limits: {
188
- used: data.limits?.used ?? 0,
189
- limit: data.limits?.limit ?? 100,
190
- tier,
191
- ...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
192
- }
163
+ success: false,
164
+ error,
165
+ isRateLimit: response.status === 429,
166
+ isAuthError: response.status === 401 || response.status === 403
167
+ };
168
+ }
169
+ if (!data.success || !data.response) {
170
+ return {
171
+ success: false,
172
+ error: data.error || "No response from Spark AI"
193
173
  };
194
- } catch (err) {
195
- if (attempt === 3) throw err;
196
- const delayMs = 1e3 * Math.pow(2, attempt - 1);
197
- await new Promise((resolve) => setTimeout(resolve, delayMs));
198
174
  }
175
+ return {
176
+ success: true,
177
+ content: data.response,
178
+ limits: {
179
+ used: data.limits?.used ?? 0,
180
+ limit: data.limits?.limit ?? 100,
181
+ tier,
182
+ ...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
183
+ }
184
+ };
185
+ } catch (err) {
186
+ if (attempt === 3) throw err;
187
+ const delayMs = 1e3 * 2 ** (attempt - 1);
188
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
199
189
  }
200
- return lastRateLimitError || { success: false, error: "Request failed after retries" };
190
+ }
191
+ return lastRateLimitError || { success: false, error: "Request failed after retries" };
192
+ }
193
+ async function callAI(systemPrompt, userPrompt, tier = "free") {
194
+ if (!apiKey()) {
195
+ return {
196
+ success: false,
197
+ error: "AI service is not configured. Please report this issue.",
198
+ isAuthError: true
199
+ };
200
+ }
201
+ try {
202
+ const token = await getOrRefreshDirectToken();
203
+ return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
201
204
  } catch (err) {
202
205
  return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
203
206
  }
@@ -347,14 +350,32 @@ function getActionForType(type) {
347
350
  }
348
351
  var noopTask = () => ({ succeed: () => {
349
352
  } });
350
- async function crawlPage(url, task = noopTask) {
353
+ var CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
354
+ var CRAWL_CACHE_MAX_ENTRIES = 50;
355
+ var crawlCache = /* @__PURE__ */ new Map();
356
+ async function crawlPage(url, task = noopTask, storageStatePath, viewport) {
357
+ const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
358
+ const cached = crawlCache.get(cacheKey);
359
+ if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
360
+ task("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
361
+ return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
362
+ }
351
363
  const browserTask = task("Launching browser");
352
364
  const browser = await chromium.launch({ headless: true });
353
- const page = await browser.newPage();
354
- browserTask.succeed("Browser launched");
365
+ const context = await browser.newContext({
366
+ ...storageStatePath ? { storageState: storageStatePath } : {},
367
+ // Crawl at the size we will record at, or the measurements below describe
368
+ // a page nobody will ever see.
369
+ ...viewport ? { viewport } : {}
370
+ });
371
+ const page = await context.newPage();
372
+ browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
355
373
  const crawlTask = task("Crawling page");
356
374
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
357
375
  const title = await page.title();
376
+ const scrollable = await page.evaluate(
377
+ "Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
378
+ );
358
379
  const rawElements = await page.evaluate(`
359
380
  (() => {
360
381
  const items = [];
@@ -484,7 +505,12 @@ async function crawlPage(url, task = noopTask) {
484
505
  });
485
506
  await browser.close();
486
507
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
487
- return { title, elements };
508
+ crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
509
+ if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
510
+ const oldest = crawlCache.keys().next().value;
511
+ if (oldest !== void 0) crawlCache.delete(oldest);
512
+ }
513
+ return { title, elements, scrollable };
488
514
  }
489
515
 
490
516
  // src/lib/plan-validation.ts
@@ -718,6 +744,80 @@ function restorePreservedValues(step, preserved, indices) {
718
744
  return { uploadIdx, mediaIdx };
719
745
  }
720
746
 
747
+ // src/lib/plan-file.ts
748
+ var MAX_HANDOFF_URL_CHARS = 32e3;
749
+ function serializePlan(plan) {
750
+ return `${JSON.stringify(plan, null, 2)}
751
+ `;
752
+ }
753
+ function parsePlanText(text) {
754
+ let raw;
755
+ try {
756
+ raw = JSON.parse(text);
757
+ } catch (e) {
758
+ return { ok: false, error: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
759
+ }
760
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
761
+ return { ok: false, error: "A plan must be a JSON object." };
762
+ }
763
+ const candidate = raw;
764
+ if (typeof candidate.url !== "string" || !candidate.url.trim()) {
765
+ return { ok: false, error: "A plan needs a `url` to record." };
766
+ }
767
+ if (!Array.isArray(candidate.steps) || candidate.steps.length === 0) {
768
+ return { ok: false, error: "A plan needs at least one step." };
769
+ }
770
+ const steps = [];
771
+ for (const [index, step] of candidate.steps.entries()) {
772
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
773
+ return { ok: false, error: `Step ${index + 1} is not an object.` };
774
+ }
775
+ const typed = step;
776
+ if (typeof typed.action !== "string" || !typed.action.trim()) {
777
+ return { ok: false, error: `Step ${index + 1} has no action.` };
778
+ }
779
+ const failure = validateStep(typed);
780
+ if (failure) return { ok: false, error: `Step ${index + 1}: ${failure}` };
781
+ steps.push({ ...typed, id: typed.id ?? index + 1 });
782
+ }
783
+ return {
784
+ ok: true,
785
+ plan: {
786
+ url: candidate.url,
787
+ title: candidate.title ?? "",
788
+ description: candidate.description ?? "",
789
+ ...candidate.lang ? { lang: candidate.lang } : {},
790
+ steps
791
+ }
792
+ };
793
+ }
794
+ function toBase64Url(text) {
795
+ const bytes = new TextEncoder().encode(text);
796
+ const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
797
+ const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(text, "utf8").toString("base64");
798
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
799
+ }
800
+ function fromBase64Url(encoded) {
801
+ const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
802
+ if (typeof atob !== "function") return Buffer.from(base64, "base64").toString("utf8");
803
+ const binary = atob(base64);
804
+ return new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
805
+ }
806
+ function encodePlan(plan) {
807
+ return toBase64Url(serializePlan(plan));
808
+ }
809
+ function decodePlan(encoded) {
810
+ try {
811
+ return parsePlanText(fromBase64Url(encoded));
812
+ } catch {
813
+ return { ok: false, error: "That link is damaged \u2014 the plan could not be decoded." };
814
+ }
815
+ }
816
+ function planEditorUrl(baseUrl, plan) {
817
+ const url = `${baseUrl.replace(/\/$/, "")}/edit#plan=${encodePlan(plan)}`;
818
+ return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
819
+ }
820
+
721
821
  // src/lib/prompts.ts
722
822
  function buildSystemPrompt(maxSteps) {
723
823
  let scope;
@@ -771,7 +871,8 @@ EXAMPLES:
771
871
  \u2022 File upload: "target": "input[type=\\"file\\"]"
772
872
  \u2022 Slider: "target": "input[type=\\"range\\"]"`;
773
873
  }
774
- function buildUserPrompt(url, title, elements, langName, description, maxSteps) {
874
+ var MIN_USEFUL_SCROLL_PX = 200;
875
+ function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
775
876
  const lang = langName ? `
776
877
  Language: Write ALL captions in ${langName}.` : "";
777
878
  const purpose = description ? `
@@ -809,8 +910,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
809
910
  ${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
810
911
  }
811
912
  const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
913
+ const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
914
+ const scrolling = cannotScroll ? `
915
+
916
+ SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
917
+ 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" : "";
812
918
  return `URL: ${url}
813
- Title: ${title}${purpose}
919
+ Title: ${title}${purpose}${scrolling}
814
920
 
815
921
  AVAILABLE ELEMENTS (grouped by action):
816
922
  ${elementSummary}
@@ -820,7 +926,7 @@ REQUIREMENTS:
820
926
  \u2022 Complete ONE user journey: input \u2192 process \u2192 output
821
927
  \u2022 For CLICK: use simplified text target like "extract frame" or "download"
822
928
  \u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
823
- \u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
929
+ \u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
824
930
 
825
931
  Return JSON:
826
932
  {
@@ -844,101 +950,36 @@ TARGET SYNTAX:
844
950
  \u2022 Text inputs: Use selector from list above`;
845
951
  }
846
952
 
847
- // src/lib/voices.ts
848
- var LANGUAGE_VOICES = {
849
- en: "en-GB-SoniaNeural",
850
- ar: "ar-LB-LaylaNeural",
851
- es: "es-ES-ElviraNeural",
852
- fr: "fr-FR-DeniseNeural",
853
- de: "de-DE-KatjaNeural",
854
- it: "it-IT-ElsaNeural",
855
- pt: "pt-BR-FranciscaNeural",
856
- ru: "ru-RU-SvetlanaNeural",
857
- zh: "zh-CN-XiaoxiaoNeural",
858
- ja: "ja-JP-NanamiNeural",
859
- ko: "ko-KR-SunHiNeural",
860
- hi: "hi-IN-SwaraNeural",
861
- tr: "tr-TR-EmelNeural",
862
- nl: "nl-NL-ColetteNeural",
863
- pl: "pl-PL-AgnieszkaNeural",
864
- sv: "sv-SE-SofieNeural",
865
- he: "he-IL-HilaNeural",
866
- th: "th-TH-PremwadeeNeural",
867
- vi: "vi-VN-HoaiMyNeural",
868
- id: "id-ID-GadisNeural",
869
- ms: "ms-MY-YasminNeural",
870
- fil: "fil-PH-BlessicaNeural",
871
- uk: "uk-UA-PolinaNeural",
872
- cs: "cs-CZ-VlastaNeural",
873
- el: "el-GR-AthinaNeural",
874
- ro: "ro-RO-AlinaNeural",
875
- hu: "hu-HU-NoemiNeural",
876
- da: "da-DK-ChristelNeural",
877
- fi: "fi-FI-NooraNeural",
878
- no: "nb-NO-PernilleNeural",
879
- sk: "sk-SK-ViktoriaNeural",
880
- bg: "bg-BG-KalinaNeural",
881
- hr: "hr-HR-GabrijelaNeural",
882
- sl: "sl-SI-PetraNeural",
883
- et: "et-EE-AnuNeural",
884
- lv: "lv-LV-EveritaNeural",
885
- lt: "lt-LT-OnaNeural",
886
- bn: "bn-IN-TanishaaNeural",
887
- ta: "ta-IN-PallaviNeural",
888
- te: "te-IN-ShrutiNeural",
889
- mr: "mr-IN-AarohiNeural",
890
- gu: "gu-IN-DhwaniNeural",
891
- kn: "kn-IN-SapnaNeural",
892
- ml: "ml-IN-SobhanaNeural",
893
- ur: "ur-PK-UzmaNeural",
894
- fa: "fa-IR-DilaraNeural",
895
- sw: "sw-KE-ZuriNeural",
896
- af: "af-ZA-AdriNeural",
897
- ca: "ca-ES-JoanaNeural",
898
- cy: "cy-GB-NiaNeural",
899
- ga: "ga-IE-OrlaNeural",
900
- mt: "mt-MT-GraceNeural",
901
- is: "is-IS-GudrunNeural",
902
- mk: "mk-MK-MarijaNeural",
903
- sr: "sr-RS-SophieNeural",
904
- bs: "bs-BA-VesnaNeural",
905
- sq: "sq-AL-AnilaNeural",
906
- am: "am-ET-MekdesNeural",
907
- az: "az-AZ-BabekNeural",
908
- ka: "ka-GE-EkaNeural",
909
- kk: "kk-KZ-AigulNeural",
910
- mn: "mn-MN-YesUINeural",
911
- ne: "ne-NP-HemkalaNeural",
912
- ps: "ps-AF-LatifaNeural",
913
- si: "si-LK-ThiliniNeural",
914
- so: "so-SO-UbaxNeural",
915
- zu: "zu-ZA-ThandoNeural"
916
- };
917
-
918
953
  // src/lib/SparkStripe-feature-gating.ts
919
954
  var APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
920
955
  var BASE = "https://sparkstripe.com";
921
956
  var TIERS = {
922
- "price_1TADnPEf9opkuWid9NtqOYON": "pro",
923
- "price_1TADnPEf9opkuWidgbvvX9ma": "pro",
924
- "price_1TADm7Ef9opkuWid3I9tquIB": "pro",
925
- "price_1TADm8Ef9opkuWid1RSA2wCa": "pro",
926
- "price_1TCREmEf9opkuWidvhuqzqDM": "pro",
927
- "price_1TCREmEf9opkuWidacf1tiC7": "pro"
957
+ price_1TADnPEf9opkuWid9NtqOYON: "pro",
958
+ price_1TADnPEf9opkuWidgbvvX9ma: "pro",
959
+ price_1TADm7Ef9opkuWid3I9tquIB: "pro",
960
+ price_1TADm8Ef9opkuWid1RSA2wCa: "pro",
961
+ price_1TCREmEf9opkuWidvhuqzqDM: "pro",
962
+ price_1TCREmEf9opkuWidacf1tiC7: "pro"
928
963
  };
929
964
  var RANK = {
930
- "free": 0,
931
- "pro": 1
965
+ free: 0,
966
+ pro: 1
967
+ };
968
+ var FEATURES = {
969
+ free: [],
970
+ pro: [
971
+ "Everything in Free",
972
+ "No watermark",
973
+ "Full video composition (voiceover + captions + music + transitions)",
974
+ "Unlimited videos",
975
+ "Priority support"
976
+ ]
932
977
  };
933
- var FEATURES = { "free": [], "pro": [
934
- "Everything in Free",
935
- "No watermark",
936
- "Full video composition (voiceover + captions + music + transitions)",
937
- "Unlimited videos",
938
- "Priority support"
939
- ] };
940
978
  async function _fetch(email, init) {
941
- const r = await fetch(`${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`, init);
979
+ const r = await fetch(
980
+ `${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`,
981
+ init
982
+ );
942
983
  if (!r.ok) throw new Error(`spark-stripe: ${r.status}`);
943
984
  return r.json();
944
985
  }
@@ -981,6 +1022,9 @@ function pricingUrl(opts) {
981
1022
  if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
982
1023
  return u.toString();
983
1024
  }
1025
+
1026
+ // src/lib/voices.ts
1027
+ import { WIDE_VOICES } from "@spark-apps/video-kit";
984
1028
  export {
985
1029
  ACTIONS_REQUIRING_TARGET,
986
1030
  EXCLUDED_LINK_PATTERNS,
@@ -988,13 +1032,16 @@ export {
988
1032
  FIX_PLAN_SYSTEM_PROMPT,
989
1033
  INTERACTIVE_SELECTORS,
990
1034
  INVALID_TARGETS,
991
- LANGUAGE_VOICES,
1035
+ WIDE_VOICES as LANGUAGE_VOICES,
1036
+ MAX_HANDOFF_URL_CHARS,
992
1037
  REFINE_PLAN_SYSTEM_PROMPT,
993
1038
  buildSystemPrompt,
994
1039
  buildUserPrompt,
995
1040
  callAI,
996
1041
  collectPreservedValues,
997
1042
  crawlPage,
1043
+ decodePlan,
1044
+ encodePlan,
998
1045
  filterValidSteps,
999
1046
  getErrorMessage,
1000
1047
  getLanguageName,
@@ -1004,7 +1051,7 @@ export {
1004
1051
  getTierByEmail,
1005
1052
  hasAccess,
1006
1053
  hasTrialRemaining,
1007
- hexToASS,
1054
+ hexToAss as hexToASS,
1008
1055
  isCanceling,
1009
1056
  isExcludedLink,
1010
1057
  isInvalidTarget,
@@ -1014,12 +1061,15 @@ export {
1014
1061
  normalizeUrl,
1015
1062
  parseAIPlanResponse,
1016
1063
  parsePartial,
1064
+ parsePlanText,
1065
+ planEditorUrl,
1017
1066
  pricingUrl,
1018
1067
  restorePreservedValues,
1019
1068
  rgbaToHex,
1069
+ serializePlan,
1020
1070
  shouldSkipLink,
1021
1071
  showPaywall,
1022
1072
  toTitleCase,
1023
1073
  validateStep,
1024
- windowsPathToWSL
1074
+ windowsDrivePathToWsl as windowsPathToWSL
1025
1075
  };
package/mcp.mjs ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+
3
+ // QuickPeek MCP server — demo-video generation over stdio.
4
+ //
5
+ // .mjs, not .js: this package is CommonJS (the CLI bundle is CJS), so a
6
+ // plain .js here would be parsed as CommonJS and its imports would fail.
7
+ //
8
+ // A thin bootstrap: transport wiring, the protocol-stdout proxy, and
9
+ // dispatch. Tool schemas and handlers live in src/mcp/ (bundled by tsup
10
+ // into dist/mcp-tools.mjs), calling the command functions directly rather
11
+ // than shelling out to the `qp` CLI.
12
+ //
13
+ // Deliberately NO delete tools: outputs are new files and the server never
14
+ // removes anything a user recorded.
15
+
16
+ import { readFileSync } from 'node:fs';
17
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
20
+ import { setElicit, TOOL_HANDLERS, TOOLS } from './dist/mcp-tools.mjs';
21
+
22
+ const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
23
+
24
+ // Captured once, before anything ever patches process.stdout.write
25
+ // (withSilencedStdout in dist/mcp-tools.mjs mutates it during tool calls).
26
+ const REAL_STDOUT_WRITE = process.stdout.write.bind(process.stdout);
27
+
28
+ // The transport resolves `.write` dynamically off whatever stdout object it
29
+ // is handed — the SAME property withSilencedStdout mutates. Recording a
30
+ // demo takes minutes and prints throughout, so a silenced call would
31
+ // otherwise swallow or misdirect unrelated protocol responses. This proxy
32
+ // always uses the pristine write captured above.
33
+ const protocolStdout = new Proxy(process.stdout, {
34
+ get(target, prop, receiver) {
35
+ if (prop === 'write') return REAL_STDOUT_WRITE;
36
+ const value = Reflect.get(target, prop, receiver);
37
+ return typeof value === 'function' ? value.bind(target) : value;
38
+ },
39
+ });
40
+
41
+ const server = new Server(
42
+ { name: 'quickpeek', version: pkg.version },
43
+ { capabilities: { tools: {} } }
44
+ );
45
+
46
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
47
+
48
+ function toErrorResult(e) {
49
+ return {
50
+ content: [{ type: 'text', text: e?.message ?? String(e) }],
51
+ isError: true,
52
+ };
53
+ }
54
+
55
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
56
+ const { name, arguments: args } = request.params;
57
+ const handler = TOOL_HANDLERS[name];
58
+ if (!handler) return toErrorResult(new Error(`Unknown tool: ${name}`));
59
+ try {
60
+ return await handler(args ?? {});
61
+ } catch (e) {
62
+ // Never crash the server on a tool failure — surface it as a tool error.
63
+ return toErrorResult(e);
64
+ }
65
+ });
66
+
67
+ async function main() {
68
+ const transport = new StdioServerTransport(process.stdin, protocolStdout);
69
+ await server.connect(transport);
70
+
71
+ // Elicitation is optional in MCP. The capability is checked per call, not
72
+ // here: the client only declares it in `initialize`, which arrives after
73
+ // connect() resolves. null means "cannot prompt" and start_auth then falls
74
+ // back to a follow-up finish_auth call.
75
+ setElicit(async (message) => {
76
+ if (!server.getClientCapabilities()?.elicitation) return null;
77
+ const result = await server.elicitInput({
78
+ message,
79
+ requestedSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ signedIn: {
83
+ type: 'boolean',
84
+ title: 'Signed in?',
85
+ description: 'Confirm once you have finished signing in, so the session can be saved.',
86
+ },
87
+ },
88
+ required: ['signedIn'],
89
+ },
90
+ });
91
+ return result.action === 'accept' && result.content?.signedIn === true;
92
+ });
93
+ }
94
+
95
+ main().catch((e) => {
96
+ console.error(`quickpeek-mcp failed to start: ${e?.message ?? e}`);
97
+ process.exit(1);
98
+ });