@feralfile/cli 2.10.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +31 -23
  2. package/config.json.example +6 -1
  3. package/dist/index.js +10 -2
  4. package/dist/package.json +5 -2
  5. package/dist/src/commands/build.js +40 -0
  6. package/dist/src/commands/config.js +1 -0
  7. package/dist/src/commands/device.js +85 -40
  8. package/dist/src/commands/enrich.js +18 -23
  9. package/dist/src/commands/find.js +144 -106
  10. package/dist/src/commands/helpers/device-control.js +937 -0
  11. package/dist/src/commands/helpers/hidden-alias-help.js +37 -0
  12. package/dist/src/commands/helpers/playlist-display.js +2 -15
  13. package/dist/src/commands/helpers/prompt.js +85 -10
  14. package/dist/src/commands/play.js +162 -84
  15. package/dist/src/commands/report.js +251 -0
  16. package/dist/src/commands/setup.js +82 -7
  17. package/dist/src/commands/status.js +4 -1
  18. package/dist/src/config.js +256 -6
  19. package/dist/src/main.js +14 -5
  20. package/dist/src/utilities/control/codec.js +578 -0
  21. package/dist/src/utilities/control/control-errors.js +157 -0
  22. package/dist/src/utilities/control/controller-store.js +964 -0
  23. package/dist/src/utilities/control/jwk.js +141 -0
  24. package/dist/src/utilities/control/pairing-channel.js +52 -0
  25. package/dist/src/utilities/control/pairing.js +416 -0
  26. package/dist/src/utilities/control/registration.js +391 -0
  27. package/dist/src/utilities/control/relay-pairing-channel.js +191 -0
  28. package/dist/src/utilities/control/secure-transport.js +850 -0
  29. package/dist/src/utilities/control/strict-json.js +376 -0
  30. package/dist/src/utilities/device-lookup.js +36 -11
  31. package/dist/src/utilities/device-network.js +129 -0
  32. package/dist/src/utilities/device-upsert.js +1 -1
  33. package/dist/src/utilities/feral-file-artwork.js +2 -2
  34. package/dist/src/utilities/ff1-compatibility.js +87 -11
  35. package/dist/src/utilities/ff1-device.js +161 -78
  36. package/dist/src/utilities/ff1-relayer.js +34 -14
  37. package/dist/src/utilities/handoff-client.js +23 -9
  38. package/dist/src/utilities/index.js +270 -259
  39. package/dist/src/utilities/media-url.js +117 -0
  40. package/dist/src/utilities/neort-marketplace.js +3 -3
  41. package/dist/src/utilities/nft-indexer.js +12 -376
  42. package/dist/src/utilities/playlist-builder.js +11 -2
  43. package/dist/src/utilities/playlist-cast.js +6 -6
  44. package/dist/src/utilities/playlist-source.js +223 -34
  45. package/dist/src/utilities/raster-client.js +101 -14
  46. package/dist/src/utilities/relay-client.js +774 -0
  47. package/dist/src/utilities/ssh-access.js +192 -58
  48. package/dist/src/utilities/support-report.js +511 -0
  49. package/dist/src/utilities/temporary-channel.js +139 -0
  50. package/dist/src/utilities/token-item.js +428 -0
  51. package/dist/src/utilities/token-source-contract.js +55 -0
  52. package/dist/src/utilities/token-source-indexer.js +99 -0
  53. package/dist/src/utilities/token-source-raster.js +378 -0
  54. package/dist/src/utilities/token-source.js +48 -0
  55. package/docs/CONFIGURATION.md +331 -28
  56. package/docs/EXAMPLES.md +33 -33
  57. package/docs/PROJECT_SPEC.md +55 -36
  58. package/docs/README.md +145 -43
  59. package/package.json +5 -2
package/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ff-cli Documentation
2
2
 
3
- ff-cli is a set of deterministic commands for building DP-1 (Display Protocol 1) playlists and playing them on an Art Computer. There's no built-in chat — natural language belongs in your coding agent (Claude Code, Codex), which drives ff-cli through the `ff-control` skill. No LLM API key required. This doc covers install, config, and day‑to‑day usage.
3
+ ff-cli is a set of deterministic commands for building DP-1 (Display Protocol 1) playlists and displaying them on an Art Computer. There's no built-in chat — natural language belongs in your coding agent (Claude Code, Codex), which drives ff-cli through the `ff-control` skill. No LLM API key required. This doc covers install, config, and day‑to‑day usage.
4
4
 
