@muten/mobile
Native mobile abilities for muten apps — notifications, haptics,
share, and a background watcher — without touching the muten core. It's a plugin: it adds abilities from the
outside through the two seams muten already exposes — a use import for callable JS (oracle-validated, so
lint == runtime) and Capacitor for the native side (wired by cap sync). On the web every call falls back to the
platform API (Web Notifications / navigator.vibrate / Web Share) or is a harmless no-op, so the same code runs
everywhere.
Install
npm i @muten/mobile
# for the native side, add the Capacitor targets you use:
npm i -D @capacitor/haptics @capacitor/share # local-notifications is used by notify()
cap sync discovers @muten/mobile (it declares capacitor.android.src) and wires its native module — the
MobileWatch plugin + a generic foreground WatchService. Nothing else to register.
Use it from muten
# a page or store — `use` the abilities you need:
use notify, vibrate, share, startWatch from "@muten/mobile"
# fire a native notification (tap opens the app at `route`):
effect { notify("Claude has a question", "acme-api — wire up Stripe", "/ask/{s.id}") }
# haptic + native share:
Button "Share" -> shareIt
Button "Buzz" -> buzz
API
| Function | What it does |
|---|---|
notify(title, body, route?) |
native notification (status bar / lock screen); web → Web Notification. route opens the app there on tap. |
vibrate(ms?) |
haptic feedback; web → navigator.vibrate. |
share(text, url?) |
native share sheet; web → Web Share API. |
ensureNotifications() |
request notification permission once (call on mount before notifying). |
onNotificationTap(cb) |
run cb(route) when a notification is tapped. |
startWatch(config) / stopWatch() |
native background watcher: a foreground service keeps a WebSocket to config.url alive independently of the WebView and fires notifications per config.rules even when the app is backgrounded or closed. Native only; no-op on web. |
isNative() |
true inside a real app shell, false on the web. |
startWatch config
startWatch({
url: "ws://192.168.1.20:4599/ws",
arrayKey: "sessions", // json[arrayKey] is the list (default "sessions")
idKey: "id", stateKey: "state", agentKey: "agent", taskKey: "task",
rules: { // fire a notification when a row enters one of these states
awaiting: { title: "Needs your OK" },
asking: { title: "Has a question" },
settled: { title: "Finished" }
}
})
Why a plugin, not a core feature
muten is a web-first, platform-agnostic UI DSL — its core never mentions Android. Everything mobile lives here, in the plugin, so the language stays what it is. If you build a web app, you never see a word of this.