@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.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/config.json.example +96 -0
- package/dist/index.js +54 -0
- package/dist/src/ai-orchestrator/index.js +1019 -0
- package/dist/src/ai-orchestrator/registry.js +96 -0
- package/dist/src/commands/build.js +69 -0
- package/dist/src/commands/chat.js +189 -0
- package/dist/src/commands/config.js +68 -0
- package/dist/src/commands/device.js +278 -0
- package/dist/src/commands/helpers/config-files.js +62 -0
- package/dist/src/commands/helpers/device-discovery.js +111 -0
- package/dist/src/commands/helpers/playlist-display.js +161 -0
- package/dist/src/commands/helpers/prompt.js +65 -0
- package/dist/src/commands/helpers/ssh-helpers.js +44 -0
- package/dist/src/commands/play.js +110 -0
- package/dist/src/commands/publish.js +115 -0
- package/dist/src/commands/setup.js +225 -0
- package/dist/src/commands/sign.js +41 -0
- package/dist/src/commands/ssh.js +108 -0
- package/dist/src/commands/status.js +126 -0
- package/dist/src/commands/validate.js +18 -0
- package/dist/src/config.js +441 -0
- package/dist/src/intent-parser/index.js +1382 -0
- package/dist/src/intent-parser/utils.js +108 -0
- package/dist/src/logger.js +82 -0
- package/dist/src/main.js +459 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utilities/address-validator.js +242 -0
- package/dist/src/utilities/device-default.js +36 -0
- package/dist/src/utilities/device-lookup.js +107 -0
- package/dist/src/utilities/device-normalize.js +62 -0
- package/dist/src/utilities/device-upsert.js +91 -0
- package/dist/src/utilities/domain-resolver.js +291 -0
- package/dist/src/utilities/ed25519-key-derive.js +155 -0
- package/dist/src/utilities/feed-fetcher.js +471 -0
- package/dist/src/utilities/ff1-compatibility.js +269 -0
- package/dist/src/utilities/ff1-device.js +250 -0
- package/dist/src/utilities/ff1-discovery.js +330 -0
- package/dist/src/utilities/functions.js +308 -0
- package/dist/src/utilities/index.js +469 -0
- package/dist/src/utilities/nft-indexer.js +1024 -0
- package/dist/src/utilities/playlist-builder.js +523 -0
- package/dist/src/utilities/playlist-publisher.js +131 -0
- package/dist/src/utilities/playlist-send.js +260 -0
- package/dist/src/utilities/playlist-signer.js +204 -0
- package/dist/src/utilities/playlist-signing-role.js +41 -0
- package/dist/src/utilities/playlist-source.js +128 -0
- package/dist/src/utilities/playlist-verifier.js +274 -0
- package/dist/src/utilities/ssh-access.js +145 -0
- package/dist/src/utils.js +48 -0
- package/docs/CONFIGURATION.md +206 -0
- package/docs/EXAMPLES.md +390 -0
- package/docs/FUNCTION_CALLING.md +96 -0
- package/docs/PROJECT_SPEC.md +228 -0
- package/docs/README.md +348 -0
- package/docs/RELEASING.md +73 -0
- 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.
|