claude-code with OpenAI mode fix

This commit is contained in:
HitMargin
2026-04-04 01:21:00 +08:00
commit c9f95fc34d
3050 changed files with 557030 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
{
"name": "@ant/claude-for-chrome-mcp",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts"
}

View File

@@ -0,0 +1,11 @@
export const BROWSER_TOOLS: any[] = []
export class ClaudeForChromeContext {}
export class Logger {}
export type PermissionMode = any
export function createClaudeForChromeMcpServer(..._args: any[]): any {
return null
}

View File

@@ -0,0 +1,7 @@
{
"name": "@ant/computer-use-input",
"version": "1.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts"
}

View File

@@ -0,0 +1,183 @@
/**
* @ant/computer-use-input — macOS 键鼠模拟实现
*
* 使用 macOS 原生工具实现:
* - AppleScript (osascript) — 应用信息、键盘输入
* - CGEvent via AppleScript-ObjC bridge — 鼠标操作、位置查询
*
* 仅 macOS 支持。其他平台返回 { isSupported: false }
*/
import { $ } from 'bun'
interface FrontmostAppInfo {
bundleId: string
appName: string
}
// AppleScript key code mapping
const KEY_MAP: Record<string, number> = {
return: 36, enter: 36, tab: 48, space: 49, delete: 51, backspace: 51,
escape: 53, esc: 53,
left: 123, right: 124, down: 125, up: 126,
f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97,
f7: 98, f8: 100, f9: 101, f10: 109, f11: 103, f12: 111,
home: 115, end: 119, pageup: 116, pagedown: 121,
}
const MODIFIER_MAP: Record<string, string> = {
command: 'command down', cmd: 'command down', meta: 'command down', super: 'command down',
shift: 'shift down',
option: 'option down', alt: 'option down',
control: 'control down', ctrl: 'control down',
}
async function osascript(script: string): Promise<string> {
const result = await $`osascript -e ${script}`.quiet().nothrow().text()
return result.trim()
}
async function jxa(script: string): Promise<string> {
const result = await $`osascript -l JavaScript -e ${script}`.quiet().nothrow().text()
return result.trim()
}
function jxaSync(script: string): string {
const result = Bun.spawnSync({
cmd: ['osascript', '-l', 'JavaScript', '-e', script],
stdout: 'pipe', stderr: 'pipe',
})
return new TextDecoder().decode(result.stdout).trim()
}
function buildMouseJxa(eventType: string, x: number, y: number, btn: number, clickState?: number): string {
let script = `ObjC.import("CoreGraphics"); var p = $.CGPointMake(${x},${y}); var e = $.CGEventCreateMouseEvent(null, $.${eventType}, p, ${btn});`
if (clickState !== undefined) {
script += ` $.CGEventSetIntegerValueField(e, $.kCGMouseEventClickState, ${clickState});`
}
script += ` $.CGEventPost($.kCGHIDEventTap, e);`
return script
}
// ---- Implementation functions ----
async function moveMouse(x: number, y: number, _animated: boolean): Promise<void> {
await jxa(buildMouseJxa('kCGEventMouseMoved', x, y, 0))
}
async function key(keyName: string, action: 'press' | 'release'): Promise<void> {
if (action === 'release') return
const lower = keyName.toLowerCase()
const keyCode = KEY_MAP[lower]
if (keyCode !== undefined) {
await osascript(`tell application "System Events" to key code ${keyCode}`)
} else {
await osascript(`tell application "System Events" to keystroke "${keyName.length === 1 ? keyName : lower}"`)
}
}
async function keys(parts: string[]): Promise<void> {
const modifiers: string[] = []
let finalKey: string | null = null
for (const part of parts) {
const mod = MODIFIER_MAP[part.toLowerCase()]
if (mod) modifiers.push(mod)
else finalKey = part
}
if (!finalKey) return
const lower = finalKey.toLowerCase()
const keyCode = KEY_MAP[lower]
const modStr = modifiers.length > 0 ? ` using {${modifiers.join(', ')}}` : ''
if (keyCode !== undefined) {
await osascript(`tell application "System Events" to key code ${keyCode}${modStr}`)
} else {
await osascript(`tell application "System Events" to keystroke "${finalKey.length === 1 ? finalKey : lower}"${modStr}`)
}
}
async function mouseLocation(): Promise<{ x: number; y: number }> {
const result = await jxa('ObjC.import("CoreGraphics"); var e = $.CGEventCreate(null); var p = $.CGEventGetLocation(e); p.x + "," + p.y')
const [xStr, yStr] = result.split(',')
return { x: Math.round(Number(xStr)), y: Math.round(Number(yStr)) }
}
async function mouseButton(
button: 'left' | 'right' | 'middle',
action: 'click' | 'press' | 'release',
count?: number,
): Promise<void> {
const pos = await mouseLocation()
const btn = button === 'left' ? 0 : button === 'right' ? 1 : 2
const downType = btn === 0 ? 'kCGEventLeftMouseDown' : btn === 1 ? 'kCGEventRightMouseDown' : 'kCGEventOtherMouseDown'
const upType = btn === 0 ? 'kCGEventLeftMouseUp' : btn === 1 ? 'kCGEventRightMouseUp' : 'kCGEventOtherMouseUp'
if (action === 'click') {
for (let i = 0; i < (count ?? 1); i++) {
await jxa(buildMouseJxa(downType, pos.x, pos.y, btn, i + 1))
await jxa(buildMouseJxa(upType, pos.x, pos.y, btn, i + 1))
}
} else if (action === 'press') {
await jxa(buildMouseJxa(downType, pos.x, pos.y, btn))
} else {
await jxa(buildMouseJxa(upType, pos.x, pos.y, btn))
}
}
async function mouseScroll(amount: number, direction: 'vertical' | 'horizontal'): Promise<void> {
const script = direction === 'vertical'
? `ObjC.import("CoreGraphics"); var e = $.CGEventCreateScrollWheelEvent(null, 0, 1, ${amount}); $.CGEventPost($.kCGHIDEventTap, e);`
: `ObjC.import("CoreGraphics"); var e = $.CGEventCreateScrollWheelEvent(null, 0, 2, 0, ${amount}); $.CGEventPost($.kCGHIDEventTap, e);`
await jxa(script)
}
async function typeText(text: string): Promise<void> {
const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
await osascript(`tell application "System Events" to keystroke "${escaped}"`)
}
function getFrontmostAppInfo(): FrontmostAppInfo | null {
try {
const result = Bun.spawnSync({
cmd: ['osascript', '-e', `
tell application "System Events"
set frontApp to first application process whose frontmost is true
set appName to name of frontApp
set bundleId to bundle identifier of frontApp
return bundleId & "|" & appName
end tell
`],
stdout: 'pipe',
stderr: 'pipe',
})
const output = new TextDecoder().decode(result.stdout).trim()
if (!output || !output.includes('|')) return null
const [bundleId, appName] = output.split('|', 2)
return { bundleId: bundleId!, appName: appName! }
} catch {
return null
}
}
// ---- Exports ----
export class ComputerUseInputAPI {
declare moveMouse: (x: number, y: number, animated: boolean) => Promise<void>
declare key: (key: string, action: 'press' | 'release') => Promise<void>
declare keys: (parts: string[]) => Promise<void>
declare mouseLocation: () => Promise<{ x: number; y: number }>
declare mouseButton: (button: 'left' | 'right' | 'middle', action: 'click' | 'press' | 'release', count?: number) => Promise<void>
declare mouseScroll: (amount: number, direction: 'vertical' | 'horizontal') => Promise<void>
declare typeText: (text: string) => Promise<void>
declare getFrontmostAppInfo: () => FrontmostAppInfo | null
declare isSupported: true
}
interface ComputerUseInputUnsupported {
isSupported: false
}
export type ComputerUseInput = ComputerUseInputAPI | ComputerUseInputUnsupported
// Plain object with all methods as own properties — compatible with require()
export const isSupported = process.platform === 'darwin'
export { moveMouse, key, keys, mouseLocation, mouseButton, mouseScroll, typeText, getFrontmostAppInfo }

