@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.
- package/LICENSE +0 -0
- package/README.md +4 -0
- package/dist/assets/qp.wm +0 -0
- package/dist/highlight.css +0 -0
- package/dist/index.d.mts +3 -5
- package/dist/index.mjs +199 -198
- package/dist/mcp-tools.mjs +5909 -0
- package/dist/qp.js +1316 -1154
- package/dist/{voices-BtYZey4Y.d.mts → scraping-BK8pIJhm.d.mts} +30 -27
- package/dist/web.d.mts +2 -1
- package/dist/web.mjs +129 -176
- package/mcp.mjs +75 -0
- package/package.json +80 -76
|
@@ -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
|
-
*
|
|
69
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
107
|
-
async function
|
|
97
|
+
var cachedDirectToken = null;
|
|
98
|
+
async function getOrRefreshDirectToken() {
|
|
108
99
|
const now = Date.now();
|
|
109
|
-
if (
|
|
110
|
-
return
|
|
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 (!
|
|
119
|
-
throw new Error(data.error || `Token exchange failed:
|
|
112
|
+
if (!data.success || !data.token) {
|
|
113
|
+
throw new Error(data.error || `Token exchange failed: unexpected response`);
|
|
120
114
|
}
|
|
121
|
-
|
|
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
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
354
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
931
|
-
|
|
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(
|
|
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
|
+
});
|