DEV Community

Cover image for Build a Mac Tool to Fix Grammar Using TypeScript, OpenAI API, and Automator
Radzion Chachura
Radzion Chachura

Posted on • Originally published at radzion.com

Build a Mac Tool to Fix Grammar Using TypeScript, OpenAI API, and Automator

🐙 GitHub

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.

Core Functionality: The fixGrammar Function

The 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()
}
Enter fullscreen mode Exit fullscreen mode

Setting Up the OpenAI API Key

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"
Enter fullscreen mode Exit fullscreen mode

Optimizing for Performance

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.

Processing Input and Output

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()
Enter fullscreen mode Exit fullscreen mode

Capturing Input with 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())
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

Bundling the Script with 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

After running yarn build, an index.js file will be generated in the dist directory. This bundled file will be executed from AppleScript.

Executing JavaScript with 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
Enter fullscreen mode Exit fullscreen mode

Automating Grammar Correction

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

Create a Mac Tool to Fix Grammar of Selected Text

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.

Core Functionality: The fixGrammar Function

The 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.

Setting Up the OpenAI API Key

To use this function, you’ll need to set up your OpenAI API key in your environment variables.

Optimizing for Performance

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.

Processing Input and Output

The script reads the input text from standard input, processes it through the fixGrammar function, and outputs the corrected text to standard output.

Capturing Input with readStdin

The readStdin function is responsible for capturing input text from the standard input and returning a promise that resolves with the trimmed text.

Bundling the Script with 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.

Executing JavaScript with 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.

Automating Grammar Correction

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.

Handling Clipboard Delays

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.

Simplifying Setup with a Bash Script

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\""
Enter fullscreen mode Exit fullscreen mode

Finalizing the Build Process

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.

Integrating with Automator

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.

Create Quick Action with Automator

Assigning a Shortcut for Quick Access

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.

Add a Shortcut

Granting Necessary Permissions

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.

Give permission to Accessibility

Conclusion

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.

Top comments (0)