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,226 @@
|
|
|
1
|
+
// Pure-JS audio capture module. Replaces the pre-compiled NAPI binary with
|
|
2
|
+
// subprocess backends: SoX (rec/play) as primary, ALSA (arecord/aplay) as
|
|
3
|
+
// Linux fallback. The public API is identical to the former NAPI wrapper so
|
|
4
|
+
// callers (voice.ts) require no changes.
|
|
5
|
+
|
|
6
|
+
import { type ChildProcess, spawn, spawnSync } from 'child_process'
|
|
7
|
+
|
|
8
|
+
const SAMPLE_RATE = 16000
|
|
9
|
+
const CHANNELS = 1
|
|
10
|
+
// SoX silence-detection parameters — mirror the cpal native module's
|
|
11
|
+
// auto-stop-on-silence behaviour.
|
|
12
|
+
const SILENCE_DURATION_SECS = '2.0'
|
|
13
|
+
const SILENCE_THRESHOLD = '3%'
|
|
14
|
+
|
|
15
|
+
// ─── State ─────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
let isRecording = false
|
|
18
|
+
let recordProcess: ChildProcess | null = null
|
|
19
|
+
// Track explicit stops per child so a late close event from the previous
|
|
20
|
+
// recorder cannot suppress or spuriously trigger callbacks for the next one.
|
|
21
|
+
const explicitlyStoppedRecorders = new WeakSet<ChildProcess>()
|
|
22
|
+
|
|
23
|
+
let isPlaying = false
|
|
24
|
+
let playProcess: ChildProcess | null = null
|
|
25
|
+
|
|
26
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function hasCommand(cmd: string): boolean {
|
|
29
|
+
const result = spawnSync(cmd, ['--version'], { stdio: 'ignore', timeout: 3000 })
|
|
30
|
+
return result.error === undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Prefer SoX because it supports built-in silence detection. Fall back to
|
|
34
|
+
// arecord on Linux when SoX is absent.
|
|
35
|
+
function recordingBackend(): 'sox' | 'arecord' | null {
|
|
36
|
+
if (hasCommand('rec')) return 'sox'
|
|
37
|
+
if (process.platform === 'linux' && hasCommand('arecord')) return 'arecord'
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Prefer SoX play; fall back to aplay on Linux.
|
|
42
|
+
function playbackBackend(): 'sox' | 'aplay' | null {
|
|
43
|
+
if (hasCommand('play')) return 'sox'
|
|
44
|
+
if (process.platform === 'linux' && hasCommand('aplay')) return 'aplay'
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export function isNativeAudioAvailable(): boolean {
|
|
51
|
+
const platform = process.platform
|
|
52
|
+
if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
return recordingBackend() !== null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function startNativeRecording(
|
|
59
|
+
onData: (data: Buffer) => void,
|
|
60
|
+
onEnd: () => void,
|
|
61
|
+
): boolean {
|
|
62
|
+
if (isRecording) return false
|
|
63
|
+
|
|
64
|
+
const backend = recordingBackend()
|
|
65
|
+
if (!backend) return false
|
|
66
|
+
|
|
67
|
+
let child: ChildProcess
|
|
68
|
+
|
|
69
|
+
if (backend === 'arecord') {
|
|
70
|
+
// arecord has no built-in silence detection; the caller is responsible
|
|
71
|
+
// for stopping via stopNativeRecording() (push-to-talk usage).
|
|
72
|
+
child = spawn(
|
|
73
|
+
'arecord',
|
|
74
|
+
[
|
|
75
|
+
'-f', 'S16_LE', // signed 16-bit little-endian
|
|
76
|
+
'-r', String(SAMPLE_RATE),
|
|
77
|
+
'-c', String(CHANNELS),
|
|
78
|
+
'-t', 'raw', // raw PCM, no WAV header
|
|
79
|
+
'-q', // suppress progress output
|
|
80
|
+
'-', // write to stdout
|
|
81
|
+
],
|
|
82
|
+
{ stdio: ['ignore', 'pipe', 'ignore'] },
|
|
83
|
+
)
|
|
84
|
+
} else {
|
|
85
|
+
// SoX with silence detection: process exits naturally after
|
|
86
|
+
// SILENCE_DURATION_SECS of silence, triggering onEnd automatically.
|
|
87
|
+
// --buffer 1024 forces small flushes so onData fires promptly.
|
|
88
|
+
child = spawn(
|
|
89
|
+
'rec',
|
|
90
|
+
[
|
|
91
|
+
'-q',
|
|
92
|
+
'--buffer', '1024',
|
|
93
|
+
'-t', 'raw',
|
|
94
|
+
'-r', String(SAMPLE_RATE),
|
|
95
|
+
'-e', 'signed',
|
|
96
|
+
'-b', '16',
|
|
97
|
+
'-c', String(CHANNELS),
|
|
98
|
+
'-',
|
|
99
|
+
'silence', '1', '0.1', SILENCE_THRESHOLD,
|
|
100
|
+
'1', SILENCE_DURATION_SECS, SILENCE_THRESHOLD,
|
|
101
|
+
],
|
|
102
|
+
{ stdio: ['ignore', 'pipe', 'ignore'] },
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
recordProcess = child
|
|
107
|
+
isRecording = true
|
|
108
|
+
|
|
109
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
110
|
+
onData(chunk)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const handleClose = () => {
|
|
114
|
+
if (recordProcess === child) {
|
|
115
|
+
recordProcess = null
|
|
116
|
+
isRecording = false
|
|
117
|
+
}
|
|
118
|
+
const wasStoppedExplicitly = explicitlyStoppedRecorders.has(child)
|
|
119
|
+
explicitlyStoppedRecorders.delete(child)
|
|
120
|
+
// Only fire onEnd for natural termination (silence detection).
|
|
121
|
+
// Explicit stopNativeRecording() calls suppress it, matching cpal behaviour.
|
|
122
|
+
if (!wasStoppedExplicitly) {
|
|
123
|
+
onEnd()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
child.on('close', handleClose)
|
|
128
|
+
child.on('error', handleClose)
|
|
129
|
+
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function stopNativeRecording(): void {
|
|
134
|
+
if (recordProcess) {
|
|
135
|
+
explicitlyStoppedRecorders.add(recordProcess)
|
|
136
|
+
recordProcess.kill('SIGTERM')
|
|
137
|
+
recordProcess = null
|
|
138
|
+
}
|
|
139
|
+
isRecording = false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function isNativeRecordingActive(): boolean {
|
|
143
|
+
return isRecording
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function startNativePlayback(sampleRate: number, channels: number): boolean {
|
|
147
|
+
if (isPlaying) stopNativePlayback()
|
|
148
|
+
|
|
149
|
+
const backend = playbackBackend()
|
|
150
|
+
if (!backend) return false
|
|
151
|
+
|
|
152
|
+
let child: ChildProcess
|
|
153
|
+
|
|
154
|
+
if (backend === 'aplay') {
|
|
155
|
+
child = spawn(
|
|
156
|
+
'aplay',
|
|
157
|
+
[
|
|
158
|
+
'-f', 'S16_LE',
|
|
159
|
+
'-r', String(sampleRate),
|
|
160
|
+
'-c', String(channels),
|
|
161
|
+
'-t', 'raw',
|
|
162
|
+
'-q',
|
|
163
|
+
'-',
|
|
164
|
+
],
|
|
165
|
+
{ stdio: ['pipe', 'ignore', 'ignore'] },
|
|
166
|
+
)
|
|
167
|
+
} else {
|
|
168
|
+
child = spawn(
|
|
169
|
+
'play',
|
|
170
|
+
[
|
|
171
|
+
'-q',
|
|
172
|
+
'-t', 'raw',
|
|
173
|
+
'-r', String(sampleRate),
|
|
174
|
+
'-e', 'signed',
|
|
175
|
+
'-b', '16',
|
|
176
|
+
'-c', String(channels),
|
|
177
|
+
'-',
|
|
178
|
+
],
|
|
179
|
+
{ stdio: ['pipe', 'ignore', 'ignore'] },
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
playProcess = child
|
|
184
|
+
isPlaying = true
|
|
185
|
+
|
|
186
|
+
const handleClose = () => {
|
|
187
|
+
if (playProcess === child) {
|
|
188
|
+
playProcess = null
|
|
189
|
+
isPlaying = false
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
child.on('close', handleClose)
|
|
194
|
+
child.on('error', handleClose)
|
|
195
|
+
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function writeNativePlaybackData(data: Buffer): void {
|
|
200
|
+
playProcess?.stdin?.write(data)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function stopNativePlayback(): void {
|
|
204
|
+
if (playProcess) {
|
|
205
|
+
playProcess.stdin?.end()
|
|
206
|
+
playProcess.kill('SIGTERM')
|
|
207
|
+
playProcess = null
|
|
208
|
+
}
|
|
209
|
+
isPlaying = false
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function isNativePlaying(): boolean {
|
|
213
|
+
return isPlaying
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Returns the microphone authorization status.
|
|
217
|
+
// macOS: 0 (notDetermined) — TCC cannot be queried from pure JS; the system
|
|
218
|
+
// permission prompt appears on the first recording attempt.
|
|
219
|
+
// Linux: 3 (authorized) — no system-level microphone permission API.
|
|
220
|
+
// Windows: 3 (authorized) — registry check requires Win32 API; assume OK.
|
|
221
|
+
// Other/unavailable: 0 (notDetermined).
|
|
222
|
+
export function microphoneAuthorizationStatus(): number {
|
|
223
|
+
const platform = process.platform
|
|
224
|
+
if (platform === 'linux' || platform === 'win32') return 3
|
|
225
|
+
return 0 // darwin or unknown — cannot check without native code
|
|
226
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process'
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from 'fs'
|
|
3
|
+
import { createRequire } from 'module'
|
|
4
|
+
import { tmpdir } from 'os'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
|
|
9
|
+
export type ClipboardImageResult = {
|
|
10
|
+
png: Buffer
|
|
11
|
+
originalWidth: number
|
|
12
|
+
originalHeight: number
|
|
13
|
+
width: number
|
|
14
|
+
height: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ImageProcessor = {
|
|
18
|
+
metadata(): Promise<{ width: number; height: number; format: string }>
|
|
19
|
+
resize(
|
|
20
|
+
width: number,
|
|
21
|
+
height: number,
|
|
22
|
+
options?: { fit?: string; withoutEnlargement?: boolean },
|
|
23
|
+
): ImageProcessor
|
|
24
|
+
jpeg(quality?: number): ImageProcessor
|
|
25
|
+
png(options?: {
|
|
26
|
+
compressionLevel?: number
|
|
27
|
+
palette?: boolean
|
|
28
|
+
colors?: number
|
|
29
|
+
}): ImageProcessor
|
|
30
|
+
webp(quality?: number): ImageProcessor
|
|
31
|
+
toBuffer(): Promise<Buffer>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type NativeModule = {
|
|
35
|
+
processImage: (input: Buffer) => Promise<ImageProcessor>
|
|
36
|
+
readClipboardImage?: (
|
|
37
|
+
maxWidth: number,
|
|
38
|
+
maxHeight: number,
|
|
39
|
+
) => ClipboardImageResult | null
|
|
40
|
+
hasClipboardImage?: () => boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type SharpInstance = {
|
|
44
|
+
metadata(): Promise<{ width?: number; height?: number; format?: string }>
|
|
45
|
+
resize(
|
|
46
|
+
width: number,
|
|
47
|
+
height: number,
|
|
48
|
+
options?: { fit?: string; withoutEnlargement?: boolean },
|
|
49
|
+
): SharpInstance
|
|
50
|
+
jpeg(options?: { quality?: number }): SharpInstance
|
|
51
|
+
png(options?: {
|
|
52
|
+
compressionLevel?: number
|
|
53
|
+
palette?: boolean
|
|
54
|
+
colors?: number
|
|
55
|
+
}): SharpInstance
|
|
56
|
+
webp(options?: { quality?: number }): SharpInstance
|
|
57
|
+
toBuffer(): Promise<Buffer>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type SharpFactory = (input: Buffer) => SharpInstance
|
|
61
|
+
type MaybeDefault<T> = T | { default: T }
|
|
62
|
+
|
|
63
|
+
const PNG_SIGNATURE = Buffer.from([
|
|
64
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
let cachedSharpFactory: SharpFactory | null = null
|
|
68
|
+
|
|
69
|
+
function getSharpFactory(): SharpFactory {
|
|
70
|
+
if (cachedSharpFactory) {
|
|
71
|
+
return cachedSharpFactory
|
|
72
|
+
}
|
|
73
|
+
const imported = require('sharp') as MaybeDefault<SharpFactory>
|
|
74
|
+
cachedSharpFactory =
|
|
75
|
+
typeof imported === 'function' ? imported : imported.default
|
|
76
|
+
return cachedSharpFactory
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createSharpProcessor(input: Buffer): ImageProcessor {
|
|
80
|
+
const sharpFactory = getSharpFactory()
|
|
81
|
+
let pipeline = sharpFactory(input)
|
|
82
|
+
|
|
83
|
+
const processor: ImageProcessor = {
|
|
84
|
+
async metadata() {
|
|
85
|
+
const metadata = await pipeline.metadata()
|
|
86
|
+
return {
|
|
87
|
+
width: metadata.width ?? 0,
|
|
88
|
+
height: metadata.height ?? 0,
|
|
89
|
+
format: metadata.format ?? 'unknown',
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
resize(width, height, options) {
|
|
93
|
+
pipeline = pipeline.resize(width, height, options)
|
|
94
|
+
return processor
|
|
95
|
+
},
|
|
96
|
+
jpeg(quality) {
|
|
97
|
+
pipeline = pipeline.jpeg(
|
|
98
|
+
quality === undefined ? undefined : { quality },
|
|
99
|
+
)
|
|
100
|
+
return processor
|
|
101
|
+
},
|
|
102
|
+
png(options) {
|
|
103
|
+
pipeline = pipeline.png(options)
|
|
104
|
+
return processor
|
|
105
|
+
},
|
|
106
|
+
webp(quality) {
|
|
107
|
+
pipeline = pipeline.webp(
|
|
108
|
+
quality === undefined ? undefined : { quality },
|
|
109
|
+
)
|
|
110
|
+
return processor
|
|
111
|
+
},
|
|
112
|
+
toBuffer() {
|
|
113
|
+
return pipeline.toBuffer()
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return processor
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createFallbackModule(): NativeModule {
|
|
121
|
+
return {
|
|
122
|
+
async processImage(input: Buffer) {
|
|
123
|
+
return createSharpProcessor(input)
|
|
124
|
+
},
|
|
125
|
+
hasClipboardImage: process.platform === 'darwin' ? hasClipboardImage : undefined,
|
|
126
|
+
readClipboardImage:
|
|
127
|
+
process.platform === 'darwin' ? readClipboardImage : undefined,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Raw binding accessor. Callers that need optional exports (e.g. clipboard
|
|
132
|
+
// functions) reach through this; keeping the wrappers on the caller side lets
|
|
133
|
+
// feature() tree-shake the property access strings out of external builds.
|
|
134
|
+
export function getNativeModule(): NativeModule | null {
|
|
135
|
+
return createFallbackModule()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function escapeAppleScriptString(value: string): string {
|
|
139
|
+
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readPngDimensions(
|
|
143
|
+
buffer: Buffer,
|
|
144
|
+
): { width: number; height: number } | null {
|
|
145
|
+
if (buffer.length < 24) {
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
if (!buffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
if (buffer.toString('ascii', 12, 16) !== 'IHDR') {
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
width: buffer.readUInt32BE(16),
|
|
156
|
+
height: buffer.readUInt32BE(20),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeClipboardPng(targetPath: string): Buffer | null {
|
|
161
|
+
const escaped = escapeAppleScriptString(targetPath)
|
|
162
|
+
const result = spawnSync(
|
|
163
|
+
'/usr/bin/osascript',
|
|
164
|
+
[
|
|
165
|
+
'-e',
|
|
166
|
+
'set png_data to (the clipboard as «class PNGf»)',
|
|
167
|
+
'-e',
|
|
168
|
+
`set fp to open for access POSIX file "${escaped}" with write permission`,
|
|
169
|
+
'-e',
|
|
170
|
+
'write png_data to fp',
|
|
171
|
+
'-e',
|
|
172
|
+
'close access fp',
|
|
173
|
+
],
|
|
174
|
+
{ stdio: 'pipe' },
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if (result.status !== 0) {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
return readFileSync(targetPath)
|
|
183
|
+
} catch {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resizePngSync(
|
|
189
|
+
inputPath: string,
|
|
190
|
+
outputPath: string,
|
|
191
|
+
width: number,
|
|
192
|
+
height: number,
|
|
193
|
+
): Buffer | null {
|
|
194
|
+
const result = spawnSync(
|
|
195
|
+
'/usr/bin/sips',
|
|
196
|
+
[
|
|
197
|
+
'-s',
|
|
198
|
+
'format',
|
|
199
|
+
'png',
|
|
200
|
+
'-z',
|
|
201
|
+
String(height),
|
|
202
|
+
String(width),
|
|
203
|
+
inputPath,
|
|
204
|
+
'--out',
|
|
205
|
+
outputPath,
|
|
206
|
+
],
|
|
207
|
+
{ stdio: 'pipe' },
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if (result.status !== 0) {
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
return readFileSync(outputPath)
|
|
216
|
+
} catch {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function hasClipboardImage(): boolean {
|
|
222
|
+
const result = spawnSync(
|
|
223
|
+
'/usr/bin/osascript',
|
|
224
|
+
['-e', 'the clipboard as «class PNGf»'],
|
|
225
|
+
{ stdio: 'pipe' },
|
|
226
|
+
)
|
|
227
|
+
return result.status === 0
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readClipboardImage(
|
|
231
|
+
maxWidth: number,
|
|
232
|
+
maxHeight: number,
|
|
233
|
+
): ClipboardImageResult | null {
|
|
234
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'claude-image-processor-'))
|
|
235
|
+
const clipboardPath = join(tempDir, 'clipboard.png')
|
|
236
|
+
const resizedPath = join(tempDir, 'clipboard-resized.png')
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const buffer = writeClipboardPng(clipboardPath)
|
|
240
|
+
if (!buffer) {
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const original = readPngDimensions(buffer)
|
|
245
|
+
if (!original) {
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let png = buffer
|
|
250
|
+
let width = original.width
|
|
251
|
+
let height = original.height
|
|
252
|
+
|
|
253
|
+
const safeMaxWidth = Math.max(1, Math.floor(maxWidth))
|
|
254
|
+
const safeMaxHeight = Math.max(1, Math.floor(maxHeight))
|
|
255
|
+
const scale = Math.min(
|
|
256
|
+
1,
|
|
257
|
+
safeMaxWidth / Math.max(width, 1),
|
|
258
|
+
safeMaxHeight / Math.max(height, 1),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if (scale < 1) {
|
|
262
|
+
const targetWidth = Math.max(1, Math.round(width * scale))
|
|
263
|
+
const targetHeight = Math.max(1, Math.round(height * scale))
|
|
264
|
+
const resized = resizePngSync(
|
|
265
|
+
clipboardPath,
|
|
266
|
+
resizedPath,
|
|
267
|
+
targetWidth,
|
|
268
|
+
targetHeight,
|
|
269
|
+
)
|
|
270
|
+
if (resized) {
|
|
271
|
+
const resizedDimensions = readPngDimensions(resized)
|
|
272
|
+
if (resizedDimensions) {
|
|
273
|
+
png = resized
|
|
274
|
+
width = resizedDimensions.width
|
|
275
|
+
height = resizedDimensions.height
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
png,
|
|
282
|
+
originalWidth: original.width,
|
|
283
|
+
originalHeight: original.height,
|
|
284
|
+
width,
|
|
285
|
+
height,
|
|
286
|
+
}
|
|
287
|
+
} finally {
|
|
288
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface SharpInstancePublic {
|
|
293
|
+
metadata(): Promise<{ width: number; height: number; format: string }>
|
|
294
|
+
resize(
|
|
295
|
+
width: number,
|
|
296
|
+
height: number,
|
|
297
|
+
options?: { fit?: string; withoutEnlargement?: boolean },
|
|
298
|
+
): SharpInstancePublic
|
|
299
|
+
jpeg(options?: { quality?: number }): SharpInstancePublic
|
|
300
|
+
png(options?: {
|
|
301
|
+
compressionLevel?: number
|
|
302
|
+
palette?: boolean
|
|
303
|
+
colors?: number
|
|
304
|
+
}): SharpInstancePublic
|
|
305
|
+
webp(options?: { quality?: number }): SharpInstancePublic
|
|
306
|
+
toBuffer(): Promise<Buffer>
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Factory function that matches sharp's API
|
|
310
|
+
export function sharp(input: Buffer): SharpInstancePublic {
|
|
311
|
+
let processorPromise: Promise<ImageProcessor> | null = null
|
|
312
|
+
|
|
313
|
+
// Create a chain of operations
|
|
314
|
+
const operations: Array<(proc: ImageProcessor) => void> = []
|
|
315
|
+
|
|
316
|
+
// Track how many operations have been applied to avoid re-applying
|
|
317
|
+
let appliedOperationsCount = 0
|
|
318
|
+
|
|
319
|
+
// Get or create the processor (without applying operations)
|
|
320
|
+
async function ensureProcessor(): Promise<ImageProcessor> {
|
|
321
|
+
if (!processorPromise) {
|
|
322
|
+
processorPromise = (async () => {
|
|
323
|
+
const mod = getNativeModule()
|
|
324
|
+
if (!mod) {
|
|
325
|
+
throw new Error('Native image processor module not available')
|
|
326
|
+
}
|
|
327
|
+
return mod.processImage(input)
|
|
328
|
+
})()
|
|
329
|
+
}
|
|
330
|
+
return processorPromise
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Apply any pending operations to the processor
|
|
334
|
+
function applyPendingOperations(proc: ImageProcessor): void {
|
|
335
|
+
for (let i = appliedOperationsCount; i < operations.length; i++) {
|
|
336
|
+
const op = operations[i]
|
|
337
|
+
if (op) {
|
|
338
|
+
op(proc)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
appliedOperationsCount = operations.length
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const instance: SharpInstancePublic = {
|
|
345
|
+
async metadata() {
|
|
346
|
+
const proc = await ensureProcessor()
|
|
347
|
+
return proc.metadata()
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
resize(
|
|
351
|
+
width: number,
|
|
352
|
+
height: number,
|
|
353
|
+
options?: { fit?: string; withoutEnlargement?: boolean },
|
|
354
|
+
) {
|
|
355
|
+
operations.push(proc => {
|
|
356
|
+
proc.resize(width, height, options)
|
|
357
|
+
})
|
|
358
|
+
return instance
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
jpeg(options?: { quality?: number }) {
|
|
362
|
+
operations.push(proc => {
|
|
363
|
+
proc.jpeg(options?.quality)
|
|
364
|
+
})
|
|
365
|
+
return instance
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
png(options?: {
|
|
369
|
+
compressionLevel?: number
|
|
370
|
+
palette?: boolean
|
|
371
|
+
colors?: number
|
|
372
|
+
}) {
|
|
373
|
+
operations.push(proc => {
|
|
374
|
+
proc.png(options)
|
|
375
|
+
})
|
|
376
|
+
return instance
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
webp(options?: { quality?: number }) {
|
|
380
|
+
operations.push(proc => {
|
|
381
|
+
proc.webp(options?.quality)
|
|
382
|
+
})
|
|
383
|
+
return instance
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
async toBuffer() {
|
|
387
|
+
const proc = await ensureProcessor()
|
|
388
|
+
applyPendingOperations(proc)
|
|
389
|
+
return proc.toBuffer()
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return instance
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export default sharp
|