602 lines
20 KiB
Markdown
602 lines
20 KiB
Markdown
# Lua Plugins
|
|
|
|
cliamp has a Lua 5.1 plugin system. Plugins can hook into playback events (scrobbling, notifications, status bar output) and add custom visualizers. Each plugin runs in an isolated VM. A crash in one plugin cannot affect others or the player.
|
|
|
|
Plugins live in `~/.config/cliamp/plugins/`. Create the directory:
|
|
|
|
```
|
|
mkdir -p ~/.config/cliamp/plugins
|
|
```
|
|
|
|
Plugins run only after their exact contents have been approved. Existing and manually copied plugins start untrusted; approve one with `cliamp plugins trust <name>`.
|
|
|
|
## Plugin manager
|
|
|
|
```sh
|
|
cliamp plugins # show help
|
|
cliamp plugins list # list installed plugins
|
|
cliamp plugins install <source> # install a plugin
|
|
cliamp plugins trust <name> # approve installed plugin contents
|
|
cliamp plugins remove <name> # remove a plugin
|
|
```
|
|
|
|
Install and trust display the source, SHA-256, declared permissions, and implicit filesystem/network access before prompting. Use `--yes` only after independently reviewing the same content in non-interactive environments. Approvals are stored in `plugins/.trust.json`; editing a plugin changes its hash and disables it until it is approved again. Unknown permission names are rejected.
|
|
|
|
### Install sources
|
|
|
|
| Format | Example |
|
|
|--------|---------|
|
|
| GitHub | `user/repo` |
|
|
| GitHub with tag | `user/repo@v1.0` |
|
|
| GitLab | `gitlab:user/repo` |
|
|
| GitLab with tag | `gitlab:user/repo@v1.0` |
|
|
| Codeberg | `codeberg:user/repo` |
|
|
| Codeberg with tag | `codeberg:user/repo@v1.0` |
|
|
| Direct URL | `https://example.com/plugin.lua` |
|
|
|
|
### Naming convention
|
|
|
|
Plugin repositories **must** be named `cliamp-plugin-<name>` with the entry point `<name>.lua` at the repo root. The `cliamp-plugin-` prefix is stripped on install, so `cliamp-plugin-soap-bubbles` (containing `soap-bubbles.lua`) installs as `soap-bubbles`.
|
|
|
|
```sh
|
|
cliamp plugins install bjarneo/cliamp-plugin-lastfm
|
|
cliamp plugins install bjarneo/cliamp-plugin-lastfm@v1.0
|
|
cliamp plugins install gitlab:user/my-visualizer
|
|
cliamp plugins install codeberg:user/my-plugin
|
|
cliamp plugins install https://example.com/my-plugin.lua
|
|
cliamp plugins remove lastfm
|
|
```
|
|
|
|
## Quick start
|
|
|
|
### Now-playing file (for Waybar, Polybar, etc.)
|
|
|
|
```lua
|
|
-- ~/.config/cliamp/plugins/now-playing.lua
|
|
local p = plugin.register({
|
|
name = "now-playing",
|
|
type = "hook",
|
|
description = "Write now-playing to /tmp for status bars",
|
|
})
|
|
|
|
p:on("track.change", function(track)
|
|
cliamp.fs.write("/tmp/cliamp-now-playing", track.artist .. " - " .. track.title)
|
|
end)
|
|
|
|
p:on("playback.state", function(ev)
|
|
if ev.status == "paused" then
|
|
cliamp.fs.write("/tmp/cliamp-now-playing", "paused")
|
|
end
|
|
end)
|
|
|
|
p:on("app.quit", function()
|
|
cliamp.fs.remove("/tmp/cliamp-now-playing")
|
|
end)
|
|
```
|
|
|
|
### Desktop notification on track change
|
|
|
|
```lua
|
|
-- ~/.config/cliamp/plugins/notify.lua
|
|
local p = plugin.register({
|
|
name = "notify",
|
|
type = "hook",
|
|
})
|
|
|
|
p:on("track.change", function(track)
|
|
local title = track.artist .. " - " .. track.title
|
|
os.execute('notify-send "cliamp" "' .. title .. '"')
|
|
end)
|
|
```
|
|
|
|
Note: `os.execute` is removed by the sandbox. Public HTTP endpoints are available through `cliamp.http`; private, loopback, link-local, multicast, and unspecified addresses are blocked. For local automation, write to an allowlisted file that a watcher picks up or declare the permission-gated `exec` capability.
|
|
|
|
### Webhook
|
|
|
|
```lua
|
|
-- ~/.config/cliamp/plugins/webhook.lua
|
|
local p = plugin.register({
|
|
name = "webhook",
|
|
type = "hook",
|
|
})
|
|
|
|
local url = p:config("url")
|
|
|
|
p:on("track.change", function(track)
|
|
if not url then return end
|
|
cliamp.http.post(url, {
|
|
json = { title = track.title, artist = track.artist, album = track.album }
|
|
})
|
|
end)
|
|
```
|
|
|
|
```toml
|
|
# config.toml
|
|
[plugins.webhook]
|
|
url = "https://example.com/hook"
|
|
```
|
|
|
|
## Plugin structure
|
|
|
|
### Single file
|
|
|
|
```
|
|
~/.config/cliamp/plugins/myplugin.lua
|
|
```
|
|
|
|
### Directory with init.lua
|
|
|
|
```
|
|
~/.config/cliamp/plugins/myplugin/
|
|
init.lua
|
|
helpers.lua
|
|
```
|
|
|
|
The directory name becomes the plugin name. Only `init.lua` is loaded automatically.
|
|
|
|
## Registration
|
|
|
|
Every plugin must call `plugin.register()` to be recognized. Files that don't call it are silently skipped.
|
|
|
|
```lua
|
|
local p = plugin.register({
|
|
name = "myplugin", -- required
|
|
type = "hook", -- "hook" or "visualizer"
|
|
version = "1.0.0", -- optional
|
|
description = "What it does", -- optional
|
|
})
|
|
```
|
|
|
|
The returned object `p` provides two methods:
|
|
|
|
| Method | Description |
|
|
|--------|-------------|
|
|
| `p:on(event, callback)` | Subscribe to a playback event |
|
|
| `p:config(key)` | Read a config value from `[plugins.myplugin]` in config.toml |
|
|
|
|
## Events
|
|
|
|
Plugins subscribe to events with `p:on(event, callback)`. Callbacks run asynchronously in goroutines and have a 5-second timeout.
|
|
|
|
### Available events
|
|
|
|
| Event | Callback argument | When |
|
|
|-------|-------------------|------|
|
|
| `track.change` | `{title, artist, album, genre, year, path, duration, stream}` | New track starts |
|
|
| `track.scrobble` | Same + `{played_secs}` | Track played >= 50% or >= 4 min |
|
|
| `playback.state` | `{status, title, artist, album, path, duration, stream, position}` | Any playback state change (play, pause, stop, seek, volume, track transition) |
|
|
| `player.seek` | `{position, duration}` (seconds) | A seek completes |
|
|
| `player.volume` | `{db}` | Volume changes |
|
|
| `player.eq` | `{bands, preset}` | An EQ band or preset changes |
|
|
| `player.mode` | `{shuffle, repeat}` | Shuffle toggled or repeat mode cycled |
|
|
| `queue.change` | `{count, index, queued}` | Playlist or play-next queue changes |
|
|
| `app.start` | `{}` | After all plugins loaded |
|
|
| `app.quit` | `{}` | Before shutdown |
|
|
|
|
The `status` field in `playback.state` is one of: `"playing"`, `"paused"`, `"stopped"`. The `repeat` field in `player.mode` is one of: `"Off"`, `"All"`, `"One"` (matching `cliamp.player.repeat_mode()`). In `player.eq`, `bands` is an array of 10 dB values.
|
|
|
|
The `player.*` and `queue.change` events are fired by diffing state after each UI update, so they cover every change regardless of source (keypress, IPC, MPRIS, or another plugin).
|
|
|
|
## Plugin object methods
|
|
|
|
The object returned by `plugin.register(...)` exposes additional methods beyond `:on()` / `:config()`:
|
|
|
|
### `p:bind(key, [description,] callback)` — keyboard binding (requires `permissions = {"keymap"}`)
|
|
|
|
```lua
|
|
local p = plugin.register({
|
|
name = "my-plugin",
|
|
type = "hook",
|
|
permissions = {"keymap"},
|
|
})
|
|
|
|
-- Listed in the Ctrl+K overlay under "— plugins —":
|
|
p:bind("x", "Extract chapters", function(key) ... end)
|
|
|
|
-- Not listed (hidden binding):
|
|
p:bind("ctrl+e", function(key) ... end)
|
|
```
|
|
|
|
Returns `true` on success, or `false, reason` if the key is already owned by cliamp's core UI or the plugin lacks the `keymap` permission. Pass a description string as the middle argument to surface the binding in the `Ctrl+K` keymap overlay; omit it for an internal-only binding.
|
|
|
|
Key strings are in Bubbletea's `msg.String()` form: lowercase letters, `ctrl+` / `shift+` / `alt+` prefixes (e.g. `"x"`, `"ctrl+e"`, `"shift+f1"`). Case-insensitive.
|
|
|
|
Plugin keys only fire in the main view — overlays like the file browser, theme picker, and keymap itself capture their own input. Core reserves all keys documented in `docs/keybindings.md`; trying to bind one of those logs a warning and returns `false`.
|
|
|
|
Use `p:unbind(key)` to release a binding.
|
|
|
|
### `p:command(name, callback)` — shell-invokable command
|
|
|
|
```lua
|
|
p:command("run", function(args)
|
|
-- args is an array of strings passed after the command name
|
|
return "done: " .. args[1]
|
|
end)
|
|
```
|
|
|
|
The callback can return a string, which is printed by the CLI client. Commands are invoked from the shell via `cliamp plugins call <plugin-name> <command> [args...]` and dispatched to the running cliamp over IPC. Since dispatch runs in the running player, commands don't need a separate permission (they're user-initiated).
|
|
|
|
List all registered commands with `cliamp plugins commands`. Commands can run for up to 5 minutes before timing out.
|
|
|
|
## Lua API
|
|
|
|
All APIs are under the `cliamp` global table.
|
|
|
|
### cliamp.player (read-only)
|
|
|
|
```lua
|
|
cliamp.player.state() --> "playing" | "paused" | "stopped"
|
|
cliamp.player.position() --> number (seconds)
|
|
cliamp.player.duration() --> number (seconds)
|
|
cliamp.player.volume() --> number (dB, -30 to +6)
|
|
cliamp.player.speed() --> number (ratio, 1.0 = normal)
|
|
cliamp.player.mono() --> boolean
|
|
cliamp.player.repeat_mode() --> "Off" | "All" | "One"
|
|
cliamp.player.shuffle() --> boolean
|
|
cliamp.player.eq_bands() --> table of 10 dB values
|
|
```
|
|
|
|
### cliamp.track (read-only)
|
|
|
|
```lua
|
|
cliamp.track.title() --> string
|
|
cliamp.track.artist() --> string
|
|
cliamp.track.album() --> string
|
|
cliamp.track.genre() --> string
|
|
cliamp.track.year() --> number
|
|
cliamp.track.track_number() --> number
|
|
cliamp.track.path() --> string
|
|
cliamp.track.is_stream() --> boolean
|
|
cliamp.track.duration_secs() --> number
|
|
```
|
|
|
|
### cliamp.queue
|
|
|
|
Read the playlist freely; mutating it requires `permissions = {"control"}`. All
|
|
indices are 0-based, matching `cliamp.queue.current()`.
|
|
|
|
```lua
|
|
-- read (no permission)
|
|
cliamp.queue.list() --> array of {title, artist, album, path, index, queued}
|
|
cliamp.queue.count() --> number of tracks
|
|
cliamp.queue.current() --> 0-based index of the current track
|
|
|
|
-- mutate (requires "control")
|
|
cliamp.queue.add(path) -- resolve a file/dir/URL and append
|
|
cliamp.queue.jump(index) -- make index current and play it
|
|
cliamp.queue.remove(index) -- remove the track at index
|
|
cliamp.queue.move(from, to) -- reorder a track
|
|
```
|
|
|
|
`add` accepts anything the CLI accepts: a local file or directory, an HTTP
|
|
stream, an M3U/PLS URL, or a YouTube/yt-dlp URL. Resolution happens off the UI
|
|
thread, so a slow URL never blocks playback.
|
|
|
|
### cliamp.http
|
|
|
|
```lua
|
|
-- GET
|
|
local body, status = cliamp.http.get("https://api.example.com/data", {
|
|
headers = { Authorization = "Bearer token" }
|
|
})
|
|
|
|
-- POST with JSON
|
|
local body, status = cliamp.http.post("https://api.example.com/scrobble", {
|
|
json = { artist = "Radiohead", track = "Everything In Its Right Place" }
|
|
})
|
|
|
|
-- POST with form body
|
|
local body, status = cliamp.http.post(url, {
|
|
headers = { ["Content-Type"] = "application/x-www-form-urlencoded" },
|
|
body = "key=value&foo=bar"
|
|
})
|
|
```
|
|
|
|
Restrictions: 5-second timeout, 1 MB response body cap.
|
|
|
|
### cliamp.fs
|
|
|
|
```lua
|
|
cliamp.fs.write(path, content) -- overwrite file
|
|
cliamp.fs.append(path, content) -- append to file
|
|
cliamp.fs.read(path) --> string (max 1 MB)
|
|
cliamp.fs.remove(path) -- delete file
|
|
cliamp.fs.exists(path) --> boolean
|
|
cliamp.fs.mkdir(path) -- create directory (recursive)
|
|
cliamp.fs.listdir(path) --> {names}, err
|
|
```
|
|
|
|
Writes are restricted to the system temp directory (`/tmp/` on Unix), `~/.config/cliamp/`, `~/.local/share/cliamp/`, and `~/Music/cliamp/`. Reads are allowed from anywhere. On Windows, if `HOME` is unset, the config directory portion resolves to `%APPDATA%\cliamp`.
|
|
|
|
### cliamp.json
|
|
|
|
```lua
|
|
local tbl = cliamp.json.decode('{"key": "value"}')
|
|
local str = cliamp.json.encode({ key = "value" })
|
|
```
|
|
|
|
### cliamp.store
|
|
|
|
A persistent per-plugin key/value store. Values (strings, numbers, booleans,
|
|
and tables) survive restarts. No permission required: each plugin sees only its
|
|
own namespace, so one plugin can never read another's keys.
|
|
|
|
```lua
|
|
cliamp.store.set(key, value) -- value: string|number|boolean|table
|
|
cliamp.store.get(key) --> value or nil
|
|
cliamp.store.delete(key)
|
|
cliamp.store.keys() --> sorted array of keys
|
|
cliamp.store.clear()
|
|
```
|
|
|
|
Backed by `~/.local/share/cliamp/plugins/<name>/store.json`, written owner-only
|
|
(0600). Use it for play counts, offline scrobble queues, resume positions, or
|
|
remembered settings, not for large data.
|
|
|
|
```lua
|
|
local counts = cliamp.store.get("counts") or {}
|
|
counts[cliamp.track.path()] = (counts[cliamp.track.path()] or 0) + 1
|
|
cliamp.store.set("counts", counts)
|
|
```
|
|
|
|
### cliamp.crypto
|
|
|
|
```lua
|
|
cliamp.crypto.md5("hello") --> hex string
|
|
cliamp.crypto.sha256("hello") --> hex string
|
|
cliamp.crypto.hmac_sha256("secret", "msg") --> hex string
|
|
```
|
|
|
|
### cliamp.log
|
|
|
|
```lua
|
|
cliamp.log.info("loaded successfully")
|
|
cliamp.log.warn("missing config key")
|
|
cliamp.log.error("request failed: " .. err)
|
|
cliamp.log.debug("response: " .. body)
|
|
```
|
|
|
|
Logs are written to `~/.config/cliamp/plugins.log` with timestamps and `[plugin-name]` prefix.
|
|
|
|
### cliamp.player control (requires permissions)
|
|
|
|
Plugins that declare `permissions = {"control"}` can send commands to the player:
|
|
|
|
```lua
|
|
local p = plugin.register({
|
|
name = "my-controller",
|
|
type = "hook",
|
|
permissions = {"control"},
|
|
})
|
|
|
|
cliamp.player.next() -- skip to next track
|
|
cliamp.player.prev() -- go to previous track
|
|
cliamp.player.play_pause() -- toggle play/pause
|
|
cliamp.player.stop() -- stop playback
|
|
cliamp.player.set_volume(-5) -- set volume in dB (-30 to +6)
|
|
cliamp.player.set_speed(1.25) -- set playback speed (0.25 to 2.0)
|
|
cliamp.player.seek(30) -- seek to 30 seconds
|
|
cliamp.player.toggle_mono() -- toggle mono output
|
|
cliamp.player.set_eq_preset("Rock") -- switch to built-in preset (sets bands + UI label)
|
|
cliamp.player.set_eq_preset("Metal", {6,4,1,-1,-2,2,4,6,6,5}) -- custom preset with bands
|
|
cliamp.player.set_eq_band(1, 6) -- set EQ band 1 to +6 dB (bands 1-10, -12 to +12)
|
|
```
|
|
|
|
Without `permissions = {"control"}`, these functions log a warning and do nothing.
|
|
|
|
### cliamp.notify
|
|
|
|
```lua
|
|
cliamp.notify("Song Title") -- notification with title only
|
|
cliamp.notify("Song Title", "Artist Name") -- notification with title and body
|
|
```
|
|
|
|
Sends a desktop notification via `notify-send`. Works with mako, dunst, and other notification daemons.
|
|
|
|
### cliamp.exec (requires permissions)
|
|
|
|
Plugins that declare `permissions = {"exec"}` can spawn subprocesses from a configurable binary allowlist. Default allowlist: `yt-dlp`, `ffmpeg`. Extend it in `config.toml`:
|
|
|
|
```toml
|
|
[plugins]
|
|
allowed_binaries = "ffprobe, curl" # merged with defaults
|
|
```
|
|
|
|
```lua
|
|
local p = plugin.register({
|
|
name = "my-downloader",
|
|
type = "hook",
|
|
permissions = {"exec"},
|
|
})
|
|
|
|
local handle, err = cliamp.exec.run("yt-dlp", {"--dump-json", url}, {
|
|
on_stdout = function(line) ... end, -- optional, called per line
|
|
on_stderr = function(line) ... end, -- optional
|
|
on_exit = function(code) ... end, -- optional, fires exactly once
|
|
cwd = "/tmp/work", -- optional; must be in write allowlist
|
|
timeout = 300, -- optional seconds, hard cap 1800
|
|
})
|
|
|
|
handle:cancel() -- terminate the process
|
|
handle:alive() -- --> boolean
|
|
```
|
|
|
|
**Safety rails:**
|
|
|
|
- Binary must be in the allowlist. Argv is argv — no shell, no expansion.
|
|
- `args` must be a flat array of strings. Nested tables / non-strings are rejected.
|
|
- Subprocess env is minimal (`PATH`, `HOME`, `LANG`) — secrets in the parent env are not passed through.
|
|
- Output is capped at 4 MiB per process (stdout + stderr combined); further lines are dropped silently.
|
|
- Concurrency capped at 4 running processes per plugin.
|
|
- All processes owned by a plugin are killed on plugin unload and on cliamp exit.
|
|
- Negative `on_exit` codes signal cancellation/timeout (`-1`) or spawn failure (`-2`).
|
|
|
|
Without `permissions = {"exec"}`, `cliamp.exec.run` returns `nil, "exec permission required"`.
|
|
|
|
### cliamp.message
|
|
|
|
```lua
|
|
cliamp.message("Scrobble Sent") -- show for default duration
|
|
cliamp.message("Syncing Library", 5) -- show for 5 seconds
|
|
```
|
|
|
|
Displays a transient message in the status bar at the bottom of the UI. The
|
|
duration argument is optional (seconds); omit it to use the default TTL. Durations above 60 seconds are clamped.
|
|
|
|
### cliamp.sleep
|
|
|
|
```lua
|
|
cliamp.sleep(2.5) -- block for 2.5 seconds (max 10)
|
|
```
|
|
|
|
Blocks the plugin's Lua VM. Other hooks for the same plugin will queue until the sleep finishes. Prefer `cliamp.timer.after()` for non-blocking delays.
|
|
|
|
### cliamp.timer
|
|
|
|
```lua
|
|
-- Run once after 5 seconds
|
|
local id = cliamp.timer.after(5.0, function()
|
|
cliamp.log.info("timer fired")
|
|
end)
|
|
|
|
-- Run every 30 seconds
|
|
local id = cliamp.timer.every(30.0, function()
|
|
-- periodic task
|
|
end)
|
|
|
|
-- Cancel
|
|
cliamp.timer.cancel(id)
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Plugin-specific config goes in `config.toml` under `[plugins.<name>]`:
|
|
|
|
```toml
|
|
[plugins.lastfm]
|
|
api_key = "abc123"
|
|
api_secret = "secret"
|
|
session_key = "sk-xxx"
|
|
|
|
[plugins.webhook]
|
|
url = "https://example.com/hook"
|
|
```
|
|
|
|
Access in Lua:
|
|
|
|
```lua
|
|
local api_key = p:config("api_key") --> "abc123" or nil
|
|
```
|
|
|
|
### Disabling plugins
|
|
|
|
Disable a specific plugin:
|
|
|
|
```toml
|
|
[plugins.webhook]
|
|
enabled = false
|
|
```
|
|
|
|
Or disable multiple at once:
|
|
|
|
```toml
|
|
[plugins]
|
|
disabled = webhook, discord-rpc
|
|
```
|
|
|
|
## Visualizer plugins
|
|
|
|
Plugins with `type = "visualizer"` add custom visualizer modes that appear in the `v` key cycle alongside built-in modes.
|
|
|
|
```lua
|
|
-- ~/.config/cliamp/plugins/simple-bars.lua
|
|
local p = plugin.register({
|
|
name = "simple-bars",
|
|
type = "visualizer",
|
|
})
|
|
|
|
-- Called every frame (~20 FPS during playback).
|
|
-- bands: table of 10 numbers (0.0-1.0), indices 1-10
|
|
-- frame: monotonic counter
|
|
-- rows: available terminal rows (changes in fullscreen mode)
|
|
-- cols: available terminal columns
|
|
-- Must return a multi-line string.
|
|
function p:render(bands, frame, rows, cols)
|
|
local lines = {}
|
|
local chars = { " ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█" }
|
|
|
|
for row = 5, 1, -1 do
|
|
local line = ""
|
|
for i = 1, 10 do
|
|
local level = bands[i]
|
|
local threshold = (row - 1) / 5
|
|
if level > threshold then
|
|
line = line .. "██████ "
|
|
else
|
|
line = line .. " "
|
|
end
|
|
end
|
|
table.insert(lines, line)
|
|
end
|
|
|
|
return table.concat(lines, "\n")
|
|
end
|
|
```
|
|
|
|
### Visualizer callbacks
|
|
|
|
| Callback | Signature | Required |
|
|
|----------|-----------|----------|
|
|
| `p:render(bands, frame, rows, cols)` | Returns string | Yes |
|
|
| `p:init(rows, cols)` | Setup when selected | No |
|
|
| `p:destroy()` | Cleanup when deselected | No |
|
|
|
|
Render has a 10 ms budget per frame. If it exceeds this, the previous frame is reused to prevent UI jank.
|
|
|
|
## Sandbox
|
|
|
|
For security, plugins run with restricted access. The sandbox removes dangerous standard library functions and restricts file system access.
|
|
|
|
### Removed functions
|
|
|
|
| Removed | Replacement |
|
|
|---------|-------------|
|
|
| `os.execute`, `os.remove`, `os.rename`, `os.exit`, `os.setlocale`, `os.tmpname` | Use `cliamp.fs`, `cliamp.http`, or permission-gated `cliamp.exec` |
|
|
| `io` module (all of it) | Use `cliamp.fs` |
|
|
| `dofile`, `loadfile`, `load`, `loadstring`, `require`, `module`, `package`, `debug` | Not available |
|
|
|
|
### Kept functions
|
|
|
|
`os.time()`, `os.date()`, `os.clock()`, `os.getenv()` are available.
|
|
|
|
### File system restrictions
|
|
|
|
**Reads:** Allowed from any path (max 1 MB per read).
|
|
|
|
**Writes/removes/mkdir** are restricted to these directories only:
|
|
|
|
- `/tmp/` (and the system temp directory)
|
|
- `~/.config/cliamp/`
|
|
- `~/.local/share/cliamp/`
|
|
- `~/Music/cliamp/`
|
|
|
|
Attempts to write outside these directories will raise a Lua error. Directory traversal (`..`) is blocked.
|
|
|
|
### Isolation
|
|
|
|
- Each plugin runs in its own Lua VM. Plugins cannot access each other's state or variables.
|
|
- A crash in one plugin does not affect other plugins or the player.
|
|
- Public network access is available via `cliamp.http` (no raw socket access). Private, loopback, link-local, multicast, and unspecified destinations are blocked after DNS resolution and across redirects.
|
|
- `os.execute` is removed. Permission-gated `cliamp.exec` can spawn only configured allowlisted binaries.
|
|
|
|
## Debugging
|
|
|
|
Check `~/.config/cliamp/plugins.log` for plugin output and errors:
|
|
|
|
```
|
|
2025-03-29 14:30:01 [now-playing] info: Now playing: Everything In Its Right Place
|
|
2025-03-29 14:30:01 [webhook] error: track.change handler error: connection refused
|
|
```
|
|
|
|
Use `cliamp.log.debug()` liberally during development.
|