feat: 实现 4 个 NAPI 包 — modifiers/image-processor/audio-capture/url-handler

- modifiers-napi: 使用 Bun FFI 调用 macOS CGEventSourceFlagsState 检测修饰键
- image-processor-napi: 集成 sharp 库,macOS 剪贴板图像读取 (osascript)
- audio-capture-napi: 基于 SoX/arecord 的跨平台音频录制
- url-handler-napi: 完善函数签名(保持 null fallback)
- 修复 image-processor 类型兼容性问题

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best 2026-04-01 01:07:42 +08:00
parent fac9341e73
commit 7e15974be9
8 changed files with 398 additions and 23 deletions

View File

@ -150,6 +150,9 @@
"packages/image-processor-napi": {
"name": "image-processor-napi",
"version": "1.0.0",
"dependencies": {
"sharp": "^0.33.5",
},
},
"packages/modifiers-napi": {
"name": "modifiers-napi",
@ -661,12 +664,16 @@
"code-excerpt": ["code-excerpt@4.0.0", "https://registry.npmmirror.com/code-excerpt/-/code-excerpt-4.0.0.tgz", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
"color": ["color@4.2.3", "https://registry.npmmirror.com/color/-/color-4.2.3.tgz", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"color-convert": ["color-convert@2.0.1", "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-diff-napi": ["color-diff-napi@workspace:packages/color-diff-napi"],
"color-name": ["color-name@1.1.4", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"color-string": ["color-string@1.9.1", "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
"combined-stream": ["combined-stream@1.0.8", "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"commander": ["commander@13.1.0", "https://registry.npmmirror.com/commander/-/commander-13.1.0.tgz", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
@ -855,6 +862,8 @@
"ipaddr.js": ["ipaddr.js@1.9.1", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-arrayish": ["is-arrayish@0.3.4", "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
"is-docker": ["is-docker@3.0.0", "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
@ -1081,6 +1090,8 @@
"signal-exit": ["signal-exit@4.1.0", "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-swizzle": ["simple-swizzle@0.2.4", "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
"ssri": ["ssri@13.0.1", "https://registry.npmmirror.com/ssri/-/ssri-13.0.1.tgz", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="],
"stack-utils": ["stack-utils@2.0.6", "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
@ -1489,6 +1500,8 @@
"http-proxy-agent/agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"image-processor-napi/sharp": ["sharp@0.33.5", "https://registry.npmmirror.com/sharp/-/sharp-0.33.5.tgz", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
"minipass-flush/minipass": ["minipass@3.3.6", "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-pipeline/minipass": ["minipass@3.3.6", "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@ -1627,6 +1640,44 @@
"gtoken/gaxios/uuid": ["uuid@9.0.1", "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"image-processor-napi/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
"image-processor-napi/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
"image-processor-napi/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
"image-processor-napi/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
"image-processor-napi/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
"image-processor-napi/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
"image-processor-napi/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="],
"image-processor-napi/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
"image-processor-napi/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
"image-processor-napi/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
"image-processor-napi/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
"image-processor-napi/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
"image-processor-napi/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="],
"image-processor-napi/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
"image-processor-napi/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
"image-processor-napi/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
"image-processor-napi/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="],
"image-processor-napi/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="],
"image-processor-napi/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
"qrcode/yargs/cliui": ["cliui@6.0.0", "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
"qrcode/yargs/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],

View File

@ -1,14 +1,151 @@
// Auto-generated stub — replace with real implementation
// audio-capture-napi: cross-platform audio capture using SoX (rec) on macOS
// and arecord (ALSA) on Linux. Replaces the original cpal-based native module.
import { type ChildProcess, spawn, spawnSync } from 'child_process'
// ─── State ───────────────────────────────────────────────────────────
let recordingProcess: ChildProcess | null = null
let availabilityCache: boolean | null = null
// ─── Helpers ─────────────────────────────────────────────────────────
function commandExists(cmd: string): boolean {
const result = spawnSync(cmd, ['--version'], {
stdio: 'ignore',
timeout: 3000,
})
return result.error === undefined
}
// ─── Public API ──────────────────────────────────────────────────────
/**
* Check whether a supported audio recording command is available.
* Returns true if `rec` (SoX) is found on macOS, or `arecord` (ALSA) on Linux.
* Windows is not supported and always returns false.
*/
export function isNativeAudioAvailable(): boolean {
if (availabilityCache !== null) {
return availabilityCache
}
if (process.platform === 'win32') {
availabilityCache = false
return false
}
if (process.platform === 'darwin') {
// macOS: use SoX rec
availabilityCache = commandExists('rec')
return availabilityCache
}
if (process.platform === 'linux') {
// Linux: prefer arecord, fall back to rec
availabilityCache = commandExists('arecord') || commandExists('rec')
return availabilityCache
}
availabilityCache = false
return false
}
/**
* Check whether a recording is currently in progress.
*/
export function isNativeRecordingActive(): boolean {
return false
return recordingProcess !== null && !recordingProcess.killed
}
export function stopNativeRecording(): void {}
/**
* Stop the active recording process, if any.
*/
export function stopNativeRecording(): void {
if (recordingProcess) {
const proc = recordingProcess
recordingProcess = null
if (!proc.killed) {
proc.kill('SIGTERM')
}
}
}
/**
* Start recording audio. Raw PCM data (16kHz, 16-bit signed, mono) is
* streamed via the onData callback. onEnd is called when recording stops
* (either from silence detection or process termination).
*
* Returns true if recording started successfully, false otherwise.
*/
export function startNativeRecording(
_onData: (data: Buffer) => void,
_onEnd: () => void,
onData: (data: Buffer) => void,
onEnd: () => void,
): boolean {
return false
// Don't start if already recording
if (isNativeRecordingActive()) {
stopNativeRecording()
}
if (!isNativeAudioAvailable()) {
return false
}
let child: ChildProcess
if (process.platform === 'darwin' || (process.platform === 'linux' && commandExists('rec'))) {
// Use SoX rec: output raw PCM 16kHz 16-bit signed mono to stdout
child = spawn(
'rec',
[
'-q', // quiet
'--buffer',
'1024', // small buffer for low latency
'-t', 'raw', // raw PCM output
'-r', '16000', // 16kHz sample rate
'-e', 'signed', // signed integer encoding
'-b', '16', // 16-bit
'-c', '1', // mono
'-', // output to stdout
],
{ stdio: ['pipe', 'pipe', 'pipe'] },
)
} else if (process.platform === 'linux' && commandExists('arecord')) {
// Use arecord: output raw PCM 16kHz 16-bit signed LE mono to stdout
child = spawn(
'arecord',
[
'-f', 'S16_LE', // signed 16-bit little-endian
'-r', '16000', // 16kHz sample rate
'-c', '1', // mono
'-t', 'raw', // raw PCM, no header
'-q', // quiet
'-', // output to stdout
],
{ stdio: ['pipe', 'pipe', 'pipe'] },
)
} else {
return false
}
recordingProcess = child
child.stdout?.on('data', (chunk: Buffer) => {
onData(chunk)
})
// Consume stderr to prevent backpressure
child.stderr?.on('data', () => {})
child.on('close', () => {
recordingProcess = null
onEnd()
})
child.on('error', () => {
recordingProcess = null
onEnd()
})
return true
}

View File

@ -4,5 +4,8 @@
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts"
"types": "./src/index.ts",
"dependencies": {
"sharp": "^0.33.5"
}
}

View File

@ -1,6 +1,125 @@
export function getNativeModule(): null {
import sharpModule from 'sharp'
export const sharp = sharpModule
interface NativeModule {
hasClipboardImage(): boolean
readClipboardImage(
maxWidth?: number,
maxHeight?: number,
): {
png: Buffer
width: number
height: number
originalWidth: number
originalHeight: number
} | null
}
function createDarwinNativeModule(): NativeModule {
return {
hasClipboardImage(): boolean {
try {
const result = Bun.spawnSync({
cmd: [
'osascript',
'-e',
'try\nthe clipboard as «class PNGf»\nreturn "yes"\non error\nreturn "no"\nend try',
],
stdout: 'pipe',
stderr: 'pipe',
})
const output = result.stdout.toString().trim()
return output === 'yes'
} catch {
return false
}
},
readClipboardImage(
maxWidth?: number,
maxHeight?: number,
) {
try {
// Use osascript to read clipboard image as PNG data and write to a temp file,
// then read the temp file back
const tmpPath = `/tmp/claude_clipboard_native_${Date.now()}.png`
const script = `
set png_data to (the clipboard as «class PNGf»)
set fp to open for access POSIX file "${tmpPath}" with write permission
write png_data to fp
close access fp
return "${tmpPath}"
`
const result = Bun.spawnSync({
cmd: ['osascript', '-e', script],
stdout: 'pipe',
stderr: 'pipe',
})
if (result.exitCode !== 0) {
return null
}
const file = Bun.file(tmpPath)
// Use synchronous read via Node compat
const fs = require('fs')
const buffer: Buffer = fs.readFileSync(tmpPath)
// Clean up temp file
try {
fs.unlinkSync(tmpPath)
} catch {
// ignore cleanup errors
}
if (buffer.length === 0) {
return null
}
// Read PNG dimensions from IHDR chunk
// PNG header: 8 bytes signature, then IHDR chunk
// IHDR starts at offset 8 (4 bytes length) + 4 bytes "IHDR" + 4 bytes width + 4 bytes height
let width = 0
let height = 0
if (buffer.length > 24 && buffer[12] === 0x49 && buffer[13] === 0x48 && buffer[14] === 0x44 && buffer[15] === 0x52) {
width = buffer.readUInt32BE(16)
height = buffer.readUInt32BE(20)
}
const originalWidth = width
const originalHeight = height
// If maxWidth/maxHeight are specified and the image exceeds them,
// we still return the full PNG - the caller handles resizing via sharp
// But we report the capped dimensions
if (maxWidth && maxHeight) {
if (width > maxWidth || height > maxHeight) {
const scale = Math.min(maxWidth / width, maxHeight / height)
width = Math.round(width * scale)
height = Math.round(height * scale)
}
}
return {
png: buffer,
width,
height,
originalWidth,
originalHeight,
}
} catch {
return null
}
},
}
}
export function getNativeModule(): NativeModule | null {
if (process.platform === 'darwin') {
return createDarwinNativeModule()
}
return null
}
const stub: any = {}
export default stub
export default sharp

View File

@ -1,5 +1,68 @@
export function prewarm(): void {}
import { dlopen, FFIType, suffix } from "bun:ffi";
export function isModifierPressed(_modifier: string): boolean {
return false
const FLAG_SHIFT = 0x20000;
const FLAG_CONTROL = 0x40000;
const FLAG_OPTION = 0x80000;
const FLAG_COMMAND = 0x100000;
const modifierFlags: Record<string, number> = {
shift: FLAG_SHIFT,
control: FLAG_CONTROL,
option: FLAG_OPTION,
command: FLAG_COMMAND,
};
// kCGEventSourceStateCombinedSessionState = 0
const kCGEventSourceStateCombinedSessionState = 0;
let cgEventSourceFlagsState: ((stateID: number) => number) | null = null;
function loadFFI(): void {
if (cgEventSourceFlagsState !== null || process.platform !== "darwin") {
return;
}
try {
const lib = dlopen(
`/System/Library/Frameworks/Carbon.framework/Carbon`,
{
CGEventSourceFlagsState: {
args: [FFIType.i32],
returns: FFIType.u64,
},
}
);
cgEventSourceFlagsState = (stateID: number): number => {
return Number(lib.symbols.CGEventSourceFlagsState(stateID));
};
} catch {
// If loading fails, keep the function null so isModifierPressed returns false
cgEventSourceFlagsState = null;
}
}
export function prewarm(): void {
loadFFI();
}
export function isModifierPressed(modifier: string): boolean {
if (process.platform !== "darwin") {
return false;
}
loadFFI();
if (cgEventSourceFlagsState === null) {
return false;
}
const flag = modifierFlags[modifier];
if (flag === undefined) {
return false;
}
const currentFlags = cgEventSourceFlagsState(
kCGEventSourceStateCombinedSessionState
);
return (currentFlags & flag) !== 0;
}

View File

@ -1,3 +1,3 @@
export async function waitForUrlEvent(): Promise<string | null> {
export async function waitForUrlEvent(timeoutMs?: number): Promise<string | null> {
return null
}

View File

@ -44,9 +44,9 @@ export async function getImageProcessor(): Promise<SharpFunction> {
try {
// Use the native image processor module
const imageProcessor = await import('image-processor-napi')
const sharp = (imageProcessor as Record<string, SharpFunction>).sharp || imageProcessor.default
imageProcessorModule = { default: sharp }
return sharp
const sharpFn = (imageProcessor.sharp ?? imageProcessor.default) as SharpFunction
imageProcessorModule = { default: sharpFn }
return sharpFn
} catch {
// Fall back to sharp if native module is not available
// biome-ignore lint/suspicious/noConsole: intentional warning

View File

@ -106,10 +106,10 @@ export async function hasImageInClipboard(): Promise<boolean> {
// as an unhandled rejection in useClipboardImageHint's setTimeout.
try {
const { getNativeModule } = await import('image-processor-napi')
const nativeModule = getNativeModule() as Record<string, Function> | null
const hasImage = nativeModule?.hasClipboardImage
if (hasImage) {
return hasImage()
const nativeModule = getNativeModule()
if (nativeModule && 'hasClipboardImage' in nativeModule) {
const hasImage = (nativeModule as unknown as Record<string, Function>).hasClipboardImage
if (hasImage) return hasImage()
}
} catch (e) {
logError(e as Error)
@ -136,8 +136,10 @@ export async function getImageFromClipboard(): Promise<ImageWithDimensions | nul
) {
try {
const { getNativeModule } = await import('image-processor-napi')
const nativeModule = getNativeModule() as Record<string, Function> | null
const readClipboard = nativeModule?.readClipboardImage
const nativeModule = getNativeModule()
const readClipboard = nativeModule && 'readClipboardImage' in nativeModule
? (nativeModule as unknown as Record<string, Function>).readClipboardImage
: undefined
if (!readClipboard) {
throw new Error('native clipboard reader unavailable')
}