@spark-apps/quickpeek 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -204
- package/dist/highlight.css +15 -15
- package/dist/index.d.mts +79 -3
- package/dist/index.mjs +254 -52
- package/dist/mcp-tools.mjs +1295 -603
- package/dist/qp.js +1272 -729
- package/dist/{scraping-CHN4ClyT.d.mts → scraping-C334VxX5.d.mts} +45 -4
- package/dist/web.d.mts +2 -2
- package/dist/web.mjs +64 -24
- package/mcp.mjs +5 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Configuration types and defaults for QuickPeek
|
|
3
3
|
*/
|
|
4
|
-
declare const VERSION = "1.2.
|
|
4
|
+
declare const VERSION = "1.2.3";
|
|
5
5
|
declare const CONFIG_FILE = "quickpeek.config.json";
|
|
6
6
|
type UserTier = 'free' | 'pro';
|
|
7
7
|
interface Config {
|
|
@@ -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,
|
|
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-
|
|
2
|
-
export { A as AIError, a as AIResponse, b as AIResult, E as EXCLUDED_LINK_PATTERNS,
|
|
1
|
+
import { P as Plan } from './scraping-C334VxX5.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-C334VxX5.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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
try {
|
|
163
|
+
data = bodyText ? JSON.parse(bodyText) : {};
|
|
164
|
+
} catch {
|
|
147
165
|
}
|
|
148
166
|
if (!response.ok) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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:
|
|
157
|
-
|
|
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 || "
|
|
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)
|
|
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
|
|
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 ?
|
|
507
|
+
selector = el.id ? idSelector(el.id) : 'input[type="range"]';
|
|
469
508
|
} else if (el.type === "checkbox") {
|
|
470
|
-
selector = el.id ?
|
|
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 =
|
|
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
|
|
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] ?? "
|
|
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}
|
|
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) => {
|