Introducing kmjs

Hey everyone!

I'm excited to share kmjs, an open-source TypeScript toolkit I created for programmatically interfacing with Keyboard Maestro, designed for constructing and executing macros entirely in-memory.

It handles all the boilerplate required for constructing each 'virtual' macro's XML, and then running it, and returning the result. Each virtual action has been tested through an automated routine I created, which generates and imports its possible configurations into KM as a real macro, then extracts that, and then compares the expected result with the generated result.

Here's an example of creating a virtual macro and running it:

import { createVirtualNotification, createVirtualPause, runVirtualMacro, } from "kmjs";

// 1. Define your sequence of virtual actions

const actions = [

createVirtualNotification({

title: "Hello from kmjs!",

body: "This is a virtual macro.",

sound: "Glass",

}),

createVirtualPause({ time: 1 }), // Pause for 1 second

createVirtualNotification({

title: "Still here!",

body: "The virtual macro has finished.",

sound: "Ping",

}),

];

// 2. Run the sequence in-memory via KM

runVirtualMacro(actions, "My First Virtual Macro");

Virtual macros can return values, too, of course.

I also built a 'query' framework, which takes KM's tokens and wraps them in a single virtual macro, which then returns the relevant value. For example, getting the current mouse position in JS is now as simple as:

import { getMousePosition } from "kmjs/queries";

const positionString = getMousePosition();

// e.g. "1234,876"*

Beyond the virtual actions/macros, it's packed with various tools and functions that utilise KM, including workflows focused on working with variables and macros that exist in real macros in the GUI. It also includes various JavaScript utilities and tools conceptualised/created by the community, re-packaged as TypeScript functions as a part of this project.

I built this as part of a commercial JS project in which I wanted to use KM for building automated testing routines. Then, of course, as every KM user is too familiar with, the little side project ballooned exponentially into what it is now. As such, not every action has a virtual equivalent (yet). I focused on what would be immediately useful for GUI automation, plus a few extras.

I'd rather not inundate this forum post with everything in kmjs, but I also wanted to highlight the keyboard handling utilities. While creating this, I was bewildered that there are no centralised or complete examples of ready-to-use AppleScript keyboard code mappings. As such, I've built out a complete system for keystroke and keyboard shortcut normalisation (which the virtual actions for keystrokes use). You can input:

  • Human-Readable String : Common shortcuts like "Cmd+Shift+S".
  • JavaScript event.code String : Web-standard codes like "KeyS" or "Digit1".
  • Raw Numeric Key Code : A number representing an AppleScript key code, like 36 for the Return key.
  • AppleScript Map : An existing map object like { 256: 1 } (Cmd+S), which is passed through unmodified.

... and produce an object with a single key-value pair that represents a modifier combination and keycode: { [modifierMask]: [keyCode] }. These are then separated out for use in KM actions. I suspect this part of the project might be useful to many people beyond those using KM.

As someone who's benefited enormously from this community, I want to give back with something I hope helps you in your KM-related projects.

Cheers!

