Quick Start
lokictl ships next to the server binary in every release
package (see Installation). On the machine
running the server it finds the server without configuration; for a
remote server, see the end of this section.
-
Confirm the server is reachable. No credential is required.
lokictl health -
Authorize the CLI. This opens a browser page on the server, signs
you in if needed, and stores a long-lived API key in the CLI config
file.
On a machine without a browser, use a password instead:
lokictl loginlokictl login --password <password> --username <user> -
Verify the stored credential.
lokictl whoami -
List the tasks the server can run, then run one and wait for it.
lokictl task list -o table
lokictl task show describe
lokictl job run describe --apply missing "D:/photos/2024" --wait --timeout 30m--waitblocks until the job finishes and prints the final job object; the exit code is 0 forcompletedand 3 otherwise. To watch the job's log while it runs, use--follow. The same flags re-run or resume an existing job:lokictl job run describe --apply missing "D:/photos/2024" --follow
lokictl job copy <id> --follow -
Query the library.
lokictl media query --tag sunset --exclude-tag blurry
lokictl media visual "a red car in the snow" --limit 20
lokictl db query "SELECT COUNT(*) FROM media"
To use a remote server, set LOKICTL_SERVER or pass
--server before login. The stored config
then carries both the URL and the key.
lokictl --server http://nas.local:10111 login
Authentication
Every endpoint except /health requires a credential unless
the server has Allow Public Access enabled, in which case
read-only endpoints accept anonymous requests. The CLI sends its token
as Authorization: Bearer <token> on every request.
Two kinds of token are accepted:
| Token | Prefix | Lifetime | Revocable | Obtained by |
|---|---|---|---|---|
| API key | lk_ | Until revoked | Yes (lokictl key revoke, or Config → API Keys in the web UI) | lokictl login (browser flow), lokictl key create, web UI |
| JWT | none | 1 year | No (rotate the server's JWT secret to invalidate all) | lokictl login --password, POST /auth/login |
API keys are the recommended credential for the CLI. They are tied to a user, named, listable, and individually revocable.
Browser Authorization
lokictl loginlokictl login --no-browser
The default login is a loopback authorization flow (RFC 8252 with PKCE). The sequence is:
- The CLI opens a listener on an ephemeral
127.0.0.1port and generates a random state value and a PKCE code verifier. - The CLI opens the system browser at
<server>/auth/cli/authorizewith the port, state, code challenge, and a key name (lokictl@<hostname>). With--no-browser, the URL is printed instead. - If the browser has no server session, the server redirects to
/loginand back. - The server shows an approval page naming the user, the key name, and the loopback port. Approving redirects the browser to
http://127.0.0.1:<port>/callbackwith a one-time code. Denying redirects witherror=access_denied. - The CLI exchanges the code and its verifier at
POST /auth/cli/token. The server mints a newlk_API key owned by the approving user and returns it. - The CLI writes the key and the server URL to the config file and prints the key metadata.
Constraints:
- The browser must run on the same machine as the CLI. The callback is a loopback address. Over SSH, use password login.
- The one-time code expires two minutes after approval. The CLI stops waiting for the browser after three minutes and exits 1 with a hint to use
--password. - The server refuses to authorize the CLI while the temporary
admin/adminaccount is the only user. Complete first-run setup first. - Each successful login creates a new key. Old keys remain valid until revoked. Use
lokictl key listto audit them.
Password Login
lokictl login --password <password> [--username <user>]
| Flag | Default | Description |
|---|---|---|
--password P | Password. Presence of this flag selects the password flow. | |
--username U | admin | Username. |
Calls POST /auth/login and stores the returned JWT in the
config file. JWTs issued to API clients are valid for one year and
cannot be revoked individually. A warning is printed if the login
succeeded as the temporary default admin, since most endpoints return
403 until a real user exists.
API Keys
lokictl key create --name <name> [--username <user>] [--save]lokictl key listlokictl key revoke --id <id>
| Flag | Description |
|---|---|
--name N | Required. Display name for the key. |
--username U | Owner. Defaults to the user the current credential authenticates as. |
--save | Write the new key to the config file as this CLI's token. |
--id N | Numeric key id from key list. |
The plaintext key is returned once, in the key field of the
create response. The server stores only a hash. The
list output shows id, owner, name, prefix, creation time,
and last-used time.
A key may also be presented in an X-API-Key header. The
CLI always uses the Authorization header.
Token Precedence
--tokenflagLOKICTL_TOKENenvironment variabletokenfield in the config file- No credential. Only
/healthand, with public access enabled, read-only endpoints succeed.
Authentication Errors
| HTTP status | Meaning | Action |
|---|---|---|
| 401 | No credential, an expired JWT, or a revoked key. | Run lokictl login again, or create a new key. |
403 with setup_required | Authenticated as the temporary default admin. | Complete first-run setup in a browser. |
| 403 (other) | Cross-origin or same-origin check failed on the authorize page. | Retry the login from a browser on the same machine. |
Connection
Server URL Resolution
The base URL is resolved from the first source that supplies a value, in this order:
--serverflagLOKICTL_SERVERenvironment variableserverfield in the config file- Auto-detected default:
http://localhost:<port>
The auto-detected port is LOWKEY_PORT if set, otherwise the
port field of the local server's own config.json
(in the server data directory), otherwise 10111. A CLI on
the same machine as the server therefore follows the server's port
without configuration.
The base URL is used verbatim as a prefix. Include the scheme. Do not
include a trailing path. A reverse proxy that terminates TLS is
supported with an https:// URL.
Config File
lokictl login and lokictl key create --save
write a JSON file with two optional fields. The file is created with
mode 0600 and its directory with 0700.
| Platform | Path |
|---|---|
| Windows | %APPDATA%\lokictl\config.json |
| Linux | $XDG_CONFIG_HOME/lokictl/config.json (default ~/.config/lokictl/config.json) |
| macOS | ~/Library/Application Support/lokictl/config.json |
Set LOKICTL_CONFIG_DIR to relocate the directory.
{ "server": "http://nas.local:10111", "token": "lk_..."}
The file may be written by hand. Either field may be omitted. A missing file is not an error.
Installation
The binary ships alongside the server in every release package.
| Platform | Location after install |
|---|---|
| Windows (installer) | lokictl.exe in the server's install directory, next to media-server.exe |
| Windows (zip) | lokictl.exe at the archive root |
| Linux (tar.gz) | lokictl at the archive root, next to lowkeymediaserver |
| macOS (dmg) | lokictl inside the application bundle, next to the lowkeymediaserver binary |
| Docker image | Not included. Download a release archive for the host platform and point it at the container with --server. |
The binary has no runtime dependencies. Copy it anywhere on the
PATH. To build from source, run the following from the
repository root:
npm run build:cli# equivalent:cd media-server && go build -o lokictl ./cmd/lokictl
Synopsis
lokictl [global flags] <command> [subcommand] [arguments] [command flags]
lokictl is a client for the Lowkey Media Server
HTTP API. Every command maps to one or more API
calls. Results are written to standard output as JSON. Errors are written
to standard error as a single JSON object. Exit codes are stable. The
tool is designed to be driven by scripts and AI agents as well as by
people.
Global flags must appear before the command name. Flags that follow the
command belong to that command. Run lokictl help to print
the command table for the installed version.
Code is coloured by role:
literal
--flag
<placeholder>
/path
ENV_VAR
"value"
[optional]
Global Flags
Global flags are recognized only before the command name.
| Flag | Default | Description |
|---|---|---|
--server URL | see resolution | Server base URL. |
--token TOKEN | see precedence | API key or JWT. |
-o, --output json|table | json | Output format. table renders arrays of flat objects as tab-separated rows with a header line. Values that are not arrays of objects fall back to JSON. |
--timeout DUR | 30s | HTTP request timeout as a Go duration (90s, 10m). Streaming commands (--follow, job logs, cleanup log streams) ignore it. Commands with their own --timeout flag use that flag for the wait, not the request. |
-q, --quiet | off | Minimal output. job run and job copy without --wait/--follow print the bare job id instead of a JSON object; deps download --wait stops printing progress. Every other command is unchanged. |
-h, --help | Print the command table and exit 0. |
Flags accept both --flag value and --flag=value.
Output Contract
Standard Output
On success, stdout carries exactly one JSON document, pretty-printed with two-space indentation and without HTML escaping. The document is the server response for the primary API call, or a CLI-composed object for commands that make several calls. Commands that await a job print the final job object.
With -o table, an array of objects is printed as TSV: a
header row of the first element's keys in order, followed by any
additional keys found in later elements sorted alphabetically, then one
row per element. Nested values are JSON-encoded inside the cell.
Standard Error
On failure, stderr carries exactly one JSON object on one line:
{"error": "<message>", "status": 404, "hint": "<suggestion>", "detail": <parsed server body>}
| Field | Present when | Description |
|---|---|---|
error | always | Human-readable message. |
status | HTTP error | HTTP status code returned by the server. |
hint | known condition | Suggested next action (for example, re-run login on 401). |
detail | server body is JSON | The server's error body, parsed. |
Usage errors print {"error": "usage: ..."} followed by the
command's flag list. Progress output from --follow,
--wait, and streamed task logs is also written to stderr as
plain lines, so stdout remains parseable.
Exit Codes
| Code | Meaning |
|---|---|
0 | Success. |
1 | Server returned an error status, or a network failure occurred. |
2 | Usage error: unknown command, missing argument, invalid flag, or a destructive command without --yes. |
3 | An awaited job or workflow ended in error or cancelled, or the wait timed out. |
Destructive Commands
The following commands exit 2 without contacting the server unless
--yes is present:
job clear, media delete, media forget,
tag delete, tag unassign-bulk,
category delete, group delete,
workflow delete, deps delete,
index delete, index prune.
-y is accepted as a synonym.
category delete additionally refuses, with exit 2, to
remove a category that still owns tag assignments unless
--allow-nonempty is also present.
media cleanup and media thumbnail-cleanup delete
data without --yes. Use --dry-run first.
pool destroy deletes cloud resources without
--yes.
Commands
Each entry lists the command form, its flags, the API endpoint it calls,
and the shape of its output. Angle brackets mark required positional
arguments; square brackets mark optional ones. Media paths are the
absolute paths stored in the library (or s3://bucket/key
for object storage roots).
Discovery
| Command | Endpoint | Description |
|---|---|---|
health | GET /health | Server status, SSE connection stats, and job counts by state. No credential required. |
stats | GET /api/stats | Library statistics and metadata coverage. Returns {"ready": false} until the first snapshot has been computed after startup. |
whoami | GET /auth/status | The user the current token authenticates as, plus publicAccess and defaultStartPath. |
help | Print the command table. |
login, whoami
See Authentication.
key
| Command | Endpoint | Output |
|---|---|---|
key create --name N [--username U] [--save] | POST /auth/keys | {status, key, id, name, username, prefix}. key is the plaintext, shown once. |
key list | GET /auth/keys | {keys: [{id, username, name, prefix, created_at, last_used_at}]} |
key revoke --id N | DELETE /auth/keys?id=N | {status: "deleted"} |
job
| Command | Endpoint | Description |
|---|---|---|
job run <task> [args...] [--field k=v]... [--wait] [--follow] [--timeout D] | POST /create | Create a job. See Job Input Syntax. Without --wait/--follow, prints {id, state: "pending"} (with -q, the id alone). |
job list [--state S] | GET /jobs/list | All jobs. --state filters client-side: pending, in_progress, paused, completed, cancelled, error. |
job get <id> | GET /jobs/list | One job object. |
job wait <id> [--timeout D] | GET /jobs/list (polled) | Block until the job is terminal. Prints the final job. Exit 3 on error, cancel, or timeout. --timeout 0 (default) waits forever. |
job logs <id> | GET /stream, GET /jobs/list (polled) | Stream a running job's stdout lines to stdout and exit when the job finishes, with the job wait exit codes. Output is not persisted by the server, so a job that has already finished is refused (exit 1). |
job cancel <id> | POST /job/{id}/cancel | Cancel a pending or running job. |
job pause <id> | POST /job/{id}/pause | Pause at the next item boundary. Completed items are kept. |
job resume <id> [--wait] [--follow] [--timeout D] | POST /job/{id}/resume | Resume a paused job. Prints {status, action, id}, or with --wait/--follow, tracks the job to the end like job run. |
job copy <id> [--wait] [--follow] [--timeout D] | POST /job/{id}/copy | Clone a job (same task, arguments, and input) into a new pending job. Prints {id, message} for the new job (with -q, the id alone), or with --wait/--follow, tracks the new job to the end like job run. |
job remove <id> | POST /job/{id}/remove | Delete the job record. |
job clear --yes | POST /jobs/clear | Delete every finished job. Running, pending, and paused jobs are kept. Prints {cleared_count, message}. |
Flags for job run, job copy, and job resume (--field is job run only):
| Flag | Description |
|---|---|
--field k=v | Send an option value verbatim as --k v, bypassing the server's token splitter. Repeatable. Use this for values containing spaces, quotes, or newlines. |
--wait | Poll until the job is terminal and print the final job object. Exit 3 on error, cancel, or timeout. |
--follow | Print the job's stdout lines to stderr while waiting. Implies --wait. The CLI subscribes to GET /stream before creating the job, buffering until the id is known, so a job that starts printing immediately loses no lines. It stops reading on the job's own terminal update event, which the server sends after the job's last line, so a task's closing summary is always shown. |
--timeout D | Maximum wait. 0 waits forever. |
Job object fields: id, command,
arguments, input, original_input,
state (pending, in_progress,
paused, completed, cancelled,
error; the last three are terminal), dependencies,
workflow_id, created_at, claimed_at,
completed_at, errored_at,
progress_done, progress_total,
output_files, source_files.
task
| Command | Endpoint | Description |
|---|---|---|
task list | GET /tasks | Every registered task with its option schema. |
task show <id> | GET /tasks | One task's {id, name, options}. |
Each option has name, label, type
(string, bool, enum,
multi-enum, number), optional
choices, default, required, and
description. An option named x is passed to
job run as --x value, or --x alone
for booleans.
workflow
| Command | Endpoint | Description |
|---|---|---|
workflow list | GET /workflows | Saved workflows. |
workflow get <id> | GET /workflows/{id} | One saved workflow with its DAG. |
workflow create --name N --dag FILE|- | POST /workflows/create | Save a workflow. See DAG Format. |
workflow update <id> [--name N] [--dag FILE|-] | PUT /workflows/{id} | Replace the name, the DAG, or both. Omitted fields keep their current value. |
workflow delete <id> --yes | DELETE /workflows/{id} | Delete a saved workflow. |
workflow run <id> [--input S] [--wait] [--timeout D] | POST /workflows/{id}/run | Instantiate the DAG as jobs. --input is injected into the root tasks. Prints {ids}, or with --wait, the final job objects. --timeout is per job. |
workflow run-adhoc --dag FILE|- [--wait] [--timeout D] | POST /workflow | Run a DAG without saving it. |
media
| Command | Endpoint | Description |
|---|---|---|
media query [flags] | POST /api/media/query | Composable predicate query. See flags below. |
media search <text> | POST /api/media/search | Substring match on descriptions. |
media similar <path> [--limit N] | GET /api/media/similar | Items visually similar to a library item, ranked. Default limit 50. |
media visual <text> [--limit N] | GET /api/media/search/visual | Text-to-image search over the embedding index. Default limit 50. |
media image-search <image-file> | POST /api/media/search/image | Reverse image search with a local file (uploaded as the request body, 32 MB cap). |
media metadata <path> | POST /api/media/metadata | Width, height, size, description, transcript, hash, and (video) duration. |
media tags <path> | POST /api/media/tags | Tags on one item with category, weight, and timestamp. |
media describe <path> (--text D | --clear) | POST /api/media/description | Set or clear the description. |
media transcript <path> [--text T | --clear] | POST /api/media/transcript | Read (no flags), set, or clear the transcript. |
media rate <path> [--elo E] [--views N] [--wins N] [--losses N] | POST /api/media/rating | Read (no flags) or set Battle Mode rating fields. Only supplied fields change. |
media thumbs <path> [--regenerate] [--cache C] [--timestamp S] | POST /api/thumbnails, POST /api/thumbnails/regenerate | List cached thumbnails, or regenerate one. --cache is thumbnail_path_100, thumbnail_path_600 (default), or thumbnail_path_1200. --timestamp selects the video frame. |
media generate <path> --type T [--field k=v]... [--wait] [--follow] [--timeout D] | POST /create | Run the metadata task for one item. --type is description, transcript, hash, dimensions, or a comma-separated list. |
media move <from> <to> [--prefix] [--dry-run] | POST /api/media/move | Re-point every database reference after a file or folder was moved on disk. Nothing on disk is touched. |
media forget <path> --yes | POST /api/media/forget | Delete every database row that names the path. The file is kept. |
media delete <path> --yes | POST /api/media/delete | Delete the item from the library. Same database effect as forget. |
Flags for media query:
| Flag | Predicate | Description |
|---|---|---|
--tag T | tag | Require this tag. Repeatable. |
--exclude-tag T | tag, exclude | Exclude this tag. Repeatable. |
--path P | path | Path substring. |
--description D | description | Description substring. |
--hash H | hash | Content hash. |
--similar PATH | similar | Rank by visual similarity to a library item. |
--visual TEXT | visual | Rank by text-to-image similarity. |
--mode AND|OR | How predicates combine. Default AND. | |
--predicates FILE|- | Raw predicate JSON array from a file or stdin. Overrides all predicate flags. Use this for category, orientation, faces, clip, face, per-predicate join, and blended nodes. The schema is documented in the API reference. | |
--limit N | Client-side cap on returned items. Default 50. |
Query results are objects with path, width,
height, elo, battles, and, when a
tag predicate drives the query, tagLabel, weight,
and timeStamp. When a visual predicate is present the array
is ordered by descending score.
Flags for media move:
| Flag | Description |
|---|---|
--prefix | Treat both arguments as folders and move every item underneath. Matching is segment-aligned: moving 2023 does not affect 2023extra. |
--dry-run | Perform the move in a transaction and roll it back. Reported counts are exactly what a real run would change. |
The move re-points the media row, tag assignments, embeddings, faces, scan markers, and battle log in one transaction. A destination that already belongs to another item returns 409 with the conflicting paths; a move never merges two items.
media cleanup
lokictl media cleanup [--dir D] [--dry-run] [--max-missing-percent N] [--skip-orphans] [--detach] [--timeout D]
Runs the cleanup task as a job and streams its log to
stderr. The task forgets media whose files no longer exist, removing
every tag, embedding, face, and battle row that names them, then sweeps
dangling sidecar rows. Files are scanned, then re-verified before
deletion. Offline volumes are skipped. The final job object is printed
to stdout with the full log in stdout.
| Flag | Default | Description |
|---|---|---|
--dir D | whole library | Restrict the scan to items under this directory. |
--dry-run | off | Report what would be forgotten without writing. |
--max-missing-percent N | 50 | Hold back any volume with more than N% of its items missing. Set to 100 to purge a volume that is gone for good. |
--skip-orphans | off | Skip the final sweep of dangling rows. |
--detach | off | Create the job and return its id without waiting. |
--timeout D | 0 | Maximum wait. |
media thumbnail-cleanup
lokictl media thumbnail-cleanup [--dir D] [--dry-run] [--db PATH]... [--no-discover] [--manifest FILE] [--detach] [--timeout D]
Runs the thumbnail-cleanup task as a job. It deletes cached
thumbnails that no library still references. Every SQLite database
found beside the configured one is treated as a library sharing the
cache, so thumbnails owned by a sibling library are kept.
| Flag | Description |
|---|---|
--dir D | Scope to thumbnails whose media path is under this folder (recursive). The log names the media path of every removed file. |
--dry-run | Report only. |
--db PATH | Add another library database to the ownership check. Repeatable. |
--no-discover | Do not auto-discover sibling databases; use only the configured one and any --db values. |
--manifest FILE | Write a TSV of every cached file and which library owns it. |
--detach | Create the job and return without waiting. |
--timeout D | Maximum wait. |
index
| Command | Endpoint | Description |
|---|---|---|
index status | GET /api/index/status | In-memory index state, active model, media total, count missing for the active model, orphaned vectors, and per-model stored-vector stats. |
index models | GET /api/index/models | Embedding model registry with active and indexed flags. |
index missing [--model M] [--limit N] | GET /api/index/missing | Paths without an embedding. --limit 0 returns the count only. Default 100, max 10000. |
index get <path> [--model M] [--vector] | GET /api/embeddings | Stored embedding rows for one item. --vector includes the float array. |
index delete <path> [--model M] --yes | DELETE /api/embeddings | Delete one item's vectors (all models unless --model). |
index prune --yes | POST /api/embeddings/prune | Delete vectors whose media row no longer exists. |
index rebuild | POST /api/index/rebuild | Rebuild the in-memory index for the active model. Large libraries need a longer global --timeout. |
index embed [paths...] [--field k=v]... [--wait] [--follow] [--timeout D] | POST /create | Alias for job run embed. |
db
| Command | Endpoint | Description |
|---|---|---|
db query "SQL" [--arg V]... [--limit N] [--timeout-ms N] | POST /api/db/query | Run one read-only statement. |
db tables | POST /api/db/query | Tables and views. |
db schema [table] | POST /api/db/query | CREATE statements for one table or all. |
| Flag | Default | Description |
|---|---|---|
--arg V | Bind value for the next ? placeholder. Repeatable, positional. Sent as a string; SQLite type affinity coerces it. | |
--limit N | 1000 | Row cap. Maximum 10000. |
--timeout-ms N | 5000 | Server-side statement timeout. Maximum 30000. |
The server accepts a single SELECT or WITH
statement and opens the connection in query_only mode.
The response is {columns, rows, row_count, truncated, elapsed_ms}
where rows is an array of arrays.
taxonomy, tag, category
| Command | Endpoint | Description |
|---|---|---|
taxonomy | GET /api/taxonomy | Every category with its tags. |
taxonomy --category C | GET /api/taxonomy/tags | One category's tags. |
taxonomy categories | GET /api/taxonomy/categories | Category list only. |
tag list [--category C] | GET /api/tags/list | Tags with media_count. |
tag create <label> --category C [--weight W] | POST /api/tags | Create a tag. |
tag delete <label> --category C --yes | DELETE /api/tags | Delete a tag and all its assignments. |
tag rename <old> <new> | POST /api/tags/rename | Rename a tag. |
tag move <label> --category C | POST /api/tags/move | Move a tag to another category. |
tag weight <label> --weight W | POST /api/tags/weight | Set a tag's sort weight. |
tag count <label> | POST /api/tags/count | Distinct media count for a tag. |
tag has <media-path> <label> --category C | GET /media/has-tag | {has_tag: bool} |
tag assign <media-path> <label> --category C [--timestamp S] | POST /api/assignments | Tag one item. --timestamp attaches the tag to a video moment. |
tag assign-bulk <label> --category C [--timestamp S] [paths... | --stdin] | POST /api/assignments | Tag many items in one request. |
tag unassign <media-path> <label> | DELETE /api/assignments | Remove a tag from one item. |
tag unassign-bulk <label> [paths... | --stdin] --yes | DELETE /api/assignments | Remove a tag from many items. |
tag assignment-weight <media-path> <label> --weight W [--timestamp S] | POST /api/assignments/weight | Set the weight of one assignment. |
tag timestamp <media-path> <label> (--from S --to S | --remove --at S) | PUT/DELETE /api/tags/timestamp | Move or remove a video tag timestamp. |
category create <label> | POST /api/categories | Create a category. |
category delete <label> [--dry-run] --yes [--allow-nonempty] | GET /api/categories/impact, then DELETE /api/categories | Delete a category with its tags, groups, and their assignments. Always previews first: --dry-run prints {status: "dry-run", wouldRemove: {tags, groups, assignments, media}} and stops. A category that owns assignments needs --allow-nonempty as well as --yes. A server that cannot preview is refused, not trusted. |
category rename <old> <new> | POST /api/categories/rename | Rename a category. |
category count <label> | GET /api/taxonomy/category-count | Distinct media count for a category. |
category grouping <label> on|off | POST /api/categories/tag-grouping | Show or hide the category's tag groups in the taxonomy panel. |
--stdin reads one path per line. Blank lines are ignored.
group
Tag groups are a visual level between a category and its tags, for
example a Series category with one group per show holding
per-season tags. Grouping changes only how the taxonomy panel lays
tags out; it never changes what a tag query matches. Every command
except group remove requires --category,
because group labels are unique per category.
| Command | Endpoint | Description |
|---|---|---|
group list --category C | GET /api/taxonomy/groups | The category's groups in display order. |
group create <label> --category C | POST /api/tag-groups | Create a group at the end of the category's list. |
group add <group> --category C [tags... | --stdin] | POST /api/tag-groups, then PUT /api/tags/group per tag | Put tags into a group, creating the group if it does not exist. A tag belongs to at most one group; adding it moves it. |
group remove [tags... | --stdin] | PUT /api/tags/group | Ungroup tags (sends an empty groupLabel). The tags stay in their category. |
group rename <old> <new> --category C | POST /api/tag-groups/rename | Rename a group; its tags follow. |
group weight <label> --category C --weight W | POST /api/tag-groups/weight | Set the sort weight that orders groups within the category. |
group hide <label> --category C | PUT /api/tag-groups/hidden | Hide the group from the taxonomy panel. Hidden groups reappear with the viewer setting Show Hidden Tag Groups. |
group show <label> --category C | PUT /api/tag-groups/hidden | Unhide the group. |
group view <label> card|list|auto --category C | PUT /api/tag-groups/view-mode | Display mode for the group's tags. auto follows the category's mode. |
group delete <label> --category C --yes | DELETE /api/tag-groups | Delete the group. Its tags stay, ungrouped. |
--stdin reads one tag label per line. To turn grouping off
for a whole category without deleting groups, use
category grouping <label> off.
pool
GPU worker pools place the local AI engines (retouch,
reshoot, segment, and the other
loki-* engines) on remote workers: loki-worker nodes
reached over HTTP, and RunPod serverless endpoints. Commands that
change placement read the engines configuration with
GET /api/engines, edit it, and write it back with
PUT /api/engines, printing the server's response. Adding a
pool while the strategy is local switches the strategy to
pool. The same settings are on the server's Config page,
GPU Engines tab.
| Command | Endpoint | Description |
|---|---|---|
pool status | GET /api/engines | Engines configuration and the live status of every target. With -o table, one row per target: target, kind, healthy, busy (running/slots), warm (engines with resident models), version, error. |
pool check | POST /api/engines/check | Probe every worker now instead of waiting for the next health check. |
pool strategy local|pool|hybrid [--fallback=true|false] | PUT /api/engines | local: this machine's GPU only (the default). pool: remote workers only. hybrid: this GPU is one more worker. --fallback=true lets pool run an engine locally when no worker is reachable; omitting the flag keeps the current value. |
pool local-only on|off | PUT /api/engines | Run every engine on this machine regardless of strategy and routes, keeping the pools configured for later. |
pool route <engine> local| | PUT /api/engines | Override the strategy for one engine, for example to keep segment local. pool:NAME pins the engine to one pool. default removes the override. Engine names are accepted with or without the loki- prefix. |
pool add-node <pool> <url> [--token T] [--name N] [--no-test] | POST /api/engines/probe, PUT /api/engines | Add a loki-worker node, creating the pool if needed. The node is probed first; --no-test skips the probe. --token is the worker's LOKI_WORKER_TOKEN. A RunPod pool cannot take nodes. |
pool add-runpod <pool> --endpoint ID [--api-key KEY] [--max-jobs N] [transfer flags] | PUT /api/engines | Add an existing RunPod serverless endpoint running the worker image as a new pool. --api-key defaults to $RUNPOD_API_KEY; --max-jobs (default 4) should match the endpoint's max workers. |
pool remove <pool> [<node-url>] | PUT /api/engines | Remove a pool, or one node of it. Cloud resources keep running; use pool destroy to delete them. |
pool warm <pool|all> <engine>... [--model fast] | POST /api/engines/warm | Download and load engines' models on a pool's workers ahead of use. --model fast warms the reshoot FastH3 variant. |
pool create runpod-pods| | RunPod REST API, then PUT /api/engines | Provision workers on RunPod and add them as a pool. Each pod gets its own generated worker token. If creating a pod fails, the pods already created are deleted. Workers take a few minutes to start and download models on first use. |
pool scale <pool> --count N [--gpu T] [--image I] [--volume-gb N] [--runpod-key K] | RunPod REST API, then PUT /api/engines | Add pods, or delete the newest, until the pool has N. Only for pools made by pool create runpod-pods. |
pool destroy <pool> [--runpod-key K] | RunPod REST API, then PUT /api/engines | Delete the pool's RunPod pods, endpoint, and template, then remove the pool. If any deletion fails the pool is kept, so the command can be retried. Pools not created by pool create are only removed. |
Flags for pool create:
| Flag | Applies to | Default | Description |
|---|---|---|---|
--runpod-key K | both | $RUNPOD_API_KEY | RunPod API key used to provision. Required. |
--image I | both | ghcr.io/stevecastle/loki-worker:latest | Worker image. |
--gpu T | both | RTX 4090, L40S, RTX 6000 Ada, RTX 5090, H100 80GB | Allowed RunPod GPU types. Repeatable or comma-separated. |
--engines E | both | all | Serve only these engines. Workers never load other engines' models, so these stay warm. Route the engines to the pool with pool route. |
--datacenter D | both | any | Allowed data centers (for example EU-RO-1). Repeatable. |
--network-volume ID | both | RunPod network volume: models download once and are shared by every worker. | |
--registry-auth ID | both | RunPod registry credential for a private image. | |
--count N | pods | 1 | Number of pods. |
--cloud C | pods | SECURE | SECURE or COMMUNITY. |
--volume-gb N | pods | 150 | Pod volume for models, when no network volume is given. |
--preload E | pods | Engines whose models load at pod start, for example retouch,segment. | |
--max-workers N | serverless | 3 | Most workers at once. Also the pool's jobs-in-flight limit. |
--min-workers N | serverless | 0 | Workers kept running. 0 scales to zero. |
--idle S | serverless | 120 | Seconds a worker, and its loaded models, stays after its last job. |
--max-job-secs S | serverless | 3600 | Execution timeout per job. |
--endpoint-key K | serverless | $RUNPOD_ENDPOINT_KEY, else --runpod-key | Key the server stores to run jobs. Use a restricted key with write access to the endpoint only. |
Transfer flags (pool add-runpod,
pool create runpod-serverless) give a serverless pool an
S3 bucket to pass files through. Without one, files up to 7 MB travel
inline in the request: stills work, video references do not.
| Flag | Description |
|---|---|
--transfer-root LABEL | Use one of the server's S3 storage roots, by label. |
--bucket B | Or a bucket of the pool's own, with the flags below. |
--s3-endpoint URL | S3 endpoint. Empty means AWS. |
--region R | Bucket region. |
--prefix P | Key prefix inside the bucket. |
--access-key K, --secret-key S | Bucket credentials. Default $AWS_ACCESS_KEY_ID and $AWS_SECRET_ACCESS_KEY. |
deps
| Command | Endpoint | Description |
|---|---|---|
deps status | GET /api/deps/status | State of every bundled binary, optional tool, and downloadable model. |
deps download <model-id> [--wait] [--timeout D] | POST /api/deps/models/{id}/download | Start a download. --wait polls status until installed. Default wait timeout 30m. |
deps verify <model-id> | POST /api/deps/models/{id}/verify | Checksum every file. Returns {id, files: {relPath: "ok" | error}}. |
deps delete <model-id> --yes | DELETE /api/deps/models/{id} | Remove a downloaded model. |
Model ids: wd-eva02-large-tagger-v3,
siglip2-base-patch16-224, dinov2-base,
yunet, sface, anime-head,
ccip, faster-whisper.
config, fs, upload
| Command | Endpoint | Description |
|---|---|---|
config get | GET /api/config | Active configuration. Secrets are redacted. |
config set --json '{...}'|@file|- | GET /api/config then POST /config | Merge fields over the current config and save. Redacted secrets and storage roots are never echoed back, so edit credentials in the web Config page. |
fs list [path] | POST /api/fs/list | Browse a storage root. Empty path lists the roots. Returns {entries, parent, roots}. |
fs scan <path> [--recursive] | POST /api/fs/scan | List media files under a directory. Returns {library, cursor}. |
upload <file>... [--dest DIR] [--no-ingest] | POST /api/upload | Multipart upload. Files land under uploads/ in the default root, or in --dest (must be inside a root). An ingest job is queued unless --no-ingest. |
api
lokictl api <METHOD> <path> [--body JSON|@file|-]
Calls any endpoint with the resolved server URL and credential
attached. path is appended to the base URL and may include
a query string. --body accepts literal JSON, @file,
or - for stdin, and is sent with
Content-Type: application/json. A JSON response is
pretty-printed; any other body is printed as-is. A non-2xx status exits 1
with the standard error object.
lokictl api GET "/api/people"lokictl api POST /api/faces/tuning --body '{"thresholdOffset": 0.02}'lokictl api DELETE "/api/faces/all?confirm=true"
Job Input Syntax
job run joins the task id and every positional argument
into one string and sends it as the input field of
POST /create. The server splits that string on spaces,
honouring double quotes, and interprets it as:
<task-id> [option tokens...] <input>
- The first token is the task id.
- The last token is the job's
input(a path, URL, or query, depending on the task). - Everything between is passed to the task as its argument list. Options are
--name valueor--flag. - A task with no input takes exactly one token.
The splitter has no escape syntax. A token containing a double quote is
rejected. Values with spaces must be quoted at the shell level so they
reach the CLI as one argument; the CLI re-quotes them. For values
containing quotes or newlines, use --field name=value, which
is sent out of band and appended as --name value without
passing through the splitter.
lokictl job run ffmpeg-scale --width 1280 "C:/vids/in.mp4" --followlokictl job run describe --apply all --field prompt="Describe the scene. Include \"mood\"." "D:/photos" --wait
Per-item tasks (describe, transcribe,
hash, dimensions, autotag,
embed, faces, process) accept a
directory, a single file, or a SQL query returning paths as the input.
Inspect the task's options with task show; the
--apply option (missing or all)
controls whether items that already have the result are reprocessed.
Workflow DAG Format
workflow create, workflow update, and
workflow run-adhoc read a JSON array of task nodes. Each
node has the following fields:
| Field | Type | Description |
|---|---|---|
id | string | Node id, unique within the DAG. Used by dependencies. |
command | string | Task id. |
arguments | string[] | Option tokens, already split. |
input | string | Job input. Root nodes receive the workflow's --input when it is given. |
dependencies | string[] | Ids of nodes that must complete first. A failed dependency cancels this node. |
pos_x, pos_y | number | Optional editor layout. Ignored at run time. |
[ {"id": "ingest", "command": "ingest", "arguments": ["--recursive"], "input": ""}, {"id": "hash", "command": "hash", "arguments": ["--apply", "missing"], "input": "", "dependencies": ["ingest"]}, {"id": "embed", "command": "embed", "arguments": [], "input": "", "dependencies": ["hash"]}, {"id": "describe", "command": "describe", "arguments": [], "input": "", "dependencies": ["hash"]}]
A file whose top-level value is an object with a dag key is
also accepted, so the output of workflow get can be fed
back to workflow create.
Recipes
Run a task and wait for the result
lokictl task show metadatalokictl job run metadata --type description --apply all "C:/pics/x.jpg" --wait --timeout 10m# exit 0 completed; exit 3 error, cancelled, or timeout
Watch a long job's output live
lokictl job run ffmpeg-scale --width 1280 "C:/vids/in.mp4" --follow# stdout lines stream to stderr; the final job JSON lands on stdoutlokictl job copy <id> --follow # re-run a job and tail it to the endlokictl job resume <id> --follow # resume a paused job and tail itlokictl job copy <id> --follow 2>job.log >job.json; echo "exit $?"
Forget media that no longer exists
lokictl media cleanup --dry-runlokictl media cleanup --dir "D:/photos/2019" --dry-runlokictl media cleanuplokictl media cleanup --max-missing-percent 100 # a volume that is gone for good
Reclaim thumbnail cache space
lokictl media thumbnail-cleanup --dry-run --manifest C:/tmp/thumbs.tsvlokictl media thumbnail-cleanup --dir "D:/photos/2019"lokictl media thumbnail-cleanup
Ask the database anything
lokictl db tableslokictl db schema media_tag_by_categorylokictl db query "SELECT hash, COUNT(*) n FROM media WHERE hash IS NOT NULL GROUP BY hash HAVING n > 1 ORDER BY n DESC" --limit 50lokictl db query "SELECT path FROM media WHERE path LIKE ?" --arg "%/vacation/%"
Search the library four ways
lokictl media query --tag sunset --exclude-tag blurry --mode ANDlokictl media visual "a red car in the snow" --limit 20lokictl media similar "C:/pics/x.jpg"lokictl media image-search "C:/downloads/some.jpg"
Blended query through raw predicates
echo '[{"type":"similar","value":"C:/pics/x.jpg","nodes":[{"kind":"text","value":"at night","weight":0.5},{"kind":"text","value":"blurry","weight":0.3,"negative":true}]},{"type":"tag","value":"portrait"}]' \ | lokictl media query --predicates - --limit 30
Keep the embedding index healthy
lokictl index statuslokictl index missing --limit 0lokictl index embed --query "SELECT path FROM media" --waitlokictl index prune --yeslokictl --timeout 10m index rebuild
Curate tags in bulk
lokictl tag list -o tablelokictl db query "SELECT path FROM media WHERE path LIKE '%/vacation/%'" -q \ | jq -r '.rows[][]' | lokictl tag assign-bulk vacation --category trips --stdinlokictl tag unassign-bulk blurry --stdin --yes < paths.txt
Write metadata directly, or have AI generate it
lokictl media describe "C:/pics/x.jpg" --text "Two dogs on a beach"lokictl media transcript "C:/vids/talk.mp4"lokictl media generate "C:/vids/talk.mp4" --type transcript --waitlokictl media rate "C:/pics/x.jpg" --elo 1600
Move files on disk and in the library
The move task moves files and re-points every library
reference in one step: tags, embeddings, faces, scan markers, battle
log, duplicate groups, the in-memory vector and face indexes, and
.vtt transcript sidecars. Moves across drives or
filesystems copy, flush, and then remove the source. Items already at
the destination count as in place, so re-running a partial move is
safe. If any item fails, the job ends in error with a list
of what was not moved.
The input is files, a folder, or a query. A single folder's media files
move into --target with their sub-folders kept; files that
are not media stay where they are, and folders the move empties are
removed. Several folders keep their own names under the target. Loose
files lose their common parent folder.
lokictl task show movelokictl job run move --target "Z:/Archive/2023" "D:/photos/2023" --follow
Files already moved by another tool
lokictl media move "C:/pics/old.jpg" "D:/archive/old.jpg"lokictl media move "C:/pics/2023" "D:/archive/2023" --prefix --dry-runlokictl media move "C:/pics/2023" "D:/archive/2023" --prefixlokictl media forget "C:/pics/gone.jpg" --yes
Run a saved workflow
lokictl workflow listlokictl workflow run <id> --input "C:/pics/new" --wait
Group tags inside a category
lokictl group add "Dirty Pair" --category Series "Dirty Pair - Season 1" "Dirty Pair - Season 2"lokictl group list --category Series -o tablelokictl group weight "Dirty Pair" --category Series --weight 1lokictl group view "Dirty Pair" list --category Serieslokictl group delete "Dirty Pair" --category Series --yes # tags stay, ungrouped
Delete a category safely
lokictl category delete Anime --dry-runlokictl category delete Anime --yes # only if it owns no assignmentslokictl category delete Anime --yes --allow-nonempty # deliberately untag its media
Run engines on RunPod
export RUNPOD_API_KEY=...lokictl pool create runpod-pods gpu --count 2 --preload retouch,segmentlokictl -o table pool statuslokictl pool route segment local # keep selection on this machinelokictl pool local-only on # everything back here, pools keptlokictl pool destroy gpu # delete the pods and the pool
Install a model
lokictl deps statuslokictl deps download siglip2-base-patch16-224 --wait
CI or automation with a scoped key
lokictl key create --name ci# store the printed key as a secret, then:LOKICTL_SERVER=https://media.example.com LOKICTL_TOKEN=lk_... lokictl health
Environment Variables
| Variable | Description |
|---|---|
LOKICTL_SERVER | Server base URL. Overrides the config file; overridden by --server. |
LOKICTL_TOKEN | API key or JWT. Overrides the config file; overridden by --token. |
LOKICTL_CONFIG_DIR | Directory holding config.json. |
LOWKEY_PORT | Read only for the auto-detected default URL when no server is configured. |
RUNPOD_API_KEY | Default for pool create/scale/destroy --runpod-key and pool add-runpod --api-key. |
RUNPOD_ENDPOINT_KEY | Default for pool create runpod-serverless --endpoint-key. |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY | Defaults for the pool transfer-bucket credentials. |
Notes and Limits
- The server streams job stdout over SSE and does not persist it, except for the cleanup tasks, whose final job object carries the log.
--followonjob runandjob copysees every line because it subscribes before the job exists;job logsandjob resume --followsee lines from the moment they attach. db queryaccepts oneSELECTorWITHstatement. Rows are capped at 1000 by default and 10000 at most;truncatedistruewhen the cap was hit.--argvalues are strings. SQLite affinity coerces them for comparisons against numeric columns.- Job input tokens containing a double quote are rejected. Use
--field. statsreturns{"ready": false}until the server has finished its first library count after startup.config setcannot write secrets or storage roots, becauseconfig getredacts them and the merge would clear them.- The CLI sends
Accept: application/jsonso that authentication failures return a 401 JSON body instead of a redirect to the login page. - The
apicommand does not add a trailing newline to non-JSON bodies.