@feralfile/cli 3.0.0 → 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 (45) hide show
  1. package/README.md +20 -17
  2. package/config.json.example +1 -1
  3. package/dist/index.js +10 -2
  4. package/dist/package.json +5 -2
  5. package/dist/src/commands/build.js +1 -1
  6. package/dist/src/commands/device.js +84 -39
  7. package/dist/src/commands/enrich.js +2 -2
  8. package/dist/src/commands/find.js +71 -37
  9. package/dist/src/commands/helpers/device-control.js +937 -0
  10. package/dist/src/commands/helpers/hidden-alias-help.js +37 -0
  11. package/dist/src/commands/helpers/playlist-display.js +2 -15
  12. package/dist/src/commands/play.js +39 -26
  13. package/dist/src/commands/report.js +251 -0
  14. package/dist/src/commands/setup.js +1 -1
  15. package/dist/src/config.js +162 -6
  16. package/dist/src/utilities/control/codec.js +578 -0
  17. package/dist/src/utilities/control/control-errors.js +157 -0
  18. package/dist/src/utilities/control/controller-store.js +964 -0
  19. package/dist/src/utilities/control/jwk.js +141 -0
  20. package/dist/src/utilities/control/pairing-channel.js +52 -0
  21. package/dist/src/utilities/control/pairing.js +416 -0
  22. package/dist/src/utilities/control/registration.js +391 -0
  23. package/dist/src/utilities/control/relay-pairing-channel.js +191 -0
  24. package/dist/src/utilities/control/secure-transport.js +850 -0
  25. package/dist/src/utilities/control/strict-json.js +376 -0
  26. package/dist/src/utilities/device-network.js +129 -0
  27. package/dist/src/utilities/device-upsert.js +1 -1
  28. package/dist/src/utilities/ff1-compatibility.js +87 -11
  29. package/dist/src/utilities/ff1-device.js +161 -78
  30. package/dist/src/utilities/ff1-relayer.js +34 -14
  31. package/dist/src/utilities/handoff-client.js +23 -9
  32. package/dist/src/utilities/neort-marketplace.js +3 -3
  33. package/dist/src/utilities/playlist-builder.js +3 -3
  34. package/dist/src/utilities/playlist-cast.js +6 -6
  35. package/dist/src/utilities/playlist-source.js +7 -7
  36. package/dist/src/utilities/raster-client.js +1 -1
  37. package/dist/src/utilities/relay-client.js +774 -0
  38. package/dist/src/utilities/ssh-access.js +192 -58
  39. package/dist/src/utilities/support-report.js +511 -0
  40. package/dist/src/utilities/temporary-channel.js +139 -0
  41. package/docs/CONFIGURATION.md +270 -28
  42. package/docs/EXAMPLES.md +31 -31
  43. package/docs/PROJECT_SPEC.md +32 -19
  44. package/docs/README.md +113 -36
  45. package/package.json +5 -2
@@ -15,7 +15,7 @@ It is derived from the behavior and interfaces implemented in this repository as
15
15
  - Project: `ff-cli`
16
16
  - Type: Node.js CLI
17
17
  - System role: a control and integration surface for FF1 and DP-1 workflows
18
- - Primary purpose: turn resolved sources or structured parameters into valid DP-1 playlists deterministically, then validate them and optionally sign, publish, and play them through FF1 and feed paths
18
+ - Primary purpose: turn resolved sources or structured parameters into valid DP-1 playlists deterministically, then validate them and optionally sign, publish, and display them through FF1 and feed paths
19
19
 
20
20
  There is no built-in natural-language interface and no model orchestration. Natural language belongs in the user's coding agent (Claude Code, Codex), which drives ff-cli's deterministic commands via the `ff-control` skill. ff-cli requires no LLM API key.
21
21
 
@@ -25,11 +25,11 @@ In the Feral File architecture bands, `ff-cli` sits primarily in the presentatio
25
25
 
26
26
  The CLI supports the broader Feral File goal of making it effortless to live with digital art every day.
27
27
 
28
- Its value is reducing friction in the publish-to-play path:
28
+ Its value is reducing friction in the publish-to-display path:
29
29
 
30
30
  - assemble playlists quickly
31
31
  - keep outputs DP-1 conformant
32
- - make FF1 playback and feed publishing easier to exercise
32
+ - make displaying on FF1 and feed publishing easier to exercise
33
33
  - provide deterministic tooling that agents and scripts can drive reliably
34
34
 
35
35
  The CLI should strengthen the Gold Path, not invent a parallel product model.
@@ -39,7 +39,7 @@ The CLI should strengthen the Gold Path, not invent a parallel product model.
39
39
  Current likely users:
40
40
 
41
41
  - internal engineers working on FF1, DP-1, feed, and device flows
