In this post, we’ll create a highly useful Mac tool to fix the grammar of any selected text. We’ll build it using TypeScript, the OpenAI API, and AppleScript, integrated into Automator for seamless functionality. You can find all the code here.
fixGrammar
FunctionThe heart of this tool is the fixGrammar
function—a straightforward utility that takes a piece of text as input and returns a grammatically corrected version.
import { shouldBePresent } from "@lib/utils/assert/shouldBePresent"
import OpenAI from "openai"
export const fixGrammar = async (input: string): Promise<string> => {
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
const content = `Fix any grammar errors and improve the clarity of the following text while preserving its original meaning:\n\n${input}`
const { choices } = await openai.chat.completions.create({
model: "gpt-4o-mini",
max_tokens: 1024,
temperature: 0.0,
messages: [
{
role: "user",
content,
},
],
})
const [{ message }] = choices
return shouldBePresent(message.content).trim()
}
To use this function, you’ll need to set up your OpenAI API key in your environment variables.
export OPENAI_API_KEY="your-api-key"
We use a mini
model to keep the responses fast and cost-effective, as this task doesn’t require a highly advanced model. The max_tokens
parameter defines the maximum number of tokens generated in the response, while the temperature
parameter controls the response's randomness. Setting it to 0.0
ensures deterministic and consistent outputs.
The script reads the input text from standard input, processes it through the fixGrammar
function, and outputs the corrected text to standard output.
import { readStdin } from "./utils/readStdin"
import { fixGrammar } from "./core/fixGrammar"
async function main() {
const input = await readStdin()
const output = await fixGrammar(input)
process.stdout.write(output)
}
main()
readStdin
The readStdin
function is responsible for capturing input text from the standard input and returning a promise that resolves with the trimmed text.
export function readStdin(): Promise<string> {
return new Promise((resolve) => {
let data = ""
process.stdin.setEncoding("utf8")
process.stdin.on("data", (chunk) => {
data += chunk
})
process.stdin.on("end", () => {
resolve(data.trim())
})
})
}
esbuild
To execute our script with node
, we need to bundle it into a single file using esbuild
. To accomplish this, we’ll add a build
script to our package.json
:
{
"name": "@product/grammar",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "esbuild index.ts --bundle --platform=node --target=node22 --outfile=dist/index.js"
},
"dependencies": {
"openai": "^4.77.0"
},
"devDependencies": {
"esbuild": "^0.24.2"
}
}
After running yarn build
, an index.js
file will be generated in the dist
directory. This bundled file will be executed from AppleScript.
For AppleScript to execute the JavaScript file, it needs to know the path to the node
executable and the script file. Since these paths vary between users depending on their setup, we’ll avoid hardcoding them. Instead, we’ll create a FixGrammar.template.applescript
file and use a bash script to generate a FixGrammar.applescript
file with the correct values for each user.
try
-- Copy the selected text
tell application "System Events"
keystroke "c" using command down
end tell
delay 0.2
-- Read the clipboard
set selectedText to the clipboard
if selectedText is "" then
display dialog "No text was detected in the clipboard." buttons {"OK"} default button 1
return
end if
-- Call Node script to correct grammar (direct path)
-- Absolute path to Node
set nodePath to "{{nodePath}}"
-- Path to your compiled grammar script
set scriptPath to "{{scriptPath}}"
-- Build the shell command:
set shellCommand to "echo " & quoted form of selectedText & " | " & nodePath & " " & quoted form of scriptPath
-- Run it
set correctedText to do shell script shellCommand
-- Put corrected text into the clipboard
set the clipboard to correctedText
delay 0.2
-- Paste (Cmd+V)
tell application "System Events"
keystroke "v" using command down
end tell
on error errMsg number errNum
-- Show a dialog if there's an error
display dialog "Error: " & errMsg & " (#" & errNum & ")" buttons {"OK"} default button 1
end try
This AppleScript automates grammar correction for selected text. It starts by copying the selected text to the clipboard and verifying its content. The script then constructs a shell command using placeholders for the node
executable and script paths, which will be replaced dynamically. It runs the Node.js script to process the text, updates the clipboard with the corrected version, and pastes it back. If an error occurs, a dialog displays the error message for troubleshooting.
markdown Copy code
In this post, we’ll create a highly useful Mac tool to fix the grammar of any selected text. We’ll build it using TypeScript, the OpenAI API, and AppleScript, integrated into Automator for seamless functionality. You can find all the code here.
fixGrammar
FunctionThe heart of this tool is the fixGrammar
function—a straightforward utility that takes a piece of text as input and returns a grammatically corrected version.
To use this function, you’ll need to set up your OpenAI API key in your environment variables.
We use a mini
model to keep the responses fast and cost-effective. The max_tokens
parameter defines the maximum number of tokens generated in the response, while the temperature
parameter controls the response's randomness. Setting it to 0.0
ensures deterministic and consistent outputs.
The script reads the input text from standard input, processes it through the fixGrammar
function, and outputs the corrected text to standard output.
readStdin
The readStdin
function is responsible for capturing input text from the standard input and returning a promise that resolves with the trimmed text.
esbuild
To execute our script with node
, we need to bundle it into a single file using esbuild
. To accomplish this, we’ll add a build
script to our package.json
.
After running yarn build
, an index.js
file will be generated in the dist
directory. This bundled file will be executed from AppleScript.
For AppleScript to execute the JavaScript file, it needs to know the path to the node
executable and the script file. Since these paths vary between users, we’ll avoid hardcoding them. Instead, we’ll create a FixGrammar.template.applescript
file and dynamically generate a FixGrammar.applescript
file with the correct values for each user.
This AppleScript automates grammar correction for selected text. It starts by copying the selected text to the clipboard and verifying its content. The script then constructs a shell command using placeholders for the node
executable and script paths, which will be replaced dynamically. It processes the text and pastes the corrected version back.
The delay 0.2
commands ensure that the clipboard and system events have enough time to process changes, such as copying or pasting text. On faster Macs, users can experiment with lowering this value (e.g., delay 0.1
) to speed up the process, while slower systems may require a slightly higher delay to ensure reliability.
Next, we can create a bash script to streamline the setup process. This script will replace the placeholders in FixGrammar.template.applescript
with the correct paths for node
and the compiled JavaScript file, and it will also run esbuild
to bundle the script. Once complete, all you need to do is set the OPENAI_API_KEY
environment variable and run . ./build.sh
to finalize the setup.
#!/bin/bash
# Exit if any command fails
set -e
trap 'echo "An error occurred."; exit 1' ERR
# Ensure OPENAI_API_KEY is set
if [[ -z "${OPENAI_API_KEY}" ]]; then
echo "Error: OPENAI_API_KEY environment variable is not set."
exit 1
fi
# Run the build script with OPENAI_API_KEY injected
yarn run build --define:process.env.OPENAI_API_KEY="'${OPENAI_API_KEY}'"
# Check if the build was successful
if [[ $? -eq 0 ]]; then
echo "Build completed successfully."
else
echo "Build failed."
# exit 1
fi
# Define the paths
TEMPLATE_FILE="FixGrammar.template.applescript"
OUTPUT_FILE="FixGrammar.applescript"
# Get the absolute paths
NODE_PATH=$(which node)
SCRIPT_PATH=$(cd "$(dirname "./dist/index.js")" && pwd)/index.js
# Check if the template file exists
if [[ ! -f $TEMPLATE_FILE ]]; then
echo "Error: Template file $TEMPLATE_FILE not found."
exit 1
fi
# Replace placeholders in the template and write to output file
sed -e "s|{{nodePath}}|$NODE_PATH|g" -e "s|{{scriptPath}}|$SCRIPT_PATH|g" "$TEMPLATE_FILE" > "$OUTPUT_FILE"
# Make the output script readable
chmod 644 "$OUTPUT_FILE"
echo "FixGrammar.applescript has been generated successfully."
OUTPUT_FILE_ABSOLUTE_PATH=$(cd "$(dirname "$OUTPUT_FILE")" && pwd)/$(basename "$OUTPUT_FILE")
echo ""
echo "To use the generated FixGrammar.applescript in Automator, create a new workflow and add the following Shell Script action:"
echo ""
echo "osascript \"$OUTPUT_FILE_ABSOLUTE_PATH\""
The build process is handled by running yarn run build
, injecting the OPENAI_API_KEY
into the environment during compilation. After successfully bundling the project, the script determines the absolute paths for the node
executable and the compiled JavaScript file. These paths are then substituted into the FixGrammar.template.applescript
file using sed
, replacing placeholders with the actual values.
Once the AppleScript file is generated, it is saved as FixGrammar.applescript
and given appropriate read permissions. The script concludes by providing clear instructions on how to use the generated file in Automator, including an example command to execute it with osascript
.
Copy the output osascript
command generated by the script. Next, open Automator and create a new Quick Action. Set the workflow to receive no input in any application. Then, add a Run Shell Script action to the workflow and paste the copied osascript
command into the action. Finally, save the workflow, and your grammar correction tool is ready to use.
To run the workflow using a shortcut, go to System Preferences > Keyboard > Shortcuts > Services. Locate your workflow under General, then assign it a custom shortcut.
To ensure the workflow functions correctly, grant the necessary permissions in System Preferences > Security & Privacy > Privacy > Accessibility. Add the Automator app to the list of allowed apps. Additionally, grant Accessibility permissions to any apps where you plan to use the shortcut.
With this setup, you now have a powerful Mac tool to fix grammar in any selected text effortlessly. By combining TypeScript, AppleScript, and Automator, this workflow integrates seamlessly into your system, saving time and ensuring polished text with just a shortcut.