surf-cli 2.9.0 → 2.11.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/README.md +61 -4
- package/dist/content/accessibility-tree.js +11 -0
- package/dist/content/accessibility-tree.js.map +1 -0
- package/dist/content/visual-indicator.js +111 -0
- package/dist/content/visual-indicator.js.map +1 -0
- package/dist/manifest.json +11 -2
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +61 -261
- package/dist/service-worker/index.js.map +1 -1
- package/native/activity-journal.cjs +55 -0
- package/native/chatgpt-client-response.cjs +336 -0
- package/native/chatgpt-client-selection.cjs +119 -0
- package/native/chatgpt-client-ui.cjs +481 -0
- package/native/chatgpt-client.cjs +254 -664
- package/native/cli.cjs +100 -273
- package/native/do-executor.cjs +52 -475
- package/native/do-parser.cjs +8 -249
- package/native/host-helpers.cjs +32 -15
- package/native/host-sessions.cjs +6 -1
- package/native/host.cjs +228 -6
- package/native/network-export.cjs +20 -17
- package/native/network-store.cjs +38 -58
- package/native/oracle-cli.cjs +434 -0
- package/native/oracle-context.cjs +311 -0
- package/native/oracle-host.cjs +301 -0
- package/native/oracle-jobs.cjs +253 -0
- package/native/playbook-authoring.cjs +44 -0
- package/native/playbook-cli.cjs +157 -0
- package/native/playbook-client.cjs +259 -0
- package/native/playbook-receipts.cjs +109 -0
- package/native/playbook-records.cjs +208 -0
- package/native/playbook-runtime.cjs +177 -0
- package/native/playbooks.cjs +235 -0
- package/native/private-state.cjs +156 -0
- package/native/redaction.cjs +104 -0
- package/native/workflow-definition.cjs +369 -0
- package/native/workflow-runtime.cjs +225 -0
- package/package.json +2 -1
- package/playbooks/page/ops/read.json +22 -0
- package/playbooks/page/playbook.json +7 -0
- package/skills/surf/SKILL.md +72 -1
- package/dist/content/index.js +0 -116
- package/dist/content/index.js.map +0 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { appendPrivateJsonLine, atomicWriteFile, ensurePrivateDir, getPrivateStateRoot, readPrivateFile } = require("./private-state.cjs");
|
|
4
|
+
const { commandMetadata, redactCommandArgs } = require("./workflow-definition.cjs");
|
|
5
|
+
|
|
6
|
+
const MAX_JOURNAL_BYTES = 1024 * 1024;
|
|
7
|
+
const MAX_JOURNAL_EVENTS = 500;
|
|
8
|
+
|
|
9
|
+
function journalPath(root = getPrivateStateRoot()) {
|
|
10
|
+
return path.join(root, "activity-journal", "events.jsonl");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function compactJournal(filePath, root) {
|
|
14
|
+
const stat = fs.statSync(filePath);
|
|
15
|
+
if (stat.size <= MAX_JOURNAL_BYTES) return;
|
|
16
|
+
const lines = readPrivateFile(filePath, { root, encoding: "utf8" }).trim().split("\n").filter(Boolean).slice(-MAX_JOURNAL_EVENTS);
|
|
17
|
+
atomicWriteFile(filePath, `${lines.join("\n")}\n`, { root, encoding: "utf8" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function appendActivity(event, { root = getPrivateStateRoot() } = {}) {
|
|
21
|
+
const filePath = journalPath(root);
|
|
22
|
+
ensurePrivateDir(path.dirname(filePath), root);
|
|
23
|
+
appendPrivateJsonLine(filePath, { version: 1, ...event }, { root });
|
|
24
|
+
compactJournal(filePath, root);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function journalCommand(command, args, options = {}) {
|
|
28
|
+
const metadata = commandMetadata(command);
|
|
29
|
+
if (!metadata.recordable) return;
|
|
30
|
+
appendActivity({
|
|
31
|
+
type: "tool.issued",
|
|
32
|
+
command: metadata.name,
|
|
33
|
+
argsRedacted: redactCommandArgs(command, args, options.includeInputValues === true),
|
|
34
|
+
effect: metadata.effect,
|
|
35
|
+
...(options.tabId ? { tabId: options.tabId } : {}),
|
|
36
|
+
...(options.origin ? { origin: options.origin } : {}),
|
|
37
|
+
startedAt: new Date().toISOString(),
|
|
38
|
+
}, options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sinceMilliseconds(value) {
|
|
42
|
+
if (!value) return 60 * 60 * 1000;
|
|
43
|
+
const match = String(value).match(/^(\d+)(m|h|d)$/);
|
|
44
|
+
if (!match) throw new Error("--since must be a duration such as 30m, 1h, or 2d");
|
|
45
|
+
return Number(match[1]) * { m: 60000, h: 3600000, d: 86400000 }[match[2]];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readRecent({ since = "1h", root = getPrivateStateRoot() } = {}) {
|
|
49
|
+
const filePath = journalPath(root);
|
|
50
|
+
const content = readPrivateFile(filePath, { root, allowMissing: true, fallback: "", encoding: "utf8" });
|
|
51
|
+
const cutoff = Date.now() - sinceMilliseconds(since);
|
|
52
|
+
return content.split("\n").filter(Boolean).map((line) => JSON.parse(line)).filter((event) => Date.parse(event.startedAt || event.timestamp) >= cutoff);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { appendActivity, journalCommand, journalPath, readRecent };
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
const { throwIfAborted } = require("./abort.cjs");
|
|
2
|
+
const { SELECTORS, delay, evaluate } = require("./chatgpt-client-ui.cjs");
|
|
3
|
+
|
|
4
|
+
function normalizePromptEcho(prompt) {
|
|
5
|
+
return String(prompt || "")
|
|
6
|
+
.replace(/\s+/g, " ")
|
|
7
|
+
.trim()
|
|
8
|
+
.slice(0, 200);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function matchesPromptEcho(text, promptEcho) {
|
|
12
|
+
const expected = normalizePromptEcho(promptEcho);
|
|
13
|
+
return expected.length > 0 && normalizePromptEcho(text) === expected;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cleanChatGPTResponseText(rawText) {
|
|
17
|
+
if (!rawText) return "";
|
|
18
|
+
|
|
19
|
+
const chromeLines = new Set([
|
|
20
|
+
"copy",
|
|
21
|
+
"good response",
|
|
22
|
+
"bad response",
|
|
23
|
+
"read aloud",
|
|
24
|
+
"edit",
|
|
25
|
+
"retry",
|
|
26
|
+
"continue generating",
|
|
27
|
+
"share",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const lines = [];
|
|
31
|
+
let inCodeFence = false;
|
|
32
|
+
|
|
33
|
+
for (const line of String(rawText).replace(/\r\n?/g, "\n").split("\n")) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
const isFenceLine = trimmed.startsWith("```");
|
|
36
|
+
const normalizedLine = inCodeFence || isFenceLine ? line.replace(/[\t ]+$/g, "") : line;
|
|
37
|
+
|
|
38
|
+
lines.push({
|
|
39
|
+
text: normalizedLine,
|
|
40
|
+
trimmed,
|
|
41
|
+
isChrome: trimmed.length > 0 && chromeLines.has(trimmed.toLowerCase()),
|
|
42
|
+
inCodeFence,
|
|
43
|
+
isFenceLine,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (isFenceLine) {
|
|
47
|
+
inCodeFence = !inCodeFence;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
while (lines.length > 0 && lines[0].trimmed.length === 0) {
|
|
52
|
+
lines.shift();
|
|
53
|
+
}
|
|
54
|
+
while (lines.length > 0 && lines[lines.length - 1].trimmed.length === 0) {
|
|
55
|
+
lines.pop();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let trailingChromeStart = lines.length;
|
|
59
|
+
while (trailingChromeStart > 0) {
|
|
60
|
+
const line = lines[trailingChromeStart - 1];
|
|
61
|
+
if (line.inCodeFence || line.isFenceLine || !line.isChrome) break;
|
|
62
|
+
trailingChromeStart--;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const trailingChromeCount = lines.length - trailingChromeStart;
|
|
66
|
+
if (trailingChromeCount >= 2) {
|
|
67
|
+
lines.splice(trailingChromeStart);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
while (lines.length > 0 && lines[0].trimmed.length === 0) {
|
|
71
|
+
lines.shift();
|
|
72
|
+
}
|
|
73
|
+
while (lines.length > 0 && lines[lines.length - 1].trimmed.length === 0) {
|
|
74
|
+
lines.pop();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return lines.map((line) => line.text).join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function extractLatestAssistantSnapshot(candidates) {
|
|
81
|
+
if (!Array.isArray(candidates)) return null;
|
|
82
|
+
|
|
83
|
+
let latestEmptyAssistant = null;
|
|
84
|
+
|
|
85
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
86
|
+
const candidate = candidates[i];
|
|
87
|
+
if (!candidate?.isAssistant) continue;
|
|
88
|
+
|
|
89
|
+
const snapshot = {
|
|
90
|
+
...candidate,
|
|
91
|
+
text: cleanChatGPTResponseText(candidate?.text || ""),
|
|
92
|
+
turnIndex: i,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (snapshot.text) {
|
|
96
|
+
return snapshot;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!latestEmptyAssistant) {
|
|
100
|
+
latestEmptyAssistant = snapshot;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return latestEmptyAssistant;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeResponseSnapshot(rawSnapshot) {
|
|
108
|
+
const candidates = rawSnapshot?.candidates;
|
|
109
|
+
return {
|
|
110
|
+
latestAssistant: extractLatestAssistantSnapshot(candidates),
|
|
111
|
+
assistantCount: Array.isArray(candidates)
|
|
112
|
+
? candidates.filter((candidate) => candidate?.isAssistant).length
|
|
113
|
+
: 0,
|
|
114
|
+
stopVisible: Boolean(rawSnapshot?.stopVisible),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isNewAssistantContent(
|
|
119
|
+
latestAssistant,
|
|
120
|
+
baselineAssistant,
|
|
121
|
+
assistantCount = 0,
|
|
122
|
+
baselineAssistantCount = 0,
|
|
123
|
+
) {
|
|
124
|
+
if (!latestAssistant) return false;
|
|
125
|
+
if (!baselineAssistant) return true;
|
|
126
|
+
if (
|
|
127
|
+
latestAssistant.messageId &&
|
|
128
|
+
baselineAssistant.messageId &&
|
|
129
|
+
latestAssistant.messageId !== baselineAssistant.messageId
|
|
130
|
+
) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const currentText = latestAssistant.text || "";
|
|
135
|
+
const baselineText = baselineAssistant.text || "";
|
|
136
|
+
|
|
137
|
+
if (assistantCount > baselineAssistantCount) {
|
|
138
|
+
if (latestAssistant.turnIndex !== baselineAssistant.turnIndex) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
if (currentText !== baselineText) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return currentText !== baselineText;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isChatGPTResponseComplete(snapshot, stableCycles, stableMs) {
|
|
151
|
+
if (!snapshot?.text) return false;
|
|
152
|
+
if (snapshot.stopVisible) return false;
|
|
153
|
+
if (snapshot.hasFinishedActions) return true;
|
|
154
|
+
return stableCycles >= 6 && stableMs >= 1200;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function scopeSnapshotToPrompt(rawSnapshot, promptEcho) {
|
|
158
|
+
const candidates = Array.isArray(rawSnapshot?.candidates) ? rawSnapshot.candidates : [];
|
|
159
|
+
let userIndex = -1;
|
|
160
|
+
for (let index = candidates.length - 1; index >= 0; index--) {
|
|
161
|
+
const candidate = candidates[index];
|
|
162
|
+
if (candidate?.isUser && matchesPromptEcho(candidate.text, promptEcho)) {
|
|
163
|
+
userIndex = index;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (userIndex === -1) return { ...rawSnapshot, candidates: [] };
|
|
168
|
+
|
|
169
|
+
const following = candidates.slice(userIndex + 1);
|
|
170
|
+
const nextUserIndex = following.findIndex((candidate) => candidate?.isUser);
|
|
171
|
+
return {
|
|
172
|
+
...rawSnapshot,
|
|
173
|
+
candidates: nextUserIndex === -1 ? following : following.slice(0, nextUserIndex),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function readChatGPTResponseSnapshot(cdp) {
|
|
178
|
+
return evaluate(
|
|
179
|
+
cdp,
|
|
180
|
+
`(() => {
|
|
181
|
+
const scope = document.querySelector('main') || document;
|
|
182
|
+
const CONVERSATION_SELECTOR = ${JSON.stringify(SELECTORS.conversationTurn)};
|
|
183
|
+
const ASSISTANT_SELECTOR = ${JSON.stringify(SELECTORS.assistantMessage)};
|
|
184
|
+
const CONTENT_SELECTORS = ${JSON.stringify(SELECTORS.assistantContent.split(", "))};
|
|
185
|
+
const STOP_SELECTOR = ${JSON.stringify(SELECTORS.stopButton)};
|
|
186
|
+
const FINISHED_SELECTOR = ${JSON.stringify(SELECTORS.finishedActions)};
|
|
187
|
+
|
|
188
|
+
const toCandidate = (turnNode, messageRoot = null) => {
|
|
189
|
+
const resolvedMessageRoot = messageRoot || (turnNode.matches?.(ASSISTANT_SELECTOR)
|
|
190
|
+
? turnNode
|
|
191
|
+
: turnNode.querySelector(ASSISTANT_SELECTOR));
|
|
192
|
+
const authorRoot = turnNode.matches?.('[data-message-author-role], [data-turn]')
|
|
193
|
+
? turnNode
|
|
194
|
+
: turnNode.querySelector('[data-message-author-role], [data-turn]');
|
|
195
|
+
const searchRoot = resolvedMessageRoot || authorRoot || turnNode;
|
|
196
|
+
let contentRoot = null;
|
|
197
|
+
|
|
198
|
+
for (const selector of CONTENT_SELECTORS) {
|
|
199
|
+
const match = selector === '[dir="auto"]'
|
|
200
|
+
? (searchRoot.matches?.(selector) ? searchRoot : null)
|
|
201
|
+
: (searchRoot.matches?.(selector) ? searchRoot : searchRoot.querySelector(selector));
|
|
202
|
+
if (match) {
|
|
203
|
+
contentRoot = match;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const role =
|
|
209
|
+
resolvedMessageRoot?.getAttribute('data-message-author-role') ||
|
|
210
|
+
authorRoot?.getAttribute('data-message-author-role') ||
|
|
211
|
+
turnNode.getAttribute('data-message-author-role') ||
|
|
212
|
+
null;
|
|
213
|
+
const turn =
|
|
214
|
+
resolvedMessageRoot?.getAttribute('data-turn') ||
|
|
215
|
+
authorRoot?.getAttribute('data-turn') ||
|
|
216
|
+
turnNode.getAttribute('data-turn') ||
|
|
217
|
+
null;
|
|
218
|
+
const isAssistant =
|
|
219
|
+
role === 'assistant' ||
|
|
220
|
+
turn === 'assistant' ||
|
|
221
|
+
resolvedMessageRoot !== null;
|
|
222
|
+
const isUser = role === 'user' || turn === 'user';
|
|
223
|
+
const text = (contentRoot || turnNode).innerText || (contentRoot || turnNode).textContent || '';
|
|
224
|
+
const messageId =
|
|
225
|
+
resolvedMessageRoot?.getAttribute('data-message-id') ||
|
|
226
|
+
turnNode.getAttribute('data-message-id') ||
|
|
227
|
+
null;
|
|
228
|
+
const hasFinishedActions = Boolean(turnNode.querySelector(FINISHED_SELECTOR));
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
role,
|
|
232
|
+
turn,
|
|
233
|
+
isAssistant,
|
|
234
|
+
isUser,
|
|
235
|
+
text,
|
|
236
|
+
messageId,
|
|
237
|
+
hasFinishedActions,
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
let candidates = Array.from(scope.querySelectorAll(CONVERSATION_SELECTOR)).map((turnNode) =>
|
|
242
|
+
toCandidate(turnNode)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (candidates.length === 0) {
|
|
246
|
+
candidates = Array.from(scope.querySelectorAll(ASSISTANT_SELECTOR)).map((messageRoot) =>
|
|
247
|
+
toCandidate(messageRoot, messageRoot)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
candidates,
|
|
253
|
+
stopVisible: Boolean(scope.querySelector(STOP_SELECTOR)),
|
|
254
|
+
};
|
|
255
|
+
})()`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function waitForResponse(
|
|
260
|
+
cdp,
|
|
261
|
+
timeoutMs = 2700000,
|
|
262
|
+
baselineAssistant,
|
|
263
|
+
baselineAssistantCount,
|
|
264
|
+
signal,
|
|
265
|
+
promptEcho,
|
|
266
|
+
) {
|
|
267
|
+
throwIfAborted(signal);
|
|
268
|
+
const deadline = Date.now() + timeoutMs;
|
|
269
|
+
let previousText = baselineAssistant?.text || "";
|
|
270
|
+
let stableCycles = 0;
|
|
271
|
+
let lastChangeAt = Date.now();
|
|
272
|
+
|
|
273
|
+
while (Date.now() < deadline) {
|
|
274
|
+
const rawSnapshot = await readChatGPTResponseSnapshot(cdp);
|
|
275
|
+
const snapshot = promptEcho ? scopeSnapshotToPrompt(rawSnapshot, promptEcho) : rawSnapshot;
|
|
276
|
+
|
|
277
|
+
if (!snapshot) {
|
|
278
|
+
await delay(400, signal);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const { latestAssistant, assistantCount, stopVisible } = normalizeResponseSnapshot(snapshot);
|
|
283
|
+
const currentText = latestAssistant?.text || "";
|
|
284
|
+
const hasNewAssistantContent = isNewAssistantContent(
|
|
285
|
+
latestAssistant,
|
|
286
|
+
baselineAssistant,
|
|
287
|
+
assistantCount,
|
|
288
|
+
baselineAssistantCount,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (!hasNewAssistantContent) {
|
|
292
|
+
await delay(400, signal);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (currentText !== previousText) {
|
|
297
|
+
previousText = currentText;
|
|
298
|
+
stableCycles = 0;
|
|
299
|
+
lastChangeAt = Date.now();
|
|
300
|
+
} else if (currentText) {
|
|
301
|
+
stableCycles++;
|
|
302
|
+
} else {
|
|
303
|
+
stableCycles = 0;
|
|
304
|
+
lastChangeAt = Date.now();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const stableMs = Date.now() - lastChangeAt;
|
|
308
|
+
const completionSnapshot = latestAssistant
|
|
309
|
+
? { ...latestAssistant, stopVisible }
|
|
310
|
+
: { text: "", stopVisible, hasFinishedActions: false };
|
|
311
|
+
|
|
312
|
+
if (isChatGPTResponseComplete(completionSnapshot, stableCycles, stableMs)) {
|
|
313
|
+
return {
|
|
314
|
+
text: latestAssistant.text,
|
|
315
|
+
messageId: latestAssistant.messageId,
|
|
316
|
+
turnIndex: latestAssistant.turnIndex,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await delay(400, signal);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
throw new Error("Response timeout");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
cleanChatGPTResponseText,
|
|
328
|
+
extractLatestAssistantSnapshot,
|
|
329
|
+
isChatGPTResponseComplete,
|
|
330
|
+
isNewAssistantContent,
|
|
331
|
+
matchesPromptEcho,
|
|
332
|
+
normalizePromptEcho,
|
|
333
|
+
normalizeResponseSnapshot,
|
|
334
|
+
readChatGPTResponseSnapshot,
|
|
335
|
+
waitForResponse,
|
|
336
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const CHATGPT_EFFORT_CHOICES = ["light", "standard", "extended", "heavy"];
|
|
2
|
+
|
|
3
|
+
function normalizeChatGPTModelChoice(desiredModel) {
|
|
4
|
+
const normalized = String(desiredModel || "")
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[^a-z0-9]/g, "");
|
|
7
|
+
|
|
8
|
+
if (["instant", "gpt53"].includes(normalized)) return "instant";
|
|
9
|
+
if (["thinking", "gpt54thinking"].includes(normalized)) return "thinking";
|
|
10
|
+
if (["pro", "gpt54pro"].includes(normalized)) return "pro";
|
|
11
|
+
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeChatGPTEffortChoice(desiredEffort) {
|
|
16
|
+
const normalized = String(desiredEffort || "").toLowerCase().trim();
|
|
17
|
+
return CHATGPT_EFFORT_CHOICES.includes(normalized) ? normalized : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizedWords(value) {
|
|
21
|
+
return String(value || "")
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
24
|
+
.trim()
|
|
25
|
+
.split(/\s+/)
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function modelCandidateMatches(item, targetModel) {
|
|
30
|
+
const values = [item?.label, item?.testId?.replace(/^model-switcher-/, "")].filter(Boolean);
|
|
31
|
+
return values.some((value) => {
|
|
32
|
+
if (normalizeChatGPTModelChoice(value) === targetModel) return true;
|
|
33
|
+
const variants = ["instant", "thinking", "pro"].filter((variant) =>
|
|
34
|
+
normalizedWords(value).includes(variant),
|
|
35
|
+
);
|
|
36
|
+
return variants.length === 1 && variants[0] === targetModel;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function effortCandidateMatches(item, targetEffort) {
|
|
41
|
+
const variants = new Set(
|
|
42
|
+
[item?.label, item?.testId]
|
|
43
|
+
.flatMap((value) => normalizedWords(value))
|
|
44
|
+
.filter((word) => CHATGPT_EFFORT_CHOICES.includes(word)),
|
|
45
|
+
);
|
|
46
|
+
return variants.size === 1 && variants.has(targetEffort);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function uniqueMatch(items, matches) {
|
|
50
|
+
if (!Array.isArray(items)) return null;
|
|
51
|
+
const candidates = items.filter(matches);
|
|
52
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveChatGPTModelMenuOption(items, desiredModel) {
|
|
56
|
+
const targetModel = normalizeChatGPTModelChoice(desiredModel);
|
|
57
|
+
if (!targetModel) return null;
|
|
58
|
+
return uniqueMatch(
|
|
59
|
+
items,
|
|
60
|
+
(item) =>
|
|
61
|
+
item?.role === "menuitemradio" &&
|
|
62
|
+
typeof item?.testId === "string" &&
|
|
63
|
+
item.testId.startsWith("model-switcher-") &&
|
|
64
|
+
modelCandidateMatches(item, targetModel),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function verifyChatGPTModelSelection(items, desiredModel) {
|
|
69
|
+
const targetModel = normalizeChatGPTModelChoice(desiredModel);
|
|
70
|
+
if (!targetModel) return null;
|
|
71
|
+
return uniqueMatch(
|
|
72
|
+
items,
|
|
73
|
+
(item) =>
|
|
74
|
+
(typeof item?.label === "string" || typeof item?.testId === "string") &&
|
|
75
|
+
modelCandidateMatches(item, targetModel),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveChatGPTEffortMenuOption(items, desiredEffort) {
|
|
80
|
+
const targetEffort = normalizeChatGPTEffortChoice(desiredEffort);
|
|
81
|
+
if (!targetEffort) return null;
|
|
82
|
+
return uniqueMatch(
|
|
83
|
+
items,
|
|
84
|
+
(item) =>
|
|
85
|
+
["button", "menuitem", "menuitemradio"].includes(item?.role) &&
|
|
86
|
+
effortCandidateMatches(item, targetEffort),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function verifyChatGPTEffortSelection(items, desiredEffort) {
|
|
91
|
+
const targetEffort = normalizeChatGPTEffortChoice(desiredEffort);
|
|
92
|
+
if (!targetEffort) return null;
|
|
93
|
+
return uniqueMatch(
|
|
94
|
+
items,
|
|
95
|
+
(item) =>
|
|
96
|
+
(typeof item?.label === "string" || typeof item?.testId === "string") &&
|
|
97
|
+
effortCandidateMatches(item, targetEffort),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function boundedOptionLabels(items) {
|
|
102
|
+
if (!Array.isArray(items)) return [];
|
|
103
|
+
return items
|
|
104
|
+
.map((item) => String(item?.label || "").replace(/\s+/g, " ").trim().slice(0, 80))
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.filter((label, index, labels) => labels.indexOf(label) === index)
|
|
107
|
+
.slice(0, 10);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
CHATGPT_EFFORT_CHOICES,
|
|
112
|
+
boundedOptionLabels,
|
|
113
|
+
normalizeChatGPTEffortChoice,
|
|
114
|
+
normalizeChatGPTModelChoice,
|
|
115
|
+
resolveChatGPTEffortMenuOption,
|
|
116
|
+
resolveChatGPTModelMenuOption,
|
|
117
|
+
verifyChatGPTEffortSelection,
|
|
118
|
+
verifyChatGPTModelSelection,
|
|
119
|
+
};
|