Skip to content

ljh-sh/macli

macli

OpenSSF Scorecard CI Docs License

Minimal context with maximum flexibility — macOS system tools for AI agents. Native Apple frameworks. AI-friendly JSON/TSV output.

macli turns macOS system internals into a clean CLI. SMC sensors, streaming monitor, calendar/reminders — all callable from shell pipes or LLM agents, all JSON/TSV. One ~400 KB Swift binary. No Python runtime, no osascript overhead, no GUI.

Use it when you (or your AI agent) need to ask macOS something that system_profiler / ioreg / osascript either can't answer or answer badly: CPU die temperature right now, stream 1 Hz sensor readings into awk, today's calendar as JSON.

Governance

macli is maintained by the core team listed in CODEOWNERS. All changes to main require a pull request and approval from a code owner. See CONTRIBUTING.md for details.

Mirrors: github.com/ljh-sh/macli · codeberg.org/ljh-sh/macli

Docs: ljh-sh.github.io/macli

For AI agents

Minimal context with maximum flexibility — paste this one-line prompt into Claude Code, Cursor, or any agent's system prompt:

Use `macli` for macOS system state (sensors / battery / calendar / reminders). Install if missing: `brew install ljh-sh/cli/macli`. JSON output, check `ok`. Run `macli --help` for subcommands.

Install

Homebrew (recommended)

brew install ljh-sh/cli/macli

Or tap once, then use the short name:

brew tap ljh-sh/cli
brew install macli

Direct binary

curl -L https://github.com/ljh-sh/macli/releases/latest/download/macli-darwin-universal.tar.xz | tar xJ -
sudo mv bin/macli /usr/local/bin/

The universal tarball is a fat Mach-O (arm64 + x86_64) — works on Apple Silicon and Intel Macs.

eget

Via x-cmd eget:

x eget use ljh-sh/macli              # install latest to ~/.local/bin
x eget use --tag v0.4.2 ljh-sh/macli # install a specific release

npm

After install, verify the package with npm audit signatures.

npm install -g @ljh-sh/macli

The npm package downloads the universal macOS binary from the GitHub release during install.

Build from source

Requires Swift 5.10+ / macOS 12+.

git clone https://github.com/ljh-sh/macli
cd macli
swift build -c release

At a glance

macli smc temp                              # CPU/GPU temps as JSON
macli gpu info                              # GPU name, cores, unified memory
macli display brightness                    # built-in display brightness
macli monitor --count 10 --interval 1       # stream 10 samples to awk
macli cal ls                                # list calendars as JSON

Output schema: {"ok": true, ...} on success, {"ok": false, "error": "...", "hint": "..."} on failure. Never silent.


Roadmap

See ROADMAP.md for the current roadmap and completed milestones.


SMC sensors

The headline use case. macli smc reads hardware sensors that macOS exposes only through private frameworks.

macli smc temp            # → JSON, all temperature sensors
macli smc temp --tsv      # → TSV for awk
macli smc volt            # → PMU voltage rails
macli smc curr            # → PMU current rails
macli smc all             # → everything

Sample output:

{
  "ok": true,
  "source": "HID",
  "sensors": [{"name": "PMU tdie1", "value": 57.5, "unit": "°C"}],
  "count": 45
}

Battery & power diagnostics

macli battery reads IOKit/AppleSmartBattery and returns the richest, script-friendly battery snapshot on macOS. It is a strict superset of ioreg -n AppleSmartBattery -r for useful data: 150+ fields, per-cell voltages, USB-C PD telemetry, and decoded binary blobs like the per-cell resistance table RaTableRaw.

Highlights

  • JSON / TSV / raw plist — pipe into jq, awk, spreadsheets, or time-series DBs.
  • Derived power metricshealthPercent, designWh, currentWh, estimatedFullChargeWh, instantPowerWatts, cellVoltageDelta, charger.powerWatts.
  • Decoded binary fieldsraTableRaw (per-cell uint16 resistance tables), batteryStateBytes, charger.statusBytes, mfgDataAscii.
  • Per-port USB-C PD diagnosticsfedDetails, portControllerInfo, adapter.usbHvcMenu PDOs.
  • Battery identity cross-check — compare serialNumber vs batterySerial, inspect manufactureDate and manufacturer data.

At a glance

macli battery                               # full JSON snapshot
macli battery --tsv                         # spreadsheet / awk friendly
macli battery --plist                       # raw AppleSmartBattery plist

# Core health and status
macli battery | jq '{status, stateOfCharge, healthPercent, cycleCount, temperature}'

# Power flow right now
macli battery | jq '{instantPowerWatts, batteryPower, systemPower, inputPower, charger: .charger.powerWatts}'

# Per-cell balance (delta > 0.05 V warrants attention)
macli battery | jq '{cellVoltages, cellVoltageDelta}'

# Adapter PDOs and per-port PD partners
macli battery | jq '{adapter: .adapter.usbHvcMenu, fedDetails}'

Diagnostics & scripting cookbook

# 1. Alert when health drops below 80%
macli battery | jq -e '.healthPercent < 80' && echo "consider replacement"

# 2. One-line battery report for remote support
macli battery | jq -r '"\(.deviceName) \(.cycleCount) cycles health=\(.healthPercent)% temp=\(.temperature)°C"'

# 3. Detect possible third-party / mismatched battery
macli battery | jq '{serialNumber, batterySerial, same: (.serialNumber == .batterySerial)}'

# 4. Log a time-series line every minute (CSV-ish)
macli battery | jq -r '[now, .stateOfCharge, .healthPercent, .temperature, .instantPowerWatts] | @tsv' >> battery.tsv

# 5. Compare current charge power to adapter rating
macli battery | jq '{inputPower: .inputPower, chargerPower: .charger.powerWatts, systemPower: .systemPower}'

# 6. Inspect decoded manufacturer ASCII inside the binary MfgData blob
macli battery | jq -r '.mfgDataAscii // "N/A"'

# 7. Stream battery power to awk / InfluxDB / Prometheus
macli monitor --metric battery_power --interval 1 --count 60

# 8. Find the hottest cell and the coldest cell
macli battery | jq -r '.cellVoltages | "max=\(max), min=\(min), delta=\(max - min)"'

# 9. Pretty-print the full resistance table for each cell
macli battery | jq '.raTableRaw'

# 10. Check if AC is connected but the battery is not charging (and why)
macli battery | jq '{externalConnected, isCharging, fullyCharged, notChargingReason: .charger.notChargingReason}'

macli battery is read-only — it observes and reports, but does not control charging. To set charge limits or pause charging, use System Settings → Battery → Battery Health (official), or third-party tools like batt (Apple Silicon) / bclm (Intel). See docs/battery.md for the full guide and trade-offs.


SSD health

macli ssd parses system_profiler SPNVMeDataType. It returns model, serial, capacity, SMART status, TRIM support and volumes. It does not parse detailed SMART log pages because Apple does not expose them through a public API. For wear-level data (TBW, percentage used, media errors, etc.) use smartctl:

brew install smartmontools
smartctl -a disk0

SSD compatibility:

Platform Basic info (macli ssd) Detailed SMART
Apple Silicon internal SSD Use smartctl
External Thunderbolt NVMe Use smartctl
USB/SATA adapters May appear as storage, not NVMe Use smartctl

Display & GPU

macli display reads and sets display brightness through the private DisplayServices framework. macli gpu reports the active GPU via Metal and reads core count / performance counters from AGXAccelerator in IOKit.

macli display list                      # all online displays + brightness
macli display brightness                # current brightness (0.0–1.0)
macli display brightness --set 0.5      # set built-in display brightness
macli gpu info                          # name, unified memory, core count
macli gpu info --tsv                    # tab-separated output

monitor can also stream GPU utilization (experimental, Apple Silicon only):

macli monitor --metric gpu_metrics --interval 1

Design: agent-oriented

macli follows the x-cmd agent-tool design principle: minimal context with maximum flexibility. It stays dumb — it does not compute thermal indexes, aggregate, render charts, or decide what's "hot". It returns raw sensor values, full stop. Decisions belong to the caller:

macli smc temp --tsv | awk -F'\t' '$2 > 80 {print $1, "OVERHEAT"}'
macli smc temp --tsv | sort -t$'\t' -k2 -n | tail -5    # 5 hottest sensors

This keeps macli --help short (saves tokens when an LLM loads it as context). The CLI is the API; the shell is the glue.

smc86 — Intel legacy, sunset track

smc86 is the Intel-Mac counterpart, same interface. Returns empty on Apple Silicon (Intel SMC key space was cleared). Will be removed when Intel Macs go EOL.


Streaming monitor

monitor samples sensor sources on an interval and streams TSV — one row per sample. Single process, no subprocess fork per poll, no Python interpreter per tick. Designed as a long-running pipe stage for awk.

