index.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import { execFileSync } from 'child_process'
  2. type DisplayGeometry = {
  3. id: number
  4. width: number
  5. height: number
  6. scaleFactor: number
  7. originX: number
  8. originY: number
  9. }
  10. type InstalledApp = {
  11. bundleId: string
  12. displayName: string
  13. path?: string
  14. }
  15. type RunningApp = {
  16. bundleId: string
  17. displayName: string
  18. }
  19. type ScreenshotResult = {
  20. base64: string
  21. width: number
  22. height: number
  23. displayWidth: number
  24. displayHeight: number
  25. displayId: number
  26. originX: number
  27. originY: number
  28. }
  29. const BLANK_JPEG_BASE64 =
  30. '/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxAQEBUQEBAVFRUVFRUVFRUVFRUVFRUVFRUXFhUVFRUYHSggGBolHRUVITEhJSkrLi4uFx8zODMsNygtLisBCgoKDg0OGhAQGi0mHyYtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAAEAAQMBIgACEQEDEQH/xAAXAAADAQAAAAAAAAAAAAAAAAAAAQID/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB6gD/xAAVEAEBAAAAAAAAAAAAAAAAAAABAP/aAAgBAQABBQJf/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwEf/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJf/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQABPyFf/9k='
  31. function safeExec(
  32. file: string,
  33. args: string[],
  34. ): { ok: true; stdout: string } | { ok: false } {
  35. try {
  36. const stdout = execFileSync(file, args, {
  37. encoding: 'utf8',
  38. stdio: ['ignore', 'pipe', 'ignore'],
  39. })
  40. return { ok: true, stdout: stdout.trim() }
  41. } catch {
  42. return { ok: false }
  43. }
  44. }
  45. function getDefaultDisplay(): DisplayGeometry {
  46. return {
  47. id: 0,
  48. width: 1440,
  49. height: 900,
  50. scaleFactor: 1,
  51. originX: 0,
  52. originY: 0,
  53. }
  54. }
  55. function getDisplay(displayId?: number): DisplayGeometry {
  56. const display = getDefaultDisplay()
  57. if (displayId === undefined || displayId === display.id) {
  58. return display
  59. }
  60. return { ...display, id: displayId }
  61. }
  62. function buildScreenshotResult(
  63. width: number,
  64. height: number,
  65. displayId?: number,
  66. ): ScreenshotResult {
  67. const display = getDisplay(displayId)
  68. return {
  69. base64: BLANK_JPEG_BASE64,
  70. width,
  71. height,
  72. displayWidth: display.width,
  73. displayHeight: display.height,
  74. displayId: display.id,
  75. originX: display.originX,
  76. originY: display.originY,
  77. }
  78. }
  79. function openBundle(bundleId: string): void {
  80. if (!bundleId) return
  81. safeExec('open', ['-b', bundleId])
  82. }
  83. function getRunningApps(): RunningApp[] {
  84. const result = safeExec('osascript', [
  85. '-e',
  86. 'tell application "System Events" to get the name of every application process',
  87. ])
  88. if (!result.ok || result.stdout.length === 0) return []
  89. return result.stdout
  90. .split(/\s*,\s*/u)
  91. .map(name => name.trim())
  92. .filter(Boolean)
  93. .map(name => ({
  94. bundleId: '',
  95. displayName: name,
  96. }))
  97. }
  98. function createInstalledApp(displayName: string): InstalledApp {
  99. return {
  100. bundleId: '',
  101. displayName,
  102. }
  103. }
  104. export type ComputerUseAPI = {
  105. _drainMainRunLoop(): void
  106. tcc: {
  107. checkAccessibility(): boolean
  108. checkScreenRecording(): boolean
  109. }
  110. hotkey: {
  111. registerEscape(onEscape: () => void): boolean
  112. unregister(): void
  113. notifyExpectedEscape(): void
  114. }
  115. display: {
  116. getSize(displayId?: number): DisplayGeometry
  117. listAll(): DisplayGeometry[]
  118. }
  119. apps: {
  120. prepareDisplay(
  121. allowlistBundleIds: string[],
  122. surrogateHost: string,
  123. displayId?: number,
  124. ): Promise<{ hidden: string[]; activated?: string }>
  125. previewHideSet(
  126. allowlistBundleIds: string[],
  127. displayId?: number,
  128. ): Promise<Array<{ bundleId: string; displayName: string }>>
  129. findWindowDisplays(
  130. bundleIds: string[],
  131. ): Promise<Array<{ bundleId: string; displayIds: number[] }>>
  132. appUnderPoint(
  133. x: number,
  134. y: number,
  135. ): Promise<{ bundleId: string; displayName: string } | null>
  136. listInstalled(): Promise<InstalledApp[]>
  137. iconDataUrl(path: string): string | null
  138. listRunning(): Promise<RunningApp[]>
  139. open(bundleId: string): Promise<void>
  140. unhide(bundleIds: string[]): Promise<void>
  141. }
  142. screenshot: {
  143. captureExcluding(
  144. allowedBundleIds: string[],
  145. quality: number,
  146. width: number,
  147. height: number,
  148. displayId?: number,
  149. ): Promise<ScreenshotResult>
  150. captureRegion(
  151. allowedBundleIds: string[],
  152. x: number,
  153. y: number,
  154. width: number,
  155. height: number,
  156. outW: number,
  157. outH: number,
  158. quality: number,
  159. displayId?: number,
  160. ): Promise<ScreenshotResult>
  161. }
  162. resolvePrepareCapture(
  163. allowedBundleIds: string[],
  164. surrogateHost: string,
  165. quality: number,
  166. targetW: number,
  167. targetH: number,
  168. preferredDisplayId?: number,
  169. autoResolve?: boolean,
  170. doHide?: boolean,
  171. ): Promise<
  172. ScreenshotResult & {
  173. hidden: string[]
  174. activated?: string
  175. autoResolved: boolean
  176. }
  177. >
  178. }
  179. const stub: ComputerUseAPI = {
  180. _drainMainRunLoop() {},
  181. tcc: {
  182. checkAccessibility() {
  183. return false
  184. },
  185. checkScreenRecording() {
  186. return false
  187. },
  188. },
  189. hotkey: {
  190. registerEscape(_onEscape: () => void) {
  191. return false
  192. },
  193. unregister() {},
  194. notifyExpectedEscape() {},
  195. },
  196. display: {
  197. getSize(displayId?: number) {
  198. return getDisplay(displayId)
  199. },
  200. listAll() {
  201. return [getDefaultDisplay()]
  202. },
  203. },
  204. apps: {
  205. async prepareDisplay(
  206. _allowlistBundleIds: string[],
  207. _surrogateHost: string,
  208. _displayId?: number,
  209. ) {
  210. return { hidden: [] as string[] }
  211. },
  212. async previewHideSet(
  213. _allowlistBundleIds: string[],
  214. _displayId?: number,
  215. ) {
  216. return []
  217. },
  218. async findWindowDisplays(bundleIds: string[]) {
  219. return bundleIds.map(bundleId => ({
  220. bundleId,
  221. displayIds: [],
  222. }))
  223. },
  224. async appUnderPoint(_x: number, _y: number) {
  225. return null
  226. },
  227. async listInstalled() {
  228. return getRunningApps().map(app => createInstalledApp(app.displayName))
  229. },
  230. iconDataUrl(_path: string) {
  231. return null
  232. },
  233. async listRunning() {
  234. return getRunningApps()
  235. },
  236. async open(bundleId: string) {
  237. openBundle(bundleId)
  238. },
  239. async unhide(_bundleIds: string[]) {},
  240. },
  241. screenshot: {
  242. async captureExcluding(
  243. _allowedBundleIds: string[],
  244. _quality: number,
  245. width: number,
  246. height: number,
  247. displayId?: number,
  248. ) {
  249. return buildScreenshotResult(width, height, displayId)
  250. },
  251. async captureRegion(
  252. _allowedBundleIds: string[],
  253. _x: number,
  254. _y: number,
  255. _width: number,
  256. _height: number,
  257. outW: number,
  258. outH: number,
  259. _quality: number,
  260. displayId?: number,
  261. ) {
  262. return buildScreenshotResult(outW, outH, displayId)
  263. },
  264. },
  265. async resolvePrepareCapture(
  266. _allowedBundleIds: string[],
  267. _surrogateHost: string,
  268. _quality: number,
  269. targetW: number,
  270. targetH: number,
  271. preferredDisplayId?: number,
  272. autoResolve = false,
  273. _doHide = false,
  274. ) {
  275. return {
  276. ...buildScreenshotResult(targetW, targetH, preferredDisplayId),
  277. hidden: [],
  278. autoResolved: autoResolve,
  279. }
  280. },
  281. }
  282. export default stub