Skip to content

feat: add scriptable MTP CLI reusing the Kalam engine#467

Open
dhruv-anand-aintech wants to merge 2 commits into
ganeshrvel:masterfrom
dhruv-anand-aintech:feat/mtp-cli
Open

feat: add scriptable MTP CLI reusing the Kalam engine#467
dhruv-anand-aintech wants to merge 2 commits into
ganeshrvel:masterfrom
dhruv-anand-aintech:feat/mtp-cli

Conversation

@dhruv-anand-aintech

@dhruv-anand-aintech dhruv-anand-aintech commented Jun 11, 2026

Copy link
Copy Markdown

Summary

This PR adds a small, scriptable command-line interface that drives
OpenMTP's native Kalam MTP engine from the terminal, enabling automation of
MTP file operations (list, upload, download) on macOS.

Motivation

On macOS, the standard libmtp CLI tools (mtp-detect, mtp-sendfile, …)
often cannot claim the USB interface of MTP devices — they fail with
libusb_claim_interface() = -3 (LIBUSB_ERROR_ACCESS) because IOKit will not
hand the interface to userspace libusb. OpenMTP's GUI works anyway because it
ships its own MTP engine (Kalam mode), but that engine was only reachable
through the Electron GUI — there was no scriptable access.

This makes automation impossible: CI, scripted backups, and side-loading files
onto devices like Garmin watches all require manual drag-and-drop in the GUI. I
hit this directly trying to side-load a file onto a Garmin watch and had to drag
it manually because no CLI could write to the device. This CLI fills that gap by
exposing the same working engine to the terminal.

What it does

yarn mtp-cli list-devices                 # device + storages
yarn mtp-cli ls /DCIM/Camera              # list a directory on the device
yarn mtp-cli upload ./file.fit /GARMIN/NEWFILES   # copy TO device
yarn mtp-cli download /DCIM/.../IMG.jpg ./downloads  # copy FROM device

Options: --storage <id>, --all (include hidden, for ls), --json,
-h/--help. A bin entry (openmtp-cli) is also declared.

How it reuses the existing Kalam engine (no MTP logic reimplemented)

The koffi FFI binding to kalam.dylib previously lived in Kalam.js, but its
module-level imports (log@sentry/electron/electron, binaries
electron-root-path) hard-couple it to the Electron runtime, so it can't be
imported under plain Node.

To reuse — not reinvent — the engine, this PR makes a small, backward-compatible
refactor:

  • ffi/kalam/src/KalamEngine.js (new): the entire existing koffi FFI
    binding, moved verbatim into an Electron-free class. libPath and
    logger are injected via the constructor.
  • ffi/kalam/src/Kalam.js: now a thin subclass of KalamEngine that
    injects the Electron-resolved kalamLibPath and the Sentry-backed log. Its
    public API is unchanged — new Kalam() works exactly as before, so the GUI
    (FileExplorerKalamDataSource) is untouched in behaviour.
  • ffi/kalam/cli/: the CLI. It constructs KalamEngine directly with a
    plain-Node dylib path resolver (resolveLibPath.js, mirroring the
    build/mac/bin/<arch>/kalam.dylib layout without electron-root-path) and a
    console logger, then calls the same initialize / walk / transferFiles
    / listStorages / dispose methods the GUI uses.

I verified the native engine loads and runs under plain Node (outside Electron):
yarn mtp-cli list-devices correctly loads kalam.dylib, calls Initialize,
and reports no MTP devices found when nothing is attached.

Tests

Added Jest unit tests (33 tests, all passing) covering argument parsing, storage
resolution, the dylib path resolver, and full command dispatch with the engine
mocked — so they need no physical device:

yarn test

yarn lint passes clean across the whole project.

Testing status (please read)

  • ✅ Unit-tested (arg parsing + command dispatch, engine mocked).
  • ✅ Verified the engine loads and the CLI runs end-to-end under plain Node with
    no device attached (clean no MTP devices found).
  • ⚠️ Not yet validated against physical hardware — I did not have an MTP
    device to confirm a real ls/upload/download round-trip. The transfer
    call paths mirror FileExplorerKalamDataSource exactly, but I'd appreciate a
    maintainer (or anyone with a device) confirming real-world transfers before
    this is relied upon.

Notes / open questions for maintainers

  • Base branch: see the pinned comment below — development is far behind master and predates the current Kalam engine, so this targets master. Happy to rebase onto development if you prefer.
  • The CLI is documented to run via yarn mtp-cli (node -r @babel/register),
    matching how the project runs its other .js entry scripts. The bin entry
    is wired but, like the rest of the app, assumes the babel toolchain; happy to
    adjust if you'd prefer a transpiled standalone bin in the packaged build.
  • I added jest/babel-jest as dev deps (pinned to 29.5.0 for compatibility
    with the project's @babel/core@7.20) plus a yarn test script. If you have
    a preferred test setup, I'll align to it.

Happy to iterate on naming, layout, or scope. Thanks for OpenMTP!

🤖 Generated with Claude Code

dhruv-anand-aintech and others added 2 commits June 11, 2026 15:59
Move the entire koffi FFI binding to the native kalam.dylib engine out of
Kalam.js into a new Electron-free KalamEngine class. Kalam becomes a thin
subclass that injects the Electron-resolved kalamLibPath and the Sentry-backed
log, so its public API (new Kalam()) is unchanged and GUI behaviour is
identical.

This decouples the MTP engine from the Electron runtime (Sentry, electron,
electron-root-path) so it can also be driven outside the GUI (e.g. a CLI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a small command-line interface that drives OpenMTP's native Kalam MTP
engine from the terminal, enabling automation of MTP file operations on macOS
where libusb-based CLI tools fail with libusb_claim_interface() = -3.

Subcommands: list-devices, ls, upload, download (plus --storage, --all, --json).
The CLI constructs KalamEngine directly with a plain-Node dylib path resolver
and a console logger, reusing the exact same engine the GUI uses - no MTP logic
is reimplemented.

- ffi/kalam/cli/{index,mtpCli,resolveLibPath}.js + README
- bin: openmtp-cli, and a 'yarn mtp-cli' script
- Jest unit tests for arg parsing, storage resolution, path resolution, and
  command dispatch (engine mocked; no physical device required)
- add jest/babel-jest devDeps and a 'yarn test' script

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dhruv-anand-aintech dhruv-anand-aintech changed the base branch from development to master June 11, 2026 10:30
@dhruv-anand-aintech

Copy link
Copy Markdown
Author

Note on base branch: CONTRIBUTING.md asks for PRs against development, but development is currently ~576 commits behind master and predates the modern Kalam (koffi/kalam.dylib) engine this PR builds on — the Kalam.js / FFI layer I refactor only exists in its current form on master. So I've targeted master, where this lands as a clean 12-file diff. Happy to retarget if you maintain development as the integration branch and would prefer I rebase there instead — just let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant