@spark-apps/quickpeek 1.2.0 → 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/dist/index.d.mts +2 -2
- package/dist/index.mjs +31 -14
- package/dist/mcp-tools.mjs +634 -337
- package/dist/qp.js +413 -188
- package/dist/{scraping-BK8pIJhm.d.mts → scraping-CHN4ClyT.d.mts} +7 -3
- package/dist/web.d.mts +42 -2
- package/dist/web.mjs +110 -13
- package/mcp.mjs +24 -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.
|
|
4
|
+
declare const VERSION = "1.2.1";
|
|
5
5
|
declare const CONFIG_FILE = "quickpeek.config.json";
|
|
6
6
|
type UserTier = 'free' | 'pro';
|
|
7
7
|
interface Config {
|
|
@@ -187,9 +187,13 @@ interface Plan {
|
|
|
187
187
|
*/
|
|
188
188
|
declare function crawlPage(url: string, task?: (text: string) => {
|
|
189
189
|
succeed: (text?: string) => void;
|
|
190
|
-
}, storageStatePath?: string
|
|
190
|
+
}, storageStatePath?: string, viewport?: {
|
|
191
|
+
width: number;
|
|
192
|
+
height: number;
|
|
193
|
+
}): Promise<{
|
|
191
194
|
title: string;
|
|
192
195
|
elements: ElementInfo[];
|
|
196
|
+
scrollable: number;
|
|
193
197
|
}>;
|
|
194
198
|
|
|
195
199
|
/**
|
|
@@ -229,7 +233,7 @@ declare function toTitleCase(text: string): string;
|
|
|
229
233
|
*/
|
|
230
234
|
|
|
231
235
|
declare function buildSystemPrompt(maxSteps: number): string;
|
|
232
|
-
declare function buildUserPrompt(url: string, title: string, elements: ElementInfo[], langName: string | null, description: string | undefined, maxSteps: number): string;
|
|
236
|
+
declare function buildUserPrompt(url: string, title: string, elements: ElementInfo[], langName: string | null, description: string | undefined, maxSteps: number, scrollable?: number): string;
|
|
233
237
|
|
|
234
238
|
type Tier = 'free' | 'pro';
|
|
235
239
|
declare const FEATURES: Record<string, string[]>;
|
package/dist/web.d.mts
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
|
-
|
|
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';
|
|
2
3
|
export { WIDE_VOICES as LANGUAGE_VOICES, hexToAss as hexToASS, windowsDrivePathToWsl as windowsPathToWSL } from '@spark-apps/video-kit';
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The plan file, and handing it to the browser editor.
|
|
7
|
+
*
|
|
8
|
+
* The JSON on disk is the source of truth: the CLI records from it, and the
|
|
9
|
+
* editor is a view onto it. The handover travels in a URL fragment, which no
|
|
10
|
+
* browser sends to a server — so editing a demo costs no upload, no account,
|
|
11
|
+
* and leaves no stored copy that can drift from the file being recorded.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors VidLet's lib/project validate + file-io split, so whoever maintains
|
|
14
|
+
* both codebases finds the same shape in each.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type ParseResult = {
|
|
18
|
+
ok: true;
|
|
19
|
+
plan: Plan;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Browsers accept far longer URLs than this, but shells, terminals and the
|
|
26
|
+
* `open` handlers in between are the weak link. Past it, the editor is opened
|
|
27
|
+
* empty and the user picks the file by hand.
|
|
28
|
+
*/
|
|
29
|
+
declare const MAX_HANDOFF_URL_CHARS = 32000;
|
|
30
|
+
/** Canonical on-disk form: 2-space JSON with a trailing newline. */
|
|
31
|
+
declare function serializePlan(plan: Plan): string;
|
|
32
|
+
/**
|
|
33
|
+
* Parse plan JSON, rejecting anything the recorder could not act on. Steps
|
|
34
|
+
* missing an id are numbered rather than refused: the editor drops ids when
|
|
35
|
+
* a user adds a step by hand, and renumbering is what the CLI would do too.
|
|
36
|
+
*/
|
|
37
|
+
declare function parsePlanText(text: string): ParseResult;
|
|
38
|
+
declare function encodePlan(plan: Plan): string;
|
|
39
|
+
/** Decode a `#plan=` fragment. Same validation as reading the file. */
|
|
40
|
+
declare function decodePlan(encoded: string): ParseResult;
|
|
41
|
+
/** The editor URL carrying a plan, or null when it is too long to hand over. */
|
|
42
|
+
declare function planEditorUrl(baseUrl: string, plan: Plan): string | null;
|
|
43
|
+
|
|
4
44
|
/**
|
|
5
45
|
* Plan Validation - Shared between CLI and Web
|
|
6
46
|
* Contains validation logic, prompts, and value preservation utilities
|
|
@@ -100,4 +140,4 @@ declare function restorePreservedValues<T extends BaseStep & {
|
|
|
100
140
|
mediaIdx: number;
|
|
101
141
|
};
|
|
102
142
|
|
|
103
|
-
export { ACTIONS_REQUIRING_TARGET, type BaseStep, type CLIPlan, type CLIStep, FIX_PLAN_SYSTEM_PROMPT, INVALID_TARGETS, type PreservedValues, REFINE_PLAN_SYSTEM_PROMPT, type StepAction, type StepFailure, collectPreservedValues, filterValidSteps, isInvalidTarget, isValidStep, restorePreservedValues, validateStep };
|
|
143
|
+
export { ACTIONS_REQUIRING_TARGET, type BaseStep, type CLIPlan, type CLIStep, FIX_PLAN_SYSTEM_PROMPT, INVALID_TARGETS, MAX_HANDOFF_URL_CHARS, type ParseResult, Plan, type PreservedValues, REFINE_PLAN_SYSTEM_PROMPT, type StepAction, type StepFailure, collectPreservedValues, decodePlan, encodePlan, filterValidSteps, isInvalidTarget, isValidStep, parsePlanText, planEditorUrl, restorePreservedValues, serializePlan, validateStep };
|
package/dist/web.mjs
CHANGED
|
@@ -75,7 +75,7 @@ function getErrorMessage(err) {
|
|
|
75
75
|
return err instanceof Error ? err.message : String(err);
|
|
76
76
|
}
|
|
77
77
|
function toTitleCase(text) {
|
|
78
|
-
return text.replace(/\w
|
|
78
|
+
return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
|
|
79
79
|
if (index === 0) {
|
|
80
80
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
81
81
|
}
|
|
@@ -93,7 +93,9 @@ var SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
|
93
93
|
var RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
|
|
94
94
|
var RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
|
|
95
95
|
var RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
|
|
96
|
-
|
|
96
|
+
function apiKey() {
|
|
97
|
+
return process.env.SPARK_AI_KEY || "";
|
|
98
|
+
}
|
|
97
99
|
var cachedDirectToken = null;
|
|
98
100
|
async function getOrRefreshDirectToken() {
|
|
99
101
|
const now = Date.now();
|
|
@@ -103,7 +105,7 @@ async function getOrRefreshDirectToken() {
|
|
|
103
105
|
try {
|
|
104
106
|
const response = await fetch(SPARK_AI_TOKEN, {
|
|
105
107
|
method: "POST",
|
|
106
|
-
headers: { "X-Api-Key":
|
|
108
|
+
headers: { "X-Api-Key": apiKey() }
|
|
107
109
|
});
|
|
108
110
|
if (!response.ok) {
|
|
109
111
|
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
|
|
@@ -189,7 +191,7 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
189
191
|
return lastRateLimitError || { success: false, error: "Request failed after retries" };
|
|
190
192
|
}
|
|
191
193
|
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
192
|
-
if (!
|
|
194
|
+
if (!apiKey()) {
|
|
193
195
|
return {
|
|
194
196
|
success: false,
|
|
195
197
|
error: "AI service is not configured. Please report this issue.",
|
|
@@ -351,20 +353,29 @@ var noopTask = () => ({ succeed: () => {
|
|
|
351
353
|
var CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
352
354
|
var CRAWL_CACHE_MAX_ENTRIES = 50;
|
|
353
355
|
var crawlCache = /* @__PURE__ */ new Map();
|
|
354
|
-
async function crawlPage(url, task = noopTask, storageStatePath) {
|
|
355
|
-
const
|
|
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);
|
|
356
359
|
if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
|
|
357
360
|
task("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
|
|
358
|
-
return { title: cached.title, elements: [...cached.elements] };
|
|
361
|
+
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
359
362
|
}
|
|
360
363
|
const browserTask = task("Launching browser");
|
|
361
364
|
const browser = await chromium.launch({ headless: true });
|
|
362
|
-
const context = await browser.newContext(
|
|
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
|
+
});
|
|
363
371
|
const page = await context.newPage();
|
|
364
372
|
browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
|
|
365
373
|
const crawlTask = task("Crawling page");
|
|
366
374
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
367
375
|
const title = await page.title();
|
|
376
|
+
const scrollable = await page.evaluate(
|
|
377
|
+
"Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
|
|
378
|
+
);
|
|
368
379
|
const rawElements = await page.evaluate(`
|
|
369
380
|
(() => {
|
|
370
381
|
const items = [];
|
|
@@ -494,12 +505,12 @@ async function crawlPage(url, task = noopTask, storageStatePath) {
|
|
|
494
505
|
});
|
|
495
506
|
await browser.close();
|
|
496
507
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
497
|
-
crawlCache.set(
|
|
508
|
+
crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
|
|
498
509
|
if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
|
|
499
510
|
const oldest = crawlCache.keys().next().value;
|
|
500
511
|
if (oldest !== void 0) crawlCache.delete(oldest);
|
|
501
512
|
}
|
|
502
|
-
return { title, elements };
|
|
513
|
+
return { title, elements, scrollable };
|
|
503
514
|
}
|
|
504
515
|
|
|
505
516
|
// src/lib/plan-validation.ts
|
|
@@ -733,6 +744,80 @@ function restorePreservedValues(step, preserved, indices) {
|
|
|
733
744
|
return { uploadIdx, mediaIdx };
|
|
734
745
|
}
|
|
735
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
|
+
|
|
736
821
|
// src/lib/prompts.ts
|
|
737
822
|
function buildSystemPrompt(maxSteps) {
|
|
738
823
|
let scope;
|
|
@@ -786,7 +871,8 @@ EXAMPLES:
|
|
|
786
871
|
\u2022 File upload: "target": "input[type=\\"file\\"]"
|
|
787
872
|
\u2022 Slider: "target": "input[type=\\"range\\"]"`;
|
|
788
873
|
}
|
|
789
|
-
|
|
874
|
+
var MIN_USEFUL_SCROLL_PX = 200;
|
|
875
|
+
function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
|
|
790
876
|
const lang = langName ? `
|
|
791
877
|
Language: Write ALL captions in ${langName}.` : "";
|
|
792
878
|
const purpose = description ? `
|
|
@@ -824,8 +910,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
|
|
|
824
910
|
${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
825
911
|
}
|
|
826
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" : "";
|
|
827
918
|
return `URL: ${url}
|
|
828
|
-
Title: ${title}${purpose}
|
|
919
|
+
Title: ${title}${purpose}${scrolling}
|
|
829
920
|
|
|
830
921
|
AVAILABLE ELEMENTS (grouped by action):
|
|
831
922
|
${elementSummary}
|
|
@@ -835,7 +926,7 @@ REQUIREMENTS:
|
|
|
835
926
|
\u2022 Complete ONE user journey: input \u2192 process \u2192 output
|
|
836
927
|
\u2022 For CLICK: use simplified text target like "extract frame" or "download"
|
|
837
928
|
\u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
|
|
838
|
-
\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}
|
|
839
930
|
|
|
840
931
|
Return JSON:
|
|
841
932
|
{
|
|
@@ -942,12 +1033,15 @@ export {
|
|
|
942
1033
|
INTERACTIVE_SELECTORS,
|
|
943
1034
|
INVALID_TARGETS,
|
|
944
1035
|
WIDE_VOICES as LANGUAGE_VOICES,
|
|
1036
|
+
MAX_HANDOFF_URL_CHARS,
|
|
945
1037
|
REFINE_PLAN_SYSTEM_PROMPT,
|
|
946
1038
|
buildSystemPrompt,
|
|
947
1039
|
buildUserPrompt,
|
|
948
1040
|
callAI,
|
|
949
1041
|
collectPreservedValues,
|
|
950
1042
|
crawlPage,
|
|
1043
|
+
decodePlan,
|
|
1044
|
+
encodePlan,
|
|
951
1045
|
filterValidSteps,
|
|
952
1046
|
getErrorMessage,
|
|
953
1047
|
getLanguageName,
|
|
@@ -967,9 +1061,12 @@ export {
|
|
|
967
1061
|
normalizeUrl,
|
|
968
1062
|
parseAIPlanResponse,
|
|
969
1063
|
parsePartial,
|
|
1064
|
+
parsePlanText,
|
|
1065
|
+
planEditorUrl,
|
|
970
1066
|
pricingUrl,
|
|
971
1067
|
restorePreservedValues,
|
|
972
1068
|
rgbaToHex,
|
|
1069
|
+
serializePlan,
|
|
973
1070
|
shouldSkipLink,
|
|
974
1071
|
showPaywall,
|
|
975
1072
|
toTitleCase,
|
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 { TOOL_HANDLERS, TOOLS } from './dist/mcp-tools.mjs';
|
|
20
|
+
import { 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
|
|
|
@@ -67,6 +67,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
67
67
|
async function main() {
|
|
68
68
|
const transport = new StdioServerTransport(process.stdin, protocolStdout);
|
|
69
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
|
+
});
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
main().catch((e) => {
|