@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,330 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseAvahiBrowseOutput = parseAvahiBrowseOutput;
4
+ exports.resolveAvahiResult = resolveAvahiResult;
5
+ exports.discoverFF1Devices = discoverFF1Devices;
6
+ const bonjour_service_1 = require("bonjour-service");
7
+ const child_process_1 = require("child_process");
8
+ const DEFAULT_TIMEOUT_MS = 5000;
9
+ /**
10
+ * Normalize mDNS TXT records to string values.
11
+ *
12
+ * @param {Record<string, unknown> | undefined} record - Raw TXT record object from mDNS
13
+ * @returns {Record<string, string>} Normalized TXT records
14
+ * @example
15
+ * normalizeTxtRecords({ id: 'ff1-1234', name: 'Studio FF1' });
16
+ */
17
+ function normalizeTxtRecords(record) {
18
+ if (!record) {
19
+ return {};
20
+ }
21
+ return Object.entries(record).reduce((accumulator, [key, value]) => {
22
+ if (typeof value === 'string') {
23
+ accumulator[key] = value;
24
+ return accumulator;
25
+ }
26
+ if (Buffer.isBuffer(value)) {
27
+ accumulator[key] = value.toString('utf8');
28
+ return accumulator;
29
+ }
30
+ if (typeof value === 'number' || typeof value === 'boolean') {
31
+ accumulator[key] = String(value);
32
+ }
33
+ return accumulator;
34
+ }, {});
35
+ }
36
+ /**
37
+ * Normalize mDNS hostnames by trimming a trailing dot.
38
+ *
39
+ * @param {string} host - Raw host from mDNS results
40
+ * @returns {string} Normalized host
41
+ * @example
42
+ * normalizeMdnsHost('ff1-1234.local.');
43
+ */
44
+ function normalizeMdnsHost(host) {
45
+ return host.endsWith('.') ? host.slice(0, -1) : host;
46
+ }
47
+ /**
48
+ * Extract a hostname-based ID from an mDNS host when possible.
49
+ *
50
+ * @param {string} host - Normalized mDNS host
51
+ * @returns {string} Hostname-based ID when available
52
+ * @example
53
+ * getHostnameId('ff1-03vdu3x1.local');
54
+ */
55
+ function getHostnameId(host) {
56
+ if (!host) {
57
+ return '';
58
+ }
59
+ if (host.includes(':')) {
60
+ return '';
61
+ }
62
+ if (/^[0-9.]+$/.test(host)) {
63
+ return '';
64
+ }
65
+ if (host.endsWith('.local')) {
66
+ return host.split('.')[0] || '';
67
+ }
68
+ if (!host.includes('.')) {
69
+ return host;
70
+ }
71
+ return '';
72
+ }
73
+ /**
74
+ * Parse avahi-browse -t -r output into FF1DiscoveredDevice list.
75
+ * Handles resolved records (lines starting with '=') with hostname/address/port/txt fields.
76
+ * Exported for testing.
77
+ */
78
+ function parseAvahiBrowseOutput(output) {
79
+ const devices = new Map();
80
+ const lines = output.split('\n');
81
+ let current = null;
82
+ const flushCurrent = () => {
83
+ if (!current?.rawHost) {
84
+ return;
85
+ }
86
+ const host = normalizeMdnsHost(current.rawHost).toLowerCase();
87
+ const key = `${host}:${current.port ?? 1111}`;
88
+ const id = getHostnameId(host) || current.id;
89
+ const newAddresses = current.rawAddresses ?? [];
90
+ // Merge with an existing entry for the same key (e.g. IPv4 + IPv6 records
91
+ // for the same .local hostname both resolve to the same host:port key).
92
+ // Prefer TXT-sourced metadata from whichever record has it; a later partial
93
+ // record must not clobber a previously complete name/id/txt.
94
+ const existing = devices.get(key);
95
+ const mergedAddresses = [...(existing?.addresses ?? []), ...newAddresses].filter((addr, i, arr) => arr.indexOf(addr) === i); // deduplicate
96
+ // Select the richer txt: ignore empty objects (avahi emits txt=[] → {}) so a
97
+ // partial record with no real TXT data does not block a later complete payload.
98
+ const existingTxtContent = existing?.txt && Object.keys(existing.txt).length > 0 ? existing.txt : undefined;
99
+ const currentTxtContent = current.txt && Object.keys(current.txt).length > 0 ? current.txt : undefined;
100
+ // Prefer whichever txt is non-empty; if both are non-empty, keep the first seen.
101
+ const mergedTxt = existingTxtContent ?? currentTxtContent;
102
+ // For name: prefer TXT-sourced name from whichever record has content.
103
+ const mergedName = existingTxtContent?.name ||
104
+ currentTxtContent?.name ||
105
+ existing?.name ||
106
+ current.name ||
107
+ id ||
108
+ host;
109
+ // For id: prefer existing id (already validated) over newly derived id.
110
+ const mergedId = existing?.id || id;
111
+ devices.set(key, {
112
+ name: mergedName,
113
+ host,
114
+ port: current.port ?? 1111,
115
+ id: mergedId,
116
+ txt: mergedTxt,
117
+ addresses: mergedAddresses.length > 0 ? mergedAddresses : undefined,
118
+ });
119
+ };
120
+ for (const line of lines) {
121
+ // Resolved record header: "= wlan0 IPv4 FF1-HH9JSNOC _ff1._tcp local"
122
+ if (/^=\s/.test(line)) {
123
+ flushCurrent();
124
+ const parts = line.trim().split(/\s+/);
125
+ // parts: ['=', 'wlan0', 'IPv4', 'My Device Name', '_ff1._tcp', 'local']
126
+ // Service name may be multi-word; find the type token to bound the slice.
127
+ // Use a prefix regex so "_ff1._tcp.local" and "_ff1._tcp" both match.
128
+ // Preserve original case — resolveConfiguredDevice does exact-match lookups.
129
+ const typeIndex = parts.findIndex((p) => /^_ff1\._tcp/.test(p));
130
+ const serviceName = typeIndex > 3 ? parts.slice(3, typeIndex).join(' ') : parts[3] || '';
131
+ current = { name: serviceName };
132
+ continue;
133
+ }
134
+ if (!current) {
135
+ continue;
136
+ }
137
+ const hostnameMatch = line.match(/^\s+hostname\s*=\s*\[(.+)\]/);
138
+ if (hostnameMatch) {
139
+ current.rawHost = hostnameMatch[1];
140
+ continue;
141
+ }
142
+ const addressMatch = line.match(/^\s+address\s*=\s*\[(.+)\]/);
143
+ if (addressMatch) {
144
+ if (!current.rawAddresses) {
145
+ current.rawAddresses = [];
146
+ }
147
+ current.rawAddresses.push(addressMatch[1].trim());
148
+ continue;
149
+ }
150
+ const portMatch = line.match(/^\s+port\s*=\s*\[(\d+)\]/);
151
+ if (portMatch) {
152
+ current.port = parseInt(portMatch[1], 10);
153
+ continue;
154
+ }
155
+ const txtMatch = line.match(/^\s+txt\s*=\s*\[(.+)\]/);
156
+ if (txtMatch) {
157
+ const txt = {};
158
+ const pairs = txtMatch[1].matchAll(/"([^=]+)=([^"]*)"/g);
159
+ for (const [, k, v] of pairs) {
160
+ txt[k] = v;
161
+ }
162
+ current.txt = txt;
163
+ if (txt.name) {
164
+ current.name = txt.name;
165
+ }
166
+ if (txt.id) {
167
+ current.id = txt.id;
168
+ }
169
+ continue;
170
+ }
171
+ }
172
+ // Flush last record
173
+ flushCurrent();
174
+ return Array.from(devices.values()).sort((a, b) => a.name.localeCompare(b.name));
175
+ }
176
+ /**
177
+ * Resolve the result of an avahi-browse subprocess call into an FF1DiscoveryResult
178
+ * or null (which triggers the Bonjour fallback).
179
+ *
180
+ * Rules:
181
+ * - Clean exit (error === null): parse stdout and return whatever was found.
182
+ * - Non-zero exit + usable stdout: avahi-browse can be killed by the execFile
183
+ * timeout after emitting fully resolved records. Use the parsed devices if
184
+ * ≥1 was found; otherwise return empty result (avahi is present but slow —
185
+ * do NOT fall back to Bonjour, which is less reliable on Linux).
186
+ * - Timeout (error.killed === true) + no usable stdout: avahi is present but
187
+ * the scan produced nothing before the deadline. Return empty rather than
188
+ * falling back to Bonjour.
189
+ * - Command not found (ENOENT): avahi-browse is not installed — return null
190
+ * so the caller falls back to Bonjour.
191
+ * - Other error + no usable stdout: treat as unavailable — null.
192
+ *
193
+ * Exported for unit testing.
194
+ */
195
+ function resolveAvahiResult(error, stdout) {
196
+ if (error) {
197
+ if (stdout) {
198
+ try {
199
+ const devices = parseAvahiBrowseOutput(stdout);
200
+ if (devices.length > 0) {
201
+ return { devices };
202
+ }
203
+ }
204
+ catch {
205
+ // unparseable output — fall through
206
+ }
207
+ }
208
+ // Timeout: avahi is present but the scan was slow. Return empty rather than
209
+ // falling back to Bonjour, which is unreliable on Linux.
210
+ if (error.killed) {
211
+ return { devices: [], error: 'avahi-browse timed out before finding any devices' };
212
+ }
213
+ // ENOENT: avahi-browse not installed — fall back to Bonjour.
214
+ // All other errors with no usable output: also fall back.
215
+ return null;
216
+ }
217
+ try {
218
+ return { devices: parseAvahiBrowseOutput(stdout) };
219
+ }
220
+ catch {
221
+ return null;
222
+ }
223
+ }
224
+ /**
225
+ * Discover FF1 devices using avahi-browse (Linux).
226
+ * Returns null if avahi-browse is not available.
227
+ */
228
+ function discoverViaAvahi(options) {
229
+ return new Promise((resolve) => {
230
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
231
+ (0, child_process_1.execFile)('avahi-browse', ['-t', '-r', '_ff1._tcp'], { timeout: timeoutMs }, (error, stdout, _stderr) => {
232
+ resolve(resolveAvahiResult(error, stdout));
233
+ });
234
+ });
235
+ }
236
+ /**
237
+ * Discover FF1 devices via mDNS using the `_ff1._tcp` service.
238
+ * On Linux, uses avahi-browse for reliable multi-device discovery.
239
+ * Falls back to bonjour-service on other platforms or if avahi is unavailable.
240
+ *
241
+ * @param {Object} [options] - Discovery options
242
+ * @param {number} [options.timeoutMs] - How long to browse before returning results (bonjour fallback only)
243
+ * @returns {Promise<FF1DiscoveryResult>} Discovered FF1 devices and optional error
244
+ * @throws {Error} Never throws; returns empty list on errors
245
+ * @example
246
+ * const result = await discoverFF1Devices();
247
+ */
248
+ async function discoverFF1Devices(options = {},
249
+ // Injectable for testing — callers should omit these; defaults use the real implementations.
250
+ _avahiDiscovery = discoverViaAvahi, _bonjourDiscovery = discoverViaBonjour) {
251
+ if (process.platform === 'linux') {
252
+ const avahiResult = await _avahiDiscovery(options);
253
+ if (avahiResult !== null) {
254
+ return avahiResult;
255
+ }
256
+ }
257
+ return _bonjourDiscovery(options);
258
+ }
259
+ function discoverViaBonjour(options) {
260
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
261
+ return new Promise((resolve) => {
262
+ let resolved = false;
263
+ const finish = (result, error) => {
264
+ if (resolved) {
265
+ return;
266
+ }
267
+ resolved = true;
268
+ resolve({ devices: result, error });
269
+ };
270
+ try {
271
+ const bonjour = new bonjour_service_1.Bonjour();
272
+ const devices = new Map();
273
+ const browser = bonjour.find({ type: 'ff1', protocol: 'tcp' });
274
+ const finalize = (error) => {
275
+ try {
276
+ browser.stop();
277
+ bonjour.destroy();
278
+ }
279
+ catch (_error) {
280
+ finish([], error || 'mDNS discovery failed while stopping the browser');
281
+ return;
282
+ }
283
+ const result = Array.from(devices.values()).sort((left, right) => left.name.localeCompare(right.name));
284
+ finish(result, error);
285
+ };
286
+ const timeoutHandle = setTimeout(() => finalize(), timeoutMs);
287
+ browser.on('up', (service) => {
288
+ const host = normalizeMdnsHost(service.host || service.fqdn || '');
289
+ if (!host) {
290
+ return;
291
+ }
292
+ const port = service.port || 1111;
293
+ const txt = normalizeTxtRecords(service.txt);
294
+ const name = txt.name || service.name || txt.id || host;
295
+ const hostId = getHostnameId(host);
296
+ const id = hostId || txt.id || undefined;
297
+ const key = `${host}:${port}`;
298
+ const addresses = Array.isArray(service.addresses) && service.addresses.length > 0
299
+ ? service.addresses
300
+ : undefined;
301
+ devices.set(key, {
302
+ name,
303
+ host,
304
+ port,
305
+ id,
306
+ fqdn: service.fqdn,
307
+ txt,
308
+ addresses,
309
+ });
310
+ });
311
+ browser.on('error', (error) => {
312
+ clearTimeout(timeoutHandle);
313
+ const message = error instanceof Error ? error.message : String(error);
314
+ try {
315
+ browser.stop();
316
+ bonjour.destroy();
317
+ }
318
+ catch (_error) {
319
+ finish([], `mDNS discovery failed: ${message || 'failed to stop browser after error'}`);
320
+ return;
321
+ }
322
+ finalize(`mDNS discovery failed: ${message || 'discovery error'}`);
323
+ });
324
+ }
325
+ catch (error) {
326
+ const message = error instanceof Error ? error.message : String(error);
327
+ finish([], `mDNS discovery failed: ${message || 'discovery error'}`);
328
+ }
329
+ });
330
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Function Calling Layer
3
+ * These are the actual implementations called by AI orchestrator via function calling
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
+ const chalk = require('chalk');
39
+ const playlistBuilder = require('./playlist-builder');
40
+ const ff1Device = require('./ff1-device');
41
+ const domainResolver = require('./domain-resolver');
42
+ const logger = require('../logger');
43
+ /**
44
+ * Build DP1 v1.0.0 compliant playlist
45
+ *
46
+ * This is the actual implementation called by AI orchestrator's function calling.
47
+ * Uses core playlist-builder utilities.
48
+ *
49
+ * @param {Object} params - Build parameters
50
+ * @param {Array<Object>} params.items - Playlist items
51
+ * @param {string} [params.title] - Playlist title (auto-generated if not provided)
52
+ * @param {string} [params.slug] - Playlist slug (auto-generated if not provided)
53
+ * @returns {Promise<Object>} DP1 playlist
54
+ * @example
55
+ * const playlist = await buildDP1Playlist({ items, title: 'My Playlist' });
56
+ */
57
+ async function buildDP1Playlist(params) {
58
+ const { items, title, slug } = params;
59
+ return await playlistBuilder.buildDP1Playlist({ items, title, slug });
60
+ }
61
+ /**
62
+ * Send playlist to FF1 device
63
+ *
64
+ * This is the actual implementation called by AI orchestrator's function calling.
65
+ *
66
+ * @param {Object} params - Send parameters
67
+ * @param {Object} params.playlist - DP1 playlist
68
+ * @param {string} [params.deviceName] - Device name (null for first device)
69
+ * @returns {Promise<Object>} Result
70
+ * @returns {boolean} returns.success - Whether send succeeded
71
+ * @returns {string} [returns.deviceHost] - Device host address
72
+ * @returns {string} [returns.deviceName] - Device name
73
+ * @returns {string} [returns.error] - Error message if failed
74
+ * @example
75
+ * const result = await sendPlaylistToDevice({ playlist, deviceName: 'MyDevice' });
76
+ */
77
+ async function sendPlaylistToDevice(params) {
78
+ const { playlist, deviceName } = params;
79
+ const result = await ff1Device.sendPlaylistToDevice({
80
+ playlist,
81
+ deviceName,
82
+ });
83
+ if (result.success) {
84
+ console.log(chalk.green('\nSent to device'));
85
+ if (result.deviceName) {
86
+ console.log(chalk.dim(` ${result.deviceName}`));
87
+ }
88
+ }
89
+ else {
90
+ console.error(chalk.red('\nSend failed'));
91
+ if (result.error) {
92
+ console.error(chalk.red(` ${result.error}`));
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+ /**
98
+ * Resolve blockchain domain names to addresses
99
+ *
100
+ * This is the actual implementation called by AI orchestrator's function calling.
101
+ * Supports ENS (.eth) and TNS (.tez) domains with batch resolution.
102
+ *
103
+ * @param {Object} params - Resolution parameters
104
+ * @param {Array<string>} params.domains - Array of domain names to resolve
105
+ * @param {boolean} [params.displayResults] - Whether to display results (default: true)
106
+ * @returns {Promise<Object>} Resolution result
107
+ * @returns {boolean} returns.success - Whether at least one domain was resolved
108
+ * @returns {Object} returns.domainMap - Map of domain to resolved address
109
+ * @returns {Array<Object>} returns.resolutions - Detailed resolution results
110
+ * @returns {Array<string>} returns.errors - Array of error messages
111
+ * @example
112
+ * const result = await resolveDomains({ domains: ['vitalik.eth', 'alice.tez'] });
113
+ * console.log(result.domainMap); // { 'vitalik.eth': '0x...', 'alice.tez': 'tz...' }
114
+ */
115
+ async function resolveDomains(params) {
116
+ const { domains, displayResults = false } = params;
117
+ if (!domains || !Array.isArray(domains) || domains.length === 0) {
118
+ const error = 'No domains provided for resolution';
119
+ console.error(chalk.red(`\n${error}`));
120
+ return {
121
+ success: false,
122
+ domainMap: {},
123
+ resolutions: [],
124
+ errors: [error],
125
+ };
126
+ }
127
+ const result = await domainResolver.resolveDomainsBatch(domains);
128
+ if (displayResults) {
129
+ domainResolver.displayResolutionResults(result);
130
+ }
131
+ return result;
132
+ }
133
+ /**
134
+ * Verify a playlist against DP-1 specification
135
+ *
136
+ * This is the actual implementation called by AI orchestrator's function calling.
137
+ * Uses dp1-js parse/structure validation (same as the `validate` CLI). Does not
138
+ * verify signatures; use the `verify` CLI for cryptographic checks. Must be
139
+ * called before sending a playlist to a device from the orchestrator.
140
+ *
141
+ * @param {Object} params - Verification parameters
142
+ * @param {Object} params.playlist - Playlist object to verify
143
+ * @returns {Promise<Object>} Verification result
144
+ * @returns {boolean} returns.valid - Whether playlist is valid
145
+ * @returns {string} [returns.error] - Error message if invalid
146
+ * @returns {Array<Object>} [returns.details] - Detailed validation errors
147
+ * @example
148
+ * const result = await verifyPlaylist({ playlist });
149
+ * if (result.valid) {
150
+ * console.log('Playlist is valid');
151
+ * } else {
152
+ * console.error('Invalid:', result.error);
153
+ * }
154
+ */
155
+ async function verifyPlaylist(params) {
156
+ const { playlist } = params;
157
+ if (!playlist) {
158
+ return {
159
+ valid: false,
160
+ error: 'No playlist provided for verification',
161
+ };
162
+ }
163
+ logger.verbose(chalk.cyan('\nValidate playlist'));
164
+ // Dynamic import to avoid circular dependency
165
+ const playlistVerifier = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
166
+ const validate = playlistVerifier.validatePlaylist ||
167
+ (playlistVerifier.default && playlistVerifier.default.validatePlaylist) ||
168
+ playlistVerifier.default;
169
+ if (typeof validate !== 'function') {
170
+ return {
171
+ valid: false,
172
+ error: 'Playlist verifier is not available',
173
+ };
174
+ }
175
+ const result = await validate(playlist);
176
+ if (result.valid) {
177
+ logger.verbose(chalk.green('Playlist looks good'));
178
+ if (playlist.title) {
179
+ logger.verbose(chalk.dim(` Title: ${playlist.title}`));
180
+ }
181
+ if (playlist.items) {
182
+ logger.verbose(chalk.dim(` Items: ${playlist.items.length}`));
183
+ }
184
+ logger.verbose();
185
+ }
186
+ else {
187
+ console.error(chalk.red('Playlist has issues'));
188
+ console.error(chalk.red(` ${result.error}`));
189
+ if (result.details && result.details.length > 0) {
190
+ console.log(chalk.yellow('\n Missing or invalid fields:'));
191
+ result.details.forEach((detail) => {
192
+ console.log(chalk.yellow(` • ${detail.path}: ${detail.message}`));
193
+ });
194
+ }
195
+ console.log();
196
+ }
197
+ return result;
198
+ }
199
+ /**
200
+ * Verify and validate Ethereum and Tezos addresses
201
+ *
202
+ * This function is called by the intent parser to validate addresses entered by users.
203
+ * It provides detailed feedback on address validity and format issues.
204
+ *
205
+ * @param {Object} params - Verification parameters
206
+ * @param {Array<string>} params.addresses - Array of addresses to verify
207
+ * @returns {Promise<Object>} Verification result
208
+ * @returns {boolean} returns.valid - Whether all addresses are valid
209
+ * @returns {Array<Object>} returns.results - Detailed validation for each address
210
+ * @returns {Array<string>} returns.errors - List of validation errors
211
+ * @example
212
+ * const result = await verifyAddresses({
213
+ * addresses: ['0x1234567890123456789012345678901234567890', 'tz1VSUr8wwNhLAzempoch5d6hLKEUNvD14']
214
+ * });
215
+ * if (!result.valid) {
216
+ * result.errors.forEach(err => console.error(err));
217
+ * }
218
+ */
219
+ async function verifyAddresses(params) {
220
+ const { addresses } = params;
221
+ if (!addresses || !Array.isArray(addresses) || addresses.length === 0) {
222
+ return {
223
+ valid: false,
224
+ results: [],
225
+ errors: ['No addresses provided for verification'],
226
+ };
227
+ }
228
+ // Dynamic import to avoid circular dependency
229
+ const addressValidator = await Promise.resolve().then(() => __importStar(require('./address-validator')));
230
+ const validateAddresses = addressValidator.validateAddresses ||
231
+ (addressValidator.default && addressValidator.default.validateAddresses) ||
232
+ addressValidator.default;
233
+ if (typeof validateAddresses !== 'function') {
234
+ return {
235
+ valid: false,
236
+ results: [],
237
+ errors: ['Address validator is not available'],
238
+ };
239
+ }
240
+ const result = validateAddresses(addresses);
241
+ // Display results
242
+ if (!result.valid) {
243
+ console.error(chalk.red('\nAddress validation failed'));
244
+ result.errors.forEach((err) => {
245
+ console.error(chalk.red(` • ${err}`));
246
+ });
247
+ console.log();
248
+ }
249
+ return result;
250
+ }
251
+ /**
252
+ * Get list of configured FF1 devices
253
+ *
254
+ * This function retrieves the list of all configured FF1 devices from config.
255
+ * Called by intent parser to resolve generic device references like "FF1", "my device".
256
+ *
257
+ * @returns {Promise<Object>} Device list result
258
+ * @returns {boolean} returns.success - Whether devices were retrieved
259
+ * @returns {Array<Object>} returns.devices - Array of device configurations
260
+ * @returns {string} returns.devices[].name - Device name
261
+ * @returns {string} returns.devices[].host - Device host URL
262
+ * @returns {string} [returns.devices[].topicID] - Optional topic ID
263
+ * @returns {string} [returns.error] - Error message if no devices configured
264
+ * @example
265
+ * const result = await getConfiguredDevices();
266
+ * if (result.success && result.devices.length > 0) {
267
+ * const firstDevice = result.devices[0].name;
268
+ * }
269
+ */
270
+ async function getConfiguredDevices() {
271
+ const configModule = await Promise.resolve().then(() => __importStar(require('../config')));
272
+ const getFF1DeviceConfig = configModule.getFF1DeviceConfig ||
273
+ (configModule.default && configModule.default.getFF1DeviceConfig) ||
274
+ configModule.default;
275
+ if (typeof getFF1DeviceConfig !== 'function') {
276
+ return {
277
+ success: false,
278
+ devices: [],
279
+ error: 'FF1 device configuration is not available',
280
+ };
281
+ }
282
+ const deviceConfig = getFF1DeviceConfig();
283
+ if (!deviceConfig.devices || deviceConfig.devices.length === 0) {
284
+ return {
285
+ success: false,
286
+ devices: [],
287
+ error: 'No FF1 devices configured',
288
+ };
289
+ }
290
+ // Return sanitized device list (without API keys)
291
+ const devices = deviceConfig.devices.map((d) => ({
292
+ name: d.name || d.host,
293
+ host: d.host,
294
+ topicID: d.topicID,
295
+ }));
296
+ return {
297
+ success: true,
298
+ devices,
299
+ };
300
+ }
301
+ module.exports = {
302
+ buildDP1Playlist,
303
+ sendPlaylistToDevice,
304
+ resolveDomains,
305
+ verifyPlaylist,
306
+ getConfiguredDevices,
307
+ verifyAddresses,
308
+ };