5
5
  For project-level planning and future agentic work, see `./PROJECT_SPEC.md`.
6
6
 
@@ -29,12 +29,15 @@ ff-cli setup --non-interactive --key-file /run/secrets/ff-signing.key \
29
29
  ```
30
30
 
31
31
  `--key` and `--key-file` together is an error, and an empty or unreadable key file is refused rather
32
- than quietly generating a new identity for the machine. See
32
+ than quietly generating a new identity for the machine. Setup also never replaces a signing key that is
33
+ already configured: `--generate-key`, `--key`, and `--key-file` refuse when a different key is in place,
34
+ because that key owns every playlist you published (`publish --replace` and `unpublish` accept only it).
35
+ Add `--force` to replace it anyway. See
33
36
  [Where the signing key can come from](./CONFIGURATION.md#where-the-signing-key-can-come-from).
34
37
 
35
38
  See the full configuration reference here: `./CONFIGURATION.md`.
36
39
 
37
- During setup, you can pick FF1 devices to add. Use `ff-cli device add` to add more devices later, and `ff-cli device list` to see what's configured. The first device is the default for `play` commands (override with `-d`).
40
+ During setup, you can pick FF1 devices to add. Use `ff-cli device add` to add more devices later, and `ff-cli device list` to see what's configured. The first device is the default for `display` commands (override with `-d`).
38
41
 
39
42
  Manual config path:
40
43
 
@@ -52,7 +55,7 @@ ff-cli config validate
52
55
  "privateKey": "your_ed25519_private_key_hex_or_base64_here",
53
56
  "curatorName": "YOUR_CURATOR_NAME"
54
57
  },
55
- "feed": { "baseURLs": ["https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1"] },
58
+ "feed": { "baseURLs": ["https://feed.feralfile.com/api/v1"] },
56
59
  "ff1Devices": {
57
60
  "devices": [
58
61
  {
@@ -74,8 +77,8 @@ See `./CONFIGURATION.md` for environment variable mappings.
74
77
  # Resolve a URL, on-chain coords, or wallet and build a playlist
75
78
  ff-cli find https://www.artblocks.io/collection/ringers-by-dmitri-cherniak -o playlist.json
76
79
 
77
- # Build and play it on your FF1 in one step
78
- ff-cli find https://objkt.com/tokens/hicetnunc/111068 --play
80
+ # Build and display it on your FF1 in one step
81
+ ff-cli find https://objkt.com/tokens/hicetnunc/111068 --display
79
82
 
80
83
  # Build from pre-structured params (JSON file or stdin)
81
84
  ff-cli build examples/params-example.json -o playlist.json
@@ -85,28 +88,29 @@ For development in this repo:
85
88
 
86
89
  ```bash
87
90
  npm run build
88
- node dist/index.js find https://objkt.com/tokens/hicetnunc/111068 --play
91
+ node dist/index.js find https://objkt.com/tokens/hicetnunc/111068 --display
89
92
  ```
90
93
 
91
94
  If you're running from source without a build, use:
92
95
 
93
96
  ```bash
