@feralfile/cli 1.6.0 → 2.0.0

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.
@@ -1,1393 +0,0 @@
1
- "use strict";
2
- /**
3
- * Intent Parser
4
- * Parses user intent and breaks down into structured requirements.
5
- * Each requirement specifies: blockchain, contract address, token ID, source (media URL).
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.intentParserFunctionSchemas = void 0;
45
- exports.processIntentParserRequest = processIntentParserRequest;
46
- exports.buildIntentParserSystemPrompt = buildIntentParserSystemPrompt;
47
- const openai_1 = __importDefault(require("openai"));
48
- const chalk_1 = __importDefault(require("chalk"));
49
- const config_1 = require("../config");
50
- const utils_1 = require("./utils");
51
- const logger = __importStar(require("../logger"));
52
- // Cache for AI clients
53
- const clientCache = new Map();
54
- /**
55
- * Create AI client for intent parser
56
- *
57
- * @param {string} [modelName] - Model name
58
- * @returns {OpenAI} OpenAI client
59
- */
60
- function createIntentParserClient(modelName) {
61
- const config = (0, config_1.getConfig)();
62
- const selectedModel = modelName || config.defaultModel;
63
- if (clientCache.has(selectedModel)) {
64
- return clientCache.get(selectedModel);
65
- }
66
- const modelConfig = (0, config_1.getModelConfig)(selectedModel);
67
- const client = new openai_1.default({
68
- apiKey: modelConfig.apiKey,
69
- baseURL: modelConfig.baseURL,
70
- timeout: modelConfig.timeout,
71
- maxRetries: modelConfig.maxRetries,
72
- });
73
- clientCache.set(selectedModel, client);
74
- return client;
75
- }
76
- /**
77
- * Build intent parser system prompt
78
- *
79
- * @returns {string} System prompt for intent parser
80
- */
81
- function buildIntentParserSystemPrompt() {
82
- const deviceConfig = (0, config_1.getFF1DeviceConfig)();
83
- const hasDevices = deviceConfig.devices && deviceConfig.devices.length > 0;
84
- let deviceInfo = '';
85
- if (hasDevices) {
86
- const deviceList = deviceConfig.devices
87
- .map((d, i) => ` ${i + 1}. ${d.name || d.host}`)
88
- .filter((line) => !line.includes('undefined'))
89
- .join('\n');
90
- if (deviceList) {
91
- deviceInfo = `\n\nAVAILABLE FF1 DEVICES:\n${deviceList}`;
92
- }
93
- }
94
- return `SYSTEM: FF1 Intent Parser
95
-
96
- ROLE
97
- - Turn user text into deterministic parameters for non‑AI execution. Keep public output minimal and structured.
98
-
99
- REASONING (private scratchpad)
100
- - Use Plan→Check→Act→Reflect for each step.
101
- - Default to a single deterministic path.
102
- - Only branch in two cases:
103
- 1) Multiple plausible feed candidates after search.
104
- 2) Verification failure requiring targeted repair.
105
- - When branching, keep BEAM_WIDTH=2, DEPTH_LIMIT=2.
106
- - Score candidates by: correctness, coverage, determinism, freshness, cost.
107
- - Keep reasoning hidden; publicly print one status sentence before each tool call.
108
-
109
- OUTPUT CONTRACT
110
- - BUILD → call parse_requirements with { requirements: Requirement[], playlistSettings?: { title?: string | null, slug?: string | null, durationPerItem?: number, preserveOrder?: boolean, deviceName?: string, feedServer?: { baseUrl: string, apiKey?: string } } }
111
- - SEND → call confirm_send_playlist with { filePath: string, deviceName?: string }
112
- - PUBLISH (existing file) → call confirm_publish_playlist with { filePath: string, feedServer: { baseUrl: string, apiKey?: string } }
113
- - QUESTION → answer briefly (no tool call)
114
- - Use correct types; never truncate addresses/tokenIds; tokenIds are strings; quantity is a number.
115
-
116
- REQUIREMENT TYPES (BUILD)
117
- - build_playlist: { type, blockchain: "ethereum"|"tezos", contractAddress, tokenIds?: string[], quantity?: number, source?: string }
118
- • USE THIS when user mentions "contract" with a quantity: "N items from [blockchain] contract [address]"
119
- • tokenIds is OPTIONAL - omit it when user wants random tokens from a contract
120
- • Examples:
121
- - "tokens 1, 2, 3 from contract 0x123" → build_playlist with tokenIds: ["1", "2", "3"]
122
- - "100 items from ethereum contract 0xABC" → build_playlist with quantity: 100, NO tokenIds
123
- - "50 random tokens from tezos contract KT1..." → build_playlist with quantity: 50, NO tokenIds
124
- - feral_file_artwork: { type, artworkId } for Feral File artwork URLs or public artwork ids.
125
- • USE THIS only for feralfile.com/exhibitions/artwork URLs or explicit Feral File public artwork ids
126
- • Do not use this for ordinary blockchain contract prompts
127
- - query_address: { type, ownerAddress: 0x…|tz…|domain.eth|domain.tez, quantity?: number | "all" }
128
- • USE THIS for owner/wallet addresses WITHOUT the word "contract"
129
- • Patterns: "N items from [address]", "NFTs from [address]", "tokens owned by [address]"
130
- • Domains (.eth/.tez) are ALWAYS owner addresses
131
- • Example: "100 items from 0xABC" (without mentioning "contract") → query_address
132
- • When user says "all", "all tokens", "all NFTs" → use quantity="all" (string, not number)
133
- - fetch_feed: { type, playlistName: string, quantity?: number (default 5) }
134
-
135
- CRITICAL DISTINCTION:
136
- - User says "contract" + address → build_playlist (queries tokens FROM that contract)
137
- - User says just address (no "contract" word) → query_address (queries tokens OWNED by that address)
138
- - User says ".eth" or ".tez" domain → ALWAYS query_address (owner domain)
139
-
140
- DOMAIN OWNER RULES (CRITICAL)
141
- - Interpret \`*.eth\` as an Ethereum OWNER DOMAIN → produce \`query_address\` with \`ownerAddress\` set to the domain string (e.g., \`reas.eth\`).
142
- - Interpret \`*.tez\` as a Tezos OWNER DOMAIN → produce \`query_address\` with \`ownerAddress\` set to the domain string (e.g., \`einstein-rosen.tez\`).
143
- - Never treat \.eth or \.tez as a contract or collection identifier.
144
- - Never invent or request \`tokenIds\` for \.eth/\.tez domains. Use \`quantity\` only.
145
-
146
- EXAMPLES (query_address - owner/wallet addresses)
147
- - "Pick 3 artworks from reas.eth" → \`query_address\` { ownerAddress: "reas.eth", quantity: 3 }
148
- - "3 from einstein-rosen.tez and play on my FF1" → \`query_address\` { ownerAddress: "einstein-rosen.tez", quantity: 3 } and set \`playlistSettings.deviceName\`
149
- - "create a playlist of 30 items from 0xABC" → \`query_address\` { ownerAddress: "0xABC", quantity: 30 }
150
- - "get 20 NFTs from 0x123" → \`query_address\` { ownerAddress: "0x123", quantity: 20 }
151
- - "get all NFTs from 0xABC" → \`query_address\` { ownerAddress: "0xABC", quantity: "all" }
152
-
153
- EXAMPLES (build_playlist - contract addresses)
154
- - "tokens 5, 10, 15 from contract 0xABC on ethereum" → \`build_playlist\` { blockchain: "ethereum", contractAddress: "0xABC", tokenIds: ["5", "10", "15"] }
155
- - "create a playlist of 100 items from ethereum contract 0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270" → \`build_playlist\` { blockchain: "ethereum", contractAddress: "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270", quantity: 100 }
156
- - "100 random tokens from tezos contract KT1abc" → \`build_playlist\` { blockchain: "tezos", contractAddress: "KT1abc", quantity: 100 }
157
- - "get 50 from contract 0xDEF" → \`build_playlist\` { blockchain: "ethereum", contractAddress: "0xDEF", quantity: 50 }
158
-
159
- EXAMPLES (feral_file_artwork - public artwork URLs)
160
- - "build a playlist from https://feralfile.com/exhibitions/artwork/abc123" → \`feral_file_artwork\` { artworkId: "abc123" }
161
- - "play Feral File artwork f0240e04d64717e319584957f6a83954b029254ad1260b6320472ea8c0c5b1cf" → \`feral_file_artwork\` { artworkId: "f0240e04d64717e319584957f6a83954b029254ad1260b6320472ea8c0c5b1cf" }
162
-
163
- EXAMPLES (fetch_feed)
164
- - "Pick 3 artworks from Social Codes and 2 from a2p. Mix them up." → \`fetch_feed\` { playlistName: "Social Codes", quantity: 3 } + \`fetch_feed\` { playlistName: "a2p", quantity: 2 }, and set \`playlistSettings.preserveOrder\` = false
165
-
166
- PLAYLIST SETTINGS EXTRACTION
167
- - durationPerItem: parse phrases (e.g., "6 seconds each" → 6); OMIT entirely when the user gives no per-item time — omitted means auto timing (video/audio play their natural length, static media uses the configured default). Never invent a number.
168
- - preserveOrder: default true; synonyms ("shuffle", "randomize", "mix", "mix them up", "scramble") → false
169
- - title/slug: optional; include only if provided by the user
170
- - deviceName: from phrases like "send to", "display on", "play on", "push to"${hasDevices ? '\n- available devices:\n' + deviceInfo.replace('\n\nAVAILABLE FF1 DEVICES:\n', '') : ''}
171
-
172
- GENERIC DEVICE RESOLUTION (CRITICAL)
173
- - When the user references a generic device like "FF1", "my FF1", "my device", "my display", or similar (without a specific name), you MUST:
174
- 1. Immediately call get_configured_devices() to retrieve the list of devices
175
- 2. Extract the first device's name from the returned list
176
- 3. Use that exact device name in playlistSettings.deviceName
177
- 4. After resolving, acknowledge the resolved device name in your bullet summary (e.g., "send to device: Living Room")
178
- - Example: "push to my FF1" → call get_configured_devices() → use devices[0].name as deviceName → show "device: Living Room" in bullets
179
- - Do NOT ask the user which device to use when they say generic names like "FF1" or "my device"
180
-
181
- MISSING INFO POLICY (ASK AT MOST ONE QUESTION)
182
- - build_playlist: ask for blockchain/contract/tokenIds if unclear
183
- - fetch_feed: ask for playlistName if unclear
184
- - query_address: ask for owner/domain if unclear
185
- - send: ask for device name only if user specifies a device by name and it's ambiguous; for generic references, always use get_configured_devices
186
-
187
- ADDRESS VALIDATION (CRITICAL)
188
- - When user enters any Ethereum (0x...) or Tezos (tz.../KT1/KT...) addresses, IMMEDIATELY call verify_addresses() BEFORE parsing requirements
189
- - This includes: contract addresses in build_playlist, owner addresses in query_address, or any wallet/contract address mentioned
190
- - Example: user says "get tokens from 0xABC" → first call verify_addresses(['0xABC']) → get validation result → then parse_requirements
191
- - If verify_addresses returns valid=false, show user the error and ask them to provide the correct address
192
- - If valid=true, the validation result shows the blockchain type (ethereum or tezos) - use this information when parsing requirements
193
- - IMPORTANT: When user explicitly mentions blockchain (e.g., "ethereum contract" or "tezos contract"), you already know the blockchain type - DO NOT ask for it again
194
-
195
- FREE‑FORM COLLECTION NAMES
196
- - Treat as fetch_feed; do not guess contracts. If user says "some", default quantity = 5.
197
-
198
- FEED NAME HEURISTICS (CRITICAL)
199
- - If a source is named without an address or domain (no 0x… / tz… / *.eth / *.tez), interpret it as a feed playlist name and produce \`fetch_feed\` immediately.
200
- - Prefer acting over asking: only ask when there are zero matches or multiple plausible feed candidates after search.
201
- - Multi‑source phrasing like "X and Y" should yield multiple \`fetch_feed\` requirements, each with its own \`quantity\` when specified.
202
- - Never convert a plain name into a contract query; keep it as \`fetch_feed\`.
203
-
204
- SEND INTENT
205
- - Triggers: display/push/send/cast/send to device/play on FF1
206
- - Always call confirm_send_playlist with filePath (default "./playlist.json") and optional deviceName
207
- - Device selection: exact match → case‑insensitive → if multiple/none → ask user to choose
208
-
209
- PUBLISH INTENT (CRITICAL)
210
- - Triggers: "publish", "publish to my feed", "push to feed", "send to feed", "publish to feed"
211
- - Distinguish from FF1 device commands: "publish" = feed server, "display/send to device" = FF1 device
212
- - TWO MODES:
213
-
214
- MODE 1: BUILD AND PUBLISH (user includes sources/requirements)
215
- - Example: "Get tokens from 0xabc and publish to feed"
216
- - When user mentions publishing WITH sources/requirements:
217
- 1. Immediately call get_feed_servers() to retrieve available feed servers
218
- 2. If only 1 server → use it directly in playlistSettings.feedServer
219
- 3. If 2+ servers → ask user "Which feed server?" with numbered list (e.g., "1) https://feed.feralfile.com 2) http://localhost:8787")
220
- 4. After selection, set playlistSettings.feedServer = { baseUrl, apiKey } from selected server
221
- 5. Acknowledge in Settings bullets (e.g., "publish to: https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1")
222
- - User can request both device display AND publishing (e.g., "send to FF1 and publish to feed") → set both deviceName and feedServer
223
- - Publishing happens automatically after playlist verification passes
224
-
225
- MODE 2: PUBLISH EXISTING FILE (user mentions "publish playlist" or "publish the playlist")
226
- - Triggers: "publish playlist", "publish the playlist", "publish this playlist", "publish last playlist"
227
- - Default file path: "./playlist.json" (unless user specifies a different path like "publish ./playlist-temp.json")
228
- - When user wants to publish an existing file WITHOUT specifying sources:
229
- 1. Immediately call get_feed_servers() to retrieve available feed servers
230
- 2. If only 1 server → use it directly
231
- 3. If 2+ servers → ask user "Which feed server?" with numbered list
232
- 4. After selection, call confirm_publish_playlist with { filePath: "./playlist.json" (or user-specified path), feedServer: { baseUrl, apiKey } }
233
- - DO NOT ask for sources/requirements in this mode—user wants to publish an already-created playlist file
234
-
235
- COMMUNICATION STYLE
236
- - Acknowledge briefly: "Got it." or "Understood." (one line).
237
- - Do not repeat the user's request; do not paraphrase it.
238
- - Bullet the extracted facts using friendly labels (no camelCase):
239
- • What we're building (sources/collections/addresses)
240
- • Settings (duration per item, keep order or shuffle, device, title/slug if provided)
241
- - Prefer human units and plain words (e.g., "2 minutes per item", "send to device: Living Room").
242
- - When device is resolved via get_configured_devices, ALWAYS show the resolved device name in Settings bullets (e.g., "send to device: Living Room").
243
- - Use clear, direct language; no filler or corporate jargon; neutral, warm tone.
244
- - Immediately call the function when ready. No extra narration.`;
245
- }
246
- /**
247
- * Intent parser function schemas
248
- */
249
- const intentParserFunctionSchemas = [
250
- {
251
- type: 'function',
252
- function: {
253
- name: 'get_configured_devices',
254
- description: 'Get the list of configured FF1 devices. Call this IMMEDIATELY when the user references a generic device name like "FF1", "my FF1", "my device", "my display", or similar. Use the first device from the returned list.',
255
- parameters: {
256
- type: 'object',
257
- properties: {},
258
- },
259
- },
260
- },
261
- {
262
- type: 'function',
263
- function: {
264
- name: 'get_feed_servers',
265
- description: 'Get the list of configured feed servers for publishing playlists. Call this IMMEDIATELY when the user mentions "publish", "push to feed", "send to feed", or "publish to my feed". Return list of available feed servers.',
266
- parameters: {
267
- type: 'object',
268
- properties: {},
269
- },
270
- },
271
- },
272
- {
273
- type: 'function',
274
- function: {
275
- name: 'parse_requirements',
276
- description: 'Parse validated and complete requirements. Only call this when you have all required information for each requirement, such as blockchain/contract/token IDs, Feral File artwork id, feed playlist name, or owner address.',
277
- parameters: {
278
- type: 'object',
279
- properties: {
280
- requirements: {
281
- type: 'array',
282
- description: 'Array of parsed requirements. Each can be build_playlist (specific NFTs), feral_file_artwork (Feral File public artwork id or URL), fetch_feed (feed playlist), or query_address (all NFTs from address)',
283
- items: {
284
- type: 'object',
285
- properties: {
286
- type: {
287
- type: 'string',
288
- enum: ['build_playlist', 'feral_file_artwork', 'fetch_feed', 'query_address'],
289
- description: 'Type of requirement: build_playlist (specific NFTs), feral_file_artwork (Feral File artwork), fetch_feed (feed playlist), or query_address (all NFTs from address)',
290
- },
291
- blockchain: {
292
- type: 'string',
293
- description: 'Blockchain network (ethereum, tezos) - only for build_playlist type',
294
- },
295
- contractAddress: {
296
- type: 'string',
297
- description: 'NFT contract address - only for build_playlist type',
298
- },
299
- tokenIds: {
300
- type: 'array',
301
- description: 'Array of specific token IDs to fetch - only for build_playlist type. OPTIONAL: omit this field when user wants random tokens from the contract (e.g., "100 items from contract"). Only include when user specifies exact token IDs (e.g., "tokens 1, 2, 3").',
302
- items: {
303
- type: 'string',
304
- },
305
- },
306
- artworkId: {
307
- type: 'string',
308
- description: 'Feral File public artwork id or artwork URL - only for feral_file_artwork type',
309
- },
310
- ownerAddress: {
311
- type: 'string',
312
- description: 'Owner wallet address (0x... for Ethereum, tz... for Tezos) - only for query_address type',
313
- },
314
- source: {
315
- type: 'string',
316
- description: 'Media URL or source identifier - optional for build_playlist type',
317
- },
318
- playlistName: {
319
- type: 'string',
320
- description: 'Playlist name in the feed (can be any playlist name) - only for fetch_feed type',
321
- },
322
- quantity: {
323
- type: ['number', 'string'],
324
- description: 'Number of items to fetch. Can be a number for specific count, or "all" to fetch all available tokens (default: 5 for fetch_feed, all for query_address unless specified)',
325
- },
326
- },
327
- required: ['type'],
328
- },
329
- },
330
- playlistSettings: {
331
- type: 'object',
332
- description: 'Playlist configuration settings',
333
- properties: {
334
- title: {
335
- type: 'string',
336
- description: 'Playlist title (null for auto-generation)',
337
- },
338
- slug: {
339
- type: 'string',
340
- description: 'Playlist slug (null for auto-generation)',
341
- },
342
- durationPerItem: {
343
- type: 'number',
344
- description: 'Duration per item in seconds (e.g., 5 for "5 seconds each"). Omit when the user gives no per-item time: omitted = auto timing (video/audio play natural length per DP-1 §4.1).',
345
- },
346
- totalDuration: {
347
- type: 'number',
348
- description: 'Total playlist duration in seconds (optional)',
349
- },
350
- preserveOrder: {
351
- type: 'boolean',
352
- description: 'Whether to preserve source order (true) or randomize (false)',
353
- },
354
- deviceName: {
355
- type: 'string',
356
- description: 'Device name to display on (null for first device, omit if no display requested)',
357
- },
358
- feedServer: {
359
- type: 'object',
360
- description: 'Feed server for publishing playlist (omit if no publishing requested)',
361
- properties: {
362
- baseUrl: {
363
- type: 'string',
364
- description: 'Feed server base URL',
365
- },
366
- apiKey: {
367
- type: 'string',
368
- description: 'Optional API key for authentication',
369
- },
370
- },
371
- required: ['baseUrl'],
372
- },
373
- },
374
- },
375
- },
376
- required: ['requirements'],
377
- },
378
- },
379
- },
380
- {
381
- type: 'function',
382
- function: {
383
- name: 'confirm_send_playlist',
384
- description: 'Confirm the playlist file path or hosted URL and device name for sending. This function is called after the user mentions "send" or similar phrases.',
385
- parameters: {
386
- type: 'object',
387
- properties: {
388
- filePath: {
389
- type: 'string',
390
- description: 'Path to playlist file or playlist URL (default: "./playlist.json")',
391
- },
392
- deviceName: {
393
- type: 'string',
394
- description: 'Name of the device to send to (omit or leave empty if no specific device)',
395
- },
396
- },
397
- required: ['filePath'],
398
- },
399
- },
400
- },
401
- {
402
- type: 'function',
403
- function: {
404
- name: 'confirm_publish_playlist',
405
- description: 'Confirm the playlist file path and feed server for publishing. This function is called when the user wants to publish an existing playlist file (e.g., "publish playlist", "publish the playlist").',
406
- parameters: {
407
- type: 'object',
408
- properties: {
409
- filePath: {
410
- type: 'string',
411
- description: 'Path to the playlist file (default: "./playlist.json")',
412
- },
413
- feedServer: {
414
- type: 'object',
415
- description: 'Feed server configuration for publishing',
416
- properties: {
417
- baseUrl: {
418
- type: 'string',
419
- description: 'Feed server base URL',
420
- },
421
- apiKey: {
422
- type: 'string',
423
- description: 'Optional API key for authentication',
424
- },
425
- },
426
- required: ['baseUrl'],
427
- },
428
- },
429
- required: ['filePath', 'feedServer'],
430
- },
431
- },
432
- },
433
- {
434
- type: 'function',
435
- function: {
436
- name: 'verify_addresses',
437
- description: 'Verify and validate Ethereum (0x...) and Tezos (tz1/tz2/tz3/KT1) wallet addresses. Call this when you detect the user has entered addresses and need to validate them before proceeding to parse requirements. This helps catch format errors early.',
438
- parameters: {
439
- type: 'object',
440
- properties: {
441
- addresses: {
442
- type: 'array',
443
- description: 'Array of Ethereum or Tezos addresses to verify',
444
- items: {
445
- type: 'string',
446
- },
447
- },
448
- },
449
- required: ['addresses'],
450
- },
451
- },
452
- },
453
- ];
454
- exports.intentParserFunctionSchemas = intentParserFunctionSchemas;
455
- /**
456
- * Format markdown text for terminal display
457
- *
458
- * @param {string} text - Markdown text
459
- * @returns {string} Formatted text with styling
460
- */
461
- function formatMarkdown(text) {
462
- if (!text) {
463
- return '';
464
- }
465
- let formatted = text;
466
- // Headings: # text, ## text, ### text, etc.
467
- formatted = formatted.replace(/^(#{1,6})\s+(.+)$/gm, (_, hashes, content) => {
468
- const level = hashes.length;
469
- if (level === 1) {
470
- return chalk_1.default.bold.underline(content);
471
- }
472
- else if (level === 2) {
473
- return chalk_1.default.bold(content);
474
- }
475
- else {
476
- return chalk_1.default.bold(content);
477
- }
478
- });
479
- // Bold: **text** or __text__
480
- formatted = formatted.replace(/\*\*(.+?)\*\*/g, (_, p1) => chalk_1.default.bold(p1));
481
- formatted = formatted.replace(/__(.+?)__/g, (_, p1) => chalk_1.default.bold(p1));
482
- // Italic: *text* or _text_
483
- formatted = formatted.replace(/\*([^*]+)\*/g, (_, p1) => chalk_1.default.italic(p1));
484
- formatted = formatted.replace(/(?<!\w)_([^_]+)_(?!\w)/g, (_, p1) => chalk_1.default.italic(p1));
485
- // Inline code: `code` - light grey color
486
- formatted = formatted.replace(/`([^`]+)`/g, (_, p1) => chalk_1.default.dim(p1));
487
- // Links: [text](url) - show text in blue
488
- formatted = formatted.replace(/\[([^\]]+)\]\([^)]+\)/g, (_, p1) => chalk_1.default.blue(p1));
489
- return formatted;
490
- }
491
- function printMarkdownContent(content) {
492
- if (!content) {
493
- return;
494
- }
495
- const lines = content.split('\n');
496
- for (const line of lines) {
497
- if (line.trim()) {
498
- logger.verbose(formatMarkdown(line));
499
- }
500
- else if (line === '') {
501
- logger.verbose();
502
- }
503
- }
504
- }
505
- function sleep(ms) {
506
- return new Promise((resolve) => setTimeout(resolve, ms));
507
- }
508
- function extractDomains(text) {
509
- if (!text) {
510
- return [];
511
- }
512
- const matches = text.match(/[a-z0-9-]+\.(eth|tez)/gi);
513
- return matches ? matches.map((match) => match.toLowerCase()) : [];
514
- }
515
- function normalizeDomainInput(address, userDomains) {
516
- if (!address || userDomains.length === 0) {
517
- return address;
518
- }
519
- const lower = address.toLowerCase();
520
- if (!lower.endsWith('.eth') && !lower.endsWith('.tez')) {
521
- return address;
522
- }
523
- const baseName = lower.replace(/\.(eth|tez)$/i, '');
524
- const match = userDomains.find((domain) => domain.replace(/\.(eth|tez)$/i, '') === baseName);
525
- return match || address;
526
- }
527
- function normalizeRequirementDomains(params, userDomains) {
528
- if (userDomains.length === 0) {
529
- return params;
530
- }
531
- const normalized = params.requirements.map((req) => {
532
- if (req.type !== 'query_address' || typeof req.ownerAddress !== 'string') {
533
- return req;
534
- }
535
- return {
536
- ...req,
537
- ownerAddress: normalizeDomainInput(req.ownerAddress, userDomains),
538
- };
539
- });
540
- return {
541
- ...params,
542
- requirements: normalized,
543
- };
544
- }
545
- function buildToolResponseMessages(toolCalls, responses) {
546
- return toolCalls
547
- .filter((toolCall) => toolCall.id)
548
- .map((toolCall) => {
549
- const content = toolCall.id && Object.prototype.hasOwnProperty.call(responses, toolCall.id)
550
- ? responses[toolCall.id]
551
- : { error: `Unknown function: ${toolCall.function.name}` };
552
- return {
553
- role: 'tool',
554
- tool_call_id: toolCall.id,
555
- content: JSON.stringify(content),
556
- };
557
- });
558
- }
559
- async function processNonStreamingResponse(response) {
560
- const message = response.choices[0]?.message;
561
- if (!message) {
562
- return { message: { role: 'assistant', content: null, refusal: null } };
563
- }
564
- if (message.content) {
565
- printMarkdownContent(message.content);
566
- logger.verbose();
567
- }
568
- return { message };
569
- }
570
- async function createChatCompletion(client, requestParams, baseURL, maxRetries = 0) {
571
- const isGoogle = Boolean(baseURL && baseURL.includes('generativelanguage.googleapis.com'));
572
- const shouldStream = !isGoogle;
573
- for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
574
- try {
575
- if (shouldStream) {
576
- const stream = await client.chat.completions.create({
577
- ...requestParams,
578
- stream: true,
579
- });
580
- return await processStreamingResponse(stream);
581
- }
582
- const response = (await client.chat.completions.create({
583
- ...requestParams,
584
- stream: false,
585
- }));
586
- return await processNonStreamingResponse(response);
587
- }
588
- catch (error) {
589
- const err = error;
590
- const status = err.response?.status ?? err.status;
591
- if (status === 429 && attempt < maxRetries) {
592
- const retryAfterHeader = err.response?.headers?.['retry-after'] || err.response?.headers?.['Retry-After'];
593
- const retryAfterMs = retryAfterHeader ? Number(retryAfterHeader) * 1000 : null;
594
- const backoffMs = Math.min(10000, 2000 * Math.pow(2, attempt));
595
- const delayMs = retryAfterMs && !Number.isNaN(retryAfterMs) ? retryAfterMs : backoffMs;
596
- await sleep(delayMs);
597
- continue;
598
- }
599
- throw error;
600
- }
601
- }
602
- throw new Error('Failed to create chat completion');
603
- }
604
- /**
605
- * Process streaming response from AI
606
- *
607
- * @param {AsyncIterator} stream - OpenAI streaming response
608
- * @returns {Promise<Object>} Collected message with content and tool calls
609
- */
610
- async function processStreamingResponse(stream) {
611
- let contentBuffer = '';
612
- let toolCalls = [];
613
- const toolCallsMap = {};
614
- let role = 'assistant';
615
- let printedUpTo = 0;
616
- try {
617
- for await (const chunk of stream) {
618
- if (process.env.DEBUG_STREAMING) {
619
- console.log('\n[DEBUG] Chunk:', JSON.stringify(chunk, null, 2));
620
- }
621
- const delta = chunk.choices[0]?.delta;
622
- if (!delta) {
623
- continue;
624
- }
625
- if (delta.role) {
626
- role = delta.role;
627
- }
628
- // Collect content and print line by line
629
- if (delta.content) {
630
- contentBuffer += delta.content;
631
- const lastNewlineIndex = contentBuffer.lastIndexOf('\n', contentBuffer.length - 1);
632
- if (lastNewlineIndex >= printedUpTo) {
633
- const textToPrint = contentBuffer.substring(printedUpTo, lastNewlineIndex + 1);
634
- const lines = textToPrint.split('\n');
635
- for (const line of lines) {
636
- if (line.trim()) {
637
- const formatted = formatMarkdown(line);
638
- logger.verbose(formatted);
639
- }
640
- else if (line === '') {
641
- logger.verbose();
642
- }
643
- }
644
- printedUpTo = lastNewlineIndex + 1;
645
- }
646
- }
647
- // Collect tool calls
648
- if (delta.tool_calls) {
649
- for (const toolCallDelta of delta.tool_calls) {
650
- const index = toolCallDelta.index;
651
- if (!toolCallsMap[index]) {
652
- toolCallsMap[index] = {
653
- id: '',
654
- type: 'function',
655
- function: {
656
- name: '',
657
- arguments: '',
658
- },
659
- };
660
- }
661
- if (toolCallDelta.id) {
662
- toolCallsMap[index].id = toolCallDelta.id;
663
- }
664
- if (toolCallDelta.function) {
665
- if (toolCallDelta.function.name) {
666
- toolCallsMap[index].function.name += toolCallDelta.function.name;
667
- }
668
- if (toolCallDelta.function.arguments) {
669
- toolCallsMap[index].function.arguments += toolCallDelta.function.arguments;
670
- }
671
- }
672
- }
673
- }
674
- }
675
- }
676
- catch (error) {
677
- // Log streaming error but continue with what we have
678
- if (process.env.DEBUG) {
679
- console.error(chalk_1.default.red('\n[Streaming Error]'), error.message);
680
- }
681
- }
682
- // Print remaining content
683
- if (printedUpTo < contentBuffer.length) {
684
- const remainingText = contentBuffer.substring(printedUpTo);
685
- if (remainingText.trim()) {
686
- const formatted = formatMarkdown(remainingText);
687
- logger.verbose(formatted);
688
- }
689
- }
690
- if (contentBuffer.length > 0) {
691
- logger.verbose(); // Extra newline after AI response
692
- }
693
- // Convert toolCallsMap to array
694
- toolCalls = Object.values(toolCallsMap).filter((tc) => tc.id);
695
- const message = {
696
- role: role,
697
- content: contentBuffer.trim() || null,
698
- refusal: null,
699
- };
700
- if (toolCalls.length > 0) {
701
- message.tool_calls = toolCalls;
702
- }
703
- return { message };
704
- }
705
- /**
706
- * Process intent parser conversation
707
- *
708
- * @param {string} userRequest - User's natural language request
709
- * @param {Object} options - Options
710
- * @param {string} [options.modelName] - Model to use
711
- * @param {Object} [options.conversationContext] - Previous conversation context
712
- * @returns {Promise<Object>} Intent parser result
713
- */
714
- async function processIntentParserRequest(userRequest, options = {}) {
715
- const { modelName, conversationContext, defaultDeviceName } = options;
716
- const userDomains = extractDomains(userRequest);
717
- const client = createIntentParserClient(modelName);
718
- const modelConfig = (0, config_1.getModelConfig)(modelName);
719
- const config = (0, config_1.getConfig)();
720
- const systemMessage = buildIntentParserSystemPrompt();
721
- const messages = [
722
- { role: 'system', content: systemMessage },
723
- ];
724
- // Add conversation context if continuing
725
- if (conversationContext && conversationContext.messages) {
726
- messages.push(...conversationContext.messages);
727
- }
728
- messages.push({ role: 'user', content: userRequest });
729
- try {
730
- const requestParams = {
731
- model: modelConfig.model,
732
- messages,
733
- tools: intentParserFunctionSchemas,
734
- tool_choice: 'auto',
735
- stream: true,
736
- };
737
- // Set temperature based on model
738
- if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
739
- requestParams.temperature = modelConfig.temperature;
740
- }
741
- else if (modelConfig.temperature === 1) {
742
- requestParams.temperature = 1;
743
- }
744
- if (modelConfig.model.startsWith('gpt-')) {
745
- requestParams.max_completion_tokens = 2000;
746
- }
747
- else {
748
- requestParams.max_tokens = 2000;
749
- }
750
- const { message } = await createChatCompletion(client, requestParams, modelConfig.baseURL, modelConfig.maxRetries);
751
- // Check if AI wants to pass parsed requirements
752
- if (message.tool_calls && message.tool_calls.length > 0) {
753
- const toolCall = message.tool_calls[0];
754
- if (toolCall.function.name === 'get_configured_devices') {
755
- // Get the list of configured devices
756
- const { getConfiguredDevices } = await Promise.resolve().then(() => __importStar(require('../utilities/functions')));
757
- const result = await getConfiguredDevices();
758
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
759
- [toolCall.id]: result,
760
- });
761
- const updatedMessages = [...messages, message, ...toolResultMessages];
762
- // Continue the conversation with the device list
763
- const followUpRequest = {
764
- model: modelConfig.model,
765
- messages: updatedMessages,
766
- tools: intentParserFunctionSchemas,
767
- tool_choice: 'auto',
768
- stream: true,
769
- };
770
- if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
771
- followUpRequest.temperature = modelConfig.temperature;
772
- }
773
- else if (modelConfig.temperature === 1) {
774
- followUpRequest.temperature = 1;
775
- }
776
- if (modelConfig.model.startsWith('gpt-')) {
777
- followUpRequest.max_completion_tokens = 2000;
778
- }
779
- else {
780
- followUpRequest.max_tokens = 2000;
781
- }
782
- const { message: followUpMessage } = await createChatCompletion(client, followUpRequest, modelConfig.baseURL, modelConfig.maxRetries);
783
- // Check if AI now wants to parse requirements
784
- if (followUpMessage.tool_calls && followUpMessage.tool_calls.length > 0) {
785
- const followUpToolCall = followUpMessage.tool_calls[0];
786
- if (followUpToolCall.function.name === 'parse_requirements') {
787
- const params = normalizeRequirementDomains(JSON.parse(followUpToolCall.function.arguments), userDomains);
788
- // Apply constraints and defaults
789
- const validatedParams = (0, utils_1.applyConstraints)(params, config);
790
- return {
791
- approved: true,
792
- params: validatedParams,
793
- needsMoreInfo: false,
794
- };
795
- }
796
- else if (followUpToolCall.function.name === 'get_feed_servers') {
797
- // Get the list of configured feed servers
798
- const feedConfig = (0, config_1.getFeedConfig)();
799
- // Build the server list for the AI
800
- const serverList = feedConfig.servers?.map((server) => ({
801
- baseUrl: server.baseUrl,
802
- apiKey: server.apiKey,
803
- })) ||
804
- feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
805
- const feedToolResultMessages = buildToolResponseMessages(followUpMessage.tool_calls, {
806
- [followUpToolCall.id]: { servers: serverList },
807
- });
808
- const feedUpdatedMessages = [
809
- ...updatedMessages,
810
- followUpMessage,
811
- ...feedToolResultMessages,
812
- ];
813
- // Continue the conversation with the feed server list
814
- const feedFollowUpRequest = {
815
- model: modelConfig.model,
816
- messages: feedUpdatedMessages,
817
- tools: intentParserFunctionSchemas,
818
- tool_choice: 'auto',
819
- stream: true,
820
- };
821
- if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
822
- feedFollowUpRequest.temperature = modelConfig.temperature;
823
- }
824
- else if (modelConfig.temperature === 1) {
825
- feedFollowUpRequest.temperature = 1;
826
- }
827
- if (modelConfig.model.startsWith('gpt-')) {
828
- feedFollowUpRequest.max_completion_tokens =
829
- 2000;
830
- }
831
- else {
832
- feedFollowUpRequest.max_tokens = 2000;
833
- }
834
- const { message: feedFollowUpMessage } = await createChatCompletion(client, feedFollowUpRequest, modelConfig.baseURL, modelConfig.maxRetries);
835
- // Check if AI now wants to parse requirements or publish
836
- if (feedFollowUpMessage.tool_calls && feedFollowUpMessage.tool_calls.length > 0) {
837
- const feedToolCall = feedFollowUpMessage.tool_calls[0];
838
- if (feedToolCall.function.name === 'parse_requirements') {
839
- const params = normalizeRequirementDomains(JSON.parse(feedToolCall.function.arguments), userDomains);
840
- // Apply constraints and defaults
841
- const validatedParams = (0, utils_1.applyConstraints)(params, config);
842
- return {
843
- approved: true,
844
- params: validatedParams,
845
- needsMoreInfo: false,
846
- };
847
- }
848
- else if (feedToolCall.function.name === 'confirm_publish_playlist') {
849
- const args = JSON.parse(feedToolCall.function.arguments);
850
- const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
851
- console.log();
852
- console.log(chalk_1.default.cyan('Publishing to feed server...'));
853
- const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
854
- if (publishResult.success) {
855
- console.log(chalk_1.default.green('Published to feed server'));
856
- if (publishResult.playlistId) {
857
- console.log(chalk_1.default.dim(` Playlist ID: ${publishResult.playlistId}`));
858
- }
859
- if (publishResult.feedServer) {
860
- console.log(chalk_1.default.dim(` Server: ${publishResult.feedServer}`));
861
- }
862
- console.log();
863
- return {
864
- approved: true,
865
- params: {
866
- action: 'publish_playlist',
867
- filePath: args.filePath,
868
- feedServer: args.feedServer,
869
- playlistId: publishResult.playlistId,
870
- success: true,
871
- },
872
- needsMoreInfo: false,
873
- };
874
- }
875
- else {
876
- console.error(chalk_1.default.red('Publish failed: ' + publishResult.error));
877
- if (publishResult.message) {
878
- console.error(chalk_1.default.dim(` ${publishResult.message}`));
879
- }
880
- console.log();
881
- return {
882
- approved: false,
883
- needsMoreInfo: false,
884
- params: {
885
- action: 'publish_playlist',
886
- success: false,
887
- error: publishResult.error,
888
- },
889
- };
890
- }
891
- }
892
- }
893
- // AI might be asking a question or needs more info
894
- return {
895
- approved: false,
896
- needsMoreInfo: true,
897
- question: feedFollowUpMessage.content || undefined,
898
- messages: [...feedUpdatedMessages, feedFollowUpMessage],
899
- };
900
- }
901
- else if (followUpToolCall.function.name === 'confirm_publish_playlist') {
902
- // Handle publish after feed server selection
903
- const args = JSON.parse(followUpToolCall.function.arguments);
904
- const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
905
- console.log();
906
- console.log(chalk_1.default.cyan('Publishing to feed server...'));
907
- const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
908
- if (publishResult.success) {
909
- console.log(chalk_1.default.green('Published to feed server'));
910
- if (publishResult.playlistId) {
911
- console.log(chalk_1.default.dim(` Playlist ID: ${publishResult.playlistId}`));
912
- }
913
- if (publishResult.feedServer) {
914
- console.log(chalk_1.default.dim(` Server: ${publishResult.feedServer}`));
915
- }
916
- console.log();
917
- return {
918
- approved: true,
919
- params: {
920
- action: 'publish_playlist',
921
- filePath: args.filePath,
922
- feedServer: args.feedServer,
923
- playlistId: publishResult.playlistId,
924
- success: true,
925
- },
926
- needsMoreInfo: false,
927
- };
928
- }
929
- else {
930
- console.error(chalk_1.default.red('Publish failed: ' + publishResult.error));
931
- if (publishResult.message) {
932
- console.error(chalk_1.default.dim(` ${publishResult.message}`));
933
- }
934
- console.log();
935
- return {
936
- approved: false,
937
- needsMoreInfo: false,
938
- params: {
939
- action: 'publish_playlist',
940
- success: false,
941
- error: publishResult.error,
942
- },
943
- };
944
- }
945
- }
946
- else {
947
- const toolResultMessages = buildToolResponseMessages(followUpMessage.tool_calls, {});
948
- const validMessages = [...updatedMessages, followUpMessage, ...toolResultMessages];
949
- // AI is still asking for more information after the error
950
- return {
951
- approved: false,
952
- needsMoreInfo: true,
953
- question: followUpMessage.content ||
954
- `Encountered unknown function: ${followUpToolCall.function.name}`,
955
- messages: validMessages,
956
- };
957
- }
958
- }
959
- // AI is still asking for more information
960
- return {
961
- approved: false,
962
- needsMoreInfo: true,
963
- question: followUpMessage.content || undefined,
964
- messages: [...updatedMessages, followUpMessage],
965
- };
966
- }
967
- else if (toolCall.function.name === 'get_feed_servers') {
968
- // Get the list of configured feed servers
969
- const feedConfig = (0, config_1.getFeedConfig)();
970
- // Build the server list for the AI
971
- const serverList = feedConfig.servers?.map((server) => ({
972
- baseUrl: server.baseUrl,
973
- apiKey: server.apiKey,
974
- })) || feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
975
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
976
- [toolCall.id]: { servers: serverList },
977
- });
978
- const updatedMessages = [...messages, message, ...toolResultMessages];
979
- // Continue the conversation with the feed server list
980
- const followUpRequest = {
981
- model: modelConfig.model,
982
- messages: updatedMessages,
983
- tools: intentParserFunctionSchemas,
984
- tool_choice: 'auto',
985
- stream: true,
986
- };
987
- if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
988
- followUpRequest.temperature = modelConfig.temperature;
989
- }
990
- else if (modelConfig.temperature === 1) {
991
- followUpRequest.temperature = 1;
992
- }
993
- if (modelConfig.model.startsWith('gpt-')) {
994
- followUpRequest.max_completion_tokens = 2000;
995
- }
996
- else {
997
- followUpRequest.max_tokens = 2000;
998
- }
999
- const { message: followUpMessage } = await createChatCompletion(client, followUpRequest, modelConfig.baseURL, modelConfig.maxRetries);
1000
- // Check if AI now wants to parse requirements or publish
1001
- if (followUpMessage.tool_calls && followUpMessage.tool_calls.length > 0) {
1002
- const followUpToolCall = followUpMessage.tool_calls[0];
1003
- if (followUpToolCall.function.name === 'parse_requirements') {
1004
- const params = normalizeRequirementDomains(JSON.parse(followUpToolCall.function.arguments), userDomains);
1005
- // Apply constraints and defaults
1006
- const validatedParams = (0, utils_1.applyConstraints)(params, config);
1007
- return {
1008
- approved: true,
1009
- params: validatedParams,
1010
- needsMoreInfo: false,
1011
- };
1012
- }
1013
- else if (followUpToolCall.function.name === 'confirm_publish_playlist') {
1014
- const args = JSON.parse(followUpToolCall.function.arguments);
1015
- const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
1016
- console.log();
1017
- console.log(chalk_1.default.cyan('Publishing to feed server...'));
1018
- const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
1019
- if (publishResult.success) {
1020
- console.log(chalk_1.default.green('Published to feed server'));
1021
- if (publishResult.playlistId) {
1022
- console.log(chalk_1.default.dim(` Playlist ID: ${publishResult.playlistId}`));
1023
- }
1024
- if (publishResult.feedServer) {
1025
- console.log(chalk_1.default.dim(` Server: ${publishResult.feedServer}`));
1026
- }
1027
- console.log();
1028
- return {
1029
- approved: true,
1030
- params: {
1031
- action: 'publish_playlist',
1032
- filePath: args.filePath,
1033
- feedServer: args.feedServer,
1034
- playlistId: publishResult.playlistId,
1035
- success: true,
1036
- },
1037
- needsMoreInfo: false,
1038
- };
1039
- }
1040
- else {
1041
- console.error(chalk_1.default.red('Publish failed: ' + publishResult.error));
1042
- if (publishResult.message) {
1043
- console.error(chalk_1.default.dim(` ${publishResult.message}`));
1044
- }
1045
- console.log();
1046
- return {
1047
- approved: false,
1048
- needsMoreInfo: false,
1049
- params: {
1050
- action: 'publish_playlist',
1051
- success: false,
1052
- error: publishResult.error,
1053
- },
1054
- };
1055
- }
1056
- }
1057
- }
1058
- // AI might be asking a question or needs more info
1059
- return {
1060
- approved: false,
1061
- needsMoreInfo: true,
1062
- question: followUpMessage.content || undefined,
1063
- messages: [...updatedMessages, followUpMessage],
1064
- };
1065
- }
1066
- else if (toolCall.function.name === 'parse_requirements') {
1067
- const params = normalizeRequirementDomains(JSON.parse(toolCall.function.arguments), userDomains);
1068
- // Apply constraints and defaults
1069
- const validatedParams = (0, utils_1.applyConstraints)(params, config);
1070
- return {
1071
- approved: true,
1072
- params: validatedParams,
1073
- needsMoreInfo: false,
1074
- };
1075
- }
1076
- else if (toolCall.function.name === 'confirm_send_playlist') {
1077
- const args = JSON.parse(toolCall.function.arguments);
1078
- const { confirmPlaylistForSending } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-send')));
1079
- // Validate and confirm the playlist.
1080
- // args.deviceName may be null/undefined/"null" when the model omits it;
1081
- // fall back to the CLI --device flag so `ff-cli chat --device kitchen` works.
1082
- const resolvedDeviceName = args.deviceName && args.deviceName !== 'null' ? args.deviceName : defaultDeviceName;
1083
- const confirmation = await confirmPlaylistForSending(args.filePath, resolvedDeviceName);
1084
- if (!confirmation.success) {
1085
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
1086
- [toolCall.id]: {
1087
- success: false,
1088
- error: confirmation.error,
1089
- message: confirmation.message,
1090
- },
1091
- });
1092
- const validMessages = [...messages, message, ...toolResultMessages];
1093
- // Check if this is a device selection needed case
1094
- if (confirmation.needsDeviceSelection) {
1095
- // Multiple devices available - ask user to choose
1096
- console.log();
1097
- return {
1098
- approved: false,
1099
- needsMoreInfo: true,
1100
- question: confirmation.message || 'Please choose a device',
1101
- messages: validMessages,
1102
- };
1103
- }
1104
- // File not found or playlist invalid - ask user for more info
1105
- console.log();
1106
- return {
1107
- approved: false,
1108
- needsMoreInfo: true,
1109
- question: confirmation.message || `Failed to send playlist: ${confirmation.error}`,
1110
- messages: validMessages,
1111
- };
1112
- }
1113
- // Playlist is valid - return as approved with send_playlist action
1114
- return {
1115
- approved: true,
1116
- params: {
1117
- action: 'send_playlist',
1118
- filePath: confirmation.filePath,
1119
- deviceName: confirmation.deviceName,
1120
- playlist: confirmation.playlist,
1121
- message: confirmation.message,
1122
- },
1123
- needsMoreInfo: false,
1124
- };
1125
- }
1126
- else if (toolCall.function.name === 'confirm_publish_playlist') {
1127
- const args = JSON.parse(toolCall.function.arguments);
1128
- const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
1129
- // Publish the playlist
1130
- console.log();
1131
- console.log(chalk_1.default.cyan('Publishing to feed server...'));
1132
- const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
1133
- if (publishResult.success) {
1134
- console.log(chalk_1.default.green('Published to feed server'));
1135
- if (publishResult.playlistId) {
1136
- console.log(chalk_1.default.dim(` Playlist ID: ${publishResult.playlistId}`));
1137
- }
1138
- if (publishResult.feedServer) {
1139
- console.log(chalk_1.default.dim(` Server: ${publishResult.feedServer}`));
1140
- }
1141
- console.log();
1142
- return {
1143
- approved: true,
1144
- params: {
1145
- action: 'publish_playlist',
1146
- filePath: args.filePath,
1147
- feedServer: args.feedServer,
1148
- playlistId: publishResult.playlistId,
1149
- success: true,
1150
- },
1151
- needsMoreInfo: false,
1152
- };
1153
- }
1154
- else {
1155
- console.error(chalk_1.default.red('Publish failed: ' + publishResult.error));
1156
- if (publishResult.message) {
1157
- console.error(chalk_1.default.dim(` ${publishResult.message}`));
1158
- }
1159
- console.log();
1160
- return {
1161
- approved: false,
1162
- needsMoreInfo: false,
1163
- params: {
1164
- action: 'publish_playlist',
1165
- success: false,
1166
- error: publishResult.error,
1167
- },
1168
- };
1169
- }
1170
- }
1171
- else if (toolCall.function.name === 'verify_addresses') {
1172
- const args = JSON.parse(toolCall.function.arguments);
1173
- if (Array.isArray(args.addresses) && userDomains.length > 0) {
1174
- args.addresses = args.addresses.map((address) => normalizeDomainInput(address, userDomains));
1175
- }
1176
- const { verifyAddresses } = await Promise.resolve().then(() => __importStar(require('../utilities/functions')));
1177
- const verificationResult = await verifyAddresses({ addresses: args.addresses });
1178
- if (!verificationResult.valid) {
1179
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
1180
- [toolCall.id]: {
1181
- valid: false,
1182
- errors: verificationResult.errors,
1183
- results: verificationResult.results,
1184
- },
1185
- });
1186
- const validMessages = [...messages, message, ...toolResultMessages];
1187
- // Ask user to correct the addresses
1188
- return {
1189
- approved: false,
1190
- needsMoreInfo: true,
1191
- question: `Some addresses are invalid. ${verificationResult.errors.join(' ')} Please provide correct addresses.`,
1192
- messages: validMessages,
1193
- };
1194
- }
1195
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
1196
- [toolCall.id]: {
1197
- valid: true,
1198
- results: verificationResult.results,
1199
- },
1200
- });
1201
- const validMessages = [...messages, message, ...toolResultMessages];
1202
- // Continue conversation after validation
1203
- const followUpRequest = {
1204
- model: modelConfig.model,
1205
- messages: validMessages,
1206
- tools: intentParserFunctionSchemas,
1207
- tool_choice: 'auto',
1208
- stream: true,
1209
- };
1210
- if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
1211
- followUpRequest.temperature = modelConfig.temperature;
1212
- }
1213
- else if (modelConfig.temperature === 1) {
1214
- followUpRequest.temperature = 1;
1215
- }
1216
- if (modelConfig.model.startsWith('gpt-')) {
1217
- followUpRequest.max_completion_tokens = 2000;
1218
- }
1219
- else {
1220
- followUpRequest.max_tokens = 2000;
1221
- }
1222
- const { message: followUpMessage } = await createChatCompletion(client, followUpRequest, modelConfig.baseURL, modelConfig.maxRetries);
1223
- // Check if AI now wants to parse requirements or get feed servers
1224
- if (followUpMessage.tool_calls && followUpMessage.tool_calls.length > 0) {
1225
- const followUpToolCall = followUpMessage.tool_calls[0];
1226
- if (followUpToolCall.function.name === 'parse_requirements') {
1227
- const params = normalizeRequirementDomains(JSON.parse(followUpToolCall.function.arguments), userDomains);
1228
- // Apply constraints and defaults
1229
- const validatedParams = (0, utils_1.applyConstraints)(params, config);
1230
- return {
1231
- approved: true,
1232
- params: validatedParams,
1233
- needsMoreInfo: false,
1234
- };
1235
- }
1236
- else if (followUpToolCall.function.name === 'get_feed_servers') {
1237
- // Get the list of configured feed servers
1238
- const feedConfig = (0, config_1.getFeedConfig)();
1239
- // Build the server list for the AI
1240
- const serverList = feedConfig.servers?.map((server) => ({
1241
- baseUrl: server.baseUrl,
1242
- apiKey: server.apiKey,
1243
- })) ||
1244
- feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
1245
- // Add tool result to messages and continue conversation
1246
- const feedToolResultMessages = buildToolResponseMessages(followUpMessage.tool_calls, {
1247
- [followUpToolCall.id]: { servers: serverList },
1248
- });
1249
- const feedUpdatedMessages = [
1250
- ...validMessages,
1251
- followUpMessage,
1252
- ...feedToolResultMessages,
1253
- ];
1254
- // Continue the conversation with the feed server list
1255
- const feedFollowUpRequest = {
1256
- model: modelConfig.model,
1257
- messages: feedUpdatedMessages,
1258
- tools: intentParserFunctionSchemas,
1259
- tool_choice: 'auto',
1260
- stream: true,
1261
- };
1262
- if (modelConfig.temperature !== undefined && modelConfig.temperature !== 1) {
1263
- feedFollowUpRequest.temperature = modelConfig.temperature;
1264
- }
1265
- else if (modelConfig.temperature === 1) {
1266
- feedFollowUpRequest.temperature = 1;
1267
- }
1268
- if (modelConfig.model.startsWith('gpt-')) {
1269
- feedFollowUpRequest.max_completion_tokens =
1270
- 2000;
1271
- }
1272
- else {
1273
- feedFollowUpRequest.max_tokens = 2000;
1274
- }
1275
- const { message: feedFollowUpMessage } = await createChatCompletion(client, feedFollowUpRequest, modelConfig.baseURL, modelConfig.maxRetries);
1276
- // Check if AI now wants to parse requirements or publish
1277
- if (feedFollowUpMessage.tool_calls && feedFollowUpMessage.tool_calls.length > 0) {
1278
- const feedToolCall = feedFollowUpMessage.tool_calls[0];
1279
- if (feedToolCall.function.name === 'parse_requirements') {
1280
- const params = normalizeRequirementDomains(JSON.parse(feedToolCall.function.arguments), userDomains);
1281
- // Apply constraints and defaults
1282
- const validatedParams = (0, utils_1.applyConstraints)(params, config);
1283
- return {
1284
- approved: true,
1285
- params: validatedParams,
1286
- needsMoreInfo: false,
1287
- };
1288
- }
1289
- else if (feedToolCall.function.name === 'confirm_publish_playlist') {
1290
- const args = JSON.parse(feedToolCall.function.arguments);
1291
- const { publishPlaylist } = await Promise.resolve().then(() => __importStar(require('../utilities/playlist-publisher')));
1292
- console.log();
1293
- console.log(chalk_1.default.cyan('Publishing to feed server...'));
1294
- const publishResult = await publishPlaylist(args.filePath, args.feedServer.baseUrl, args.feedServer.apiKey);
1295
- if (publishResult.success) {
1296
- console.log(chalk_1.default.green('Published to feed server'));
1297
- if (publishResult.playlistId) {
1298
- console.log(chalk_1.default.dim(` Playlist ID: ${publishResult.playlistId}`));
1299
- }
1300
- if (publishResult.feedServer) {
1301
- console.log(chalk_1.default.dim(` Server: ${publishResult.feedServer}`));
1302
- }
1303
- console.log();
1304
- return {
1305
- approved: true,
1306
- params: {
1307
- action: 'publish_playlist',
1308
- filePath: args.filePath,
1309
- feedServer: args.feedServer,
1310
- playlistId: publishResult.playlistId,
1311
- success: true,
1312
- },
1313
- needsMoreInfo: false,
1314
- };
1315
- }
1316
- else {
1317
- console.error(chalk_1.default.red('Publish failed: ' + publishResult.error));
1318
- if (publishResult.message) {
1319
- console.error(chalk_1.default.dim(` ${publishResult.message}`));
1320
- }
1321
- console.log();
1322
- return {
1323
- approved: false,
1324
- needsMoreInfo: false,
1325
- params: {
1326
- action: 'publish_playlist',
1327
- success: false,
1328
- error: publishResult.error,
1329
- },
1330
- };
1331
- }
1332
- }
1333
- }
1334
- // AI might be asking a question or needs more info
1335
- return {
1336
- approved: false,
1337
- needsMoreInfo: true,
1338
- question: feedFollowUpMessage.content || undefined,
1339
- messages: [...feedUpdatedMessages, feedFollowUpMessage],
1340
- };
1341
- }
1342
- }
1343
- // AI might be asking a question or needs more info
1344
- if (followUpMessage.content) {
1345
- return {
1346
- approved: false,
1347
- needsMoreInfo: true,
1348
- question: followUpMessage.content,
1349
- messages: [...validMessages, followUpMessage],
1350
- };
1351
- }
1352
- return {
1353
- approved: false,
1354
- needsMoreInfo: false,
1355
- };
1356
- }
1357
- else {
1358
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {});
1359
- const validMessages = [...messages, message, ...toolResultMessages];
1360
- return {
1361
- approved: false,
1362
- needsMoreInfo: true,
1363
- question: message.content || `Encountered unknown function: ${toolCall.function.name}`,
1364
- messages: validMessages,
1365
- };
1366
- }
1367
- }
1368
- // AI is asking for more information
1369
- return {
1370
- approved: false,
1371
- needsMoreInfo: true,
1372
- question: message.content || undefined,
1373
- messages: [...messages, message],
1374
- };
1375
- }
1376
- catch (error) {
1377
- const err = error;
1378
- const status = err.response?.status ?? err.status;
1379
- const statusText = err.response?.statusText;
1380
- const responseDetails = err.response?.data && typeof err.response.data === 'string'
1381
- ? err.response.data
1382
- : err.response?.data
1383
- ? JSON.stringify(err.response.data)
1384
- : null;
1385
- const context = `model=${modelConfig.model}, baseURL=${modelConfig.baseURL}`;
1386
- const detailParts = [
1387
- err.message,
1388
- status ? `status ${status}${statusText ? ` ${statusText}` : ''}` : null,
1389
- responseDetails ? `response ${responseDetails}` : null,
1390
- ].filter(Boolean);
1391
- throw new Error(`Intent parser failed (${context}): ${detailParts.join(' | ')}`);
1392
- }
1393
- }