View File

@@ -0,0 +1,13 @@
{
"name": "@ant/computer-use-mcp",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./sentinelApps": "./src/sentinelApps.ts",
"./types": "./src/types.ts"
}
}

View File

@@ -0,0 +1,163 @@
/**
* @ant/computer-use-mcp — Stub 实现
*
* 提供类型安全的 stub所有函数返回合理的默认值。
* 在 feature('CHICAGO_MCP') = false 时不会被实际调用,
* 但确保 import 不报错且类型正确。
*/
import type {
ComputerUseHostAdapter,
CoordinateMode,
GrantFlags,
Logger,
} from './types'
// Re-export types from types.ts
export type { CoordinateMode, Logger } from './types'
export type {
ComputerUseConfig,
ComputerUseHostAdapter,
CuPermissionRequest,
CuPermissionResponse,
CuSubGates,
} from './types'
export { DEFAULT_GRANT_FLAGS } from './types'
// ---------------------------------------------------------------------------
// Types (defined here for callers that import from the main entry)
// ---------------------------------------------------------------------------
export interface DisplayGeometry {
width: number
height: number
displayId?: number
originX?: number
originY?: number
}
export interface FrontmostApp {
bundleId: string
displayName: string
}
export interface InstalledApp {
bundleId: string
displayName: string
path: string
}
export interface RunningApp {
bundleId: string
displayName: string
}
export interface ScreenshotResult {
base64: string
width: number
height: number
}
export type ResolvePrepareCaptureResult = ScreenshotResult
export interface ScreenshotDims {
width: number
height: number
displayWidth: number
displayHeight: number
displayId: number
originX: number
originY: number
}
export interface CuCallToolResultContent {
type: 'image' | 'text'
data?: string
mimeType?: string
text?: string
}
export interface CuCallToolResult {
content: CuCallToolResultContent[]
telemetry: {
error_kind?: string
[key: string]: unknown
}
}
export type ComputerUseSessionContext = Record<string, unknown>
// ---------------------------------------------------------------------------
// API_RESIZE_PARAMS — 默认的截图缩放参数
// ---------------------------------------------------------------------------
export const API_RESIZE_PARAMS = {
maxWidth: 1280,
maxHeight: 800,
maxPixels: 1280 * 800,
}
// ---------------------------------------------------------------------------
// ComputerExecutor — stub class
// ---------------------------------------------------------------------------
export class ComputerExecutor {
capabilities: Record<string, boolean> = {}
}
// ---------------------------------------------------------------------------
// Functions — 返回合理默认值的 stub
// ---------------------------------------------------------------------------
/**
* 计算目标截图尺寸。
* 在物理宽高和 API 限制之间取最优尺寸。
*/
export function targetImageSize(
physW: number,
physH: number,
_params?: typeof API_RESIZE_PARAMS,
): [number, number] {
const maxW = _params?.maxWidth ?? 1280
const maxH = _params?.maxHeight ?? 800
const scale = Math.min(1, maxW / physW, maxH / physH)
return [Math.round(physW * scale), Math.round(physH * scale)]
}
/**
* 绑定会话上下文,返回工具调度函数。
* Stub 返回一个始终返回空结果的调度器。
*/
export function bindSessionContext(
_adapter: ComputerUseHostAdapter,
_coordinateMode: CoordinateMode,
_ctx: ComputerUseSessionContext,
): (name: string, args: unknown) => Promise<CuCallToolResult> {
return async (_name: string, _args: unknown) => ({
content: [],
telemetry: {},
})
}
/**
* 构建 Computer Use 工具定义列表。
* Stub 返回空数组(无工具)。
*/
export function buildComputerUseTools(
_capabilities?: Record<string, boolean>,
_coordinateMode?: CoordinateMode,
_installedAppNames?: string[],
): Array<{ name: string; description: string; inputSchema: Record<string, unknown> }> {
return []
}
/**
* 创建 Computer Use MCP server。
* Stub 返回 null服务未启用
*/
export function createComputerUseMcpServer(
_adapter?: ComputerUseHostAdapter,
_coordinateMode?: CoordinateMode,
): null {
return null
}

