فهرست منبع

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>
claude-code-best 3 هفته پیش
والد
کامیت
7e15974be9

+ 51 - 0
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=="],

+ 143 - 6
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
 }

+ 4 - 1
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"
+    }
 }

+ 122 - 3
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

+ 66 - 3
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<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;
 }

+ 1 - 1
packages/url-handler-napi/src/index.ts

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

+ 3 - 3
src/tools/FileReadTool/imageProcessor.ts

@@ -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

+ 8 - 6
src/utils/imagePaste.ts

@@ -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')
       }