@feralfile/cli 1.1.1

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/config.json.example +96 -0
  4. package/dist/index.js +54 -0
  5. package/dist/src/ai-orchestrator/index.js +1019 -0
  6. package/dist/src/ai-orchestrator/registry.js +96 -0
  7. package/dist/src/commands/build.js +69 -0
  8. package/dist/src/commands/chat.js +189 -0
  9. package/dist/src/commands/config.js +68 -0
  10. package/dist/src/commands/device.js +278 -0
  11. package/dist/src/commands/helpers/config-files.js +62 -0
  12. package/dist/src/commands/helpers/device-discovery.js +111 -0
  13. package/dist/src/commands/helpers/playlist-display.js +161 -0
  14. package/dist/src/commands/helpers/prompt.js +65 -0
  15. package/dist/src/commands/helpers/ssh-helpers.js +44 -0
  16. package/dist/src/commands/play.js +110 -0
  17. package/dist/src/commands/publish.js +115 -0
  18. package/dist/src/commands/setup.js +225 -0
  19. package/dist/src/commands/sign.js +41 -0
  20. package/dist/src/commands/ssh.js +108 -0
  21. package/dist/src/commands/status.js +126 -0
  22. package/dist/src/commands/validate.js +18 -0
  23. package/dist/src/config.js +441 -0
  24. package/dist/src/intent-parser/index.js +1382 -0
  25. package/dist/src/intent-parser/utils.js +108 -0
  26. package/dist/src/logger.js +82 -0
  27. package/dist/src/main.js +459 -0
  28. package/dist/src/types.js +5 -0
  29. package/dist/src/utilities/address-validator.js +242 -0
  30. package/dist/src/utilities/device-default.js +36 -0
  31. package/dist/src/utilities/device-lookup.js +107 -0
  32. package/dist/src/utilities/device-normalize.js +62 -0
  33. package/dist/src/utilities/device-upsert.js +91 -0
  34. package/dist/src/utilities/domain-resolver.js +291 -0
  35. package/dist/src/utilities/ed25519-key-derive.js +155 -0
  36. package/dist/src/utilities/feed-fetcher.js +471 -0
  37. package/dist/src/utilities/ff1-compatibility.js +269 -0
  38. package/dist/src/utilities/ff1-device.js +250 -0
  39. package/dist/src/utilities/ff1-discovery.js +330 -0
  40. package/dist/src/utilities/functions.js +308 -0
  41. package/dist/src/utilities/index.js +469 -0
  42. package/dist/src/utilities/nft-indexer.js +1024 -0
  43. package/dist/src/utilities/playlist-builder.js +523 -0
  44. package/dist/src/utilities/playlist-publisher.js +131 -0
  45. package/dist/src/utilities/playlist-send.js +260 -0
  46. package/dist/src/utilities/playlist-signer.js +204 -0
  47. package/dist/src/utilities/playlist-signing-role.js +41 -0
  48. package/dist/src/utilities/playlist-source.js +128 -0
  49. package/dist/src/utilities/playlist-verifier.js +274 -0
  50. package/dist/src/utilities/ssh-access.js +145 -0
  51. package/dist/src/utils.js +48 -0
  52. package/docs/CONFIGURATION.md +206 -0
  53. package/docs/EXAMPLES.md +390 -0
  54. package/docs/FUNCTION_CALLING.md +96 -0
  55. package/docs/PROJECT_SPEC.md +228 -0
  56. package/docs/README.md +348 -0
  57. package/docs/RELEASING.md +73 -0
  58. package/package.json +76 -0
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ /**
3
+ * Playlist Verification Utility.
4
+ * Delegates signature-shape handling to dp1-js and keeps the CLI as a thin
5
+ * orchestration layer.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ var __importDefault = (this && this.__importDefault) || function (mod) {
41
+ return (mod && mod.__esModule) ? mod : { "default": mod };
42
+ };
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.verifyPlaylist = verifyPlaylist;
45
+ exports.validatePlaylist = validatePlaylist;
46
+ exports.verifyPlaylistFile = verifyPlaylistFile;
47
+ exports.printVerificationResult = printVerificationResult;
48
+ const chalk_1 = __importDefault(require("chalk"));
49
+ const fs_1 = require("fs");
50
+ const module_1 = require("module");
51
+ /**
52
+ * Cryptographically verify a playlist via dp1-js (after parsing succeeds).
53
+ *
54
+ * Unlike {@link validatePlaylist}, this forwards to dp1-js `verifyPlaylist`. The
55
+ * library verifies DP-1 v1.1.0 `signatures[]` envelopes from embedded material.
56
+ * dp1-js uses the optional public key argument only for legacy flat `signature`
57
+ * strings, not for `signatures[]`. The CLI may still derive a public key from
58
+ * `playlist.privateKey` / `PLAYLIST_PRIVATE_KEY` when `--public-key` is omitted;
59
+ * dp1-js ignores that value unless the playlist uses the legacy path. Legacy
60
+ * `signature` payloads need the matching public key via `--public-key` or
61
+ * derivation as above.
62
+ *
63
+ * Use {@link validatePlaylist} for structure-only checks (`validate` command).
64
+ *
65
+ * @param playlist - Playlist object
66
+ * @param publicKey - Optional Ed25519 key material for legacy `signature` verification.
67
+ * PEM SPKI, 64-character hex (optional `0x`), 32-byte base64, or SPKI DER base64 are normalized to PEM before calling dp1-js. If derivation from config fails or PEM normalization fails, the CLI logs a short warning, drops the optional key material, and still calls dp1-js (so DP-1 v1.1.0 `signatures[]` verification can succeed without relying on legacy key arguments).
68
+ * @returns Verification result with `valid` and optional `error` / `details`
69
+ */
70
+ async function verifyPlaylist(playlist, publicKey) {
71
+ try {
72
+ const result = await parseDp1Playlist(playlist);
73
+ if (result && 'error' in result && result.error) {
74
+ return {
75
+ valid: false,
76
+ error: result.error.message,
77
+ details: result.error.details || [],
78
+ };
79
+ }
80
+ const dp1 = await loadDp1();
81
+ const verifyFn = dp1.verifyPlaylist;
82
+ if (typeof verifyFn !== 'function') {
83
+ throw new Error('dp1-js does not expose verifyPlaylist');
84
+ }
85
+ let key = publicKey?.trim() || undefined;
86
+ if (!key) {
87
+ try {
88
+ const { getPlaylistConfig } = await Promise.resolve().then(() => __importStar(require('../config')));
89
+ const privateKeyMaterial = getPlaylistConfig().privateKey;
90
+ if (privateKeyMaterial) {
91
+ const { deriveEd25519PublicKeyForVerify } = await Promise.resolve().then(() => __importStar(require('./ed25519-key-derive')));
92
+ key = deriveEd25519PublicKeyForVerify(privateKeyMaterial);
93
+ }
94
+ }
95
+ catch (err) {
96
+ console.warn(chalk_1.default.yellow(`Could not derive a verify public key from playlist config (${err.message}); continuing verification without it.`));
97
+ key = undefined;
98
+ }
99
+ }
100
+ if (key) {
101
+ try {
102
+ const { normalizeVerifyPublicKeyToPem } = await Promise.resolve().then(() => __importStar(require('./ed25519-key-derive')));
103
+ key = normalizeVerifyPublicKeyToPem(key);
104
+ }
105
+ catch (err) {
106
+ console.warn(chalk_1.default.yellow(`Could not normalize public key for dp1-js (${err.message}); continuing verification without it.`));
107
+ key = undefined;
108
+ }
109
+ }
110
+ const ok = await verifyFn(playlist, key);
111
+ return ok ? { valid: true } : { valid: false, error: 'Playlist signature verification failed' };
112
+ }
113
+ catch (error) {
114
+ return {
115
+ valid: false,
116
+ error: `Verification failed: ${error.message}`,
117
+ };
118
+ }
119
+ }
120
+ /**
121
+ * Validate playlist structure without checking signatures.
122
+ *
123
+ * This is the parse-only path used by `validate`. It keeps the CLI
124
+ * semantics aligned with the repo contract: schema/shape validation is
125
+ * separate from cryptographic verification.
126
+ */
127
+ async function validatePlaylist(playlist) {
128
+ try {
129
+ const result = await parseDp1Playlist(playlist);
130
+ if (result && 'error' in result && result.error) {
131
+ return {
132
+ valid: false,
133
+ error: result.error.message,
134
+ details: result.error.details || [],
135
+ };
136
+ }
137
+ return { valid: true };
138
+ }
139
+ catch (error) {
140
+ return {
141
+ valid: false,
142
+ error: `Verification failed: ${error.message}`,
143
+ };
144
+ }
145
+ }
146
+ async function parseDp1Playlist(playlist) {
147
+ const module = (await loadDp1());
148
+ const parseFn = module.parseDP1Playlist;
149
+ if (typeof parseFn !== 'function') {
150
+ throw new Error('dp1-js does not expose parseDP1Playlist');
151
+ }
152
+ return parseFn(playlist);
153
+ }
154
+ /**
155
+ * Loads the published DP-1 implementation bundled with the CLI (`dp1-js`).
156
+ * Local checkout overrides via environment are intentionally unsupported so
157
+ * resolution stays deterministic across machines and CI.
158
+ */
159
+ async function loadDp1() {
160
+ const require = (0, module_1.createRequire)(__filename);
161
+ return require('dp1-js');
162
+ }
163
+ /**
164
+ * Verify playlist file
165
+ *
166
+ * Reads playlist from file and validates structure.
167
+ *
168
+ * @param {string} playlistPath - Path to playlist JSON file
169
+ * @returns {Promise<Object>} Verification result
170
+ * @returns {boolean} returns.valid - Whether playlist is valid
171
+ * @returns {Object} [returns.playlist] - Validated playlist object
172
+ * @returns {string} [returns.error] - Error message if invalid
173
+ * @returns {Array<Object>} [returns.details] - Detailed validation errors
174
+ * @example
175
+ * const result = await verifyPlaylistFile('playlist.json');
176
+ * if (result.valid) {
177
+ * console.log('Playlist is valid');
178
+ * }
179
+ */
180
+ async function verifyPlaylistFile(playlistPath) {
181
+ try {
182
+ // Check if file exists
183
+ try {
184
+ await fs_1.promises.access(playlistPath);
185
+ }
186
+ catch {
187
+ return {
188
+ valid: false,
189
+ error: `Playlist file not found: ${playlistPath}`,
190
+ };
191
+ }
192
+ // Read and parse playlist file
193
+ const playlistContent = await fs_1.promises.readFile(playlistPath, 'utf-8');
194
+ let playlistData;
195
+ try {
196
+ playlistData = JSON.parse(playlistContent);
197
+ }
198
+ catch (parseError) {
199
+ return {
200
+ valid: false,
201
+ error: `Invalid JSON: ${parseError.message}`,
202
+ };
203
+ }
204
+ // Verify using dp1-js (optional key is derived for legacy signature path only inside dp1-js).
205
+ const result = await verifyPlaylist(playlistData);
206
+ if (result.valid) {
207
+ return {
208
+ valid: true,
209
+ playlist: playlistData,
210
+ };
211
+ }
212
+ return {
213
+ valid: false,
214
+ error: result.error,
215
+ details: result.details,
216
+ };
217
+ }
218
+ catch (error) {
219
+ return {
220
+ valid: false,
221
+ error: `Failed to verify playlist file: ${error.message}`,
222
+ };
223
+ }
224
+ }
225
+ /**
226
+ * Prints verification results to the console.
227
+ *
228
+ * For failed results, `failureKind` distinguishes DP-1 structure parsing (the `validate`
229
+ * path and the first half of `verify`) from cryptographic verification (the second half
230
+ * of `verify` only). Defaults to `structure`.
231
+ *
232
+ * @param result - Verification result
233
+ * @param filename - Optional source label (path or URL)
234
+ * @param options - When `result.valid` is false, `failureKind` selects the failure headline
235
+ */
236
+ function printVerificationResult(result, filename, options) {
237
+ if (result.valid) {
238
+ console.log(chalk_1.default.green('\nPlaylist is valid'));
239
+ if (filename) {
240
+ console.log(chalk_1.default.dim(` File: ${filename}`));
241
+ }
242
+ if (result.playlist) {
243
+ console.log(chalk_1.default.dim(` Title: ${result.playlist.title}`));
244
+ console.log(chalk_1.default.dim(` Items: ${result.playlist.items?.length || 0}`));
245
+ console.log(chalk_1.default.dim(` DP Version: ${result.playlist.dpVersion}`));
246
+ if (Array.isArray(result.playlist.signatures)) {
247
+ console.log(chalk_1.default.dim(` Signatures: ${result.playlist.signatures.length}`));
248
+ }
249
+ else if (result.playlist.signature && typeof result.playlist.signature === 'string') {
250
+ console.log(chalk_1.default.dim(` Signature: ${result.playlist.signature.substring(0, 30)}...`));
251
+ }
252
+ }
253
+ console.log();
254
+ }
255
+ else {
256
+ const kind = options?.failureKind ?? 'structure';
257
+ const headline = kind === 'signature'
258
+ ? '\nPlaylist signature verification failed'
259
+ : '\nPlaylist validation failed';
260
+ console.log(chalk_1.default.red(headline));
261
+ if (filename) {
262
+ console.log(chalk_1.default.dim(` File: ${filename}`));
263
+ }
264
+ console.log(chalk_1.default.red(` Error: ${result.error}`));
265
+ if (result.details && result.details.length > 0) {
266
+ const detailsHeading = kind === 'signature' ? '\n Details:' : '\n Validation errors:';
267
+ console.log(chalk_1.default.yellow(detailsHeading));
268
+ result.details.forEach((detail) => {
269
+ console.log(chalk_1.default.yellow(` • ${detail.path}: ${detail.message}`));
270
+ });
271
+ }
272
+ console.log();
273
+ }
274
+ }
@@ -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
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.savePlaylist = savePlaylist;
7
+ exports.loadPlaylist = loadPlaylist;
8
+ exports.formatFileSize = formatFileSize;
9
+ const fs_1 = require("fs");
10
+ const path_1 = __importDefault(require("path"));
11
+ /**
12
+ * Save playlist to a JSON file
13
+ * @param {Object} playlist - The playlist object
14
+ * @param {string} filename - Output filename
15
+ * @returns {Promise<string>} Path to the saved file
16
+ */
17
+ async function savePlaylist(playlist, filename) {
18
+ const outputPath = path_1.default.resolve(process.cwd(), filename);
19
+ // Ensure the playlist is properly formatted
20
+ const formattedPlaylist = JSON.stringify(playlist, null, 2);
21
+ await fs_1.promises.writeFile(outputPath, formattedPlaylist, 'utf-8');
22
+ return outputPath;
23
+ }
24
+ /**
25
+ * Load playlist from a JSON file
26
+ * @param {string} filename - Input filename
27
+ * @returns {Promise<Object>} The playlist object
28
+ */
29
+ async function loadPlaylist(filename) {
30
+ const filePath = path_1.default.resolve(process.cwd(), filename);
31
+ const content = await fs_1.promises.readFile(filePath, 'utf-8');
32
+ return JSON.parse(content);
33
+ }
34
+ /**
35
+ * Format file size in human-readable format
36
+ * @param {number} bytes - Size in bytes
37
+ * @returns {string} Formatted size
38
+ */
39
+ function formatFileSize(bytes) {
40
+ const units = ['B', 'KB', 'MB', 'GB'];
41
+ let size = bytes;
42
+ let unitIndex = 0;
43
+ while (size >= 1024 && unitIndex < units.length - 1) {
44
+ size /= 1024;
45
+ unitIndex++;
46
+ }
47
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
48
+ }
@@ -0,0 +1,206 @@
1
+ # Configuration Guide
2
+
3
+ This guide explains how to configure ff-cli, field by field. Configuration priority is:
4
+
5
+ - `config.json` (highest)
6
+ - `.env`
7
+ - built‑in defaults (lowest)
8
+
9
+ ## Getting started
10
+
11
+ ```bash
12
+ # Create example config and edit it
13
+ npm run dev -- config init
14
+
15
+ # Validate your configuration
16
+ npm run dev -- config validate
17
+
18
+ # Show current config summary
19
+ npm run dev -- config show
20
+ ```
21
+
22
+ ## Top‑level fields
23
+
24
+ - **defaultModel** (string)
25
+ - The default AI model key to use. Must match a key under `models`.
26
+ - Used by orchestration to pick API, timeouts, and model identifier.
27
+
28
+ - **defaultDuration** (number, seconds)
29
+ - Intended default per‑item display duration. Some flows pass an explicit duration; when omitted, utilities fall back to 10s.
30
+
31
+ ## models
32
+
33
+ Each key under `models` defines a model configuration used by the AI orchestrator.
34
+
35
+ - `<modelName>.apiKey` (string): API key for the provider.
36
+ - `<modelName>.baseURL` (string): Base API URL.
37
+ - `<modelName>.model` (string): Model identifier (e.g., `grok-beta`, `gpt-4o`).
38
+ - `<modelName>.availableModels` (string[], optional): Display/help only.
39
+ - `<modelName>.timeout` (number, ms): HTTP timeout for requests.
40
+ - `<modelName>.maxRetries` (number): Retry count for requests.
41
+ - `<modelName>.temperature` (number): Generation temperature.
42
+ - `<modelName>.maxTokens` (number): Token cap.
43
+ - `<modelName>.supportsFunctionCalling` (boolean): Must be true; otherwise the CLI rejects the model.
44
+
45
+ Environment variable helpers:
46
+
47
+ - Anthropic (Claude): `ANTHROPIC_API_KEY`
48
+ - Grok: `GROK_API_KEY`, `GROK_MODEL`, `GROK_API_BASE_URL`
49
+ - OpenAI: `OPENAI_API_KEY`
50
+ - Gemini: `GEMINI_API_KEY`
51
+
52
+ ## browser
53
+
54
+ Optional settings used where headless/browser‑like behavior is needed.
55
+
56
+ - `browser.timeout` (number, ms): Operation timeout (default 90000).
57
+ - `browser.sanitizationLevel` ("none" | "low" | "medium" | "high" | 0‑3): Converted to numeric via `sanitizationLevelToNumber()`; invalid values are flagged during validation.
58
+
59
+ ## playlist
60
+
61
+ Used for signing DP‑1 playlists.
62
+
63
+ - `playlist.privateKey` (string, Ed25519 private key in hex or base64): Used by the `sign` command to create DP-1 v1.1.0 multi-signatures. The `verify` command may derive the matching public key from this value (or `PLAYLIST_PRIVATE_KEY`) when you omit `--public-key`; **dp1-js applies that derived key only when verifying legacy flat `signature` strings**, not when checking `signatures[]` envelopes. If that derivation fails, `verify` prints a warning on stderr and continues without derived key material. The derived public key is emitted as PEM so Node can decode it without ambiguity. Hex may include or omit the `0x` prefix. You can also set this via `PLAYLIST_PRIVATE_KEY` in `.env`.
64
+
65
+ **Signing and key encoding:** Signing paths (`sign`, deterministic `build` when configured, and `-k/--key` overrides) pass the private key string through to **`dp1-js`** (`SignMultiEd25519`) without an extra decoding step in ff-cli. `dp1-js` recognizes **hex** (optional `0x`) or **base64** encodings of the PKCS#8 DER blob produced by the OpenSSL examples below, then loads the key for Ed25519. Use those formats; ff-cli does not add a separate normalizer ahead of the library.
66
+ - `playlist.role` (string): DP-1 signing role that travels with the private key. Defaults to `agent` if omitted. You can also set this via `PLAYLIST_ROLE` in `.env`. Guided `ff-cli setup`, `config validate`, and `sign --role` only accept the usual DP-1 signing roles (`agent`, `feed`, `curator`, `institution`, `licensor`).
67
+ ### Generate an Ed25519 private key
68
+
69
+ You can generate a key locally. The CLI accepts either base64 (preferred) or hex
70
+
71
+ OpenSSL (recommended):
72
+
73
+ ```bash
74
+ # Base64 (preferred)
75
+ openssl genpkey -algorithm ED25519 -outform DER | base64 | tr -d '\n'
76
+
77
+ # Hex (alternative)
78
+ openssl genpkey -algorithm ED25519 -outform DER | xxd -p -c 256
79
+ ```
80
+
81
+ Paste either value into `playlist.privateKey`:
82
+
83
+ - Hex example (either is valid):
84
+ - `0xabc123...` (with prefix)
85
+ - `abc123...` (without prefix)
86
+ - Base64 example: `uQd9m8S...==`
87
+
88
+ If you need a different role, set `playlist.role` to one of the DP-1 signing roles such as `agent`, `feed`, `curator`, `institution`, or `licensor`. The CLI rejects any other string before it reaches `dp1-js`.
89
+
90
+ If you already have a base64 key and want hex, convert it:
91
+
92
+ ```bash
93
+ echo -n "<BASE64_KEY>" | base64 -d | xxd -p -c 256
94
+ ```
95
+
96
+ ## feed
97
+
98
+ DP‑1 Feed API configuration.
99
+
100
+ - `feed.baseURLs` (string[]): Array of DP‑1 Feed Operator API v1 base URLs. The CLI queries all feeds in parallel.
101
+ - Legacy support: `feed.baseURL` (string) is still accepted and normalized to an array.
102
+ - Default: `https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1` if not set.
103
+ - Compatibility: API v1 of the DP‑1 Feed Operator server. See the repository for endpoints and behavior: [dp1-feed](https://github.com/display-protocol/dp1-feed).
104
+
105
+ Endpoints used by the CLI:
106
+
107
+ - `GET /api/v1/playlists` (supports `limit`, `offset`, and sorting)
108
+ - `GET /api/v1/playlists/{id}`
109
+
110
+ Environment variable alternative:
111
+
112
+ ```env
113
+ FEED_BASE_URLS=https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1,https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1
114
+ ```
115
+
116
+ ## ff1Devices
117
+
118
+ Configure devices you want to play content on.
119
+
120
+ - `ff1Devices.devices` (array of objects):
121
+ - `name` (string): Friendly device label. Free‑form; pick anything memorable.
122
+ - `host` (string): Device base URL. For LAN devices, use `http://<ip>:1111`. The device typically listens on port `1111`.
123
+
124
+ During `ff-cli 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.
125
+
126
+ You can also manage devices independently with:
127
+
128
+ - `ff-cli device add` – Add a device interactively (with mDNS discovery), or non-interactively with `--host` and `--name`.
129
+ - `ff-cli device list` – Show all configured devices.
130
+ - `ff-cli device remove <name>` – Remove a device by name.
131
+ - `ff-cli device default <name>` – Promote a device to the top of the list so it is used when `-d` is omitted.
132
+
133
+ Setup and `device add` both preserve existing devices. Adding a device with the same host as an existing one updates it in place.
134
+
135
+ Selection rules when sending:
136
+
137
+ - If you omit `-d`, the first configured device is used.
138
+ - 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.
139
+
140
+ Compatibility checks:
141
+
142
+ - `play` 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.
143
+
144
+ - Minimum supported FF1 OS versions:
145
+ - `play` (`displayPlaylist`): `1.0.0` or newer
146
+ - `ssh` (`sshAccess`): `1.0.9` or newer
147
+
148
+ - If the CLI cannot get a version from the device (e.g. network or malformed response), it continues and sends the command.
149
+ - If the detected version is below the minimum, the command fails early with an error that includes the detected version.
150
+
151
+ Troubleshooting note:
152
+
153
+ - 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.
154
+
155
+ Examples:
156
+
157
+ ```bash
158
+ # Send to first device
159
+ npm run dev -- play playlist.json
160
+
161
+ # Play on a specific device by exact name
162
+ npm run dev -- play playlist.json -d "Living Room Display"
163
+ ```
164
+
165
+ Minimal `config.json` example (selected fields):
166
+
167
+ ```json
168
+ {
169
+ "defaultModel": "claude",
170
+ "models": {
171
+ "claude": {
172
+ "apiKey": "sk-ant-your-api-key-here",
173
+ "baseURL": "https://api.anthropic.com/v1/",
174
+ "model": "claude-sonnet-4-6",
175
+ "supportsFunctionCalling": true
176
+ }
177
+ },
178
+ "defaultDuration": 10,
179
+ "playlist": {
180
+ "privateKey": "your_ed25519_private_key_hex_or_base64_here",
181
+ "role": "agent"
182
+ },
183
+ "feed": {
184
+ "baseURLs": ["https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1"]
185
+ },
186
+ "ff1Devices": {
187
+ "devices": [
188
+ {
189
+ "name": "Living Room Display",
190
+ "host": "http://192.168.1.100:1111"
191
+ }
192
+ ]
193
+ }
194
+ }
195
+ ```
196
+
197
+ ## Security and validation
198
+
199
+ - Do not commit secrets. Keep `config.json`, `.env`, and keys out of version control.
200
+ - Validate changes regularly:
201
+
202
+ ```bash
203
+ npm run dev -- config validate
204
+ ```
205
+
206
+ If configuration is invalid, the CLI prints actionable errors and a non‑zero exit code.