macli monitor --interval 1 --metric smc_temp,smc_curr
macli monitor --count 10 --interval 0.5 --metric smc_temp \
  | awk -F'\t' 'NR>1 {sum+=$2; n++} END {print "avg:", sum/n}'
macli monitor --metric gpu_metrics --interval 1   # GPU utilization (experimental)

Flags:

  • --interval N — seconds between samples (supports decimals, default 1.0)
  • --metric list — comma-separated source or column-prefix filters (default: all). Sources: smc_temp, smc_volt, smc_curr, battery_power, gpu_metrics. Prefixes such as smc_temp_cpu select only matching columns.
  • --count N — exit after N samples (default: infinite, Ctrl-C to stop)

The header row locks column order; subsequent rows match positionally. awk -F'\t' is the intended downstream.

Why this matters: a shell loop (while; do macli smc temp; sleep 1; done) costs ~50ms of binary startup per iteration. monitor pays that once and streams samples at sub-millisecond marginal cost.


EventKit — calendar / event / reminder

EventKit.framework is Apple's native API for Calendar and Reminders. macli wraps it for the shell — JSON output, no AppleScript involved.

macli cal ls                                # list calendars
macli event ls --calendar Work --today      # today's events
macli reminder add --list Shopping "Buy milk"
macli aka set work <calendar-id>            # alias calendar IDs for stable refs

Use cases: dashboards, CI notifiers ("next event in 5 min"), reminder batching, automation hooks that need stable calendar references (aka).


Output conventions

  • Snapshot commands: JSON with {"ok": bool, ...} (default). --tsv for awk-friendly.
  • Streaming commands (monitor): TSV only, header on first line.
  • Errors: {"ok": false, "error": "...", "hint": "..."} — never silent.

FAQ

The full FAQ lives on the docs site: ljh-sh.github.io/macli/faq.
Source: docs/faq.md.

FAQ about Installation & permissions

❓ "macli cannot be opened because the developer cannot be verified"

macli ships with ad-hoc signature (no Apple Developer ID). For direct-download installs, strip the quarantine attribute:

xattr -dr com.apple.quarantine /usr/local/bin/macli

The Homebrew formula does this automatically via post_install.

brew install macli says "trust" or refuses to load the formula

Homebrew 6 added a trust step for third-party taps. Run brew trust ljh-sh/cli once, then brew install ljh-sh/cli/macli. This is a security feature, not a bug.

❓ First macli cal ls / event ls / reminder call hangs for seconds

macOS TCC is prompting for Calendar/Reminders access. Click the system dialog to grant. Subsequent calls are instant. If you missed the prompt, go to System Settings → Privacy & Security → Calendars (or Reminders) and enable the terminal you're running macli from.

FAQ about SMC — what it is, why macli exists

❓ What is SMC?

The System Management Controller (SMC) is an Apple controller embedded in every Mac. It monitors and reports CPU / GPU / SoC die temperatures, PMU voltage rails, PMU current rails, fan speeds (Intel Macs), and battery state.

On Intel Macs, SMC is queried through IOKit.framework's private AppleSMC API using 4-character keys (TCXC, TG0P, …). On Apple Silicon (M1–M4), the same data moved to a HID sensor hub — the keys are entirely different (PMU tdie1, PMU tdie2, …) and undocumented.

References — the projects that mapped this out:

❓ Why not Python / PyObjC?

Reading one sensor takes ~30 lines of C: open the AppleSMC / AppleHID IOService, serialize the key, call IOConnectCallScalarMethod, unpack the returned struct. The keys are private, the structs are private, the call convention changed between Intel and Apple Silicon.

PyObjC can call public frameworks, but the SMC key space is private. Reaching it from Python means ctypes-level struct packing that breaks with every macOS release. There is no pip install path that keeps up with Apple Silicon's new key namespace.

macli smc86 ... returns empty results on Apple Silicon

Expected. smc86 queries the Intel-Mac SMC key space, which Apple cleared on Apple Silicon. Use macli smc (not smc86) on M-series Macs.

❓ How is macli different from iStats / smcFanControl / stats / iSMC / SMCKit?

  • iStats — Ruby gem, Intel-only, last released 2018. GUI-leaning.
  • smcFanControl — macOS app for setting minimum fan speed. GUI.
  • stats — macOS menu-bar dashboard. GUI.
  • iSMC — Go CLI. Closest peer, but Go runtime adds ~5 MB to the binary.
  • SMCKit — Swift library, Intel-only. No CLI streaming, no EventKit.
  • macli — Swift CLI built for shell pipes and LLM agents. JSON/TSV only, no GUI, no Ruby/Go runtime, Apple Silicon first-class. Smallest binary in the list (~400 KB stripped).