(This is my first GitHub / npm project. Please go easy on me - I'd also greatly appreciate any advice about going about hosting and collaborating on repos)!

6 Likes

I'm teetering on the edge of my seat, but before I get too excited, could you provide any visual use-case examples? What kind of problems might kmjs solve for us tweakers?

I'm really interested in this. I'd love to create a tighter integration between my Keyboard Maestro macros and the other tools I use!

Yeah definitely!

There are many use-cases with kmjs, ranging from:

  • Using a few convenience functions on the JS side to make it easy to interface with KM variables/macros
  • Extracting KM macros to edit the XML of programmatically, and then re-importing into KM
  • Only using virtual macros and actions that exist in your JavaScript.

I got heavy into mixing JavaScript into my KM macros for a project last year, to the point many of the macros I were making were essentially wrappers for JS scripts (many of which executed KM macros themselves). Kmjs allows you to flip this around so everything can live in the JS side, which can be easier to scale for larger projects. Managing the control flow is much simpler and easier to structure (especially if a project involves multiple people).

Keyboard Maestro itself interfaces with many low-level hooks that would be incredibly involved to plug into yourself for hobby projects (such as finding mouse position or window names OS-wide), which the queries module of kmjs makes easy to do using KM as a conduit. Given that kmjs requires a valid km license to use it works well for personal / non-distributed projects of that nature.

This also enables the creation of a factory function that produces real KM macros. Let's say you wanted to produce a dozen real KM macros that do subtly different things - you could easily do this in kmjs by constructing a virtual macro in a loop of some kind and altering values/variables/actions for each iteration, and then exporting each of these directly to KM.

I included a few examples on the repo, the one below illustrating:

  • Constructing a virtual macro out of virtual actions,
  • Producing the equivalent XML for Keyboard Maestro
  • And presenting this in a format that KM can actually import as a valid macro, and then importing it into a macro group
#!/usr/bin/env node

/**
 * Demo script showing how to use the generateMacro function.
 *
 * This example demonstrates all the different ways to use generateMacro:
 * - Generate raw XML
 * - Add plist wrapping
 * - Display in text window
 * - Export to file
 * - Import to KM group
 */

const {
  generateMacro,
  createVirtualNotification,
  createVirtualPause,
  createVirtualComment,
  createVirtualInsertText,
  createVirtualSetVariable,
} = require("../bundle/kmjs.js");

console.log("πŸš€ generateMacro Demo\n");

// Create some sample virtual actions
const sampleActions = [
  createVirtualComment({
    title: "Demo Macro",
    text: "This macro was generated using the generateMacro function",
  }),
  createVirtualSetVariable({
    variable: "DemoStatus",
    text: "Starting demo...",
    scope: "local",
  }),
  createVirtualNotification({
    title: "Demo Started",
    body: "This is a generated macro demo",
    sound: "Glass",
  }),
  createVirtualPause({ time: 2 }),
  createVirtualInsertText({
    text: "Hello from generateMacro! Status: %Variable%DemoStatus%",
  }),
  createVirtualNotification({
    title: "Demo Complete",
    body: "The generated macro has finished executing",
    sound: "Ping",
  }),
];

console.log("πŸ“ Example 1: Generate raw XML");
console.log("=".repeat(50));
const rawXml = generateMacro(sampleActions);
console.log("Raw XML generated (first 200 chars):");
console.log(rawXml.substring(0, 200) + "...\n");

console.log("πŸ“¦ Example 2: Generate XML with plist wrapping");
console.log("=".repeat(50));
const wrappedXml = generateMacro(sampleActions, {
  addPlistWrapping: true,
  macroName: "Demo Macro with Plist",
});
console.log("Plist-wrapped XML generated (first 200 chars):");
console.log(wrappedXml.substring(0, 200) + "...\n");

console.log("πŸ–₯️  Example 3: Display XML in Keyboard Maestro text window");
console.log("=".repeat(50));
generateMacro(sampleActions, {
  exportTarget: { displayInTextWindow: true },
  macroName: "Display Demo",
});
console.log("XML displayed in KM text window\n");

console.log(
  "πŸ’Ύ Example 4: Export to file (commented out to avoid file creation)",
);
console.log("=".repeat(50));
console.log("// generateMacro(sampleActions, {");
console.log("//   exportTarget: { filePath: './demo-macro.kmmacros' },");
console.log("//   macroName: 'File Export Demo'");
console.log("// });");
console.log("File export example (commented out)\n");

console.log("🎯 Example 5: Import directly to Keyboard Maestro group");
console.log("=".repeat(50));
generateMacro(sampleActions, {
  exportTarget: { toKMGroup: "kmjs-demos" },
  macroName: "generateMacro Demo",
});
console.log("Macro imported to 'kmjs-demos' group in Keyboard Maestro\n");

console.log("πŸ”„ Example 6: Multiple export targets");
console.log("=".repeat(50));
generateMacro(sampleActions, {
  exportTarget: {
    displayInTextWindow: true,
    toKMGroup: "kmjs-demos",
  },
  macroName: "Multi-Export Demo",
});
console.log("Macro displayed in text window AND imported to KM group\n");

console.log("βœ… Demo complete!");
console.log("\nCheck your Keyboard Maestro editor for the 'kmjs-demos' group");
console.log(
  "to see the imported macros. You can run them to test the functionality.",
);
7 Likes

Wow, Scotty, this is an awesome piece of work.

One feature you outlined particularly caught my attention:

Automatically editing macros is something I've been thinking about off and on ever since I encountered the KMET macros.

I recently created a script for myself (that I've been getting annotated etc. to share here). I'm wondering if it might be somewhat cleaner using your system (although I'm not ready to rewrite it since it works). It's definitely the most complicated macro editing that I've done.

The script finds all blocks of actions, if-then-else, switch/case, groups, etc., and adds a comment marker to identify the end of the block, with the comment including the name/title of the block that is ending. I got stuck at one point because a KBM If-then-else simply ends at then end of the Else block and I wanted a marker that named the original IF. So that marker had to go at the same level as the IF, I didn't want it inside the Else block. That meant that I couldn't simply iterate over the actions at that level because I'd be insterting the new Comment into that list. So I ended up building a copy and when the iteration list was done, replacing it in the macro with the new copy of the XML. (I had lots of help from Claude.ai in this.)

How would you approach that problem with your system?

In your GitHub ReadMe, you say:

  • Canonical Ordering: Automatically sorts modifiers into the standard macOS order (Cmd β†’ Option β†’ Shift β†’ Control), ensuring consistent output.

The "standard" order that KBM uses is βŒƒβŒ₯β‡§βŒ˜ (you can see it if you type all four modifiers into a Hotkey setting), which I had previously thought was the standard MacOS order and which (except for Shift on a different row) matches the US MacBook keypad. I'm confused. Has KBM been different than MacOS all this time?

This is somewhat important to me because I have some places where I display the hotkey that has been recognized, and I would like the modifier order to be standard. I did some research at the time (a couple of years ago) and thought that I found out that KBM matched the Apple Doc standard. You also look like you've done extensive research, so I'd appreciate any light you can shed on this difference.

It is, at least for documentation and UI display -- see p119 of the Style Guide.

1 Like

Thanks, Nige,

"If there’s more than one modifier key, use this order: Fn (function), Control, Option, Shift, Command. " β€” pg 119, Apple Style Guide PDF, June 2025

There's a web version of the Apple Style Guide available at Apple Style Guide - Apple Support

I guess @Scotty2Hotty got confused. I remember that I tripped over that issue a lot when I was first writing user tutorials. Of course, in my early days I was on WIndows and there's a lot less standardization there, including where the modifier keys are on keyboards. If I knew the history of the convention, I'd probably remember it more easily, but that's just me. I sure wish I'd had those style guides a couple of decades ago.

1 Like

This is new to me too. Way back, I chose the order β‡§βŒƒβŒ₯⌘ because that most closely matched the layout on the left side of a standard Mac keyboard. That also (mis/)informed the order of modifiers that I chose when I moved to a programmable keyboard, where, of course, the layout is for the user to decide. I can’t change now!

They've been around for ages -- here's the relevant page of the 2013 Style Guide, but I'll see if I can go back further if I have time tomorrow.

2 Likes

Thanks. No need to rub it in.

2 Likes

I don't know how available things were back then -- many developer docs used to be behind the Developer Programme paywall, and who's going to spend $99 just to see the proper order for writing modifiers?

You can't be expected to follow guidelines you couldn't get hold of!

2 Likes