ff1-cli 1.0.2 → 1.0.3

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.
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPlaylistSourceUrl = isPlaylistSourceUrl;
4
+ exports.loadPlaylistSource = loadPlaylistSource;
5
+ const fs_1 = require("fs");
6
+ /**
7
+ * Determine whether a playlist source is an HTTP(S) URL.
8
+ *
9
+ * @param {string} source - Playlist source value
10
+ * @returns {boolean} Whether the value parses as http:// or https:// URL
11
+ */
12
+ function isPlaylistSourceUrl(source) {
13
+ const trimmed = source.trim();
14
+ if (!trimmed) {
15
+ return false;
16
+ }
17
+ try {
18
+ const parsed = new URL(trimmed);
19
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ /**
26
+ * Load a DP-1 playlist from a local file or hosted URL.
27
+ *
28
+ * @param {string} source - Playlist file path or URL
29
+ * @returns {Promise<LoadedPlaylist>} Loaded playlist payload with source metadata
30
+ * @throws {Error} When source is empty, cannot be loaded, or JSON is invalid
31
+ */
32
+ async function loadPlaylistSource(source) {
33
+ const trimmedSource = source.trim();
34
+ if (!trimmedSource) {
35
+ throw new Error('Playlist source is required');
36
+ }
37
+ if (isPlaylistSourceUrl(trimmedSource)) {
38
+ const response = await fetch(trimmedSource);
39
+ if (!response.ok) {
40
+ throw new Error(`Failed to fetch playlist URL: ${response.status} ${response.statusText}`);
41
+ }
42
+ let playlistText;
43
+ try {
44
+ playlistText = await response.text();
45
+ }
46
+ catch (error) {
47
+ throw new Error(`Failed to read playlist response from ${trimmedSource}: ${error.message}`);
48
+ }
49
+ try {
50
+ return {
51
+ playlist: JSON.parse(playlistText),
52
+ source: trimmedSource,
53
+ sourceType: 'url',
54
+ };
55
+ }
56
+ catch (error) {
57
+ throw new Error(`Invalid JSON from playlist URL ${trimmedSource}: ${error.message}`);
58
+ }
59
+ }
60
+ let fileText;
61
+ try {
62
+ fileText = await fs_1.promises.readFile(trimmedSource, 'utf-8');
63
+ }
64
+ catch (_error) {
65
+ throw new Error(`Playlist file not found at ${trimmedSource}`);
66
+ }
67
+ try {
68
+ return {
69
+ playlist: JSON.parse(fileText),
70
+ source: trimmedSource,
71
+ sourceType: 'file',
72
+ };
73
+ }
74
+ catch (error) {
75
+ throw new Error(`Invalid JSON in ${trimmedSource}: ${error.message}`);
76
+ }
77
+ }
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /**
3
+ * SSH access control for FF1 devices.
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.sendSshAccessCommand = sendSshAccessCommand;
40
+ const logger = __importStar(require("../logger"));
41
+ const ff1_compatibility_1 = require("./ff1-compatibility");
42
+ /**
43
+ * Send an SSH access command to an FF1 device.
44
+ *
45
+ * @param {Object} params - Function parameters
46
+ * @param {boolean} params.enabled - Whether to enable SSH access
47
+ * @param {string} [params.deviceName] - Device name to target (defaults to first configured)
48
+ * @param {string} [params.publicKey] - SSH public key to authorize (required for enable)
49
+ * @param {number} [params.ttlSeconds] - Time-to-live in seconds for auto-disable
50
+ * @returns {Promise<Object>} Result object
51
+ * @returns {boolean} returns.success - Whether the command succeeded
52
+ * @returns {string} [returns.device] - Device host used
53
+ * @returns {string} [returns.deviceName] - Device name used
54
+ * @returns {Object} [returns.response] - Response from device
55
+ * @returns {string} [returns.error] - Error message if failed
56
+ * @throws {Error} When device configuration is invalid or missing
57
+ * @example
58
+ * // Enable SSH for 30 minutes
59
+ * const result = await sendSshAccessCommand({
60
+ * enabled: true,
61
+ * publicKey: 'ssh-ed25519 AAAAC3... user@host',
62
+ * ttlSeconds: 1800,
63
+ * });
64
+ */
65
+ async function sendSshAccessCommand({ enabled, deviceName, publicKey, ttlSeconds, }) {
66
+ try {
67
+ if (enabled && (!publicKey || !publicKey.trim())) {
68
+ return {
69
+ success: false,
70
+ error: 'Public key is required to enable SSH access',
71
+ };
72
+ }
73
+ const resolved = (0, ff1_compatibility_1.resolveConfiguredDevice)(deviceName);
74
+ if (!resolved.success || !resolved.device) {
75
+ return {
76
+ success: false,
77
+ error: resolved.error || 'FF1 device is not configured correctly',
78
+ };
79
+ }
80
+ const device = resolved.device;
81
+ const compatibility = await (0, ff1_compatibility_1.assertFF1CommandCompatibility)(device, 'sshAccess');
82
+ if (!compatibility.compatible) {
83
+ return {
84
+ success: false,
85
+ error: compatibility.error || 'FF1 OS does not support SSH access command',
86
+ details: compatibility.version ? `Detected version ${compatibility.version}` : undefined,
87
+ };
88
+ }
89
+ let apiUrl = `${device.host}/api/cast`;
90
+ if (device.topicID && device.topicID.trim() !== '') {
91
+ apiUrl += `?topicID=${encodeURIComponent(device.topicID)}`;
92
+ logger.debug(`Using topicID: ${device.topicID}`);
93
+ }
94
+ const request = {
95
+ enabled,
96
+ };
97
+ if (publicKey && publicKey.trim()) {
98
+ request.publicKey = publicKey.trim();
99
+ }
100
+ if (typeof ttlSeconds === 'number') {
101
+ request.ttlSeconds = ttlSeconds;
102
+ }
103
+ const requestBody = {
104
+ command: 'sshAccess',
105
+ request,
106
+ };
107
+ const headers = {
108
+ 'Content-Type': 'application/json',
109
+ };
110
+ if (device.apiKey) {
111
+ headers['API-KEY'] = device.apiKey;
112
+ }
113
+ const response = await fetch(apiUrl, {
114
+ method: 'POST',
115
+ headers,
116
+ body: JSON.stringify(requestBody),
117
+ });
118
+ if (!response.ok) {
119
+ const errorText = await response.text();
120
+ logger.error(`SSH access request failed: ${response.status} ${response.statusText}`);
121
+ logger.debug(`Error details: ${errorText}`);
122
+ return {
123
+ success: false,
124
+ error: `Device returned error ${response.status}: ${response.statusText}`,
125
+ details: errorText,
126
+ };
127
+ }
128
+ const responseData = (await response.json());
129
+ logger.info('SSH access command succeeded');
130
+ logger.debug(`Device response: ${JSON.stringify(responseData)}`);
131
+ return {
132
+ success: true,
133
+ device: device.host,
134
+ deviceName: device.name || device.host,
135
+ response: responseData,
136
+ };
137
+ }
138
+ catch (error) {
139
+ logger.error(`Error sending SSH access command: ${error.message}`);
140
+ return {
141
+ success: false,
142
+ error: error.message,
143
+ };
144
+ }
145
+ }
@@ -116,11 +116,29 @@ Configure devices you want to send playlists to.
116
116
  - `name` (string): Friendly device label. Free‑form; pick anything memorable.
117
117
  - `host` (string): Device base URL. For LAN devices, use `http://<ip>:1111`. The device typically listens on port `1111`.
118
118
 
119
+ During `ff1 setup`, the CLI will attempt local discovery via mDNS (`_ff1._tcp`). If devices are found, you can pick one and the host will be filled in automatically. If discovery returns nothing, setup falls back to manual entry.
120
+
119
121
  Selection rules when sending:
120
122
 
121
123
  - If you omit `-d`, the first configured device is used.
122
124
  - If you pass `-d <name>`, the CLI matches the device by `name` (exact match). If not found, you’ll see an error listing available devices.
123
125
 
126
+ Compatibility checks:
127
+
128
+ - `send` and `ssh` perform a compatibility preflight before sending commands to FF1. The CLI gets the device version by calling `POST /api/cast` with `{ "command": "getDeviceStatus", "request": {} }` and reads `message.installedVersion` from the response.
129
+
130
+ - Minimum supported FF1 OS versions:
131
+
132
+ - `send` (`displayPlaylist`): `1.0.0` or newer
133
+ - `ssh` (`sshAccess`): `1.0.9` or newer
134
+
135
+ - If the CLI cannot get a version from the device (e.g. network or malformed response), it continues and sends the command.
136
+ - If the detected version is below the minimum, the command fails early with an error that includes the detected version.
137
+
138
+ Troubleshooting note:
139
+
140
+ - If you get an unsupported-version error, update your FF1 OS and retry. If version detection seems inconsistent, check that device host and key are correct and retry with the device directly reachable.
141
+
124
142
  Examples:
125
143
 
126
144
  ```bash
@@ -131,7 +149,7 @@ npm run dev -- send playlist.json
131
149
  npm run dev -- send playlist.json -d "Living Room Display"
132
150
  ```
133
151
 
134
- Minimal `config.json` example (selected fields):
152
+ Minimal `config.json` example (selected fields):
135
153
 
136
154
  ```json
137
155
  {
@@ -149,9 +167,7 @@ Minimal `config.json` example (selected fields):
149
167
  "privateKey": "your_ed25519_private_key_hex_or_base64_here"
150
168
  },
151
169
  "feed": {
152
- "baseURLs": [
153
- "https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1"
154
- ]
170
+ "baseURLs": ["https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1"]
155
171
  },
156
172
  "ff1Devices": {
157
173
  "devices": [
@@ -174,5 +190,3 @@ npm run dev -- config validate
174
190
  ```
175
191
 
176
192
  If configuration is invalid, the CLI prints actionable errors and a non‑zero exit code.
177
-
178
-
package/docs/EXAMPLES.md CHANGED
@@ -23,8 +23,10 @@ npm run dev -- chat "Get 3 items from Social Codes and 2 from 0xdef" -v
23
23
 
24
24
  # Switch model
25
25
  npm run dev -- chat "your request" --model grok
26
- npm run dev -- chat "your request" --model chatgpt
26
+ npm run dev -- chat "your request" --model gpt
27
27
  npm run dev -- chat "your request" --model gemini
28
+
29
+ # Model names must match keys in config.json under `models`.
28
30
  ```
29
31
 
30
32
  ## Deterministic Build (no AI)
@@ -44,7 +46,7 @@ cat examples/params-example.json | npm run dev -- build -o playlist.json
44
46
  npm run dev -- chat "Build a playlist of my Tezos works from address tz1... plus 3 from Social Codes" -v -o playlist.json
45
47
 
46
48
  # Switch model if desired
47
- npm run dev -- chat "Build playlist from Ethereum address 0x... and 2 from Social Codes" --model chatgpt -v
49
+ npm run dev -- chat "Build playlist from Ethereum address 0x... and 2 from Social Codes" --model gpt -v
48
50
  ```
49
51
 
50
52
  ### One‑shot complex prompt
@@ -92,18 +94,21 @@ npm run dev -- chat "Get 5 from Social Codes, shuffle, display on 'Living Room',
92
94
  ### How It Works
93
95
 
94
96
  **Mode 1: Build and Publish** (when sources are mentioned)
97
+
95
98
  1. Intent parser detects "publish" keywords with sources/requirements
96
99
  2. Calls `get_feed_servers` to retrieve configured servers
97
100
  3. If 1 server → uses it automatically; if 2+ servers → asks user to pick
98
101
  4. Builds playlist → verifies → publishes automatically
99
102
 
100
103
  **Mode 2: Publish Existing File** (e.g., "publish playlist")
104
+
101
105
  1. Intent parser detects "publish playlist" or similar phrases
102
106
  2. Calls `get_feed_servers` to retrieve configured servers
103
107
  3. If 1 server → uses it automatically; if 2+ servers → asks user to pick
104
108
  4. Publishes the playlist from `./playlist.json` (or specified path)
105
109
 
106
110
  Output shows:
111
+
107
112
  - Playlist build progress (Mode 1 only)
108
113
  - Device sending (if requested): `✓ Sent to device: Living Room`
109
114
  - Publishing status: `✓ Published to feed server`
@@ -114,12 +119,14 @@ Output shows:
114
119
  ```bash
115
120
  # Validate playlist
116
121
  npm run dev -- validate playlist.json
122
+ npm run dev -- validate "https://cdn.example.com/playlist.json"
117
123
 
118
124
  # Sign playlist
119
125
  npm run dev -- sign playlist.json -o signed.json
120
126
 
121
127
  # Send to device
122
128
  npm run dev -- send playlist.json -d "Living Room Display"
129
+ npm run dev -- send "https://cdn.example.com/playlist.json" -d "Living Room Display"
123
130
  ```
124
131
 
125
132
  ## Publish to Feed Server
@@ -187,18 +194,21 @@ Select server (0-based index): 0
187
194
  ### Error Handling
188
195
 
189
196
  **Validation failed:**
197
+
190
198
  ```
191
199
  ❌ Failed to publish playlist
192
200
  Playlist validation failed: dpVersion: Required; id: Required
193
201
  ```
194
202
 
195
203
  **File not found:**
204
+
196
205
  ```
197
206
  ❌ Failed to publish playlist
198
207
  Playlist file not found: /path/to/playlist.json
199
208
  ```
200
209
 
201
210
  **API error:**
211
+
202
212
  ```
203
213
  ❌ Failed to publish playlist
204
214
  Failed to publish: {"error":"unauthorized","message":"Invalid API key"}
@@ -233,7 +243,6 @@ npm run dev -- config show
233
243
  npm run dev -- config init
234
244
  ```
235
245
 
236
-
237
246
  ### Natural‑language one‑shot examples (proven)
238
247
 
239
248
  - **ETH contract + token IDs (shuffle/mix, generic device)**
@@ -328,4 +337,4 @@ npm run dev -- config init
328
337
  ```bash
329
338
  npm run dev -- chat "Compose a playlist from reas.eth (3 items); send to device" -o playlist-generic-device.json -v
330
339
  npm run dev -- chat "Compose a playlist from reas.eth (3 items); send to 'Living Room'" -o playlist-named-device.json -v
331
- ```
340
+ ```
package/docs/README.md CHANGED
@@ -40,7 +40,7 @@ See the full configuration reference here: `./CONFIGURATION.md`.
40
40
  "model": "grok-beta",
41
41
  "supportsFunctionCalling": true
42
42
  },
43
- "chatgpt": {
43
+ "gpt": {
44
44
  "apiKey": "sk-your-openai-key-here",
45
45
  "baseURL": "https://api.openai.com/v1",
46
46
  "model": "gpt-4o",
@@ -49,7 +49,7 @@ See the full configuration reference here: `./CONFIGURATION.md`.
49
49
  "gemini": {
50
50
  "apiKey": "your-gemini-key-here",
51
51
  "baseURL": "https://generativelanguage.googleapis.com/v1beta/openai/",
52
- "model": "gemini-2.0-flash-exp",
52
+ "model": "gemini-2.5-flash",
53
53
  "supportsFunctionCalling": true
54
54
  }
55
55
  },
@@ -128,13 +128,15 @@ Notes:
128
128
  - Options: `-o, --output <file>`, `-v, --verbose`
129
129
  - `play <url>` – Send a media URL directly to an FF1 device
130
130
  - Options: `-d, --device <name>`, `--skip-verify`
131
- - `validate <file>` / `verify <file>` – Validate a DP1 playlist file
131
+ - `validate <file-or-url>` / `verify <file-or-url>` – Validate a DP1 playlist file
132
132
  - `sign <file>` – Sign playlist with Ed25519
133
133
  - Options: `-k, --key <base64>`, `-o, --output <file>`
134
- - `send <file>` – Send playlist to an FF1 device
134
+ - `send <file>` – Send a local or hosted DP-1 playlist to an FF1 device
135
135
  - Options: `-d, --device <name>`, `--skip-verify`
136
136
  - `publish <file>` – Publish a playlist to a feed server
137
137
  - Options: `-s, --server <index>` (server index if multiple configured)
138
+ - `ssh <enable|disable>` – Manage SSH access on an FF1 device
139
+ - Options: `-d, --device <name>`, `--pubkey <path>`, `--ttl <duration>`
138
140
  - `config <init|show|validate>` – Manage configuration
139
141
 
140
142
  ## Usage Highlights
@@ -163,7 +165,7 @@ How it works (at a glance):
163
165
  - If `deviceName` is present, the CLI will send the validated playlist to that FF1 device.
164
166
  - If `feedServer` is present (via "publish to my feed"), the CLI will publish the playlist to the selected feed server.
165
167
 
166
- Use `--model grok|chatgpt|gemini` to switch models, or set `defaultModel` in `config.json`.
168
+ Use `--model grok|gpt|gemini` to switch models, or set `defaultModel` in `config.json`.
167
169
 
168
170
  ### Natural language publishing
169
171
 
@@ -205,10 +207,28 @@ npm run dev -- sign playlist.json -o signed.json
205
207
  # Send to device (verifies by default)
206
208
  npm run dev -- send playlist.json -d "Living Room Display"
207
209
 
210
+ # The send path now performs a compatibility preflight check against the target FF1.
211
+ # If the device reports an unsupported FF1 OS version, the command fails with
212
+ # a clear version message before any cast request is sent.
213
+ # Send a hosted DP-1 playlist
214
+ npm run dev -- send "https://cdn.example.com/playlist.json" -d "Living Room Display"
215
+
208
216
  # Play a direct URL
209
217
  npm run dev -- play "https://example.com/video.mp4" -d "Living Room Display" --skip-verify
210
218
  ```
211
219
 
220
+ ### SSH access
221
+
222
+ ```bash
223
+ # Enable SSH access for 30 minutes
224
+ ff1 ssh enable --pubkey ~/.ssh/id_ed25519.pub --ttl 30m -d "Living Room Display"
225
+
226
+ # Disable SSH access
227
+ ff1 ssh disable -d "Living Room Display"
228
+
229
+ # `ff1 ssh` also performs the same FF1 OS compatibility preflight used by `send`.
230
+ ```
231
+
212
232
  ### Publish to feed server
213
233
 
214
234
  ```bash
package/docs/RELEASING.md CHANGED
@@ -27,7 +27,12 @@ Run the appropriate script on each target platform and upload each pair to the G
27
27
  ## GitHub Actions
28
28
 
29
29
  - **Build** (`build.yml`): Trigger manually (Actions → Build → Run workflow) or on pull requests. Builds binaries on macOS, Linux, and Windows and uploads them as workflow artifacts for download.
30
- - **Release** (`release.yml`): Triggered when you **publish a release** (create a release from the repo Releases page, or publish an existing draft). Builds the same binaries, then uploads them to that release. Pushing a tag alone does not run this; only creating/publishing a release does.
30
+ - **Release** (`release.yml`): Triggered when you **publish a release** (create a release from the repo Releases page, or publish an existing draft). Validates that `package.json` matches the tag, publishes to npm, builds binaries, then uploads them to that release. Pushing a tag alone does not run this; only creating/publishing a release does.
31
+
32
+ ## npm Publish Requirements
33
+
34
+ - Set `NPM_TOKEN` in GitHub Actions secrets with an npm automation token.
35
+ - Ensure `package.json` version matches the release tag (e.g. tag `1.0.2` → `"version": "1.0.2"`). The release job fails fast when they differ.
31
36
 
32
37
  ## Installer Redirect
33
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ff1-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "CLI to fetch NFT information and build DP1 playlists using Grok API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "start": "node dist/index.js",
22
22
  "dev": "tsx index.ts",
23
23
  "prepublishOnly": "npm run build",
24
- "test": "echo \"Error: no test specified\" && exit 1",
24
+ "test": "tsx tests/ff1-compatibility.test.ts",
25
25
  "lint": "eslint .",
26
26
  "lint:fix": "eslint . --fix",
27
27
  "format": "prettier --write \"**/*.{js,ts}\"",
@@ -44,6 +44,7 @@
44
44
  "license": "MIT",
45
45
  "dependencies": {
46
46
  "axios": "^1.6.2",
47
+ "bonjour-service": "^1.3.0",
47
48
  "chalk": "^4.1.2",
48
49
  "commander": "^11.1.0",
49
50
  "dotenv": "^16.3.1",