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