From 7e15974be9056c0433a77766ff3ba1c293bf1d6b Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Wed, 1 Apr 2026 01:07:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=204=20=E4=B8=AA=20NA?= =?UTF-8?q?PI=20=E5=8C=85=20=E2=80=94=20modifiers/image-processor/audio-ca?= =?UTF-8?q?pture/url-handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- bun.lock | 51 +++++++ packages/audio-capture-napi/src/index.ts | 149 ++++++++++++++++++++- packages/image-processor-napi/package.json | 5 +- packages/image-processor-napi/src/index.ts | 125 ++++++++++++++++- packages/modifiers-napi/src/index.ts | 69 +++++++++- packages/url-handler-napi/src/index.ts | 2 +- src/tools/FileReadTool/imageProcessor.ts | 6 +- src/utils/imagePaste.ts | 14 +- 8 files changed, 398 insertions(+), 23 deletions(-) diff --git a/bun.lock b/bun.lock index 79fd739..019d398 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/packages/audio-capture-napi/src/index.ts b/packages/audio-capture-napi/src/index.ts index 36bf39d..a06c36b 100644 --- a/packages/audio-capture-napi/src/index.ts +++ b/packages/audio-capture-napi/src/index.ts @@ -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 } diff --git a/packages/image-processor-napi/package.json b/packages/image-processor-napi/package.json index 7c937ff..88442cc 100644 --- a/packages/image-processor-napi/package.json +++ b/packages/image-processor-napi/package.json @@ -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" + } } diff --git a/packages/image-processor-napi/src/index.ts b/packages/image-processor-napi/src/index.ts index ae94f74..ee90c3a 100644 --- a/packages/image-processor-napi/src/index.ts +++ b/packages/image-processor-napi/src/index.ts @@ -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 diff --git a/packages/modifiers-napi/src/index.ts b/packages/modifiers-napi/src/index.ts index 2c862ba..61509cd 100644 --- a/packages/modifiers-napi/src/index.ts +++ b/packages/modifiers-napi/src/index.ts @@ -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 = { + 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; } diff --git a/packages/url-handler-napi/src/index.ts b/packages/url-handler-napi/src/index.ts index 0497d63..0874abe 100644 --- a/packages/url-handler-napi/src/index.ts +++ b/packages/url-handler-napi/src/index.ts @@ -1,3 +1,3 @@ -export async function waitForUrlEvent(): Promise { +export async function waitForUrlEvent(timeoutMs?: number): Promise { return null } diff --git a/src/tools/FileReadTool/imageProcessor.ts b/src/tools/FileReadTool/imageProcessor.ts index 87537ad..e563b73 100644 --- a/src/tools/FileReadTool/imageProcessor.ts +++ b/src/tools/FileReadTool/imageProcessor.ts @@ -44,9 +44,9 @@ export async function getImageProcessor(): Promise { try { // Use the native image processor module const imageProcessor = await import('image-processor-napi') - const sharp = (imageProcessor as Record).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 diff --git a/src/utils/imagePaste.ts b/src/utils/imagePaste.ts index ed66c1c..ee1696a 100644 --- a/src/utils/imagePaste.ts +++ b/src/utils/imagePaste.ts @@ -106,10 +106,10 @@ export async function hasImageInClipboard(): Promise { // as an unhandled rejection in useClipboardImageHint's setTimeout. try { const { getNativeModule } = await import('image-processor-napi') - const nativeModule = getNativeModule() as Record | null - const hasImage = nativeModule?.hasClipboardImage - if (hasImage) { - return hasImage() + const nativeModule = getNativeModule() + if (nativeModule && 'hasClipboardImage' in nativeModule) { + const hasImage = (nativeModule as unknown as Record).hasClipboardImage + if (hasImage) return hasImage() } } catch (e) { logError(e as Error) @@ -136,8 +136,10 @@ export async function getImageFromClipboard(): Promise | null - const readClipboard = nativeModule?.readClipboardImage + const nativeModule = getNativeModule() + const readClipboard = nativeModule && 'readClipboardImage' in nativeModule + ? (nativeModule as unknown as Record).readClipboardImage + : undefined if (!readClipboard) { throw new Error('native clipboard reader unavailable') }