View File

@@ -0,0 +1,32 @@
/**
* Sentinel apps — 需要特殊权限警告的应用列表
*
* 包含终端、文件管理器、系统设置等敏感应用。
* Computer Use 操作这些应用时会显示额外警告。
*/
type SentinelCategory = 'shell' | 'filesystem' | 'system_settings'
const SENTINEL_MAP: Record<string, SentinelCategory> = {
// Shell / Terminal
'com.apple.Terminal': 'shell',
'com.googlecode.iterm2': 'shell',
'dev.warp.Warp-Stable': 'shell',
'io.alacritty': 'shell',
'com.github.wez.wezterm': 'shell',
'net.kovidgoyal.kitty': 'shell',
'co.zeit.hyper': 'shell',
// Filesystem
'com.apple.finder': 'filesystem',
// System Settings
'com.apple.systempreferences': 'system_settings',
'com.apple.SystemPreferences': 'system_settings',
}
export const sentinelApps: string[] = Object.keys(SENTINEL_MAP)
export function getSentinelCategory(bundleId: string): SentinelCategory | null {
return SENTINEL_MAP[bundleId] ?? null
}

View File

@@ -0,0 +1,70 @@
/**
* @ant/computer-use-mcp — Types
*
* 从调用侧反推的真实类型定义,替代 any stub。
*/
export type CoordinateMode = 'pixels' | 'normalized'
export interface CuSubGates {
pixelValidation: boolean
clipboardPasteMultiline: boolean
mouseAnimation: boolean
hideBeforeAction: boolean
autoTargetDisplay: boolean
clipboardGuard: boolean
}
export interface Logger {
silly(message: string, ...args: unknown[]): void
debug(message: string, ...args: unknown[]): void
info(message: string, ...args: unknown[]): void
warn(message: string, ...args: unknown[]): void
error(message: string, ...args: unknown[]): void
}
export interface CuPermissionRequest {
apps: Array<{ bundleId: string; displayName: string }>
requestedFlags: GrantFlags
reason: string
tccState: { accessibility: boolean; screenRecording: boolean }
willHide: string[]
}
export interface GrantFlags {
clipboardRead: boolean
clipboardWrite: boolean
systemKeyCombos: boolean
}
export interface CuPermissionResponse {
granted: string[]
denied: string[]
flags: GrantFlags
}
export const DEFAULT_GRANT_FLAGS: GrantFlags = {
clipboardRead: false,
clipboardWrite: false,
systemKeyCombos: false,
}
export interface ComputerUseConfig {
coordinateMode: CoordinateMode
enabledTools: string[]
}
export interface ComputerUseHostAdapter {
serverName: string
logger: Logger
executor: ComputerExecutor
ensureOsPermissions(): Promise<{ granted: true } | { granted: false; accessibility: boolean; screenRecording: boolean }>
isDisabled(): boolean
getSubGates(): CuSubGates
getAutoUnhideEnabled(): boolean
cropRawPatch?(base64: string, x: number, y: number, w: number, h: number): Promise<string>
}
export interface ComputerExecutor {
capabilities: Record<string, boolean>
}