94
- npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --play
97
+ npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --display
95
98
  ```
96
99
 
97
100
  ## Two ways to build a playlist
98
101
 
99
102
  ff-cli is deterministic: no chat, no model orchestration. There are two entry points for assembling a playlist.
100
103
 
101
- 1. **`ff-cli find "<input>"`** — resolve a marketplace URL, raw `chain:contract:tokenId`, or wallet address into a playable DP-1 playlist. Add `--play` to build and play in one step. This is the fastest path for a single artwork, a collection/series, or a wallet.
104
+ 1. **`ff-cli find "<input>"`** — resolve a marketplace URL, raw `chain:contract:tokenId`, or wallet address into a DP-1 playlist ready to display. Add `--display` to build and display in one step. This is the fastest path for a single artwork, a collection/series, or a wallet.
102
105
  2. **`ff-cli build <params.json>`** — build from pre-structured `requirements` and `playlistSettings` (JSON file or stdin). Use this when you already have the contract, token IDs, feed names, or artwork ids and want explicit control over ordering, durations, and title.
103
106
 
104
107
  Both paths run the same deterministic pipeline: fetch metadata, assemble a DP-1 envelope, and validate it. Validation rejects malformed data rather than passing it through. Natural language lives in your coding agent (Claude Code, Codex) via the `ff-control` skill, which translates a request into one of these two commands.
105
108
 
106
109
  ## Commands (cheat sheet)
107
110
 
108
- - `build [params.json]` – Deterministic build from JSON or stdin
111
+ - `build [params.json]` – Deterministic build from JSON or stdin. `ff-cli build --help` lists the requirement types and settings
109
112
  - Options: `-o, --output <file>`, `-v, --verbose`
113
+ - Exits 1 when any requirement finds nothing or fails, when named `tokenIds` do not all resolve, or when the send to `playlistSettings.deviceName` fails. The playlist is still saved when anything was collected; when nothing was, no file is written
110
114
  - `validate <file-or-url>` – Validate playlist structure only
111
115
  - `verify <file-or-url>` – Validate structure and verify signatures. On failure, the CLI labels structure issues separately from signature verification. dp1-js uses `--public-key` (or a key derived from `playlist.privateKey` / `PLAYLIST_PRIVATE_KEY` when omitted) **only** for legacy flat `signature` verification; DP-1 v1.1.0 `signatures[]` envelopes are verified without relying on that argument. If deriving or normalizing key material fails, the CLI prints a warning on stderr and continues without it (legacy verification still requires a usable key when the playlist uses a flat `signature`). The derived key is emitted as PEM. Supported key forms: hex with optional `0x`, PEM, or 32-byte raw public key as hex or base64
112
116
  - `sign <file>` – Sign playlist with a DP-1 v1.1.0 multi-signature envelope (private key string is forwarded to **`dp1-js`**; same hex or base64 PKCS#8 DER forms as `playlist.privateKey` in `./CONFIGURATION.md`). The command verifies the final envelope before writing output and refuses to persist tampered or otherwise unverifiable `signatures[]`.
@@ -116,14 +120,19 @@ Both paths run the same deterministic pipeline: fetch metadata, assemble a DP-1
116
120
  - Signing **appends** by default, so repeated runs accumulate endorsements. That is only correct while the signed content is unchanged: editing a signed playlist moves bytes the earlier signatures cover, and the envelope check then refuses to write the result. `--replace-signatures` discards every existing entry — including a feed's own co-signature — and signs the document as it stands. It is the required step before `publish --replace`
117
121
  - **An in-place run refuses when it would discard a still-valid signature from another key**, since only its holder could make that signature again: write the result elsewhere with `-o, --output`, which leaves the original where it is. The refusal is about what cannot be recovered — an in-place run still proceeds when the only entries dropped are your own, or ones that no longer verify
118
122
  - `--replace-signatures` names each discarded entry by role and the last 8 characters of its `kid`, and verifies it against the document as it now stands. Your own earlier signature is replaced by the same run. Another key's entry that **still verifies** — which is every entry when the content was not edited, since the signing payload excludes `signatures` — is reported as removed; keep the previous file if you want it back. An entry that does **not** verify is reported as exactly that and no more: the command has only the document as it stands, so it cannot tell an edit from an entry that was never valid, and it does not guess. Roles are carried through as found (`agent`, `institution` and `licensor` are as valid as `curator`), and a flat legacy `signature` is reported as not checkable, since it carries no key or role at all
119
- - `play <source>` – Play a playlist file, playlist URL, or media URL on an FF1 device (runs `verify` before sending; only the CLI-synthesized media URL fallback is auto-signed when a signing key is configured; use `--skip-verify` to bypass the gate)
120
- - Options: `-d, --device <name>`, `--skip-verify` (skip signature verification; not recommended)
121
- - `find <input>` – Resolve a marketplace URL, raw `chain:contract:tokenId`, or wallet address into a playable DP-1 playlist
123
+ - `display <source>` – Display a playlist file, playlist URL, media URL, or web page on an FF1 device (`play` still works for existing scripts; runs `verify` before sending; only the CLI-synthesized media/web URL fallback is auto-signed when a signing key is configured; use `--skip-verify` to bypass the gate)
124
+ - Any URL that does not load as a playlist is sent as a single item: a media file displays as media, and anything else displays as a web page
125
+ - A URL that answers 404 or 410, a host that does not resolve, and a `.json` URL that does not load as a playlist are refused, so a missing page is never cast as an error page. `--web` checks the first two as well. Other failures to read the URL (a 403 from bot protection, a 5xx, a timeout) still send it, with a note, because the device fetches it itself
126
+ - Marketplace links (Art Blocks, OpenSea, objkt, Feral File, …) are sent to `find`, which resolves the artwork. `--web` displays the page itself instead
127
+ - Options: `-d, --device <name>`, `--web` (display the URL as a web page, skipping the marketplace redirect), `--skip-verify` (skip signature verification; not recommended)
128
+ - `find <input>` – Resolve a marketplace URL, raw `chain:contract:tokenId`, or wallet address into a DP-1 playlist ready to display
122
129
  - Sources: Art Blocks, Objkt, fxhash, OpenSea, SuperRare, Feral File, Neort, Verse, raw on-chain coordinates, and wallet addresses
123
130
  - OpenSea forms: `/assets/ethereum/{contract}/{tokenId}`, `/item/ethereum/{contract}/{tokenId}`, and `/collection/{slug}` (Ethereum collections; the slug is resolved from the public collection page, no API key needed)
124
131
  - Verse forms: `/items/ethereum/{contract}/{tokenId}` and `/series/{slug}`
125
- - Options: `-o, --output <path>`, `-l, --limit <n>`, `-p, --play`, `--no-play` (never cast; combine with `-y` to build and save without playing), `-d, --device <name>`, `--publish`, `-s, --server <index>`, `-y, --yes` (skip prompts; without `-o` or `--publish` it plays the playlist on a device, so add `--no-play` to skip casting), `--skip-verify`
126
- - `enrich <file>` – Add missing artist, title, and thumbnail metadata to a playlist that already exists. Resolves each item by its `provenance.contract` and writes an inline Ref Manifest; `source`, `duration`, `title`, and `id` are never touched. Drops the signature when it changes anything, so re-sign afterwards
132
+ - Options: `-o, --output <path>`, `-l, --limit <n>`, `--display`, `--no-display` (never cast; combine with `-y` to build and save without displaying; `-p`, `--play` and `--no-play` still work for existing scripts), `-d, --device <name>`, `--publish`, `-s, --server <index>`, `-y, --yes` (skip prompts; without `-o` or `--publish` it displays the playlist on a device, so add `--no-display` to skip casting), `--skip-verify`
133
+ - A wallet address picks one work by the artist at that address. For what a wallet owns, use `build` with a `query_address` requirement
134
+ - Without a terminal, pass `-y`: a question with no terminal to answer it fails with exit 1 and names the flag, rather than waiting or exiting 0 with nothing built
135
+ - `enrich <file>` – Add missing artist, title, and thumbnail metadata to a playlist that already exists. Resolves each item by its `provenance.contract` and writes an inline Ref Manifest; `source`, `duration`, `title`, and `id` are never touched. Drops the signature when it changes anything, so re-sign afterwards. With the opt-in Raster token source no still image is supplied, so `enrich` adds artist and description but no thumbnail; thumbnails come from the default indexer source
127
136
  - DP-1 `evm` names a chain family rather than a network, so those items are skipped unless `--assume-ethereum` asserts which one they are. Getting that wrong attaches another artwork's metadata
128
137
  - Options: `-o, --output <file>`, `--force` (replace existing manifests), `--assume-ethereum`, `-v, --verbose`
129
138
  - `publish <file>` – Publish a playlist to a feed server (runs `verify` before upload and rejects unsigned or broken playlists)
@@ -133,12 +142,19 @@ Both paths run the same deterministic pipeline: fetch metadata, assemble a DP-1
133
142
  - Options: `-s, --server <index>`, `-o, --output <file>`. There is no `--key`: a fetch is a plain read and signs nothing
134
143
  - `unpublish <id-or-url>` – Delete a playlist from a feed server. Accepts a playlist id, a slug, or a feed URL. Requires the signing key to be an owner of the stored playlist, and confirms before deleting
135
144
  - Options: `-s, --server <index>`, `-y, --yes` (skip the confirmation), `-k, --key <privateKey>` (signs the delete authorization), `--key-file <path>` (the same key, read from a file)
145
+ - `report` – Send a problem report privately to Feral File support and print its reference, `Support [<id>]`. The text comes from `--message` or `--file`, or from stdin when neither is given
146
+ - Options: `--message <text>`, `--file <path>`, `--email <address>` (where support may reply: a bare ASCII address such as `name@example.com`, no display name or angle brackets), `-d, --device <name>` (adds the Art Computer's device ID, FF OS version and branch, and asks it to upload its logs under the same reference), `--dry-run` (print the JSON and send nothing)
147
+ - Exits 1 only when the report did not reach support. See [Report a problem to Feral File support](#report-a-problem-to-feral-file-support)
136
148
  - `ssh <enable|disable>` – Manage SSH access on an FF1 device
137
149
  - Options: `-d, --device <name>`, `--pubkey <path>`, `--ttl <duration>`
138
150
  - `device list` – List all configured FF1 devices
139
151
  - `device add` – Add a device with mDNS or manual `--host`, `--name`, and `--id`
140
152
  - Options: `--host <host>`, `--name <name>`, `--id <id>`
141
- - `device pair [name]` – Securely receive relayer access from Feral File mobile
153
+ - `device pair [name]` – Pair ff-cli with an FF1: on FF OS with secure control, show a QR code that an owner scans with "Add controller" in the Feral File app, then enroll with the role the owner picked (needs internet; the FF1 must already have an owner); on FF OS without secure control, display a one-time code for "Pair with ff-cli" in the app and store the relayer access it sends
154
+ - Options: `--label <label>`, `--timeout <seconds>` (10–600, default 300), `--resume` (finish a pairing an owner already approved), `-y, --yes` (move a corrupt controller file aside without asking), `--topic-handoff` (force the one-time-code path, e.g. when the FF1 is not on this LAN)
155
+ - `device controllers [name]` – List an FF1's paired controllers, Play sites with a display grant, and pending requests (owner only)
156
+ - `device revoke <controller-id> [name]` – Revoke a controller or deny its pending request (owner only)
157
+ - `device unpair [name]` – Revoke ff-cli's own access on the FF1 (best effort), then delete its controller keys (always asks first; `-y, --yes` skips)
142
158
  - `device rename <name> <new-name>` – Rename a configured FF1 device (host, API key, and default status are untouched)
143
159
  - `device remove <name>` – Remove a configured FF1 device
144
160
  - `device default <name>` – Set the default FF1 device (used when `-d` is omitted)
@@ -148,19 +164,20 @@ Both paths run the same deterministic pipeline: fetch metadata, assemble a DP-1
148
164
 
149
165
  ### Find an artwork
150
166
 
151
- `ff-cli find` resolves a URL, raw `chain:contract:tokenId`, or wallet address into a playable DP-1 playlist.
167
+ `ff-cli find` resolves a URL, raw `chain:contract:tokenId`, or wallet address into a DP-1 playlist ready to display.
152
168
 
153
169
  ```bash
154
- # Paste a URL and play it on your FF1
155
- ff-cli find https://www.artblocks.io/collection/ringers-by-dmitri-cherniak --play
170
+ # Paste a URL and display it on your FF1
171
+ ff-cli find https://www.artblocks.io/collection/ringers-by-dmitri-cherniak --display
156
172
 
157
173
  # Tezos / hic et nunc via Objkt (the alias resolves to a KT1 contract)
158
- ff-cli find https://objkt.com/tokens/hicetnunc/111068 --play
174
+ ff-cli find https://objkt.com/tokens/hicetnunc/111068 --display
159
175
 
160
- # Save without playing
176
+ # Save without displaying
161
177
  ff-cli find ethereum:0xababababab20053426ad1c782de9ea8444358070:5001410 -o send-receive.json
162
178
 
163
- # Wallet address (owner lookup); cap the number of tokens with --limit
179
+ # Wallet address: one work by the artist at that address; cap the number of tokens with --limit
180
+ # (for what a wallet owns, use build with a query_address requirement)
164
181
  ff-cli find 0xaeE022552B539dB18297D7481b6D547C622488B3 -l 10 -o wallet.json
165
182
 
166
183
  # Build and publish to a configured feed server
@@ -178,7 +195,7 @@ cat params.json | ff-cli build -o playlist.json
178
195
 
179
196
  `params.json` should include `requirements` and optional `playlistSettings`. See `examples/params-example.json`. Each requirement has a `type`:
180
197
 
181
- - `build_playlist` — `blockchain`, `contractAddress`, `tokenIds`, optional `quantity`
198
+ - `build_playlist` — `blockchain`, `contractAddress`, `tokenIds`, optional `quantity`. Omitting `tokenIds` asks for random tokens from the contract, which only the default indexer token source supports.
182
199
  - `feral_file_artwork` — `artworkId` (a Feral File public artwork id or `/exhibitions/artwork/{id}` URL)
183
200
  - `query_address` — `ownerAddress`, optional `quantity` (random selection)
184
201
  - `fetch_feed` — `playlistName`, `quantity`
@@ -211,8 +228,8 @@ copied from another document, or written before inline manifests existed has no
211
228
  and an empty tile in the app's grid — the app can draw a thumbnail from an
212
229
  image, video, or SVG source, but not from a live HTML work.
213
230
 
214
- `ff-cli enrich` fills that in from the indexer, keyed on each item's
215
- `provenance.contract`:
231
+ `ff-cli enrich` fills that in from the configured token source (the Feral
232
+ File indexer by default), keyed on each item's `provenance.contract`:
216
233
 
217
234
  ```bash
218
235
  ff-cli enrich playlist.json --assume-ethereum
@@ -222,8 +239,8 @@ ff-cli enrich playlist.json --assume-ethereum --force # replace existing manif
222
239
 
223
240
  `--assume-ethereum` is needed for most playlists. DP-1 records an EVM
224
241
  coordinate as `chain: "evm"`, which names Ethereum, Polygon, Arbitrum,
225
- Optimism, Base and Zora alike, and the indexer's answer collapses back to the
226
- same word. Enrichment will not pick one for you, because the wrong member
242
+ Optimism, Base and Zora alike, and the token source's answer collapses back to
243
+ the same word. Enrichment will not pick one for you, because the wrong member
227
244
  returns a different artwork's artist and still — into a document you then sign.
228
245
 
229
246
  It writes `inlineManifest` and nothing else. Items with no
@@ -232,7 +249,7 @@ reported rather than guessed at, and items carrying an external `ref` are left
232
249
  alone because `ref` outranks an inline manifest at the device. The playlist is
233
250
  validated before and after; on failure the file is left untouched. Enriching
234
251
  changes the document, so the signature is removed and the playlist must be
235
- re-signed before playing:
252
+ re-signed before displaying:
236
253
 
237
254
  ```bash
238
255
  ff-cli enrich playlist.json --assume-ethereum && ff-cli sign playlist.json
@@ -241,7 +258,7 @@ ff-cli enrich playlist.json --assume-ethereum && ff-cli sign playlist.json
241
258
  That role is fine for casting. Use `ff-cli sign playlist.json -r curator` if the enriched playlist is
242
259
  also going to a feed.
243
260
 
244
- ### Validate, sign, and play
261
+ ### Validate, sign, and display
245
262
 
246
263
  ```bash
247
264
  # Optional explicit validation (build flows already validate)
@@ -250,28 +267,43 @@ npm run dev -- validate playlist.json
250
267
  # Sign (uses key and role from config, or overrides via --key / --role)
251
268
  npm run dev -- sign playlist.json -o signed.json
252
269
 
253
- # Play on configured default device (verifies playlists; only media URL fallbacks may be auto-signed)
254
- npm run dev -- play playlist.json
270
+ # Display on configured default device (verifies playlists; only media URL fallbacks may be auto-signed)
271
+ npm run dev -- display playlist.json
255
272
 
256
- # Play on a specific named device
257
- npm run dev -- play playlist.json -d "Living Room Display"
273
+ # Display on a specific named device
274
+ npm run dev -- display playlist.json -d "Living Room Display"
258
275
 
259
- # The play path performs a compatibility preflight check against the target FF1.
276
+ # The display path performs a compatibility preflight check against the target FF1.
260
277
  # If the device reports an unsupported FF1 OS version, the command fails with
261
278
  # a clear version message before any cast request is sent.
262
279
  # It also retries transient local-network errors (for example intermittent
263
280
  # mDNS/Wi-Fi resolver failures) with a short backoff before returning a final error.
264
281
 
265
- # Play a hosted DP-1 playlist
266
- npm run dev -- play "https://cdn.example.com/playlist.json"
282
+ # Display a hosted DP-1 playlist
283
+ npm run dev -- display "https://cdn.example.com/playlist.json"
267
284
 
268
- # Play a media URL directly
269
- npm run dev -- play "https://example.com/video.mp4"
285
+ # Display a media URL or any web page directly
286
+ npm run dev -- display "https://example.com/video.mp4"
287
+ npm run dev -- display "https://example.com/"
288
+
289
+ # Display a page on a marketplace site as a page (display would otherwise send it to find)
290
+ npm run dev -- display --web "https://feralfile.com/"
270
291
 
271
292
  # Skip signature verification only if you must send a non-conformant payload (not recommended)
272
- npm run dev -- play playlist.json --skip-verify
293
+ npm run dev -- display playlist.json --skip-verify
273
294
  ```
274
295
 
296
+ ### Owner remote maintenance
297
+
298
+ FFOS images containing `feral-tailscale` support opt-in persistent owner SSH on
299
+ port 2222 through Tailscale. It requires the owner's authorized computer and SSH
300
+ key and remains available independently of controld. The procedure checks for
301
+ a complete installation. If support is absent, check the normal firmware release
302
+ channel; enrollment waits until a version containing it is published and installed. Follow the [owner-access procedure](https://github.com/feral-file/ffos/blob/36e25cec84505271a7232d6936bb46a19fab5cfd/docs/OWNER_TAILSCALE.md)
303
+ for enrollment, diagnostics, revocation and validation. The normal `ff-cli ssh`
304
+ commands below continue to manage temporary support SSH on port 22; disabling
305
+ support SSH does not revoke persistent owner access.
306
+
275
307
  ### SSH access
276
308
 
277
309
  ```bash
@@ -281,14 +313,52 @@ ff-cli ssh enable --pubkey ~/.ssh/id_ed25519.pub --ttl 30m -d "Living Room Displ
281
313
  # Disable SSH access
282
314
  ff-cli ssh disable -d "Living Room Display"
283
315
 
284
- # `ff-cli ssh` also performs the same FF1 OS compatibility preflight used by `play`.
316
+ # `ff-cli ssh` also performs the same FF1 OS compatibility preflight used by `display`.
317
+ ```
318
+
319
+ On newer FF OS, a claimed FF1 only accepts `ff-cli ssh` from a paired CLI: run
320
+ `ff-cli device pair "Living Room Display"` once (an owner scans the code with
321
+ "Add controller" in the Feral File app) before `ff-cli ssh enable|disable`. Unclaimed devices and older
322
+ FF OS do not require pairing. On FF OS with secure control, `ssh` needs the
323
+ **owner** role; a controller grant gets "This command needs owner access".
324
+
325
+ ### Report a problem to Feral File support
326
+
327
+ ```bash
328
+ # Report what went wrong, with the Art Computer's logs
329
+ ff-cli report -d "Living Room Display" --message "The work displays black on the Art Computer; in a desktop browser it renders."
330
+
331
+ # From a file, with an address support can reply to
332
+ ff-cli report --file report.txt --email you@example.com
333
+
334
+ # Print exactly what would be sent, and send nothing
335
+ ff-cli report -d "Living Room Display" --file report.txt --dry-run
285
336
  ```
286
337
 
338
+ The report goes privately to Feral File support, the same pipeline the app's
339
+ Support & Feedback feeds, and ff-cli prints its reference in the app's form,
340
+ `Support [<id>]`. It carries the report text, `--email` when given, and the
341
+ ff-cli, OS and Node versions; nothing else leaves the computer. A report that
342
+ contains the configured signing key is refused.
343
+
344
+ With `-d`, the report also carries the Art Computer's device ID, FF OS version
345
+ and branch from its `/api/status`, and once the report is in, ff-cli asks that
346
+ Art Computer to upload its logs under the same reference, with a short-lived
347
+ key that works for that one report only. When ff-cli is paired with the Art
348
+ Computer under secure control, the request is a sealed envelope over the LAN,
349
+ or through the relayer when the LAN cannot reach it; otherwise it is a plain
350
+ LAN request. The Art Computer takes it at once and uploads in the background,
351
+ so it needs internet. On FF OS with secure control the request needs the **owner** role;
352
+ a controller grant gets "This command needs owner access". A log request the
353
+ Art Computer does not take prints a line and leaves the exit status at 0: the
354
+ report itself reached support. The exit status is 1 only when the report did
355
+ not, and the message says what to do next.
356
+
287
357
  ### Publish to feed server
288
358
 
289
359
  ```bash
290
- # Publish to first configured feed server
291
- npm run dev -- publish playlist.json
360
+ # Publish to the first configured feed server (-s is required when more than one is configured)
361
+ npm run dev -- publish playlist.json -s 0
292
362
 
293
363
  # Publish to specific server (if multiple configured)
294
364
  npm run dev -- publish playlist.json -s 0
@@ -432,6 +502,38 @@ ff-cli device remove kitchen
432
502
  ff-cli device default office
433
503
  ```
434
504
 
505
+ ### Pair ff-cli as a controller (secure control)
506
+
507
+ Current FF OS only accepts commands from controllers an owner added. Pair once
508
+ per FF1:
509
+
510
+ ```bash
511
+ ff-cli device pair office
512
+ # Add ff-cli to office
513
+ # On an owner's phone, open this FF1 in the Feral File app, choose "Add controller", and scan this code:
514
+ # ...QR code, and the same text to paste...
515
+ # ✓ An owner added ff-cli to Office FF1 as controller. Finishing the pairing...
516
+ # ✓ ff-cli is paired with office as controller.
517
+
518
+ ff-cli device pair office --resume # finish a pairing an owner already approved
519
+ ff-cli device controllers office # owner only: who can control this FF1
520
+ ff-cli device revoke <controller-id> office
521
+ ff-cli device unpair office # revoke this ff-cli on the FF1, delete its keys
522
+ ```
523
+
524
+ The owner's app sends its answer over the Feral File relay, so pairing needs an
525
+ internet connection, and the FF1 must already have an owner: ff-cli cannot be
526
+ an FF1's first owner. ff-cli accepts only an answer bound to the code it showed
527
+ and to the configured FF1. If nobody answers before `--timeout`, the pairing is
528
+ not completed; run it again for a new code (it keeps the same controller keys).
529
+ FF1s without secure control pair with a one-time code instead ("Pair with
530
+ ff-cli" in the app); `--topic-handoff` forces that path. After pairing, `display`, `ssh` and the version preflight are sent encrypted and
531
+ signed; the FF1 decides what each controller may do. `owner` covers every
532
+ command; `controller` covers displaying art and status but not `ssh` or controller
533
+ management. Keys are stored in `~/.config/ff-cli/controllers/<device ID>.json`
534
+ (mode 0600). See `./CONFIGURATION.md#secure-control-pairing` for the file,
535
+ roles, and error messages.
536
+
435
537
  Setup preserves existing devices when adding new ones. See selection rules and examples in `./CONFIGURATION.md`.
436
538
 
437
539
  ### Playlist signing (optional)
@@ -442,7 +544,7 @@ Setup preserves existing devices when adding new ones. See selection rules and e
442
544
 
443
545
  ## Constraints
444
546
 
445
- - Max 20 items total across all requirements
547
+ - Per-requirement caps: `build_playlist` up to 20 with `tokenIds` (100 without), `query_address` up to 100 when `quantity` is a number (omitted or `"all"` takes the whole wallet), `fetch_feed` up to 20. Requirements add up; there is no total cap beyond DP-1's 1024 items
446
548
  - Per-source caps enforced in utilities
447
549
  - Per-item timing: when no duration is given, video/audio items carry **no** `duration` and `display.loop: false`, so the player advances at end-of-stream (DP-1 §4.1) — the media plays its natural length. Static items use `defaultDuration` (10s unless configured). An explicit duration always wins.
448
550
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@feralfile/cli",
3
- "version": "2.10.1",
4
- "description": "CLI for building, signing, and playing DP-1 playlists of digital art on Feral File devices",
3
+ "version": "4.0.0",
4
+ "description": "CLI for building, signing, and displaying DP-1 playlists of digital art on Feral File devices",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/feral-file/ff-cli.git"
@@ -68,10 +68,13 @@
68
68
  "dp1-js": "2.4.1",
69
69
  "ff1": "^1.0.0",
70
70
  "fuzzysort": "^3.1.0",
71
+ "jose": "6.2.12",
72
+ "qrcode-terminal": "0.12.0",
71
73
  "viem": "^2.56.3"
72
74
  },
73
75
  "devDependencies": {
74
76
  "@types/node": "^24.7.1",
77
+ "@types/qrcode-terminal": "0.12.2",
75
78
  "@typescript-eslint/eslint-plugin": "^8.46.0",
76
79
  "@typescript-eslint/parser": "^8.46.0",
77
80
  "esbuild": "^0.25.10",