@spark-apps/quickpeek 1.1.3 → 1.2.0

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.
@@ -65,8 +65,13 @@ declare const DEFAULT_CAPTIONS: CaptionStyle;
65
65
  * AI API client — Spark AI communication
66
66
  * Handles token management, rate limiting, and retries
67
67
  *
68
- * The API key is embedded at build time via tsup `define`.
69
- * Per-user rate limiting is handled server-side via email.
68
+ * Two transports:
69
+ * - Relay (default for the published CLI): quickpeek-web mints a short-lived
70
+ * per-user token from a registered email and proxies the chat server-side.
71
+ * No credential ships in the npm bundle.
72
+ * - Direct (dev/server only): when SPARK_AI_KEY is present in the runtime
73
+ * environment (never baked at build time), call Spark AI directly. This is
74
+ * the path quickpeek-web itself uses server-side.
70
75
  */
71
76
 
72
77
  interface RateLimitInfo {
@@ -89,9 +94,15 @@ interface AIError {
89
94
  }
90
95
  type AIResult = AIResponse | AIError;
91
96
  /**
92
- * Call Spark AI chat endpoint. Automatically retries on rate limit using Retry-After header.
97
+ * Direct Spark AI call using the runtime SPARK_AI_KEY (dev + server-side use).
93
98
  */
94
99
  declare function callAI(systemPrompt: string, userPrompt: string, tier?: UserTier): Promise<AIResult>;
100
+ /**
101
+ * Relayed Spark AI call for the published CLI: authenticates with the user's
102
+ * registered email via quickpeek-web, which holds the real key server-side
103
+ * and enforces per-user daily limits.
104
+ */
105
+ declare function callAIViaRelay(systemPrompt: string, userPrompt: string, email: string, tier?: UserTier): Promise<AIResult>;
95
106
  /**
96
107
  * Parse AI-generated plan JSON from response text
97
108
  */
@@ -170,12 +181,13 @@ interface Plan {
170
181
 
171
182
  /**
172
183
  * Crawl a page and extract interactive elements with smart selectors
184
+ * Results are cached in-memory for a few minutes to avoid duplicate browser launches
173
185
  * @param url - URL to crawl
174
186
  * @param task - Optional progress reporter (CLI uses ora spinners, web can omit)
175
187
  */
176
188
  declare function crawlPage(url: string, task?: (text: string) => {
177
189
  succeed: (text?: string) => void;
178
- }): Promise<{
190
+ }, storageStatePath?: string): Promise<{
179
191
  title: string;
180
192
  elements: ElementInfo[];
181
193
  }>;
@@ -187,19 +199,12 @@ declare function crawlPage(url: string, task?: (text: string) => {
187
199
  * Get full language name from ISO code
188
200
  */
189
201
  declare function getLanguageName(lang?: string): string | null;
190
- /**
191
- * Convert hex color (#RRGGBB) to ASS subtitle format (&HAABBGGRR)
192
- */
193
- declare function hexToASS(hex: string, alpha?: number): string;
202
+
194
203
  /**
195
204
  * Convert rgba() or rgb() color string to hex
196
205
  */
197
206
  declare function rgbaToHex(color: string): string;
198
- /**
199
- * Convert a Windows absolute path (D:\...) to WSL mount format (/mnt/d/...).
200
- * Returns null if the path is not a Windows drive-letter path.
201
- */
202
- declare function windowsPathToWSL(path: string): string | null;
207
+
203
208
  /**
204
209
  * Parse partial field "start-end" into trim values
205
210
  * e.g., "0-3.5" -> { trimStart: 0, trimEnd: 3.5 }
@@ -226,18 +231,6 @@ declare function toTitleCase(text: string): string;
226
231
  declare function buildSystemPrompt(maxSteps: number): string;
227
232
  declare function buildUserPrompt(url: string, title: string, elements: ElementInfo[], langName: string | null, description: string | undefined, maxSteps: number): string;
228
233
 
229
- /**
230
- * Shared constants for page scraping and element filtering
231
- * Used by both CLI and web versions
232
- */
233
- declare const EXCLUDED_LINK_PATTERNS: string[];
234
- /** CSS selectors for interactive page elements */
235
- declare const INTERACTIVE_SELECTORS: string[];
236
- /** Check if link text or href matches an excluded pattern */
237
- declare function isExcludedLink(text: string, href?: string): boolean;
238
- /** Check if a link should be skipped during crawling */
239
- declare function shouldSkipLink(text: string, href: string, currentPath: string): boolean;
240
-
241
234
  type Tier = 'free' | 'pro';
242
235
  declare const FEATURES: Record<string, string[]>;
243
236
  interface SparkSubscription {
@@ -282,6 +275,16 @@ declare function pricingUrl(opts?: {
282
275
  returnUrl?: string;
283
276
  }): string;
284
277
 
285
- declare const LANGUAGE_VOICES: Record<string, string>;
278
+ /**
279
+ * Shared constants for page scraping and element filtering
280
+ * Used by both CLI and web versions
281
+ */
282
+ declare const EXCLUDED_LINK_PATTERNS: string[];
283
+ /** CSS selectors for interactive page elements */
284
+ declare const INTERACTIVE_SELECTORS: string[];
285
+ /** Check if link text or href matches an excluded pattern */
286
+ declare function isExcludedLink(text: string, href?: string): boolean;
287
+ /** Check if a link should be skipped during crawling */
288
+ declare function shouldSkipLink(text: string, href: string, currentPath: string): boolean;
286
289
 
287
- export { type AIError as A, isPaid as B, type Config as C, DEFAULT_CAPTIONS as D, EXCLUDED_LINK_PATTERNS as E, FEATURES as F, isVerified as G, type HighlightMode as H, INTERACTIVE_SELECTORS as I, normalizeUrl as J, parseAIPlanResponse as K, LANGUAGE_VOICES as L, parsePartial as M, pricingUrl as N, rgbaToHex as O, type Plan as P, shouldSkipLink as Q, type RateLimitInfo as R, type SparkStatus as S, type Tier as T, type UserTier as U, VERSION as V, showPaywall as W, toTitleCase as X, windowsPathToWSL as Y, getTier as Z, type AIResponse as a, type AIResult as b, CONFIG_FILE as c, type CaptionStyle as d, type CaptionWordStyle as e, DEFAULT_CONFIG as f, type ElementInfo as g, type ElementType as h, type SparkSubscription as i, type SparkTrial as j, type Step as k, type SuggestedAction as l, buildSystemPrompt as m, buildUserPrompt as n, callAI as o, crawlPage as p, getErrorMessage as q, getLanguageName as r, getStatus as s, getStatusCached as t, getTierByEmail as u, hasAccess as v, hasTrialRemaining as w, hexToASS as x, isCanceling as y, isExcludedLink as z };
290
+ export { type AIError as A, isPaid as B, type Config as C, DEFAULT_CAPTIONS as D, EXCLUDED_LINK_PATTERNS as E, FEATURES as F, isVerified as G, type HighlightMode as H, INTERACTIVE_SELECTORS as I, normalizeUrl as J, parseAIPlanResponse as K, parsePartial as L, pricingUrl as M, rgbaToHex as N, shouldSkipLink as O, type Plan as P, showPaywall as Q, type RateLimitInfo as R, type SparkStatus as S, type Tier as T, type UserTier as U, VERSION as V, toTitleCase as W, getTier as X, type AIResponse as a, type AIResult as b, CONFIG_FILE as c, type CaptionStyle as d, type CaptionWordStyle as e, DEFAULT_CONFIG as f, type ElementInfo as g, type ElementType as h, type SparkSubscription as i, type SparkTrial as j, type Step as k, type SuggestedAction as l, buildSystemPrompt as m, buildUserPrompt as n, callAI as o, callAIViaRelay as p, crawlPage as q, getErrorMessage as r, getLanguageName as s, getStatus as t, getStatusCached as u, getTierByEmail as v, hasAccess as w, hasTrialRemaining as x, isCanceling as y, isExcludedLink as z };
package/dist/web.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- export { A as AIError, a as AIResponse, b as AIResult, E as EXCLUDED_LINK_PATTERNS, g as ElementInfo, h as ElementType, F as FEATURES, H as HighlightMode, I as INTERACTIVE_SELECTORS, L as LANGUAGE_VOICES, P as Plan, R as RateLimitInfo, S as SparkStatus, i as SparkSubscription, j as SparkTrial, k as Step, l as SuggestedAction, T as Tier, m as buildSystemPrompt, n as buildUserPrompt, o as callAI, p as crawlPage, q as getErrorMessage, r as getLanguageName, s as getStatus, t as getStatusCached, Z as getTier, u as getTierByEmail, v as hasAccess, w as hasTrialRemaining, x as hexToASS, y as isCanceling, z as isExcludedLink, B as isPaid, G as isVerified, J as normalizeUrl, K as parseAIPlanResponse, M as parsePartial, N as pricingUrl, O as rgbaToHex, Q as shouldSkipLink, W as showPaywall, X as toTitleCase, Y as windowsPathToWSL } from './voices-BtYZey4Y.mjs';
1
+ export { A as AIError, a as AIResponse, b as AIResult, E as EXCLUDED_LINK_PATTERNS, g as ElementInfo, h as ElementType, F as FEATURES, H as HighlightMode, I as INTERACTIVE_SELECTORS, P as Plan, R as RateLimitInfo, S as SparkStatus, i as SparkSubscription, j as SparkTrial, k as Step, l as SuggestedAction, T as Tier, m as buildSystemPrompt, n as buildUserPrompt, o as callAI, q as crawlPage, r as getErrorMessage, s as getLanguageName, t as getStatus, u as getStatusCached, X as getTier, v as getTierByEmail, w as hasAccess, x as hasTrialRemaining, y as isCanceling, z as isExcludedLink, B as isPaid, G as isVerified, J as normalizeUrl, K as parseAIPlanResponse, L as parsePartial, M as pricingUrl, N as rgbaToHex, O as shouldSkipLink, Q as showPaywall, W as toTitleCase } from './scraping-BK8pIJhm.mjs';
2
+ export { WIDE_VOICES as LANGUAGE_VOICES, hexToAss as hexToASS, windowsDrivePathToWsl as windowsPathToWSL } from '@spark-apps/video-kit';
2
3
 
3
4
  /**
4
5
  * Plan Validation - Shared between CLI and Web
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("-");
@@ -102,23 +90,29 @@ 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`;
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`;
105
96
  var API_KEY = process.env.SPARK_AI_KEY || "";
106
- var cachedToken = null;
107
- async function getOrRefreshToken() {
97
+ var cachedDirectToken = null;
98
+ async function getOrRefreshDirectToken() {
108
99
  const now = Date.now();
109
- if (cachedToken && cachedToken.expiresAt > now + 3e5) {
110
- return cachedToken.token;
100
+ if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
101
+ return cachedDirectToken.token;
111
102
  }
112
103
  try {
113
104
  const response = await fetch(SPARK_AI_TOKEN, {
114
105
  method: "POST",
115
106
  headers: { "X-Api-Key": API_KEY }
116
107
  });
108
+ if (!response.ok) {
109
+ throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
110
+ }
117
111
  const data = await response.json();
118
- if (!response.ok || !data.success || !data.token) {
119
- throw new Error(data.error || `Token exchange failed: ${response.status}`);
112
+ if (!data.success || !data.token) {
113
+ throw new Error(data.error || `Token exchange failed: unexpected response`);
120
114
  }
121
- cachedToken = {
115
+ cachedDirectToken = {
122
116
  token: data.token,
123
117
  expiresAt: data.expiresAt || now + 36e5
124
118
  };
@@ -127,6 +121,73 @@ async function getOrRefreshToken() {
127
121
  throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
128
122
  }
129
123
  }
124
+ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
125
+ let lastRateLimitError = null;
126
+ for (let attempt = 1; attempt <= 3; attempt++) {
127
+ try {
128
+ const response = await fetch(chatUrl, {
129
+ method: "POST",
130
+ headers: {
131
+ "Content-Type": "application/json",
132
+ Authorization: `Bearer ${bearerToken}`
133
+ },
134
+ body: JSON.stringify({
135
+ messages: [
136
+ { role: "system", content: systemPrompt },
137
+ { role: "user", content: userPrompt }
138
+ ]
139
+ })
140
+ });
141
+ let data = {};
142
+ const contentType = response.headers.get("content-type") ?? "";
143
+ if (contentType.includes("application/json")) {
144
+ data = await response.json();
145
+ }
146
+ if (!response.ok) {
147
+ const error = data.error || `Spark AI error: ${response.status} ${response.statusText}`;
148
+ if (response.status === 429 && attempt < 3) {
149
+ const retryAfter = response.headers.get("Retry-After");
150
+ const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** (attempt - 1);
151
+ lastRateLimitError = {
152
+ success: false,
153
+ error,
154
+ isRateLimit: true,
155
+ ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
156
+ };
157
+ await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, 3e4)));
158
+ continue;
159
+ }
160
+ return {
161
+ success: false,
162
+ error,
163
+ isRateLimit: response.status === 429,
164
+ isAuthError: response.status === 401 || response.status === 403
165
+ };
166
+ }
167
+ if (!data.success || !data.response) {
168
+ return {
169
+ success: false,
170
+ error: data.error || "No response from Spark AI"
171
+ };
172
+ }
173
+ return {
174
+ success: true,
175
+ content: data.response,
176
+ limits: {
177
+ used: data.limits?.used ?? 0,
178
+ limit: data.limits?.limit ?? 100,
179
+ tier,
180
+ ...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
181
+ }
182
+ };
183
+ } catch (err) {
184
+ if (attempt === 3) throw err;
185
+ const delayMs = 1e3 * 2 ** (attempt - 1);
186
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
187
+ }
188
+ }
189
+ return lastRateLimitError || { success: false, error: "Request failed after retries" };
190
+ }
130
191
  async function callAI(systemPrompt, userPrompt, tier = "free") {
131
192
  if (!API_KEY) {
132
193
  return {
@@ -136,68 +197,8 @@ async function callAI(systemPrompt, userPrompt, tier = "free") {
136
197
  };
137
198
  }
138
199
  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 {
172
- success: false,
173
- 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"
182
- };
183
- }
184
- 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
- }
193
- };
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
- }
199
- }
200
- return lastRateLimitError || { success: false, error: "Request failed after retries" };
200
+ const token = await getOrRefreshDirectToken();
201
+ return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
201
202
  } catch (err) {
202
203
  return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
203
204
  }
@@ -347,11 +348,20 @@ function getActionForType(type) {
347
348
  }
348
349
  var noopTask = () => ({ succeed: () => {
349
350
  } });
350
- async function crawlPage(url, task = noopTask) {
351
+ var CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
352
+ var CRAWL_CACHE_MAX_ENTRIES = 50;
353
+ var crawlCache = /* @__PURE__ */ new Map();
354
+ async function crawlPage(url, task = noopTask, storageStatePath) {
355
+ const cached = crawlCache.get(url);
356
+ if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
357
+ task("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
358
+ return { title: cached.title, elements: [...cached.elements] };
359
+ }
351
360
  const browserTask = task("Launching browser");
352
361
  const browser = await chromium.launch({ headless: true });
353
- const page = await browser.newPage();
354
- browserTask.succeed("Browser launched");
362
+ const context = await browser.newContext(storageStatePath ? { storageState: storageStatePath } : {});
363
+ const page = await context.newPage();
364
+ browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
355
365
  const crawlTask = task("Crawling page");
356
366
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
357
367
  const title = await page.title();
@@ -484,6 +494,11 @@ async function crawlPage(url, task = noopTask) {
484
494
  });
485
495
  await browser.close();
486
496
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
497
+ crawlCache.set(url, { timestamp: Date.now(), title, elements: [...elements] });
498
+ if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
499
+ const oldest = crawlCache.keys().next().value;
500
+ if (oldest !== void 0) crawlCache.delete(oldest);
501
+ }
487
502
  return { title, elements };
488
503
  }
489
504
 
@@ -844,101 +859,36 @@ TARGET SYNTAX:
844
859
  \u2022 Text inputs: Use selector from list above`;
845
860
  }
846
861
 
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
862
  // src/lib/SparkStripe-feature-gating.ts
919
863
  var APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
920
864
  var BASE = "https://sparkstripe.com";
921
865
  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"
866
+ price_1TADnPEf9opkuWid9NtqOYON: "pro",
867
+ price_1TADnPEf9opkuWidgbvvX9ma: "pro",
868
+ price_1TADm7Ef9opkuWid3I9tquIB: "pro",
869
+ price_1TADm8Ef9opkuWid1RSA2wCa: "pro",
870
+ price_1TCREmEf9opkuWidvhuqzqDM: "pro",
871
+ price_1TCREmEf9opkuWidacf1tiC7: "pro"
928
872
  };
929
873
  var RANK = {
930
- "free": 0,
931
- "pro": 1
874
+ free: 0,
875
+ pro: 1
876
+ };
877
+ var FEATURES = {
878
+ free: [],
879
+ pro: [
880
+ "Everything in Free",
881
+ "No watermark",
882
+ "Full video composition (voiceover + captions + music + transitions)",
883
+ "Unlimited videos",
884
+ "Priority support"
885
+ ]
932
886
  };
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
887
  async function _fetch(email, init) {
941
- const r = await fetch(`${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`, init);
888
+ const r = await fetch(
889
+ `${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`,
890
+ init
891
+ );
942
892
  if (!r.ok) throw new Error(`spark-stripe: ${r.status}`);
943
893
  return r.json();
944
894
  }
@@ -981,6 +931,9 @@ function pricingUrl(opts) {
981
931
  if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
982
932
  return u.toString();
983
933
  }
934
+
935
+ // src/lib/voices.ts
936
+ import { WIDE_VOICES } from "@spark-apps/video-kit";
984
937
  export {
985
938
  ACTIONS_REQUIRING_TARGET,
986
939
  EXCLUDED_LINK_PATTERNS,
@@ -988,7 +941,7 @@ export {
988
941
  FIX_PLAN_SYSTEM_PROMPT,
989
942
  INTERACTIVE_SELECTORS,
990
943
  INVALID_TARGETS,
991
- LANGUAGE_VOICES,
944
+ WIDE_VOICES as LANGUAGE_VOICES,
992
945
  REFINE_PLAN_SYSTEM_PROMPT,
993
946
  buildSystemPrompt,
994
947
  buildUserPrompt,
@@ -1004,7 +957,7 @@ export {
1004
957
  getTierByEmail,
1005
958
  hasAccess,
1006
959
  hasTrialRemaining,
1007
- hexToASS,
960
+ hexToAss as hexToASS,
1008
961
  isCanceling,
1009
962
  isExcludedLink,
1010
963
  isInvalidTarget,
@@ -1021,5 +974,5 @@ export {
1021
974
  showPaywall,
1022
975
  toTitleCase,
1023
976
  validateStep,
1024
- windowsPathToWSL
977
+ windowsDrivePathToWsl as windowsPathToWSL
1025
978
  };
package/mcp.mjs ADDED
@@ -0,0 +1,75 @@
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 { 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
+
72
+ main().catch((e) => {
73
+ console.error(`quickpeek-mcp failed to start: ${e?.message ?? e}`);
74
+ process.exit(1);
75
+ });