cursor-buddy 0.0.8 → 0.0.9

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 CHANGED
@@ -57,7 +57,7 @@ export const cursorBuddy = createCursorBuddyHandler({
57
57
  import { toNextJsHandler } from "cursor-buddy/server/next"
58
58
  import { cursorBuddy } from "@/lib/cursor-buddy"
59
59
 
60
- export const { GET, POST } = toNextJsHandler(cursorBuddy)
60
+ export const { POST } = toNextJsHandler(cursorBuddy)
61
61
  ```
62
62
 
63
63
  ### 2. Client Setup
@@ -371,13 +371,13 @@ client.stopListening()
371
371
  4. An annotated screenshot of the viewport is captured, with numbered markers on visible interactive elements, based on [agent-browser](https://github.com/vercel-labs/agent-browser) implementation.
372
372
  5. The client prefers the browser transcript; if it is unavailable or empty in `auto` mode, the recorded audio is transcribed on the server
373
373
  6. Screenshot + marker context are sent to the AI model
374
- 7. AI responds with text, optionally including a pointing tag:
375
- - Preferred: `[POINT:5:Submit]` for numbered interactive elements
376
- - Fallback: `[POINT:640,360:Error text]` for arbitrary screen coordinates
374
+ 7. AI responds with text and can optionally call the `point` tool to indicate a location on screen:
375
+ - `type: "marker"` with `markerId` for numbered interactive elements (most accurate)
376
+ - `type: "coordinates"` with `x, y` pixel coordinates for anything without a marker
377
377
  8. Response is spoken in the browser or on the server based on `speech.mode`,
378
378
  and can either wait for the full response or stream sentence-by-sentence
379
379
  based on `speech.allowStreaming`
380
- 9. If a marker tag is present, it is resolved back to the live DOM element; if a coordinate tag is present, it is mapped back to the live viewport; then the cursor animates to the target location
380
+ 9. If the AI calls the point tool, the cursor animates to the target location markers resolve to live DOM elements, coordinates map to viewport positions
381
381
  10. **If user presses hotkey again at any point, current response is interrupted**
382
382
 
383
383
  ## Security Best Practices
@@ -415,7 +415,6 @@ export const GET = POST
415
415
 
416
416
  ## TODOs
417
417
 
418
- - [ ] High: Make tool calls first class: Pointing becomes tool call (once per turn) + re-use pointing bubble UI for tool calls
419
418
  - [ ] Medium: Proper test structure without relying on `as any` for audio and voice capture
420
419
 
421
420
  ## License
@@ -42,10 +42,7 @@ type VoiceEvent = {
42
42
  error: Error;
43
43
  };
44
44
  /**
45
- * Point coordinates parsed from AI response.
46
- * Supports two formats:
47
- * - Marker-based: [POINT:5:label] - references a numbered marker
48
- * - Coordinate-based: [POINT:640,360:label] - raw pixel coordinates
45
+ * Point coordinates for cursor pointing.
49
46
  */
50
47
  interface PointingTarget {
51
48
  /** X coordinate in viewport pixels (top-left origin) */
@@ -460,4 +457,4 @@ declare class CursorBuddyClient {
460
457
  }
461
458
  //#endregion
462
459
  export { CursorBuddySnapshot as a, CursorRenderProps as c, SpeechBubbleRenderProps as d, VoiceEvent as f, CursorBuddyMediaMode as i, Point as l, WaveformRenderProps as m, BrowserSpeechPort as n, CursorBuddySpeechConfig as o, VoiceState as p, CursorBuddyClientOptions as r, CursorBuddyTranscriptionConfig as s, CursorBuddyClient as t, PointingTarget as u };
463
- //# sourceMappingURL=client-Crn8tW7w.d.mts.map
460
+ //# sourceMappingURL=client-Ba6rv-du.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-Ba6rv-du.d.mts","names":[],"sources":["../src/core/utils/elements.ts","../src/core/types.ts","../src/core/client.ts"],"mappings":"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"}
@@ -21,80 +21,6 @@ const $isEnabled = atom(true);
21
21
  atom(false);
22
22
  const $conversationHistory = atom([]);
23
23
  //#endregion
24
- //#region src/core/pointing.ts
25
- /**
26
- * Parses POINT tags from AI responses.
27
- *
28
- * Supports two formats:
29
- * - Marker-based: [POINT:5:label] - 3 parts, references a numbered marker
30
- * - Coordinate-based: [POINT:640,360:label] - 4 parts, raw pixel coordinates
31
- */
32
- const POINTING_TAG_REGEX = /\[POINT:(\d+)(?:,(\d+))?:([^\]]+)\]\s*$/;
33
- const PARTIAL_POINTING_PREFIXES = new Set([
34
- "[",
35
- "[P",
36
- "[PO",
37
- "[POI",
38
- "[POIN",
39
- "[POINT",
40
- "[POINT:"
41
- ]);
42
- function stripTrailingPointingTag(response, trimResult) {
43
- const stripped = response.replace(POINTING_TAG_REGEX, "");
44
- return trimResult ? stripped.trim() : stripped;
45
- }
46
- function getPartialPointingTagStart(response) {
47
- const lastOpenBracket = response.lastIndexOf("[");
48
- if (lastOpenBracket === -1) return -1;
49
- const suffix = response.slice(lastOpenBracket).trimEnd();
50
- if (suffix.includes("]")) return -1;
51
- if (suffix.startsWith("[POINT:")) {
52
- let start = lastOpenBracket;
53
- while (start > 0 && /\s/.test(response[start - 1] ?? "")) start--;
54
- return start;
55
- }
56
- return PARTIAL_POINTING_PREFIXES.has(suffix) ? lastOpenBracket : -1;
57
- }
58
- /**
59
- * Parse pointing tag into structured result.
60
- * Returns null if no valid POINT tag is found at the end.
61
- */
62
- function parsePointingTagRaw(response) {
63
- const match = response.match(POINTING_TAG_REGEX);
64
- if (!match) return null;
65
- const first = Number.parseInt(match[1], 10);
66
- const second = match[2] ? Number.parseInt(match[2], 10) : null;
67
- const label = match[3].trim();
68
- if (second !== null) return {
69
- type: "coordinates",
70
- x: first,
71
- y: second,
72
- label
73
- };
74
- return {
75
- type: "marker",
76
- markerId: first,
77
- label
78
- };
79
- }
80
- /**
81
- * Remove POINT tag from response text for display/TTS.
82
- */
83
- function stripPointingTag(response) {
84
- return stripTrailingPointingTag(response, true);
85
- }
86
- /**
87
- * Strip complete or partial trailing POINT syntax while the response streams.
88
- * This keeps the visible text and TTS input stable even if the tag arrives
89
- * incrementally over multiple chunks.
90
- */
91
- function stripTrailingPointingSyntax(response) {
92
- const withoutCompleteTag = stripTrailingPointingTag(response, false);
93
- const partialTagStart = getPartialPointingTagStart(withoutCompleteTag);
94
- if (partialTagStart === -1) return withoutCompleteTag.trimEnd();
95
- return withoutCompleteTag.slice(0, partialTagStart).trimEnd();
96
- }
97
- //#endregion
98
24
  //#region src/core/utils/error.ts
99
25
  /**
100
26
  * Normalize unknown thrown values into Error instances.
@@ -691,7 +617,7 @@ const DEFAULT_STYLE = {
691
617
  labelBackground: "rgba(255, 0, 0, 0.9)",
692
618
  labelColor: "#ffffff",
693
619
  borderWidth: 2,
694
- fontSize: 11,
620
+ fontSize: 15,
695
621
  labelPadding: 4
696
622
  };
697
623
  /**
@@ -1009,7 +935,7 @@ async function waitForClonedDocumentStyles(doc) {
1009
935
  }
1010
936
  function getHtml2CanvasOptions(captureMetrics) {
1011
937
  return {
1012
- scale: 1,
938
+ scale: window.devicePixelRatio,
1013
939
  useCORS: true,
1014
940
  logging: false,
1015
941
  width: captureMetrics.viewportWidth,
@@ -1584,6 +1510,47 @@ function createStateMachine(initial = "idle") {
1584
1510
  };
1585
1511
  }
1586
1512
  //#endregion
1513
+ //#region src/core/utils/ui-stream-parser.ts
1514
+ /**
1515
+ * Parse a single line from the UI message stream.
1516
+ * The stream format is SSE with "data: " prefix followed by JSON.
1517
+ */
1518
+ function parseUIStreamLine(line) {
1519
+ const trimmed = line.trim();
1520
+ if (!trimmed) return null;
1521
+ let jsonStr = trimmed;
1522
+ if (trimmed.startsWith("data: ")) jsonStr = trimmed.slice(6);
1523
+ if (jsonStr === "[DONE]") return null;
1524
+ try {
1525
+ const chunk = JSON.parse(jsonStr);
1526
+ switch (chunk.type) {
1527
+ case "text-delta": return {
1528
+ type: "text-delta",
1529
+ delta: chunk.delta ?? ""
1530
+ };
1531
+ case "tool-input-available": return {
1532
+ type: "tool-input-available",
1533
+ toolName: chunk.toolName ?? "",
1534
+ input: chunk.input
1535
+ };
1536
+ case "finish": return { type: "finish" };
1537
+ case "error": return {
1538
+ type: "error",
1539
+ errorText: chunk.errorText ?? "Unknown error"
1540
+ };
1541
+ default: return { type: "unknown" };
1542
+ }
1543
+ } catch {
1544
+ return null;
1545
+ }
1546
+ }
1547
+ /**
1548
+ * Check if a tool call is a point tool call with valid input.
1549
+ */
1550
+ function isPointToolCall(chunk) {
1551
+ return chunk.type === "tool-input-available" && chunk.toolName === "point" && chunk.input != null && typeof chunk.input === "object" && "type" in chunk.input && "label" in chunk.input;
1552
+ }
1553
+ //#endregion
1587
1554
  //#region src/core/utils/response-processor.ts
1588
1555
  const COMMON_ABBREVIATIONS = [
1589
1556
  "mr.",
@@ -1652,32 +1619,58 @@ function extractCompletedSegments(text) {
1652
1619
  };
1653
1620
  }
1654
1621
  /**
1655
- * Tracks a streaming assistant response, exposes a tag-free visible version for
1656
- * the UI, and emits speakable segments as sentence boundaries become stable.
1622
+ * Processes a streaming AI SDK UI message stream response.
1623
+ * Extracts text for display/TTS and captures point tool calls.
1657
1624
  */
1658
1625
  var ProgressiveResponseProcessor = class {
1659
- consumedVisibleTextLength = 0;
1626
+ consumedTextLength = 0;
1660
1627
  pendingShortSegment = "";
1661
- rawResponse = "";
1628
+ rawText = "";
1629
+ buffer = "";
1630
+ pointToolCall = null;
1631
+ /**
1632
+ * Push raw stream data and extract text chunks and tool calls.
1633
+ * The UI message stream format is newline-delimited JSON.
1634
+ */
1662
1635
  push(chunk) {
1663
- this.rawResponse += chunk;
1664
- const visibleText = stripTrailingPointingSyntax(this.rawResponse);
1665
- const { consumedLength, segments } = extractCompletedSegments(visibleText.slice(this.consumedVisibleTextLength));
1666
- this.consumedVisibleTextLength += consumedLength;
1636
+ this.buffer += chunk;
1637
+ const lines = this.buffer.split("\n");
1638
+ this.buffer = lines.pop() ?? "";
1639
+ const newTextParts = [];
1640
+ for (const line of lines) {
1641
+ const parsed = parseUIStreamLine(line);
1642
+ if (!parsed) continue;
1643
+ if (parsed.type === "text-delta") newTextParts.push(parsed.delta);
1644
+ else if (isPointToolCall(parsed)) {
1645
+ if (!this.pointToolCall) this.pointToolCall = parsed.input;
1646
+ }
1647
+ }
1648
+ if (newTextParts.length > 0) this.rawText += newTextParts.join("");
1649
+ const { consumedLength, segments } = extractCompletedSegments(this.rawText.slice(this.consumedTextLength));
1650
+ this.consumedTextLength += consumedLength;
1667
1651
  return {
1668
- visibleText,
1669
- speechSegments: this.coalesceSegments(segments)
1652
+ visibleText: this.rawText,
1653
+ speechSegments: this.coalesceSegments(segments),
1654
+ pointToolCall: this.pointToolCall
1670
1655
  };
1671
1656
  }
1657
+ /**
1658
+ * Finalize processing and return any remaining text/tool call.
1659
+ */
1672
1660
  finish() {
1673
- const finalResponseText = stripPointingTag(this.rawResponse);
1674
- const trailingText = finalResponseText.slice(this.consumedVisibleTextLength).trim();
1661
+ if (this.buffer) {
1662
+ const parsed = parseUIStreamLine(this.buffer);
1663
+ if (parsed?.type === "text-delta") this.rawText += parsed.delta;
1664
+ else if (parsed && isPointToolCall(parsed) && !this.pointToolCall) this.pointToolCall = parsed.input;
1665
+ this.buffer = "";
1666
+ }
1667
+ const trailingText = this.rawText.slice(this.consumedTextLength).trim();
1675
1668
  const finalSegmentParts = [this.pendingShortSegment, trailingText].filter(Boolean);
1676
1669
  this.pendingShortSegment = "";
1677
1670
  return {
1678
- fullResponse: this.rawResponse,
1679
- finalResponseText,
1680
- speechSegments: finalSegmentParts.length ? [finalSegmentParts.join(" ").trim()] : []
1671
+ finalResponseText: this.rawText.trim(),
1672
+ speechSegments: finalSegmentParts.length ? [finalSegmentParts.join(" ").trim()] : [],
1673
+ pointToolCall: this.pointToolCall
1681
1674
  };
1682
1675
  }
1683
1676
  coalesceSegments(segments) {
@@ -1839,7 +1832,7 @@ var CursorBuddyClient = class {
1839
1832
  this.options.onTranscript?.(transcript);
1840
1833
  this.notify();
1841
1834
  this.prepareSpeechMode();
1842
- const { cleanResponse, fullResponse, playbackQueue } = await this.chatAndSpeak(transcript, screenshot, signal, {
1835
+ const { cleanResponse, pointToolCall, playbackQueue } = await this.chatAndSpeak(transcript, screenshot, signal, {
1843
1836
  onFailure: failTurn,
1844
1837
  onPlaybackStart: () => {
1845
1838
  this.stateMachine.transition({ type: "RESPONSE_STARTED" });
@@ -1847,18 +1840,17 @@ var CursorBuddyClient = class {
1847
1840
  });
1848
1841
  if (turnFailure) throw turnFailure;
1849
1842
  if (signal?.aborted) return;
1850
- const parsed = parsePointingTagRaw(fullResponse);
1851
1843
  this.options.onResponse?.(cleanResponse);
1852
1844
  let pointTarget = null;
1853
- if (parsed) if (parsed.type === "marker") {
1854
- const coords = resolveMarkerToCoordinates(screenshot.markerMap, parsed.markerId);
1845
+ if (pointToolCall) if (pointToolCall.type === "marker") {
1846
+ const coords = resolveMarkerToCoordinates(screenshot.markerMap, pointToolCall.markerId);
1855
1847
  if (coords) pointTarget = {
1856
1848
  ...coords,
1857
- label: parsed.label
1849
+ label: pointToolCall.label
1858
1850
  };
1859
1851
  } else pointTarget = {
1860
- ...mapCoordinatesToViewport(parsed.x, parsed.y, screenshot),
1861
- label: parsed.label
1852
+ ...mapCoordinatesToViewport(pointToolCall.x, pointToolCall.y, screenshot),
1853
+ label: pointToolCall.label
1862
1854
  };
1863
1855
  if (pointTarget) {
1864
1856
  this.options.onPoint?.(pointTarget);
@@ -2061,7 +2053,7 @@ var CursorBuddyClient = class {
2061
2053
  this.updateResponse(finalizedResponse.finalResponseText);
2062
2054
  return {
2063
2055
  cleanResponse: finalizedResponse.finalResponseText,
2064
- fullResponse: finalizedResponse.fullResponse,
2056
+ pointToolCall: finalizedResponse.pointToolCall,
2065
2057
  playbackQueue
2066
2058
  };
2067
2059
  }
@@ -2259,4 +2251,4 @@ var CursorBuddyClient = class {
2259
2251
  //#endregion
2260
2252
  export { $buddyScale as a, $buddyRotation as i, $audioLevel as n, $cursorPosition as o, $buddyPosition as r, $pointingTarget as s, CursorBuddyClient as t };
2261
2253
 
2262
- //# sourceMappingURL=client-D73KQZf8.mjs.map
2254
+ //# sourceMappingURL=client-CSVSY-KV.mjs.map