42
- - operators and launch teammates validating publish-to-play behavior
42
+ - operators and launch teammates validating publish-to-display behavior
43
43
  - advanced users or partners building and testing playlists
44
44
  - developers using the CLI as a reference implementation for DP-1 and FF1 command flows
45
45
 
@@ -50,7 +50,7 @@ Primary use cases:
50
50
  - validate or verify a local or hosted DP-1 playlist
51
51
  - sign a playlist with an Ed25519 key
52
52
  - publish a validated playlist to a configured feed server
53
- - play a playlist or direct media URL on a configured FF1 device
53
+ - display a playlist or direct media URL on a configured FF1 device
54
54
  - manage local config and FF1 SSH access
55
55
  - exercise compatibility checks against FF1 OS versions before risky commands
56
56
 
@@ -73,11 +73,11 @@ Use these terms consistently:
73
73
 
74
74
  The current code and internal context imply these practical goals:
75
75
 
76
- - Make playlist creation and playback testing fast enough for daily use.
76
+ - Make playlist creation and display testing fast enough for daily use.
77
77
  - Keep the path from intent to valid DP-1 output deterministic and inspectable.
78
78
  - Preserve openness by relying on DP-1 as the compatibility layer instead of a CLI-specific format.
79
- - Support FF1 as the reference playback target without making correctness depend on proprietary-only infrastructure.
80
- - Serve as a reference surface for publish, verify, and play flows used elsewhere in the Feral File stack.
79
+ - Support FF1 as the reference display target without making correctness depend on proprietary-only infrastructure.
80
+ - Serve as a reference surface for publish, verify, and display flows used elsewhere in the Feral File stack.
81
81
 
82
82
  ### Current behavior
83
83
 
@@ -103,16 +103,22 @@ Based on the code today, the CLI is responsible for:
103
103
 
104
104
  - loading configuration from `config.json`, `.env`, and defaults, with `config.json` taking precedence
105
105
  - resolving marketplace URLs, on-chain coordinates, and wallet addresses into playlists (`find`)
106
- - running feed fetches, address queries, contract-based NFT queries, domain resolution, playlist building, verification, publishing, and playback
106
+ - running feed fetches, address queries, contract-based NFT queries, domain resolution, playlist building, verification, publishing, and display
107
107
  - supporting a deterministic build path from structured JSON (`build`)
108
108
  - building DP-1 playlist envelopes from NFT metadata or direct media URLs via `dp1-js` document and leaf builders (so constructed playlists stay schema-conformant), including inline Ref Manifests on items that carry token-source metadata
109
109
  - validating and verifying playlist structure and signatures
110
110
  - signing playlists when a private key is configured
111
111
  - publishing validated playlists to configured feed servers
112
- - discovering configured FF1 devices and sending playlists or direct media playback requests
113
- - pairing a device's signed relayer topic through a short-lived end-to-end
114
- encrypted mobile handoff, authenticating the CLI key with a user-compared
115
- security check, and storing it in the operating system credential vault
112
+ - discovering configured FF1 devices and sending playlists or direct media display requests
113
+ - pairing as an FF1 controller through owner-initiated registration: the CLI
114
+ shows a QR of its public keys and a one-time secret, an owner adds it in the
115
+ Feral File app, and the CLI pins the FF1 profile only from the owner's sealed
116
+ answer over the relay pairing channel, then enrolls (any relayer topic the
117
+ answer carries goes to the operating system credential vault)
118
+ - pairing an FF1 without secure control by its signed relayer topic through
119
+ a short-lived end-to-end encrypted mobile handoff, authenticating the CLI key
120
+ with a user-compared security check, and storing it in the operating system
121
+ credential vault
116
122
  - falling back from unreachable LAN delivery to the shared FF1 relayer with an
117
123
  explicit terminal notice, so sandboxed and cross-subnet control remains
118
124
  observable to the mobile app
@@ -181,7 +187,7 @@ substituted for a chain the row already named.
181
187
 
182
188
  ### Delivery
183
189
 
184
- - `play` (handles playlist files, playlist URLs, and media URLs)
190
+ - `display` (handles playlist files, playlist URLs, and media URLs; `play` is a hidden alias kept for existing scripts and agents)
185
191
  - `fetch`
186
192
  - `publish` (`--replace` sends an owner-bound replacement instead of a create)
187
193
  - `unpublish`
@@ -252,6 +258,13 @@ replacement.
252
258
 
253
259
  - `ssh enable|disable`
254
260
 
261
+ ### Support
262
+
263
+ - `report` (posts a problem report to Feral File's support-logs, which mints the `Support [<id>]`
264
+ reference and a token scoped to that one report; with `-d`, the Art Computer is then asked to upload
265
+ its logs under the same reference with its existing `uploadLogs` command. ff-cli holds no support
266
+ credential, since a key in a public package is public)
267
+
255
268
  ### Inline Ref Manifests
256
269
 
257
270
  Playlist items carry a full Ref Manifest inline (DP-1 Playlist Extension §3.6)
@@ -417,7 +430,7 @@ and nothing else. `enrich` is the repair path for those.
417
430
  - The playlist is validated before lookup and the enriched candidate is
418
431
  validated before writing. On either failure the diagnostics are printed and
419
432
  the file is left untouched, rather than replaced with a document that sign,
420
- publish, and play would each reject later.
433
+ publish, and display would each reject later.
421
434
 
422
435
  ## Deterministic-first behavior
423
436
 
@@ -494,16 +507,16 @@ Important constraints from the broader FF system:
494
507
  ### Current behavior
495
508
 
496
509
  - reliability matters more than novelty
497
- - the publish-to-play path should stay simple and testable
498
- - the CLI should help prove the path from canonical JSON to FF1 playback
510
+ - the publish-to-display path should stay simple and testable
511
+ - the CLI should help prove the path from canonical JSON to FF1 display
499
512
  - compatibility checks fail clearly when incompatibility is confirmed
500
513
  - if FF1 OS version cannot be determined during preflight, the CLI currently proceeds and logs a warning instead of hard-blocking the command
501
514
 
502
515
  ### Design direction
503
516
 
504
517
  - reliability matters more than novelty
505
- - the publish-to-play path should stay simple and testable
506
- - the CLI should help prove the path from canonical JSON to FF1 playback
518
+ - the publish-to-display path should stay simple and testable
519
+ - the CLI should help prove the path from canonical JSON to FF1 display
507
520
  - compatibility messaging should remain explicit enough that operators can distinguish confirmed incompatibility from uncertain device state
508
521
 
509
522
  ## Code and design constraints
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
 
@@ -37,7 +37,7 @@ Add `--force` to replace it anyway. See
37
37
 
38
38
  See the full configuration reference here: `./CONFIGURATION.md`.
39
39
 
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 `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`).
41
41
 
42
42
  Manual config path:
43
43
 
@@ -55,7 +55,7 @@ ff-cli config validate
55
55
  "privateKey": "your_ed25519_private_key_hex_or_base64_here",
56
56
  "curatorName": "YOUR_CURATOR_NAME"
57
57
  },
58
- "feed": { "baseURLs": ["https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1"] },
58
+ "feed": { "baseURLs": ["https://feed.feralfile.com/api/v1"] },
59
59
  "ff1Devices": {
60
60
  "devices": [
61
61
  {
@@ -77,8 +77,8 @@ See `./CONFIGURATION.md` for environment variable mappings.
77
77
  # Resolve a URL, on-chain coords, or wallet and build a playlist
78
78
  ff-cli find https://www.artblocks.io/collection/ringers-by-dmitri-cherniak -o playlist.json
79
79
 
80
- # Build and play it on your FF1 in one step
81
- 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
82
82
 
83
83
  # Build from pre-structured params (JSON file or stdin)
84
84
  ff-cli build examples/params-example.json -o playlist.json
@@ -88,20 +88,20 @@ For development in this repo:
88
88
 
89
89
  ```bash
90
90
  npm run build
91
- 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
92
92
  ```
93
93
 
94
94
  If you're running from source without a build, use:
95
95
 
96
96
  ```bash
97
- npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --play
97
+ npm run dev -- find https://objkt.com/tokens/hicetnunc/111068 --display
98
98
  ```
99
99
 
100
100
  ## Two ways to build a playlist
101
101
 
102
102
  ff-cli is deterministic: no chat, no model orchestration. There are two entry points for assembling a playlist.
103
103
 
104
- 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.
105
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.
106
106
 
107
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.
@@ -120,16 +120,16 @@ Both paths run the same deterministic pipeline: fetch metadata, assemble a DP-1
120
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`
121
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
122
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
123
- - `play <source>` – Play a playlist file, playlist URL, media URL, or web page on an FF1 device (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 plays as media, and anything else plays as a web page
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
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` plays the page itself instead
127
- - Options: `-d, --device <name>`, `--web` (play 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 playable DP-1 playlist
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
129
129
  - Sources: Art Blocks, Objkt, fxhash, OpenSea, SuperRare, Feral File, Neort, Verse, raw on-chain coordinates, and wallet addresses
130
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)
131
131
  - Verse forms: `/items/ethereum/{contract}/{tokenId}` and `/series/{slug}`
132
- - 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`
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
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
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
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
@@ -142,12 +142,19 @@ Both paths run the same deterministic pipeline: fetch metadata, assemble a DP-1
142
142
  - Options: `-s, --server <index>`, `-o, --output <file>`. There is no `--key`: a fetch is a plain read and signs nothing
143
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
144
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)
145
148
  - `ssh <enable|disable>` – Manage SSH access on an FF1 device
146
149
  - Options: `-d, --device <name>`, `--pubkey <path>`, `--ttl <duration>`
147
150
  - `device list` – List all configured FF1 devices
148
151
  - `device add` – Add a device with mDNS or manual `--host`, `--name`, and `--id`
149
152
  - Options: `--host <host>`, `--name <name>`, `--id <id>`
150
- - `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)
151
158
  - `device rename <name> <new-name>` – Rename a configured FF1 device (host, API key, and default status are untouched)
152
159
  - `device remove <name>` – Remove a configured FF1 device
153
160
  - `device default <name>` – Set the default FF1 device (used when `-d` is omitted)
@@ -157,16 +164,16 @@ Both paths run the same deterministic pipeline: fetch metadata, assemble a DP-1
157
164
 
158
165
  ### Find an artwork
159
166
 
160
- `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.
161
168
 
162
169
  ```bash
163
- # Paste a URL and play it on your FF1
164
- 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
165
172
 
166
173
  # Tezos / hic et nunc via Objkt (the alias resolves to a KT1 contract)
167
- ff-cli find https://objkt.com/tokens/hicetnunc/111068 --play
174
+ ff-cli find https://objkt.com/tokens/hicetnunc/111068 --display
168
175
 
169
- # Save without playing
176
+ # Save without displaying
170
177
  ff-cli find ethereum:0xababababab20053426ad1c782de9ea8444358070:5001410 -o send-receive.json
171
178
 
172
179
  # Wallet address: one work by the artist at that address; cap the number of tokens with --limit
@@ -242,7 +249,7 @@ reported rather than guessed at, and items carrying an external `ref` are left
242
249
  alone because `ref` outranks an inline manifest at the device. The playlist is
243
250
  validated before and after; on failure the file is left untouched. Enriching
244
251
  changes the document, so the signature is removed and the playlist must be
245
- re-signed before playing:
252
+ re-signed before displaying:
246
253
 
247
254
  ```bash
248
255
  ff-cli enrich playlist.json --assume-ethereum && ff-cli sign playlist.json
@@ -251,7 +258,7 @@ ff-cli enrich playlist.json --assume-ethereum && ff-cli sign playlist.json
251
258
  That role is fine for casting. Use `ff-cli sign playlist.json -r curator` if the enriched playlist is
252
259
  also going to a feed.
253
260
 
254
- ### Validate, sign, and play
261
+ ### Validate, sign, and display
255
262
 
256
263
  ```bash
257
264
  # Optional explicit validation (build flows already validate)
@@ -260,30 +267,30 @@ npm run dev -- validate playlist.json
260
267
  # Sign (uses key and role from config, or overrides via --key / --role)
261
268
  npm run dev -- sign playlist.json -o signed.json
262
269
 
263
- # Play on configured default device (verifies playlists; only media URL fallbacks may be auto-signed)
264
- 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
265
272
 
266
- # Play on a specific named device
267
- 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"
268
275
 
269
- # 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.
270
277
  # If the device reports an unsupported FF1 OS version, the command fails with
271
278
  # a clear version message before any cast request is sent.
272
279
  # It also retries transient local-network errors (for example intermittent
273
280
  # mDNS/Wi-Fi resolver failures) with a short backoff before returning a final error.
274
281
 
275
- # Play a hosted DP-1 playlist
276
- 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"
277
284
 
278
- # Play a media URL or any web page directly
279
- npm run dev -- play "https://example.com/video.mp4"
280
- npm run dev -- play "https://example.com/"
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/"
281
288
 
282
- # Play a page on a marketplace site as a page (play would otherwise send it to find)
283
- npm run dev -- play --web "https://feralfile.com/"
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/"
284
291
 
285
292
  # Skip signature verification only if you must send a non-conformant payload (not recommended)
286
- npm run dev -- play playlist.json --skip-verify
293
+ npm run dev -- display playlist.json --skip-verify
287
294
  ```
288
295
 
289
296
  ### Owner remote maintenance
@@ -306,9 +313,47 @@ ff-cli ssh enable --pubkey ~/.ssh/id_ed25519.pub --ttl 30m -d "Living Room Displ
306
313
  # Disable SSH access
307
314
  ff-cli ssh disable -d "Living Room Display"
308
315
 
309
- # `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`.
310
317
  ```
311
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
336
+ ```
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
+
312
357
  ### Publish to feed server
313
358
 
314
359
  ```bash
@@ -457,6 +502,38 @@ ff-cli device remove kitchen
457
502
  ff-cli device default office
458
503
  ```
459
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
+
460
537
  Setup preserves existing devices when adding new ones. See selection rules and examples in `./CONFIGURATION.md`.
461
538
 
462
539
  ### Playlist signing (optional)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@feralfile/cli",
3
- "version": "3.0.0",
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",