Package Exports
- @avavilov/apple-script
- @avavilov/apple-script/package.json
Readme
@avavilov/apple-script
Type-safe AppleScript execution library for Node.js with Zod validation. Execute AppleScript operations with full TypeScript support, automatic input/output validation, and proper error handling.
Features
- π Type-Safe: Full TypeScript support with compile-time type checking
- β Validation: Input/output validation using Zod schemas
- π― Declarative API: Define operations once, use everywhere
- π Queue Management: Automatic serialization of operations per application
- π§Ή Automatic Normalization: Best-effort, schema-guided normalization of rows output (numbers/booleans/arrays/tuples/objects)
- β‘ Protocol Standardization: Consistent encoding/decoding with control characters (see the Protocol)
- π‘οΈ Security: Safe parameter marshalling prevents injection attacks
- π Observable: Debug hooks, result callbacks, and error handlers
- β±οΈ Timeout Control: Dual timeout system (AppleScript + Node.js)
- π Retry Logic: Configurable retry mechanism for transient failures
Note: This library is macOS-only as AppleScript is an Apple technology. For cross-platform automation, consider using tools like Playwright or Puppeteer.
Installation
npm install @avavilov/apple-script zod
Quick Start
import { z } from 'zod';
import { createAppleRunner, operation } from '@avavilov/apple-script';
// Define an operation
const getCurrentURL = operation.scalar({
name: 'getCurrentURL',
input: z.object({}),
output: z.string(),
script: () => `
return URL of active tab of front window
`
});
// Create a runner
const runner = createAppleRunner({
appId: 'com.apple.Safari'
});
// Execute the operation
const result = await runner.run(getCurrentURL, {});
if (result.ok) {
console.log('Current URL:', result.data);
} else {
console.error('Error:', result.error.message);
}
Core Concepts
Operations
Operations are declarative descriptions of AppleScript tasks with strict input/output contracts:
const openURL = operation.action({
name: 'openURL',
input: z.object({
url: z.string().url()
}),
script: ({ url }) => `
try
open location ${url}
return "1"
on error
return "0"
end try
`
});
Operation Types
1. Scalar Operations
Return a single string value (see Protocol: scalar):
const getTitle = operation.scalar({
name: 'getTitle',
input: z.object({ windowIndex: z.number() }),
output: z.string(),
script: ({ windowIndex }) => `
return title of window ${windowIndex}
`
});
2. Action Operations
Return status codes (0=failure, 1=success, 2=partial) (see Protocol: action):
const closeTab = operation.action({
name: 'closeTab',
input: z.object({ tabId: z.string() }),
script: ({ tabId }) => `
repeat with w in windows
repeat with t in tabs of w
if (id of t as text) is ${tabId} then
try
close t
return "1"
on error
return "0"
end try
end if
end repeat
end repeat
return "0"
`
});
3. Rows Operations
Return tabular data as array of objects (encoded with RS/US separators; see Protocol: rows):
const listTabs = operation.rows({
name: 'listTabs',
input: z.object({}),
output: z.array(z.object({
id: z.string(),
url: z.string(),
title: z.string()
})),
script: () => `
set rows to {}
repeat with w in windows
repeat with t in tabs of w
set end of rows to {(id of t as text), URL of t, title of t}
end repeat
end repeat
return rows
`
});
Rows β Objects Mapping
The runner automatically maps rows (string arrays) to objects based on:
mapRow
function if provided (highest priority)columns
array if provided (explicit column names)- Zod schema inference from
output: z.array(z.object({...}))
// Explicit columns (recommended for clarity)
const listTabs = operation.rows({
name: 'listTabs',
columns: ['id', 'url', 'title'], // Maps row[0]βid, row[1]βurl, row[2]βtitle
output: z.array(z.object({
id: z.coerce.number(),
url: z.string().url(),
title: z.string()
})),
script: () => `...`
});
// Custom mapping function
const listTabsWithDomain = operation.rows({
name: 'listTabsWithDomain',
mapRow: ([id, url, title]) => ({
id,
title,
domain: new URL(url).hostname
}),
output: z.array(z.object({
id: z.string(),
title: z.string(),
domain: z.string()
})),
script: () => `...`
});
Note: Row mapping happens before output validation, ensuring consistent behavior regardless of validation settings. See Troubleshooting for common issues.
Automatic normalization (rows)
After mapping rows to objects, the runner can automatically normalize stringly-typed values to match your output schema. This is a best-effort, schema-guided step that handles:
- numbers: "42" β 42
- booleans: "true"/"false"/"1"/"0" β true/false
- arrays: "{1, 2, 3}" or "1,2,3" β [1, 2, 3]
- tuples: "{a, b, c}" β [a, b, c]
- objects: recursively normalizes fields using the object shape
Toggles:
- Global:
RunnerConfig.normalizeRows
(default:true
) - Per-operation:
RowsOperationDef.normalizeRows
(overrides global)
Example with AppleScript-aware helper schemas:
import { z } from 'zod';
import { operation, schemas as as } from '@avavilov/apple-script';
const listTabs = operation.rows({
name: 'listTabs',
columns: ['id', 'url', 'title', 'active', 'bounds', 'indices'],
// as.record wraps number/boolean fields to accept AppleScript string representations
// and enforces strict validation by default (unknown keys are rejected)
// You may also use explicit helpers like as.boolean/as.number
output: z.array(as.record({
id: z.string(),
url: z.string().url(),
title: z.string(),
active: as.boolean, // "true" β true, "0" β false
bounds: as.array(as.number) // "{0, 0, 800, 600}" β [0, 0, 800, 600]
})).describe('List of tabs')
});
// You can disable/enable normalization:
// const runner = createAppleRunner({ appId, normalizeRows: true });
// Or per operation: operation.rows({ normalizeRows: false, ... })
4. Sections Operations
Return grouped data with named sections (see Protocol: sections):
const closeTabs = operation.sections({
name: 'closeTabs',
input: z.object({ ids: z.array(z.string()) }),
output: z.record(z.array(z.string())),
script: ({ ids }) => `
set closedList to {}
set notFoundList to {}
-- close tabs logic here --
return {{"closed", closedList}, {"notFound", notFoundList}}
`
});
Runner Configuration
const runner = createAppleRunner({
// Required
appId: 'com.apple.Safari',
// Timeouts
defaultTimeoutSec: 12, // AppleScript timeout
defaultControllerTimeoutMs: 15000, // Node.js timeout
timeoutByKind: {
scalar: 10,
action: 8,
rows: 15,
sections: 15
},
// Behavior
ensureAppReady: true, // Launch app if not running
validateByDefault: true, // Validate input/output
normalizeRows: true, // Normalize rows to schema (numbers/booleans/arrays/tuples/objects)
maxRetries: 2, // Retry on timeout
retryDelayMs: 1000, // Delay between retries
// Hooks
debug: ({ opName, script }) => {
console.log(`[${opName}] Script:`, script);
},
onResult: ({ opName, tookMs }) => {
console.log(`[${opName}] Completed in ${tookMs}ms`);
},
onError: ({ opName, error }) => {
console.error(`[${opName}] Failed:`, error.message);
}
});
Note: The runner serializes operations per appId using an internal QueueManager. Each appId has its own FIFO queue. Queue semantics (microtask scheduling, clear() epoch cut-off, and the length property) apply per appId queue. See: src/queue/README.md.
Protocol Details
For the complete protocol specification and additional examples, see docs/protocol.md.
Encoding
The library uses ASCII control characters for data structuring:
- GS (Group Separator, ASCII 29): Top-level sections
- RS (Record Separator, ASCII 30): Rows in tables
- US (Unit Separator, ASCII 31): Fields in records
Response Format
All AppleScript responses follow this format:
- Success:
OK<GS><payload>
- Error:
ERR<GS><code><GS><message>
Error Codes
-1712
: AppleScript timeout-10001
: Missing return value-10002
: Invalid return type for rows-10003
: Invalid return type for sections-10004
: Invalid action code-10005
: Invalid return type for scalar
Security
Parameter Marshalling
All parameters are safely marshalled as AppleScript literals:
// Your input
{ url: 'https://example.com', ids: ['1', '2', '3'] }
// Becomes AppleScript variables
set __ARG__url to "https://example.com"
set __ARG__ids to {"1", "2", "3"}
Injection Prevention
String values are properly escaped:
// Input with quotes
{ message: 'Hello "World"' }
// Safe AppleScript literal
set __ARG__message to "Hello " & quote & "World" & quote & ""
Advanced Usage
AppleScript-aware helper schemas (stable public API)
The library ships Zod helpers tailored for AppleScript string outputs. They can be used directly in your schemas or with automatic normalization.
asBoolean
β accepts boolean/numeric/string representationsasNumber
β accepts number or numeric stringasArray(item)
β accepts array or list string ("{...}" or CSV)asTuple([...items])
β accepts tuple or list string with fixed lengthasBounds
β shorthand for[x, y, width, height]
usingasNumber
asRecord(shape)
β wraps boolean/number fields in a shape withasBoolean
/asNumber
and enforces strict validation by default (unknown keys cause validation errors)
Import from the root entrypoint:
import { schemas as as } from '@avavilov/apple-script';
These helpers are a stable part of the public API and will follow semver. Breaking changes, if any, will be released as a major version.
Examples:
import { z } from 'zod';
import { schemas as as } from '@avavilov/apple-script';
// A single row item - strict by default (rejects unknown keys)
const Tab = as.record({
id: z.string(),
title: z.string(),
active: as.boolean, // "1"/"true" β true
zoom: as.number, // "125" β 125
bounds: as.bounds // "{0, 0, 800, 600}" β [0,0,800,600]
});
// If you need to allow unknown keys:
const TabWithExtra = as.record({ ... }).strip(); // removes unknown keys
const TabPassthrough = as.record({ ... }).passthrough(); // keeps unknown keys
// Whole rows output
const TabsOutput = z.array(Tab);
// Tuple example from a list string
const XY = as.tuple([as.number, as.number]); // "{10, 20}" β [10, 20]
// Array-of-number from CSV/list
const Numbers = as.array(as.number); // "1,2,3" or "{1, 2, 3}" β [1,2,3]
Compatibility note: Helper schema behavior and automatic rows normalization rely on limited Zod introspection under the hood (reading common internal fields). This path is covered by unit tests in this repository. In rare cases after a Zod upgrade, normalization may gracefully degrade to a no-op for affected shapes (values remain strings) rather than throwing. You can always opt in to explicit Zod coercion or transform as a fallback.
JavaScript Execution
Execute JavaScript in browser contexts:
const executeJS = operation.scalar({
name: 'executeJS',
input: z.object({
js: z.string().describe('js') // Mark as JS code
}),
output: z.string(),
hints: {
js: { js: { maxLenKb: 512 } } // Limit size
},
script: ({ js }) => `
return execute active tab of front window javascript ${js}
`
});
// Usage
const title = await runner.run(executeJS, {
js: 'document.title'
});
Custom Timeouts
Override timeouts per operation:
// At runtime per call
await runner.run(longOperation, {}, {
controllerTimeoutMs: 45000 // Controller timeout
});
Queue Management
Operations to the same app are automatically serialized:
// These run sequentially
const promise1 = runner.run(operation1, {});
const promise2 = runner.run(operation2, {});
const promise3 = runner.run(operation3, {});
// Wait for all
await Promise.all([promise1, promise2, promise3]);
// Or drain the queue
await runner.drain();
Tip: For microtask scheduling details, clear() epoch behavior, and the length property semantics, see src/queue/README.md.
Error Handling
The library provides detailed error information:
const result = await runner.run(myOperation, input);
if (!result.ok) {
switch (result.error.kind) {
case 'TimeoutAppleEvent':
console.log('AppleScript timed out');
break;
case 'InputValidationError':
console.log('Invalid input:', result.error.metadata);
break;
case 'ScriptError':
console.log('Script failed:', result.error.message);
break;
}
}
Internals
For details about internal queueing behavior (microtask scheduling, clear() epoch cut-off, length semantics), see:
- src/queue/README.md
Architecture
The library follows a layered architecture:
- User API Layer: High-level functions (
createAppleRunner
,operation.*
) - Runner Layer: Orchestration, retries, hooks
- Operations & Queue Layer: Operation definitions, queue management, validation
- Engine Layer: Script building, marshalling,
engine/protocol
parsing - System Layer:
osascript
execution viachild_process
Additional Documentation
- Protocol Specification - Detailed protocol documentation
- Troubleshooting Guide - Common issues and solutions
- Queue Implementation - Queue semantics and behavior
- Release Guide - How to publish new versions
License
MIT