gclm-code 1.0.0 → 1.0.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/README.md +1 -1
- package/bin/gc.js +53 -25
- package/bin/install-runtime.js +253 -0
- package/package.json +10 -5
- package/vendor/manifest.json +92 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/package.json +9 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/bridgeClient.ts +1126 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/browserTools.ts +546 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/index.ts +15 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpServer.ts +96 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketClient.ts +493 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/mcpSocketPool.ts +327 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/toolCalls.ts +301 -0
- package/vendor/modules/node_modules/@ant/claude-for-chrome-mcp/src/types.ts +134 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/package.json +9 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-jxa.js +341 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/driver-swift.swift +417 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/implementation.js +204 -0
- package/vendor/modules/node_modules/@ant/computer-use-input/src/index.js +5 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/package.json +11 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/deniedApps.ts +553 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/imageResize.ts +108 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/index.ts +69 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/keyBlocklist.ts +153 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/mcpServer.ts +313 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/pixelCompare.ts +171 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/sentinelApps.ts +43 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/subGates.ts +19 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/toolCalls.ts +3872 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/tools.ts +706 -0
- package/vendor/modules/node_modules/@ant/computer-use-mcp/src/types.ts +635 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/package.json +9 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/driver-jxa.js +108 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/implementation.js +706 -0
- package/vendor/modules/node_modules/@ant/computer-use-swift/src/index.js +7 -0
- package/vendor/modules/node_modules/audio-capture-napi/package.json +8 -0
- package/vendor/modules/node_modules/audio-capture-napi/src/index.ts +226 -0
- package/vendor/modules/node_modules/image-processor-napi/package.json +11 -0
- package/vendor/modules/node_modules/image-processor-napi/src/index.ts +396 -0
- package/vendor/modules/node_modules/modifiers-napi/package.json +8 -0
- package/vendor/modules/node_modules/modifiers-napi/src/index.ts +79 -0
- package/vendor/modules/node_modules/url-handler-napi/package.json +8 -0
- package/vendor/modules/node_modules/url-handler-napi/src/index.ts +62 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { homedir, tmpdir } from 'node:os'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url)
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const DRIVER_PATH = join(__dirname, 'driver-jxa.js')
|
|
11
|
+
const FINDER_BUNDLE_ID = 'com.apple.finder'
|
|
12
|
+
const APP_SCAN_ROOTS = [
|
|
13
|
+
'/Applications',
|
|
14
|
+
'/System/Applications',
|
|
15
|
+
join(homedir(), 'Applications'),
|
|
16
|
+
]
|
|
17
|
+
const APP_SCAN_MAX_DEPTH = 3
|
|
18
|
+
const PNG_SIGNATURE = Buffer.from([
|
|
19
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
let cachedSharp = null
|
|
23
|
+
|
|
24
|
+
function sharpFactory() {
|
|
25
|
+
if (cachedSharp) {
|
|
26
|
+
return cachedSharp
|
|
27
|
+
}
|
|
28
|
+
const imported = require('sharp')
|
|
29
|
+
cachedSharp = typeof imported === 'function' ? imported : imported.default
|
|
30
|
+
return cachedSharp
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function run(command, args, options = {}) {
|
|
34
|
+
const result = spawnSync(command, args, {
|
|
35
|
+
encoding: options.encoding ?? 'utf8',
|
|
36
|
+
stdio: 'pipe',
|
|
37
|
+
...options,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (result.error) {
|
|
41
|
+
throw result.error
|
|
42
|
+
}
|
|
43
|
+
if (result.status !== 0) {
|
|
44
|
+
throw new Error((result.stderr || result.stdout || `${command} failed`).trim())
|
|
45
|
+
}
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function runDriver(payload) {
|
|
50
|
+
const result = run('/usr/bin/osascript', [
|
|
51
|
+
'-l',
|
|
52
|
+
'JavaScript',
|
|
53
|
+
DRIVER_PATH,
|
|
54
|
+
JSON.stringify(payload),
|
|
55
|
+
])
|
|
56
|
+
const output = (result.stdout ?? '').trim()
|
|
57
|
+
if (!output) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
return JSON.parse(output)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readPngDimensions(buffer) {
|
|
64
|
+
if (buffer.length < 24) return null
|
|
65
|
+
if (!buffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) return null
|
|
66
|
+
if (buffer.toString('ascii', 12, 16) !== 'IHDR') return null
|
|
67
|
+
return {
|
|
68
|
+
width: buffer.readUInt32BE(16),
|
|
69
|
+
height: buffer.readUInt32BE(20),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function displayList() {
|
|
74
|
+
return runDriver({ op: 'listDisplays' }) ?? []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function displayFor(displayId) {
|
|
78
|
+
const displays = displayList()
|
|
79
|
+
if (displays.length === 0) {
|
|
80
|
+
throw new Error('No displays were reported by the host.')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const display =
|
|
84
|
+
displayId == null
|
|
85
|
+
? displays[0]
|
|
86
|
+
: displays.find(item => item.displayId === displayId) ?? displays[0]
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
...display,
|
|
90
|
+
scaleFactor: display.scaleFactor ?? 1,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function windowIntersectsDisplay(window, display) {
|
|
95
|
+
const insideX =
|
|
96
|
+
window.x < display.originX + display.width &&
|
|
97
|
+
window.x + window.width > display.originX
|
|
98
|
+
const insideY =
|
|
99
|
+
window.y < display.originY + display.height &&
|
|
100
|
+
window.y + window.height > display.originY
|
|
101
|
+
return insideX && insideY
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function captureScreenToBuffer(displayId) {
|
|
105
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'claude-code-screen-'))
|
|
106
|
+
const outputPath = join(tempDir, 'capture.png')
|
|
107
|
+
try {
|
|
108
|
+
const args = ['-x']
|
|
109
|
+
if (displayId != null) {
|
|
110
|
+
args.push('-D', String(displayId))
|
|
111
|
+
}
|
|
112
|
+
args.push(outputPath)
|
|
113
|
+
run('/usr/sbin/screencapture', args)
|
|
114
|
+
return readFileSync(outputPath)
|
|
115
|
+
} finally {
|
|
116
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function toSizedJpegBase64(buffer, targetWidth, targetHeight, quality) {
|
|
121
|
+
const sharp = sharpFactory()
|
|
122
|
+
const pipeline = sharp(buffer)
|
|
123
|
+
.resize(targetWidth, targetHeight, {
|
|
124
|
+
fit: 'inside',
|
|
125
|
+
withoutEnlargement: true,
|
|
126
|
+
})
|
|
127
|
+
.jpeg({ quality: Math.max(1, Math.min(100, Math.round(quality * 100))) })
|
|
128
|
+
const out = await pipeline.toBuffer()
|
|
129
|
+
const meta = await sharp(out).metadata()
|
|
130
|
+
return {
|
|
131
|
+
base64: out.toString('base64'),
|
|
132
|
+
width: meta.width ?? targetWidth,
|
|
133
|
+
height: meta.height ?? targetHeight,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function captureRegionBase64(
|
|
138
|
+
displayId,
|
|
139
|
+
region,
|
|
140
|
+
outWidth,
|
|
141
|
+
outHeight,
|
|
142
|
+
quality,
|
|
143
|
+
) {
|
|
144
|
+
const source = captureScreenToBuffer(displayId)
|
|
145
|
+
const sharp = sharpFactory()
|
|
146
|
+
const display = displayFor(displayId)
|
|
147
|
+
const scaleFactor = display.scaleFactor ?? 1
|
|
148
|
+
const left = Math.max(
|
|
149
|
+
0,
|
|
150
|
+
Math.round((region.x - display.originX) * scaleFactor),
|
|
151
|
+
)
|
|
152
|
+
const top = Math.max(
|
|
153
|
+
0,
|
|
154
|
+
Math.round((region.y - display.originY) * scaleFactor),
|
|
155
|
+
)
|
|
156
|
+
const maxWidth = Math.max(1, Math.round(display.width * scaleFactor) - left)
|
|
157
|
+
const maxHeight = Math.max(1, Math.round(display.height * scaleFactor) - top)
|
|
158
|
+
const width = Math.max(
|
|
159
|
+
1,
|
|
160
|
+
Math.min(Math.round(region.w * scaleFactor), maxWidth),
|
|
161
|
+
)
|
|
162
|
+
const height = Math.max(
|
|
163
|
+
1,
|
|
164
|
+
Math.min(Math.round(region.h * scaleFactor), maxHeight),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
const cropped = await sharp(source)
|
|
168
|
+
.extract({
|
|
169
|
+
left,
|
|
170
|
+
top,
|
|
171
|
+
width,
|
|
172
|
+
height,
|
|
173
|
+
})
|
|
174
|
+
.png()
|
|
175
|
+
.toBuffer()
|
|
176
|
+
|
|
177
|
+
return toSizedJpegBase64(cropped, outWidth, outHeight, quality)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function listWindows() {
|
|
181
|
+
return runDriver({ op: 'listWindows' }) ?? []
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function listRunningApps() {
|
|
185
|
+
return runDriver({ op: 'listRunningApps' }) ?? []
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function escapeAppleScriptString(value) {
|
|
189
|
+
return String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function plistValue(plistPath, key) {
|
|
193
|
+
const result = spawnSync(
|
|
194
|
+
'/usr/bin/plutil',
|
|
195
|
+
['-extract', key, 'raw', '-o', '-', plistPath],
|
|
196
|
+
{ encoding: 'utf8', stdio: 'pipe' },
|
|
197
|
+
)
|
|
198
|
+
if (result.status !== 0) {
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
const value = String(result.stdout ?? '').trim()
|
|
202
|
+
return value || null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function appBaseName(appPath) {
|
|
206
|
+
return appPath.split('/').pop()?.replace(/\.app$/i, '') ?? null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function readAppMetadata(appPath) {
|
|
210
|
+
const infoPlistPath = join(appPath, 'Contents', 'Info.plist')
|
|
211
|
+
if (!existsSync(infoPlistPath)) {
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const bundleId = plistValue(infoPlistPath, 'CFBundleIdentifier')
|
|
216
|
+
const displayName =
|
|
217
|
+
plistValue(infoPlistPath, 'CFBundleDisplayName') ??
|
|
218
|
+
plistValue(infoPlistPath, 'CFBundleName') ??
|
|
219
|
+
appBaseName(appPath)
|
|
220
|
+
|
|
221
|
+
if (!bundleId || !displayName) {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
bundleId,
|
|
227
|
+
displayName,
|
|
228
|
+
path: appPath,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function findAppIconPath(appPath) {
|
|
233
|
+
const infoPlistPath = join(appPath, 'Contents', 'Info.plist')
|
|
234
|
+
const resourcesPath = join(appPath, 'Contents', 'Resources')
|
|
235
|
+
if (!existsSync(infoPlistPath) || !existsSync(resourcesPath)) {
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const candidateNames = [
|
|
240
|
+
plistValue(infoPlistPath, 'CFBundleIconFile'),
|
|
241
|
+
plistValue(infoPlistPath, 'CFBundleIconName'),
|
|
242
|
+
appBaseName(appPath),
|
|
243
|
+
].filter(Boolean)
|
|
244
|
+
|
|
245
|
+
for (const candidateName of candidateNames) {
|
|
246
|
+
for (const fileName of [
|
|
247
|
+
candidateName,
|
|
248
|
+
`${candidateName}.icns`,
|
|
249
|
+
`${candidateName}.png`,
|
|
250
|
+
]) {
|
|
251
|
+
const iconPath = join(resourcesPath, fileName)
|
|
252
|
+
if (existsSync(iconPath)) {
|
|
253
|
+
return iconPath
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const fallback = readdirSync(resourcesPath).find(name =>
|
|
259
|
+
name.toLowerCase().endsWith('.icns'),
|
|
260
|
+
)
|
|
261
|
+
return fallback ? join(resourcesPath, fallback) : null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function listSpotlightApplicationPaths() {
|
|
265
|
+
const result = spawnSync(
|
|
266
|
+
'/usr/bin/mdfind',
|
|
267
|
+
['kMDItemContentTypeTree == "com.apple.application-bundle"'],
|
|
268
|
+
{ encoding: 'utf8', stdio: 'pipe' },
|
|
269
|
+
)
|
|
270
|
+
if (result.status !== 0) {
|
|
271
|
+
return []
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return String(result.stdout ?? '')
|
|
275
|
+
.split('\n')
|
|
276
|
+
.map(line => line.trim())
|
|
277
|
+
.filter(Boolean)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function scanApplicationRoots() {
|
|
281
|
+
const appPaths = []
|
|
282
|
+
const queue = APP_SCAN_ROOTS.filter(root => existsSync(root)).map(root => ({
|
|
283
|
+
dir: root,
|
|
284
|
+
depth: APP_SCAN_MAX_DEPTH,
|
|
285
|
+
}))
|
|
286
|
+
|
|
287
|
+
while (queue.length > 0) {
|
|
288
|
+
const current = queue.pop()
|
|
289
|
+
if (!current) continue
|
|
290
|
+
|
|
291
|
+
let entries
|
|
292
|
+
try {
|
|
293
|
+
entries = readdirSync(current.dir, { withFileTypes: true })
|
|
294
|
+
} catch {
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const entry of entries) {
|
|
299
|
+
if (!entry.isDirectory()) continue
|
|
300
|
+
const fullPath = join(current.dir, entry.name)
|
|
301
|
+
if (entry.name.toLowerCase().endsWith('.app')) {
|
|
302
|
+
appPaths.push(fullPath)
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
if (current.depth > 0) {
|
|
306
|
+
queue.push({ dir: fullPath, depth: current.depth - 1 })
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return appPaths
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function runningAppsByBundleId() {
|
|
315
|
+
return new Map(listRunningApps().map(app => [app.bundleId, app]))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function appsOnDisplay(displayId) {
|
|
319
|
+
const display = displayFor(displayId)
|
|
320
|
+
const runningByBundle = runningAppsByBundleId()
|
|
321
|
+
const seen = new Set()
|
|
322
|
+
const apps = []
|
|
323
|
+
|
|
324
|
+
for (const window of listWindows()) {
|
|
325
|
+
if (!window.bundleId || seen.has(window.bundleId)) continue
|
|
326
|
+
if (!windowIntersectsDisplay(window, display)) continue
|
|
327
|
+
|
|
328
|
+
const running = runningByBundle.get(window.bundleId)
|
|
329
|
+
apps.push({
|
|
330
|
+
bundleId: window.bundleId,
|
|
331
|
+
displayName: running?.displayName ?? window.displayName ?? window.bundleId,
|
|
332
|
+
})
|
|
333
|
+
seen.add(window.bundleId)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return apps
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function appsToHide(exemptBundleIds, displayId) {
|
|
340
|
+
const exempt = new Set(
|
|
341
|
+
(exemptBundleIds ?? []).filter(
|
|
342
|
+
bundleId => typeof bundleId === 'string' && bundleId.length > 0,
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return appsOnDisplay(displayId).filter(
|
|
347
|
+
app => app.bundleId !== FINDER_BUNDLE_ID && !exempt.has(app.bundleId),
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function hideApp(bundleId) {
|
|
352
|
+
run('/usr/bin/osascript', [
|
|
353
|
+
'-e',
|
|
354
|
+
`tell application id "${escapeAppleScriptString(bundleId)}" to hide`,
|
|
355
|
+
])
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function openApp(bundleId) {
|
|
359
|
+
run('/usr/bin/open', ['-b', bundleId])
|
|
360
|
+
return true
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function unhideAppInBackground(bundleId) {
|
|
364
|
+
run('/usr/bin/open', ['-g', '-b', bundleId])
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function bundleIdsToDisplayIds(bundleIds) {
|
|
368
|
+
const windows = listWindows()
|
|
369
|
+
const displays = displayList()
|
|
370
|
+
|
|
371
|
+
return bundleIds.map(bundleId => {
|
|
372
|
+
const ids = new Set()
|
|
373
|
+
for (const window of windows) {
|
|
374
|
+
if (window.bundleId !== bundleId) continue
|
|
375
|
+
for (const display of displays) {
|
|
376
|
+
if (windowIntersectsDisplay(window, display)) {
|
|
377
|
+
ids.add(display.displayId)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
bundleId,
|
|
383
|
+
displayIds: [...ids].sort((a, b) => a - b),
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function appUnderPoint(x, y) {
|
|
389
|
+
const windows = listWindows()
|
|
390
|
+
for (const window of windows) {
|
|
391
|
+
const inside =
|
|
392
|
+
x >= window.x &&
|
|
393
|
+
x <= window.x + window.width &&
|
|
394
|
+
y >= window.y &&
|
|
395
|
+
y <= window.y + window.height
|
|
396
|
+
if (inside && window.bundleId) {
|
|
397
|
+
return {
|
|
398
|
+
bundleId: window.bundleId,
|
|
399
|
+
displayName: window.displayName ?? window.bundleId,
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return null
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function terminalIconDataUrl(path) {
|
|
407
|
+
if (!path || !existsSync(path)) {
|
|
408
|
+
return null
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const iconPath = findAppIconPath(path)
|
|
412
|
+
if (!iconPath) {
|
|
413
|
+
return null
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'claude-code-icon-'))
|
|
417
|
+
const outputPath = join(tempDir, 'icon.png')
|
|
418
|
+
try {
|
|
419
|
+
const result = spawnSync(
|
|
420
|
+
'/usr/bin/sips',
|
|
421
|
+
['-s', 'format', 'png', iconPath, '--out', outputPath],
|
|
422
|
+
{ encoding: 'utf8', stdio: 'pipe' },
|
|
423
|
+
)
|
|
424
|
+
if (result.status !== 0) {
|
|
425
|
+
return null
|
|
426
|
+
}
|
|
427
|
+
if (!existsSync(outputPath)) {
|
|
428
|
+
return null
|
|
429
|
+
}
|
|
430
|
+
const preview = readFileSync(outputPath)
|
|
431
|
+
return `data:image/png;base64,${preview.toString('base64')}`
|
|
432
|
+
} finally {
|
|
433
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function listInstalledApps() {
|
|
438
|
+
const paths = [
|
|
439
|
+
...listSpotlightApplicationPaths(),
|
|
440
|
+
...scanApplicationRoots(),
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
const apps = []
|
|
444
|
+
const seenBundleIds = new Set()
|
|
445
|
+
const seenPaths = new Set()
|
|
446
|
+
for (const appPath of paths) {
|
|
447
|
+
if (seenPaths.has(appPath)) continue
|
|
448
|
+
seenPaths.add(appPath)
|
|
449
|
+
|
|
450
|
+
const metadata = readAppMetadata(appPath)
|
|
451
|
+
if (!metadata || seenBundleIds.has(metadata.bundleId)) continue
|
|
452
|
+
|
|
453
|
+
seenBundleIds.add(metadata.bundleId)
|
|
454
|
+
apps.push(metadata)
|
|
455
|
+
}
|
|
456
|
+
return apps.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function unhideApps(bundleIds) {
|
|
460
|
+
for (const bundleId of Array.isArray(bundleIds) ? bundleIds : [bundleIds]) {
|
|
461
|
+
if (!bundleId) continue
|
|
462
|
+
try {
|
|
463
|
+
unhideAppInBackground(bundleId)
|
|
464
|
+
} catch {
|
|
465
|
+
// Best-effort restore.
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return true
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function prepareDisplay(allowedBundleIds, hostBundleId, displayId) {
|
|
472
|
+
const hidden = []
|
|
473
|
+
const candidates = appsToHide(
|
|
474
|
+
[...(allowedBundleIds ?? []), hostBundleId],
|
|
475
|
+
displayId,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
for (const app of candidates) {
|
|
479
|
+
try {
|
|
480
|
+
hideApp(app.bundleId)
|
|
481
|
+
hidden.push(app.bundleId)
|
|
482
|
+
} catch {
|
|
483
|
+
// Best-effort hide.
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const allowed = new Set(allowedBundleIds ?? [])
|
|
488
|
+
const running = runningAppsByBundleId()
|
|
489
|
+
const displayLocalCandidate = appsOnDisplay(displayId).find(app =>
|
|
490
|
+
allowed.has(app.bundleId),
|
|
491
|
+
)
|
|
492
|
+
const activated =
|
|
493
|
+
displayLocalCandidate?.bundleId ??
|
|
494
|
+
[...allowed].find(bundleId => running.has(bundleId))
|
|
495
|
+
|
|
496
|
+
if (!activated) {
|
|
497
|
+
return { hidden }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
openApp(activated)
|
|
502
|
+
return { hidden, activated }
|
|
503
|
+
} catch {
|
|
504
|
+
return { hidden }
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function checkAccessibility() {
|
|
509
|
+
const result = spawnSync('/usr/bin/osascript', [
|
|
510
|
+
'-e',
|
|
511
|
+
'tell application "System Events" to return UI elements enabled',
|
|
512
|
+
], {
|
|
513
|
+
encoding: 'utf8',
|
|
514
|
+
stdio: 'pipe',
|
|
515
|
+
})
|
|
516
|
+
return result.status === 0 && String(result.stdout).trim() === 'true'
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function requestAccessibility() {
|
|
520
|
+
run('/usr/bin/open', [
|
|
521
|
+
'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility',
|
|
522
|
+
])
|
|
523
|
+
return true
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function checkScreenRecording() {
|
|
527
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'claude-screen-recording-'))
|
|
528
|
+
const outputPath = join(tempDir, 'screen.png')
|
|
529
|
+
try {
|
|
530
|
+
const result = spawnSync('/usr/sbin/screencapture', ['-x', outputPath], {
|
|
531
|
+
encoding: 'utf8',
|
|
532
|
+
stdio: 'pipe',
|
|
533
|
+
})
|
|
534
|
+
return result.status === 0
|
|
535
|
+
} finally {
|
|
536
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function requestScreenRecording() {
|
|
541
|
+
run('/usr/bin/open', [
|
|
542
|
+
'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
|
|
543
|
+
])
|
|
544
|
+
return true
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function getDisplaySize(displayId) {
|
|
548
|
+
return displayFor(displayId)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function resolveTargetDisplayId(allowedBundleIds, preferredDisplayId, autoResolve) {
|
|
552
|
+
const fallbackDisplayId = displayFor(preferredDisplayId).displayId
|
|
553
|
+
if (!autoResolve || !allowedBundleIds || allowedBundleIds.length === 0) {
|
|
554
|
+
return fallbackDisplayId
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const counts = new Map()
|
|
558
|
+
for (const match of bundleIdsToDisplayIds(allowedBundleIds)) {
|
|
559
|
+
for (const displayId of match.displayIds) {
|
|
560
|
+
counts.set(displayId, (counts.get(displayId) ?? 0) + 1)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (preferredDisplayId != null && counts.has(preferredDisplayId)) {
|
|
565
|
+
return preferredDisplayId
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const bestDisplayId = [...counts.entries()].sort((a, b) => {
|
|
569
|
+
if (b[1] !== a[1]) return b[1] - a[1]
|
|
570
|
+
return a[0] - b[0]
|
|
571
|
+
})[0]?.[0]
|
|
572
|
+
|
|
573
|
+
return bestDisplayId ?? fallbackDisplayId
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function captureDisplay(displayId, outWidth, outHeight, quality) {
|
|
577
|
+
const display = displayFor(displayId)
|
|
578
|
+
const source = captureScreenToBuffer(display.displayId)
|
|
579
|
+
const sized = await toSizedJpegBase64(source, outWidth, outHeight, quality)
|
|
580
|
+
return {
|
|
581
|
+
...sized,
|
|
582
|
+
displayWidth: display.width,
|
|
583
|
+
displayHeight: display.height,
|
|
584
|
+
displayId: display.displayId,
|
|
585
|
+
originX: display.originX,
|
|
586
|
+
originY: display.originY,
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function resolvePrepareCapture(
|
|
591
|
+
allowedBundleIds,
|
|
592
|
+
hostBundleId,
|
|
593
|
+
quality,
|
|
594
|
+
outWidth,
|
|
595
|
+
outHeight,
|
|
596
|
+
preferredDisplayId,
|
|
597
|
+
autoResolve,
|
|
598
|
+
doHide,
|
|
599
|
+
) {
|
|
600
|
+
const displayId = resolveTargetDisplayId(
|
|
601
|
+
allowedBundleIds ?? [],
|
|
602
|
+
preferredDisplayId,
|
|
603
|
+
autoResolve,
|
|
604
|
+
)
|
|
605
|
+
const display = displayFor(displayId)
|
|
606
|
+
const hidden = doHide
|
|
607
|
+
? prepareDisplay(allowedBundleIds ?? [], hostBundleId, display.displayId).hidden
|
|
608
|
+
: []
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
return {
|
|
612
|
+
...(await captureDisplay(display.displayId, outWidth, outHeight, quality)),
|
|
613
|
+
hidden,
|
|
614
|
+
}
|
|
615
|
+
} catch (error) {
|
|
616
|
+
return {
|
|
617
|
+
base64: '',
|
|
618
|
+
width: 0,
|
|
619
|
+
height: 0,
|
|
620
|
+
displayWidth: display.width,
|
|
621
|
+
displayHeight: display.height,
|
|
622
|
+
displayId: display.displayId,
|
|
623
|
+
originX: display.originX,
|
|
624
|
+
originY: display.originY,
|
|
625
|
+
hidden,
|
|
626
|
+
captureError: error instanceof Error ? error.message : String(error),
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function captureExcluding(
|
|
632
|
+
_allowedBundleIds,
|
|
633
|
+
quality,
|
|
634
|
+
outWidth,
|
|
635
|
+
outHeight,
|
|
636
|
+
displayId,
|
|
637
|
+
) {
|
|
638
|
+
return captureDisplay(displayId, outWidth, outHeight, quality)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function captureRegion(
|
|
642
|
+
_allowedBundleIds,
|
|
643
|
+
x,
|
|
644
|
+
y,
|
|
645
|
+
w,
|
|
646
|
+
h,
|
|
647
|
+
outWidth,
|
|
648
|
+
outHeight,
|
|
649
|
+
quality,
|
|
650
|
+
displayId,
|
|
651
|
+
) {
|
|
652
|
+
return captureRegionBase64(
|
|
653
|
+
displayId,
|
|
654
|
+
{ x, y, w, h },
|
|
655
|
+
outWidth,
|
|
656
|
+
outHeight,
|
|
657
|
+
quality,
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function drainMainRunLoop() {
|
|
662
|
+
return true
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function registerEscape() {
|
|
666
|
+
return true
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function noop() {
|
|
670
|
+
return true
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export default {
|
|
674
|
+
_drainMainRunLoop: drainMainRunLoop,
|
|
675
|
+
resolvePrepareCapture,
|
|
676
|
+
tcc: {
|
|
677
|
+
checkAccessibility,
|
|
678
|
+
requestAccessibility,
|
|
679
|
+
checkScreenRecording,
|
|
680
|
+
requestScreenRecording,
|
|
681
|
+
},
|
|
682
|
+
hotkey: {
|
|
683
|
+
registerEscape,
|
|
684
|
+
unregister: noop,
|
|
685
|
+
notifyExpectedEscape: noop,
|
|
686
|
+
},
|
|
687
|
+
display: {
|
|
688
|
+
getSize: getDisplaySize,
|
|
689
|
+
listAll: displayList,
|
|
690
|
+
},
|
|
691
|
+
apps: {
|
|
692
|
+
prepareDisplay,
|
|
693
|
+
previewHideSet: appsToHide,
|
|
694
|
+
findWindowDisplays: bundleIdsToDisplayIds,
|
|
695
|
+
appUnderPoint,
|
|
696
|
+
listInstalled: listInstalledApps,
|
|
697
|
+
iconDataUrl: terminalIconDataUrl,
|
|
698
|
+
listRunning: listRunningApps,
|
|
699
|
+
open: openApp,
|
|
700
|
+
unhide: unhideApps,
|
|
701
|
+
},
|
|
702
|
+
screenshot: {
|
|
703
|
+
captureExcluding,
|
|
704
|
+
captureRegion,
|
|
705
|
+
},
|
|
706
|
+
}
|