FAQ about EventKit internals

❓ Why is macli cal ls faster than osascript?

osascript routes through AppleScript → Calendar.app RPC channel → permission prompts. Every cold start loads the AppleScript component. macli links EventKit.framework directly and requests permission once via the standard TCC prompt; subsequent calls are in-process.

❓ Why JSON instead of AppleScript list syntax?

AppleScript returns human-formatted strings like {calendar "Work", calendar "Home"}. Parsing that requires regex on localized strings. JSON is parseable by jq, python, awk, every LLM tool-use interface, with stable field names regardless of system language.

❓ Does macli modify my calendar data?

Read commands (cal ls, event ls) never touch state. Write commands (cal add, event add, reminder add) only run when you invoke them explicitly, with the exact arguments you pass. macli never syncs, never deletes, never auto-modifies.

FAQ about Compatibility

❓ Linux / Windows?

No. macli wraps Apple-private frameworks (IOKit, HID, EventKit) that exist only on macOS.

❓ Do I need sudo?

No. All subcommands run as the invoking user. Sensor reads go through user-space IOKit / HID APIs.

❓ Apple Silicon vs Intel?

Both supported via a single universal binary. On Apple Silicon use macli smc; on Intel use macli smc86. The binaries are identical; the subcommand selects the sensor path.

FAQ about Internals

❓ Binary size

~400 KB per arch (arm64 / x86_64), ~830 KB universal (fat Mach-O), ~110 KB arm64 tar.xz. Single static-ish binary — no Python runtime, no PyObjC bridge, no ctypes layer.

❓ Code signature

Ad-hoc. Not Apple Developer ID (would require $99/year and notarization for marginal benefit). The Homebrew formula strips com.apple.quarantine automatically. Manual installs need one xattr -dr.

❓ Is the build reproducible?

Mostly yes.

  • Reproducibility depends on pinning Xcode / LLVM versions.
  • Build hardening — SOURCE_DATE_EPOCH, ZERO_AR_DATE, deterministic mtimes, RPATH removal — lives in .x-cmd/release.common.sh.

❓ Why was speech recognition (macli speech recognize) removed?

Short version: macOS won't let a bare CLI use speech recognition.

  • SFSpeechRecognizer needs an Info.plist with NSSpeechRecognitionUsageDescription.
  • SwiftPM CLI binaries don't have one, so TCC denies access and the process crashes.
  • Fixing it means bundling macli as a .app, which isn't worth the pipeline complexity.
  • Use hear instead — it's signed, notarized, and does exactly this job.

❓ Why no aggregation / alerting built-in?

Aggregation (avg, max, rolling window) and alerting (threshold → notify) belong in awk/jq/python where you control the semantics. Embedding them in macli would mean every new stat needs a new flag, and the --help would balloon past what an LLM can cheaply load as context. See "Design: agent-oriented" above.


Changelog

See changelog/ for versioned release notes, starting with v0.0.0.md.


Acknowledgments

Reproducible builds

macli releases are built deterministically in GitHub Actions. The build pins SOURCE_DATE_EPOCH, disables variable timestamps, and produces bit-for-bit reproducible tarballs on Apple Silicon. See .x-cmd/release.common.sh for the exact environment and BUILD_INFO.txt in each release for build parameters.

Support

Code of Conduct

This project follows the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code.

License

Apache 2.0 — see LICENSE.txt.

Verifying releases

Release tarballs are signed with Sigstore/cosign. Each release includes .sigstore.json bundles for the tarballs, SHA256SUMS, and BUILD_INFO.txt. Verify a tarball with:

cosign verify-blob \
  --bundle macli-darwin-universal.tar.xz.sigstore.json \
  --certificate-identity-regexp '^https://github.com/ljh-sh/macli/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  macli-darwin-universal.tar.xz

The npm package @ljh-sh/macli is published from GitHub Actions. You can verify its provenance with:

npm install -g @ljh-sh/macli
npm audit signatures

About

0.1MB macOS system tools for AI Agents. Native Apple frameworks. AI friendly with JSON/YAML output. Minimal context with maximum flexibility.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

13 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors