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.
- package/README.md +4 -2
- package/config.json.example +10 -8
- package/dist/index.js +255 -80
- package/dist/src/ai-orchestrator/index.js +62 -5
- package/dist/src/config.js +12 -12
- package/dist/src/intent-parser/index.js +110 -84
- package/dist/src/intent-parser/utils.js +5 -2
- package/dist/src/logger.js +10 -0
- package/dist/src/utilities/ff1-compatibility.js +269 -0
- package/dist/src/utilities/ff1-device.js +9 -27
- package/dist/src/utilities/ff1-discovery.js +147 -0
- package/dist/src/utilities/functions.js +8 -26
- package/dist/src/utilities/index.js +9 -3
- package/dist/src/utilities/playlist-send.js +36 -17
- package/dist/src/utilities/playlist-source.js +77 -0
- package/dist/src/utilities/ssh-access.js +145 -0
- package/docs/CONFIGURATION.md +20 -6
- package/docs/EXAMPLES.md +13 -4
- package/docs/README.md +25 -5
- package/docs/RELEASING.md +6 -1
- package/package.json +3 -2
|
@@ -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
|
+
}
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
"
|
|
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.
|
|
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|
|
|
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).
|
|
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.
|
|
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": "
|
|
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",
|