@spark-apps/quickpeek 1.2.1 → 1.2.2

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.
@@ -27,6 +27,8 @@ interface Config {
27
27
  contrast: number;
28
28
  tempo: number;
29
29
  stepDelay: number;
30
+ outro?: string;
31
+ zoom?: number;
30
32
  };
31
33
  audio?: {
32
34
  voice?: string;
@@ -91,18 +93,48 @@ interface AIError {
91
93
  isRateLimit?: boolean;
92
94
  isAuthError?: boolean;
93
95
  retryAfterSeconds?: number;
96
+ /** Upstream HTTP status, when the failure was an HTTP response. */
97
+ status?: number;
98
+ /** Host that produced the failure, so relay vs backend is distinguishable. */
99
+ host?: string;
100
+ /** Raw response body (truncated), for the error log. */
101
+ detail?: string;
94
102
  }
95
103
  type AIResult = AIResponse | AIError;
104
+ /** Called before each backoff so the CLI can explain a long pause. */
105
+ type RetryNotice = (info: {
106
+ attempt: number;
107
+ status: number;
108
+ delayMs: number;
109
+ host: string;
110
+ }) => void;
111
+ interface ChatOptions {
112
+ onRetry?: RetryNotice;
113
+ /**
114
+ * Backoff before each 5xx retry. Defaults to SERVER_BACKOFF_MS, which is
115
+ * sized to fit inside a serverless function budget — callAI runs inside the
116
+ * quickpeek-web routes as well as the CLI. Interactive callers should pass
117
+ * CLI_BACKOFF_MS, which can afford to outwait the backend's closed window.
118
+ */
119
+ backoffMs?: number[];
120
+ }
121
+ /** Short enough to stay well inside a serverless maxDuration. */
122
+ declare const SERVER_BACKOFF_MS: number[];
123
+ /**
124
+ * Long enough to outlast the AI backend's ~45-60s closed window (measured
125
+ * 16 Aug 2026). Only safe where a human is waiting on a spinner.
126
+ */
127
+ declare const CLI_BACKOFF_MS: number[];
96
128
  /**
97
129
  * Direct Spark AI call using the runtime SPARK_AI_KEY (dev + server-side use).
98
130
  */
99
- declare function callAI(systemPrompt: string, userPrompt: string, tier?: UserTier): Promise<AIResult>;
131
+ declare function callAI(systemPrompt: string, userPrompt: string, tier?: UserTier, opts?: ChatOptions): Promise<AIResult>;
100
132
  /**
101
133
  * Relayed Spark AI call for the published CLI: authenticates with the user's
102
134
  * registered email via quickpeek-web, which holds the real key server-side
103
135
  * and enforces per-user daily limits.
104
136
  */
105
- declare function callAIViaRelay(systemPrompt: string, userPrompt: string, email: string, tier?: UserTier): Promise<AIResult>;
137
+ declare function callAIViaRelay(systemPrompt: string, userPrompt: string, email: string, tier?: UserTier, opts?: ChatOptions): Promise<AIResult>;
106
138
  /**
107
139
  * Parse AI-generated plan JSON from response text
108
140
  */
@@ -179,6 +211,15 @@ interface Plan {
179
211
  * Page crawling and interactive element extraction
180
212
  */
181
213
 
214
+ /**
215
+ * A selector matching one id.
216
+ *
217
+ * HTML ids are far more permissive than CSS identifiers: `description-(optional)`
218
+ * is a perfectly legal id, but `#description-(optional)` is invalid CSS and
219
+ * Playwright rejects it, so the step silently finds nothing. Anything that is
220
+ * not identifier-safe gets the attribute form instead.
221
+ */
222
+ declare function idSelector(id: string): string;
182
223
  /**
183
224
  * Crawl a page and extract interactive elements with smart selectors
184
225
  * Results are cached in-memory for a few minutes to avoid duplicate browser launches
@@ -291,4 +332,4 @@ declare function isExcludedLink(text: string, href?: string): boolean;
291
332
  /** Check if a link should be skipped during crawling */
292
333
  declare function shouldSkipLink(text: string, href: string, currentPath: string): boolean;
293
334
 
294
- 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 };
335
+ export { toTitleCase as $, type AIError as A, hasAccess as B, type Config as C, DEFAULT_CAPTIONS as D, EXCLUDED_LINK_PATTERNS as E, FEATURES as F, hasTrialRemaining as G, type HighlightMode as H, INTERACTIVE_SELECTORS as I, idSelector as J, isCanceling as K, isExcludedLink as L, isPaid as M, isVerified as N, normalizeUrl as O, type Plan as P, parseAIPlanResponse as Q, type RateLimitInfo as R, SERVER_BACKOFF_MS as S, type Tier as T, type UserTier as U, VERSION as V, parsePartial as W, pricingUrl as X, rgbaToHex as Y, shouldSkipLink as Z, showPaywall as _, type AIResponse as a, getTier as a0, type AIResult as b, CLI_BACKOFF_MS as c, CONFIG_FILE as d, type CaptionStyle as e, type CaptionWordStyle as f, type ChatOptions as g, DEFAULT_CONFIG as h, type ElementInfo as i, type ElementType as j, type RetryNotice as k, type SparkStatus as l, type SparkSubscription as m, type SparkTrial as n, type Step as o, type SuggestedAction as p, buildSystemPrompt as q, buildUserPrompt as r, callAI as s, callAIViaRelay as t, crawlPage as u, getErrorMessage as v, getLanguageName as w, getStatus as x, getStatusCached as y, getTierByEmail as z };
package/dist/web.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as Plan } from './scraping-CHN4ClyT.mjs';
2
- 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, 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-CHN4ClyT.mjs';
1
+ import { P as Plan } from './scraping-BgepklP3.mjs';
2
+ export { A as AIError, a as AIResponse, b as AIResult, E as EXCLUDED_LINK_PATTERNS, i as ElementInfo, j as ElementType, F as FEATURES, H as HighlightMode, I as INTERACTIVE_SELECTORS, R as RateLimitInfo, l as SparkStatus, m as SparkSubscription, n as SparkTrial, o as Step, p as SuggestedAction, T as Tier, q as buildSystemPrompt, r as buildUserPrompt, s as callAI, u as crawlPage, v as getErrorMessage, w as getLanguageName, x as getStatus, y as getStatusCached, a0 as getTier, z as getTierByEmail, B as hasAccess, G as hasTrialRemaining, K as isCanceling, L as isExcludedLink, M as isPaid, N as isVerified, O as normalizeUrl, Q as parseAIPlanResponse, W as parsePartial, X as pricingUrl, Y as rgbaToHex, Z as shouldSkipLink, _ as showPaywall, $ as toTitleCase } from './scraping-BgepklP3.mjs';
3
3
  export { WIDE_VOICES as LANGUAGE_VOICES, hexToAss as hexToASS, windowsDrivePathToWsl as windowsPathToWSL } from '@spark-apps/video-kit';
4
4
 
5
5
  /**
package/dist/web.mjs CHANGED
@@ -87,6 +87,7 @@ function toTitleCase(text) {
87
87
  }
88
88
 
89
89
  // src/lib/api-client.ts
90
+ var SERVER_BACKOFF_MS = [2e3, 4e3];
90
91
  var SPARK_AI_BASE = "https://sparkbrain.app";
91
92
  var SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
92
93
  var SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
@@ -96,6 +97,20 @@ var RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
96
97
  function apiKey() {
97
98
  return process.env.SPARK_AI_KEY || "";
98
99
  }
100
+ function hostOf(url) {
101
+ try {
102
+ return new URL(url).host;
103
+ } catch {
104
+ return "AI service";
105
+ }
106
+ }
107
+ function firstLine(text) {
108
+ return truncate((text ?? "").trim().split("\n")[0] ?? "", 200);
109
+ }
110
+ function truncate(text, max) {
111
+ const trimmed = (text ?? "").trim();
112
+ return trimmed.length > max ? `${trimmed.slice(0, max)}\u2026` : trimmed;
113
+ }
99
114
  var cachedDirectToken = null;
100
115
  async function getOrRefreshDirectToken() {
101
116
  const now = Date.now();
@@ -123,8 +138,10 @@ async function getOrRefreshDirectToken() {
123
138
  throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
124
139
  }
125
140
  }
126
- async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
127
- let lastRateLimitError = null;
141
+ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier, opts = {}) {
142
+ const { onRetry, backoffMs = SERVER_BACKOFF_MS } = opts;
143
+ let lastRetriableError = null;
144
+ const host = hostOf(chatUrl);
128
145
  for (let attempt = 1; attempt <= 3; attempt++) {
129
146
  try {
130
147
  const response = await fetch(chatUrl, {
@@ -140,36 +157,53 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
140
157
  ]
141
158
  })
142
159
  });
160
+ const bodyText = await response.text().catch(() => "");
143
161
  let data = {};
144
- const contentType = response.headers.get("content-type") ?? "";
145
- if (contentType.includes("application/json")) {
146
- data = await response.json();
162
+ try {
163
+ data = bodyText ? JSON.parse(bodyText) : {};
164
+ } catch {
147
165
  }
148
166
  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 = {
167
+ const upstream = data.error || firstLine(bodyText) || response.statusText || "no error body";
168
+ const error = `${host} ${response.status}: ${upstream}`;
169
+ const detail = truncate(bodyText, 800);
170
+ const isRetriable = response.status === 429 || response.status >= 500;
171
+ if (isRetriable && attempt < 3) {
172
+ const retryAfter2 = response.headers.get("Retry-After");
173
+ const delayMs = retryAfter2 ? parseInt(retryAfter2, 10) * 1e3 : response.status >= 500 ? backoffMs[attempt - 1] ?? backoffMs[backoffMs.length - 1] ?? 2e3 : 1e3 * 2 ** (attempt - 1);
174
+ onRetry?.({ attempt, status: response.status, delayMs, host });
175
+ lastRetriableError = {
154
176
  success: false,
155
177
  error,
156
- isRateLimit: true,
157
- ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
178
+ isRateLimit: response.status === 429,
179
+ status: response.status,
180
+ host,
181
+ ...detail ? { detail } : {},
182
+ ...retryAfter2 ? { retryAfterSeconds: parseInt(retryAfter2, 10) } : {}
158
183
  };
159
184
  await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, 3e4)));
160
185
  continue;
161
186
  }
187
+ const retryAfter = response.headers.get("Retry-After");
188
+ const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : Number.NaN;
162
189
  return {
163
190
  success: false,
164
191
  error,
165
192
  isRateLimit: response.status === 429,
166
- isAuthError: response.status === 401 || response.status === 403
193
+ isAuthError: response.status === 401 || response.status === 403,
194
+ status: response.status,
195
+ host,
196
+ ...detail ? { detail } : {},
197
+ ...Number.isFinite(retryAfterSeconds) ? { retryAfterSeconds } : {}
167
198
  };
168
199
  }
169
200
  if (!data.success || !data.response) {
170
201
  return {
171
202
  success: false,
172
- error: data.error || "No response from Spark AI"
203
+ error: `${host} 200 but no content: ${data.error || firstLine(bodyText) || "empty response"}`,
204
+ status: response.status,
205
+ host,
206
+ ...bodyText ? { detail: truncate(bodyText, 800) } : {}
173
207
  };
174
208
  }
175
209
  return {
@@ -183,14 +217,16 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
183
217
  }
184
218
  };
185
219
  } catch (err) {
186
- if (attempt === 3) throw err;
220
+ if (attempt === 3) {
221
+ return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
222
+ }
187
223
  const delayMs = 1e3 * 2 ** (attempt - 1);
188
224
  await new Promise((resolve) => setTimeout(resolve, delayMs));
189
225
  }
190
226
  }
191
- return lastRateLimitError || { success: false, error: "Request failed after retries" };
227
+ return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
192
228
  }
193
- async function callAI(systemPrompt, userPrompt, tier = "free") {
229
+ async function callAI(systemPrompt, userPrompt, tier = "free", opts = {}) {
194
230
  if (!apiKey()) {
195
231
  return {
196
232
  success: false,
@@ -200,7 +236,7 @@ async function callAI(systemPrompt, userPrompt, tier = "free") {
200
236
  }
201
237
  try {
202
238
  const token = await getOrRefreshDirectToken();
203
- return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
239
+ return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier, opts);
204
240
  } catch (err) {
205
241
  return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
206
242
  }
@@ -331,6 +367,9 @@ function shouldSkipLink(text, href, currentPath) {
331
367
  }
332
368
 
333
369
  // src/lib/crawl.ts
370
+ function idSelector(id) {
371
+ return /^[A-Za-z_-][\w-]*$/.test(id) ? `#${id}` : `[id="${id.replace(/(["\\])/g, "\\$1")}"]`;
372
+ }
334
373
  function getActionForType(type) {
335
374
  switch (type) {
336
375
  case "button":
@@ -465,9 +504,9 @@ async function crawlPage(url, task = noopTask, storageStatePath, viewport) {
465
504
  } else if (el.isFile || el.type === "file") {
466
505
  selector = 'input[type="file"]';
467
506
  } else if (el.type === "range") {
468
- selector = el.id ? `#${el.id}` : 'input[type="range"]';
507
+ selector = el.id ? idSelector(el.id) : 'input[type="range"]';
469
508
  } else if (el.type === "checkbox") {
470
- selector = el.id ? `#${el.id}` : 'input[type="checkbox"]';
509
+ selector = el.id ? idSelector(el.id) : 'input[type="checkbox"]';
471
510
  } else if (el.type === "button") {
472
511
  if (btnTexts[el.text] === 1) {
473
512
  selector = `text="${el.text}"`;
@@ -477,7 +516,7 @@ async function crawlPage(url, task = noopTask, storageStatePath, viewport) {
477
516
  } else if (el.type === "link") {
478
517
  selector = `text="${el.text}"`;
479
518
  } else if (el.id) {
480
- selector = `#${el.id}`;
519
+ selector = idSelector(el.id);
481
520
  } else if (el.name) {
482
521
  selector = `[name="${el.name}"]`;
483
522
  } else if (el.placeholder) {
@@ -952,7 +991,8 @@ TARGET SYNTAX:
952
991
 
953
992
  // src/lib/SparkStripe-feature-gating.ts
954
993
  var APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
955
- var BASE = "https://sparkstripe.com";
994
+ var OWNER_SLUG = process.env.SPARK_OWNER_SLUG ?? "spark";
995
+ var BASE = "https://sparkpay.dev";
956
996
  var TIERS = {
957
997
  price_1TADnPEf9opkuWid9NtqOYON: "pro",
958
998
  price_1TADnPEf9opkuWidgbvvX9ma: "pro",
@@ -995,7 +1035,7 @@ async function getStatusCached(email) {
995
1035
  function getTier(d) {
996
1036
  if (!d.subscription || !d.registered) return "free";
997
1037
  if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
998
- return TIERS[d.subscription.price_id] ?? "free";
1038
+ return TIERS[d.subscription.price_id] ?? "pro";
999
1039
  }
1000
1040
  var isPaid = (d) => getTier(d) !== "free";
1001
1041
  var hasAccess = (d, min) => (RANK[getTier(d)] ?? 0) >= (RANK[min] ?? 0);
@@ -1017,7 +1057,7 @@ async function isVerified(email) {
1017
1057
  }
1018
1058
  }
1019
1059
  function pricingUrl(opts) {
1020
- const u = new URL(`${BASE}/pricing/${APP_ID}`);
1060
+ const u = new URL(`${BASE}/${OWNER_SLUG}/${APP_ID}`);
1021
1061
  if (opts?.email) u.searchParams.set("email", opts.email);
1022
1062
  if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
1023
1063
  return u.toString();
package/mcp.mjs CHANGED
@@ -17,7 +17,7 @@ import { readFileSync } from 'node:fs';
17
17
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
18
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
19
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
20
- import { setElicit, TOOL_HANDLERS, TOOLS } from './dist/mcp-tools.mjs';
20
+ import { setCanElicit, setElicit, TOOL_HANDLERS, TOOLS } from './dist/mcp-tools.mjs';
21
21
 
22
22
  const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
23
23
 
@@ -90,6 +90,10 @@ async function main() {
90
90
  });
91
91
  return result.action === 'accept' && result.content?.signedIn === true;
92
92
  });
93
+
94
+ // Read per call for the same reason as above: `initialize` has not arrived
95
+ // yet. Lets tools skip opening a sign-in window no prompt could ever confirm.
96
+ setCanElicit(() => !!server.getClientCapabilities()?.elicitation);
93
97
  }
94
98
 
95
99
  main().catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spark-apps/quickpeek",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Auto-generate demo videos for your web app with AI",
5
5
  "main": "dist/qp.js",
6
6
  "bin": {