View File

@@ -0,0 +1,8 @@
{
"name": "@ant/computer-use-swift",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts"
}

View File

@@ -0,0 +1,388 @@
/**
* @ant/computer-use-swift — macOS 实现
*
* 用 AppleScript/JXA/screencapture 替代原始 Swift 原生模块。
* 提供显示器信息、应用管理、截图等功能。
*
* 仅 macOS 支持。
*/
import { readFileSync, unlinkSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
// ---------------------------------------------------------------------------
// Types (exported for callers)
// ---------------------------------------------------------------------------
export interface DisplayGeometry {
width: number
height: number
scaleFactor: number
displayId: number
}
export interface PrepareDisplayResult {
activated: string
hidden: string[]
}
export interface AppInfo {
bundleId: string
displayName: string
}
export interface InstalledApp {
bundleId: string
displayName: string
path: string
iconDataUrl?: string
}
export interface RunningApp {
bundleId: string
displayName: string
}
export interface ScreenshotResult {
base64: string
width: number
height: number
}
export interface ResolvePrepareCaptureResult {
base64: string
width: number
height: number
}
export interface WindowDisplayInfo {
bundleId: string
displayIds: number[]
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function jxaSync(script: string): string {
const result = Bun.spawnSync({
cmd: ['osascript', '-l', 'JavaScript', '-e', script],
stdout: 'pipe', stderr: 'pipe',
})
return new TextDecoder().decode(result.stdout).trim()
}
function osascriptSync(script: string): string {
const result = Bun.spawnSync({
cmd: ['osascript', '-e', script],
stdout: 'pipe', stderr: 'pipe',
})
return new TextDecoder().decode(result.stdout).trim()
}
async function osascript(script: string): Promise<string> {
const proc = Bun.spawn(['osascript', '-e', script], {
stdout: 'pipe', stderr: 'pipe',
})
const text = await new Response(proc.stdout).text()
await proc.exited
return text.trim()
}
async function jxa(script: string): Promise<string> {
const proc = Bun.spawn(['osascript', '-l', 'JavaScript', '-e', script], {
stdout: 'pipe', stderr: 'pipe',
})
const text = await new Response(proc.stdout).text()
await proc.exited
return text.trim()
}
// ---------------------------------------------------------------------------
// DisplayAPI
// ---------------------------------------------------------------------------
interface DisplayAPI {
getSize(displayId?: number): DisplayGeometry
listAll(): DisplayGeometry[]
}
const displayAPI: DisplayAPI = {
getSize(displayId?: number): DisplayGeometry {
const all = this.listAll()
if (displayId !== undefined) {
const found = all.find(d => d.displayId === displayId)
if (found) return found
}
return all[0] ?? { width: 1920, height: 1080, scaleFactor: 2, displayId: 1 }
},
listAll(): DisplayGeometry[] {
try {
const raw = jxaSync(`
ObjC.import("CoreGraphics");
var displays = $.CGDisplayCopyAllDisplayModes ? [] : [];
var active = $.CGGetActiveDisplayList(10, null, Ref());
var countRef = Ref();
$.CGGetActiveDisplayList(0, null, countRef);
var count = countRef[0];
var idBuf = Ref();
$.CGGetActiveDisplayList(count, idBuf, countRef);
var result = [];
for (var i = 0; i < count; i++) {
var did = idBuf[i];
var w = $.CGDisplayPixelsWide(did);
var h = $.CGDisplayPixelsHigh(did);
var mode = $.CGDisplayCopyDisplayMode(did);
var pw = $.CGDisplayModeGetPixelWidth(mode);
var sf = pw > 0 && w > 0 ? pw / w : 2;
result.push({width: w, height: h, scaleFactor: sf, displayId: did});
}
JSON.stringify(result);
`)
return (JSON.parse(raw) as DisplayGeometry[]).map(d => ({
width: Number(d.width), height: Number(d.height),
scaleFactor: Number(d.scaleFactor), displayId: Number(d.displayId),
}))
} catch {
// Fallback: use NSScreen via JXA
try {
const raw = jxaSync(`
ObjC.import("AppKit");
var screens = $.NSScreen.screens;
var result = [];
for (var i = 0; i < screens.count; i++) {
var s = screens.objectAtIndex(i);
var frame = s.frame;
var desc = s.deviceDescription;
var screenNumber = desc.objectForKey($("NSScreenNumber")).intValue;
var backingFactor = s.backingScaleFactor;
result.push({
width: Math.round(frame.size.width),
height: Math.round(frame.size.height),
scaleFactor: backingFactor,
displayId: screenNumber
});
}
JSON.stringify(result);
`)
return (JSON.parse(raw) as DisplayGeometry[]).map(d => ({
width: Number(d.width),
height: Number(d.height),
scaleFactor: Number(d.scaleFactor),
displayId: Number(d.displayId),
}))
} catch {
return [{ width: 1920, height: 1080, scaleFactor: 2, displayId: 1 }]
}
}
},
}
// ---------------------------------------------------------------------------
// AppsAPI
// ---------------------------------------------------------------------------
interface AppsAPI {
prepareDisplay(allowlistBundleIds: string[], surrogateHost: string, displayId?: number): Promise<PrepareDisplayResult>
previewHideSet(bundleIds: string[], displayId?: number): Promise<AppInfo[]>
findWindowDisplays(bundleIds: string[]): Promise<WindowDisplayInfo[]>
appUnderPoint(x: number, y: number): Promise<AppInfo | null>
listInstalled(): Promise<InstalledApp[]>
iconDataUrl(path: string): string | null
listRunning(): RunningApp[]
open(bundleId: string): Promise<void>
unhide(bundleIds: string[]): Promise<void>
}
const appsAPI: AppsAPI = {
async prepareDisplay(
_allowlistBundleIds: string[],
_surrogateHost: string,
_displayId?: number,
): Promise<PrepareDisplayResult> {
return { activated: '', hidden: [] }
},
async previewHideSet(
_bundleIds: string[],
_displayId?: number,
): Promise<AppInfo[]> {
return []
},
async findWindowDisplays(bundleIds: string[]): Promise<WindowDisplayInfo[]> {
// Each running app is assumed to be on display 1
return bundleIds.map(bundleId => ({ bundleId, displayIds: [1] }))
},
async appUnderPoint(_x: number, _y: number): Promise<AppInfo | null> {
// Use JXA to find app at mouse position via accessibility
try {
const result = await jxa(`
ObjC.import("CoreGraphics");
ObjC.import("AppKit");
var pt = $.CGPointMake(${_x}, ${_y});
// Get frontmost app as a fallback
var app = $.NSWorkspace.sharedWorkspace.frontmostApplication;
JSON.stringify({bundleId: app.bundleIdentifier.js, displayName: app.localizedName.js});
`)
return JSON.parse(result)
} catch {
return null
}
},
async listInstalled(): Promise<InstalledApp[]> {
try {
const result = await osascript(`
tell application "System Events"
set appList to ""
repeat with appFile in (every file of folder "Applications" of startup disk whose name ends with ".app")
set appPath to POSIX path of (appFile as alias)
set appName to name of appFile
set appList to appList & appPath & "|" & appName & "\\n"
end repeat
return appList
end tell
`)
return result.split('\n').filter(Boolean).map(line => {
const [path, name] = line.split('|', 2)
// Derive bundleId from Info.plist would be ideal, but use path-based fallback
const displayName = (name ?? '').replace(/\.app$/, '')
return {
bundleId: `com.app.${displayName.toLowerCase().replace(/\s+/g, '-')}`,
displayName,
path: path ?? '',
}
})
} catch {
return []
}
},
iconDataUrl(_path: string): string | null {
return null
},
listRunning(): RunningApp[] {
try {
const raw = jxaSync(`
var apps = Application("System Events").applicationProcesses.whose({backgroundOnly: false});
var result = [];
for (var i = 0; i < apps.length; i++) {
try {
var a = apps[i];
result.push({bundleId: a.bundleIdentifier(), displayName: a.name()});
} catch(e) {}
}
JSON.stringify(result);
`)
return JSON.parse(raw)
} catch {
return []
}
},
async open(bundleId: string): Promise<void> {
await osascript(`tell application id "${bundleId}" to activate`)
},
async unhide(bundleIds: string[]): Promise<void> {
for (const bundleId of bundleIds) {
await osascript(`
tell application "System Events"
set visible of application process (name of application process whose bundle identifier is "${bundleId}") to true
end tell
`)
}
},
}
// ---------------------------------------------------------------------------
// ScreenshotAPI
// ---------------------------------------------------------------------------
interface ScreenshotAPI {
captureExcluding(
allowedBundleIds: string[], quality: number,
targetW: number, targetH: number, displayId?: number,
): Promise<ScreenshotResult>
captureRegion(
allowedBundleIds: string[],
x: number, y: number, w: number, h: number,
outW: number, outH: number, quality: number, displayId?: number,
): Promise<ScreenshotResult>
}
async function captureScreenToBase64(args: string[]): Promise<{ base64: string; width: number; height: number }> {
const tmpFile = join(tmpdir(), `cu-screenshot-${Date.now()}.png`)
const proc = Bun.spawn(['screencapture', ...args, tmpFile], {
stdout: 'pipe', stderr: 'pipe',
})
await proc.exited
try {
const buf = readFileSync(tmpFile)
const base64 = buf.toString('base64')
// Parse PNG header for dimensions (bytes 16-23)
const width = buf.readUInt32BE(16)
const height = buf.readUInt32BE(20)
return { base64, width, height }
} finally {
try { unlinkSync(tmpFile) } catch {}
}
}
const screenshotAPI: ScreenshotAPI = {
async captureExcluding(
_allowedBundleIds: string[],
_quality: number,
_targetW: number,
_targetH: number,
displayId?: number,
): Promise<ScreenshotResult> {
const args = ['-x'] // silent
if (displayId !== undefined) {
args.push('-D', String(displayId))
}
return captureScreenToBase64(args)
},
async captureRegion(
_allowedBundleIds: string[],
x: number, y: number, w: number, h: number,
_outW: number, _outH: number, _quality: number,
displayId?: number,
): Promise<ScreenshotResult> {
const args = ['-x', '-R', `${x},${y},${w},${h}`]
if (displayId !== undefined) {
args.push('-D', String(displayId))
}
return captureScreenToBase64(args)
},
}
// ---------------------------------------------------------------------------
// ComputerUseAPI — Main export
// ---------------------------------------------------------------------------
export class ComputerUseAPI {
apps: AppsAPI = appsAPI
display: DisplayAPI = displayAPI
screenshot: ScreenshotAPI = screenshotAPI
async resolvePrepareCapture(
allowedBundleIds: string[],
_surrogateHost: string,
quality: number,
targetW: number,
targetH: number,
displayId?: number,
_autoResolve?: boolean,
_doHide?: boolean,
): Promise<ResolvePrepareCaptureResult> {
return this.screenshot.captureExcluding(allowedBundleIds, quality, targetW, targetH, displayId)
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "audio-capture-napi",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts"
}

View File

@@ -0,0 +1,151 @@
// 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 recordingProcess !== null && !recordingProcess.killed
}
/**
* 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,
): boolean {
// 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

@@ -0,0 +1,11 @@
{
"name": "color-diff-napi",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"highlight.js": "latest"
}
}

View File

@@ -0,0 +1,102 @@
import { describe, expect, test } from "bun:test";
import { __test } from "../index";
const { ansi256FromRgb, colorToEscape, detectColorMode, detectLanguage, tokenize } = __test;
describe("ansi256FromRgb", () => {
test("black maps to index 16", () => {
expect(ansi256FromRgb(0, 0, 0)).toBe(16);
});
test("pure red maps to cube red", () => {
expect(ansi256FromRgb(255, 0, 0)).toBe(196);
});
test("pure green maps to cube green", () => {
expect(ansi256FromRgb(0, 255, 0)).toBe(46);
});
test("pure blue maps to cube blue", () => {
expect(ansi256FromRgb(0, 0, 255)).toBe(21);
});
test("grey values map to grey ramp", () => {
const idx = ansi256FromRgb(128, 128, 128);
// Should be in the grey ramp range (232-255)
expect(idx).toBeGreaterThanOrEqual(232);
expect(idx).toBeLessThanOrEqual(255);
});
});
describe("colorToEscape", () => {
test("palette index < 8 uses standard ANSI codes", () => {
const color = { r: 1, g: 0, b: 0, a: 0 }; // palette index 1
expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[31m"); // fg red
expect(colorToEscape(color, false, "truecolor")).toBe("\x1b[41m"); // bg red
});
test("palette index 8-15 uses bright ANSI codes", () => {
const color = { r: 9, g: 0, b: 0, a: 0 }; // bright red
expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[91m");
});
test("alpha=1 returns terminal default", () => {
const color = { r: 0, g: 0, b: 0, a: 1 };
expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[39m");
expect(colorToEscape(color, false, "truecolor")).toBe("\x1b[49m");
});
test("truecolor uses RGB escape", () => {
const color = { r: 100, g: 150, b: 200, a: 255 };
expect(colorToEscape(color, true, "truecolor")).toBe("\x1b[38;2;100;150;200m");
});
test("color256 uses 256-color escape", () => {
const color = { r: 100, g: 150, b: 200, a: 255 };
const result = colorToEscape(color, true, "color256");
expect(result).toMatch(/^\x1b\[38;5;\d+m$/);
});
});
describe("detectColorMode", () => {
test("returns ansi for ansi-containing theme names", () => {
expect(detectColorMode("ansi")).toBe("ansi");
expect(detectColorMode("base16-ansi-dark")).toBe("ansi");
});
test("returns truecolor or color256 for non-ansi themes", () => {
const mode = detectColorMode("monokai");
expect(["truecolor", "color256"]).toContain(mode);
});
});
describe("detectLanguage", () => {
test("detects language from file extension", () => {
expect(detectLanguage("index.ts")).toBe("ts");
expect(detectLanguage("main.py")).toBe("py");
expect(detectLanguage("style.css")).toBe("css");
});
test("detects language from known filenames", () => {
expect(detectLanguage("Makefile")).toBe("makefile");
expect(detectLanguage("Dockerfile")).toBe("dockerfile");
});
test("returns null for unknown extensions", () => {
expect(detectLanguage("file.xyz123")).toBeNull();
});
});
describe("tokenize", () => {
test("returns array of tokens", () => {
const result = tokenize("hello world");
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);
});
test("preserves original text when joined", () => {
const text = "foo bar baz";
const tokens = tokenize(text);
expect(tokens.join("")).toBe(text);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
{
"name": "image-processor-napi",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"sharp": "^0.33.5"
}
}

View File

@@ -0,0 +1,125 @@
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
}
export default sharp

View File

@@ -0,0 +1,8 @@
{
"name": "modifiers-napi",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts"
}

View File

@@ -0,0 +1,67 @@
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 ffi = require("bun:ffi") as typeof import("bun:ffi");
const lib = ffi.dlopen(
`/System/Library/Frameworks/Carbon.framework/Carbon`,
{
CGEventSourceFlagsState: {
args: [ffi.FFIType.i32],
returns: ffi.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

@@ -0,0 +1,8 @@
{
"name": "url-handler-napi",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts"
}

View File

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