surf-cli 2.16.0 → 2.16.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.
@@ -50,12 +50,16 @@ function modelCandidateMatches(item, targetModel) {
50
50
  }
51
51
 
52
52
  function effortCandidateMatches(item, targetEffort) {
53
- const variants = new Set(
54
- [item?.label, item?.testId]
55
- .flatMap((value) => normalizedWords(value))
56
- .filter((word) => CHATGPT_EFFORT_CHOICES.includes(word)),
53
+ const labelVariants = new Set(
54
+ normalizedWords(item?.label).filter((word) => CHATGPT_EFFORT_CHOICES.includes(word)),
57
55
  );
58
- return variants.size === 1 && variants.has(targetEffort);
56
+ if (labelVariants.size > 0) {
57
+ return labelVariants.size === 1 && labelVariants.has(targetEffort);
58
+ }
59
+ const testIdVariants = new Set(
60
+ normalizedWords(item?.testId).filter((word) => CHATGPT_EFFORT_CHOICES.includes(word)),
61
+ );
62
+ return testIdVariants.size === 1 && testIdVariants.has(targetEffort);
59
63
  }
60
64
 
61
65
  function uniqueMatch(items, matches) {
@@ -191,14 +191,33 @@ async function readPicker(cdp, kind, click = false) {
191
191
  `(() => {
192
192
  ${buildClickDispatcher()}
193
193
  const kind = ${JSON.stringify(kind)};
194
+ const effortChoices = new Set(${JSON.stringify(CHATGPT_EFFORT_CHOICES)});
195
+ const effortOwnerLabels = new Set([...effortChoices, 'thinking']);
196
+ const normalize = (value) => String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
197
+ const labelFor = (node) => {
198
+ const labelledBy = String(node.getAttribute?.('aria-labelledby') || '')
199
+ .split(/\s+/)
200
+ .map((id) => document.getElementById(id)?.textContent || '')
201
+ .join(' ');
202
+ const text = (node.innerText || node.textContent || '').replace(/\s+/g, ' ').trim();
203
+ const aria = (node.getAttribute?.('aria-label') || '').replace(/\s+/g, ' ').trim();
204
+ const title = (node.getAttribute?.('title') || '').replace(/\s+/g, ' ').trim();
205
+ return [text, aria, labelledBy, title].filter(Boolean).join(' | ');
206
+ };
194
207
  let nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) => {
195
- const value = ((node.getAttribute?.('aria-label') || '') + ' ' + (node.textContent || '')).toLowerCase();
196
- if (kind !== 'model') return value.includes('thinking') || value.includes('pro');
208
+ const value = normalize(labelFor(node));
209
+ if (kind !== 'model') {
210
+ const words = value.split(/\s+/).filter(Boolean);
211
+ const hasEffort = words.some((word) => effortOwnerLabels.has(word));
212
+ const looksLikeModel = node.getAttribute?.('data-testid') === 'model-switcher-dropdown-button' ||
213
+ value.includes('current model') || value.includes('gpt') || value.includes('instant');
214
+ return hasEffort && !looksLikeModel;
215
+ }
197
216
  return value.includes('gpt') || value.includes('thinking') || value.includes('instant');
198
217
  });
199
218
  if (kind === 'model' && nodes.length === 0) {
200
219
  nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) => {
201
- const value = ((node.getAttribute?.('aria-label') || '') + ' ' + (node.textContent || '')).toLowerCase();
220
+ const value = normalize(labelFor(node));
202
221
  return value.includes('pro');
203
222
  });
204
223
  }
@@ -208,13 +227,19 @@ async function readPicker(cdp, kind, click = false) {
208
227
  );
209
228
  }
210
229
  const items = nodes.map((node) => {
211
- const text = (node.textContent || '').replace(/\\s+/g, ' ').trim();
230
+ const text = (node.innerText || node.textContent || '').replace(/\\s+/g, ' ').trim();
212
231
  const aria = (node.getAttribute?.('aria-label') || '').replace(/\\s+/g, ' ').trim();
232
+ const labelledBy = String(node.getAttribute?.('aria-labelledby') || '')
233
+ .split(/\\s+/)
234
+ .map((id) => document.getElementById(id)?.textContent || '')
235
+ .join(' ')
236
+ .replace(/\\s+/g, ' ')
237
+ .trim();
213
238
  const title = (node.getAttribute?.('title') || '').replace(/\\s+/g, ' ').trim();
214
239
  return {
215
240
  role: node.getAttribute?.('role') || (node.tagName === 'BUTTON' ? 'button' : null),
216
- label: [text, aria, title].filter(Boolean).join(' | ').slice(0, 240),
217
- displayLabel: (text || aria || title).slice(0, 80),
241
+ label: [text, aria, labelledBy, title].filter(Boolean).join(' | ').slice(0, 240),
242
+ displayLabel: (text || aria || labelledBy || title).slice(0, 80),
218
243
  testId: node.getAttribute?.('data-testid') || null,
219
244
  };
220
245
  });
@@ -370,6 +395,10 @@ async function selectEffort(cdp, desiredEffort, timeoutMs = 8000, signal) {
370
395
  throwIfAborted(signal);
371
396
  const normalizedEffort = normalizeChatGPTEffortChoice(desiredEffort);
372
397
  if (!normalizedEffort) throw verificationError("effort", desiredEffort, [], true);
398
+ const current = await readPicker(cdp, "effort");
399
+ const currentVerified = verifyChatGPTEffortSelection(current?.items, normalizedEffort);
400
+ if (currentVerified) return currentVerified.displayLabel || currentVerified.label;
401
+
373
402
  const picker = await readPicker(cdp, "effort", true);
374
403
  if (picker?.items?.length !== 1) throw verificationError("effort", desiredEffort);
375
404
  await delay(300, signal);
@@ -407,6 +436,13 @@ async function typePrompt(cdp, inputCdp, prompt, signal) {
407
436
  const node = document.querySelector(selector);
408
437
  if (!node) continue;
409
438
  dispatchClickSequence(node);
439
+ if ('value' in node) {
440
+ node.value = '';
441
+ node.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward' }));
442
+ } else {
443
+ node.textContent = '';
444
+ node.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward' }));
445
+ }
410
446
  if (typeof node.focus === 'function') node.focus();
411
447
  const doc = node.ownerDocument;
412
448
  const selection = doc?.getSelection?.();
@@ -152,10 +152,6 @@ async function dispatch(options) {
152
152
  modelVerified = await selectModel(cdp, model, 8000, signal);
153
153
  log(`Verified model: ${modelVerified}`);
154
154
  }
155
- if (effort) {
156
- effortVerified = await selectEffort(cdp, effort, 8000, signal);
157
- log(`Verified effort: ${effortVerified}`);
158
- }
159
155
  if (file) {
160
156
  if (!uploadFile) {
161
157
  throw new Error(
@@ -177,6 +173,10 @@ async function dispatch(options) {
177
173
  }
178
174
  await typePrompt(cdp, inputCdp, prompt, signal);
179
175
  log("Prompt typed");
176
+ if (effort) {
177
+ effortVerified = await selectEffort(cdp, effort, 8000, signal);
178
+ log(`Verified effort: ${effortVerified}`);
179
+ }
180
180
  const baseline = normalizeResponseSnapshot(await readChatGPTResponseSnapshot(cdp));
181
181
  if (beforeSubmit) await raceAbort(beforeSubmit, signal);
182
182
  await clickSend(cdp, inputCdp, signal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.16.0",
3
+ "version": "2.16.1",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",
@@ -71,7 +71,7 @@
71
71
  "@vitest/coverage-v8": "^4.1.9",
72
72
  "@vitest/ui": "^4.1.9",
73
73
  "pi-subagents": "^0.52.0",
74
- "puppeteer": "25.6.0",
74
+ "puppeteer": "25.8.0",
75
75
  "typebox": "^1.3.11",
76
76
  "typescript": "^7.0.2",
77
77
  "vite": "^8.1.4",