ff1-cli 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/config.json.example +1 -1
- package/dist/index.js +16 -2
- package/dist/src/config.js +6 -28
- package/dist/src/intent-parser/index.js +70 -54
- package/docs/CONFIGURATION.md +3 -3
- package/docs/EXAMPLES.md +5 -10
- package/docs/README.md +3 -3
- package/docs/RELEASING.md +28 -4
- package/package.json +1 -9
package/README.md
CHANGED
|
@@ -10,6 +10,14 @@ FF1-CLI turns a simple prompt into a DP-1–conformant playlist you can preview
|
|
|
10
10
|
npm i -g ff1-cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Install (curl)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
curl -fsSL https://feralfile.com/ff1-cli-install | bash
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Installs a prebuilt binary for macOS/Linux (no Node.js required).
|
|
20
|
+
|
|
13
21
|
## One-off Usage (npx)
|
|
14
22
|
|
|
15
23
|
```bash
|
package/config.json.example
CHANGED
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
38
|
};
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
// Suppress punycode deprecation
|
|
40
|
+
// Suppress punycode deprecation warnings from dependencies
|
|
41
41
|
process.removeAllListeners('warning');
|
|
42
42
|
process.on('warning', (warning) => {
|
|
43
43
|
if (warning.name === 'DeprecationWarning' && warning.message.includes('punycode')) {
|
|
@@ -51,8 +51,22 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
51
51
|
const fs_1 = require("fs");
|
|
52
52
|
const crypto_1 = __importDefault(require("crypto"));
|
|
53
53
|
const readline = __importStar(require("readline"));
|
|
54
|
+
const fs_2 = require("fs");
|
|
55
|
+
const path_1 = require("path");
|
|
54
56
|
const config_1 = require("./src/config");
|
|
55
57
|
const main_1 = require("./src/main");
|
|
58
|
+
// Load version from package.json
|
|
59
|
+
// Try built location first (dist/index.js -> ../package.json)
|
|
60
|
+
// Fall back to dev location (index.ts -> ./package.json)
|
|
61
|
+
let packageJsonPath = (0, path_1.resolve)((0, path_1.dirname)(__filename), '..', 'package.json');
|
|
62
|
+
try {
|
|
63
|
+
(0, fs_2.readFileSync)(packageJsonPath, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Dev mode: tsx runs from project root
|
|
67
|
+
packageJsonPath = (0, path_1.resolve)((0, path_1.dirname)(__filename), 'package.json');
|
|
68
|
+
}
|
|
69
|
+
const { version: packageVersion } = JSON.parse((0, fs_2.readFileSync)(packageJsonPath, 'utf8'));
|
|
56
70
|
const program = new commander_1.Command();
|
|
57
71
|
const placeholderPattern = /YOUR_|your_/;
|
|
58
72
|
/**
|
|
@@ -130,7 +144,7 @@ async function promptYesNo(ask, question, defaultYes = true) {
|
|
|
130
144
|
program
|
|
131
145
|
.name('ff1')
|
|
132
146
|
.description('CLI to fetch NFT information and build DP1 playlists using AI (Grok, ChatGPT, Gemini)')
|
|
133
|
-
.version(
|
|
147
|
+
.version(packageVersion)
|
|
134
148
|
.addHelpText('after', `\nQuick start:\n 1) ff1 setup\n 2) ff1 chat\n\nDocs: https://github.com/feralfile/ff1-cli\n`);
|
|
135
149
|
program
|
|
136
150
|
.command('setup')
|
package/dist/src/config.js
CHANGED
|
@@ -49,7 +49,7 @@ function loadConfig() {
|
|
|
49
49
|
maxTokens: parseInt(process.env.MAX_TOKENS || '4000', 10),
|
|
50
50
|
supportsFunctionCalling: true,
|
|
51
51
|
},
|
|
52
|
-
|
|
52
|
+
chatgpt: {
|
|
53
53
|
apiKey: process.env.OPENAI_API_KEY || '',
|
|
54
54
|
baseURL: 'https://api.openai.com/v1',
|
|
55
55
|
model: 'gpt-4o',
|
|
@@ -80,7 +80,7 @@ function loadConfig() {
|
|
|
80
80
|
feed: {
|
|
81
81
|
baseURLs: process.env.FEED_BASE_URLS
|
|
82
82
|
? process.env.FEED_BASE_URLS.split(',')
|
|
83
|
-
: ['https://feed.
|
|
83
|
+
: ['https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1'],
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
86
|
// Try to load config.json if it exists
|
|
@@ -202,7 +202,7 @@ function getFeedConfig() {
|
|
|
202
202
|
}
|
|
203
203
|
else {
|
|
204
204
|
// Default feed URL
|
|
205
|
-
urls = ['https://feed.
|
|
205
|
+
urls = ['https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1'];
|
|
206
206
|
}
|
|
207
207
|
return {
|
|
208
208
|
baseURLs: urls,
|
|
@@ -241,24 +241,9 @@ function getFF1DeviceConfig() {
|
|
|
241
241
|
* @returns {boolean} returns.supportsFunctionCalling - Whether model supports function calling
|
|
242
242
|
* @throws {Error} If model is not configured or doesn't support function calling
|
|
243
243
|
*/
|
|
244
|
-
function resolveModelName(config, modelName) {
|
|
245
|
-
const selectedModel = modelName || config.defaultModel;
|
|
246
|
-
if (config.models[selectedModel]) {
|
|
247
|
-
return selectedModel;
|
|
248
|
-
}
|
|
249
|
-
const aliasMap = {
|
|
250
|
-
gpt: 'chatgpt',
|
|
251
|
-
chatgpt: 'gpt',
|
|
252
|
-
};
|
|
253
|
-
const alias = aliasMap[selectedModel];
|
|
254
|
-
if (alias && config.models[alias]) {
|
|
255
|
-
return alias;
|
|
256
|
-
}
|
|
257
|
-
return selectedModel;
|
|
258
|
-
}
|
|
259
244
|
function getModelConfig(modelName) {
|
|
260
245
|
const config = getConfig();
|
|
261
|
-
const selectedModel =
|
|
246
|
+
const selectedModel = modelName || config.defaultModel;
|
|
262
247
|
if (!config.models[selectedModel]) {
|
|
263
248
|
throw new Error(`Model "${selectedModel}" is not configured. Available models: ${Object.keys(config.models).join(', ')}`);
|
|
264
249
|
}
|
|
@@ -285,7 +270,7 @@ function validateConfig(modelName) {
|
|
|
285
270
|
const errors = [];
|
|
286
271
|
try {
|
|
287
272
|
const config = getConfig();
|
|
288
|
-
const selectedModel =
|
|
273
|
+
const selectedModel = modelName || config.defaultModel;
|
|
289
274
|
if (!config.models[selectedModel]) {
|
|
290
275
|
errors.push(`Model "${selectedModel}" is not configured. Available: ${Object.keys(config.models).join(', ')}`);
|
|
291
276
|
return { valid: false, errors };
|
|
@@ -377,12 +362,5 @@ async function createSampleConfig(targetPath) {
|
|
|
377
362
|
*/
|
|
378
363
|
function listAvailableModels() {
|
|
379
364
|
const config = getConfig();
|
|
380
|
-
|
|
381
|
-
if (modelNames.has('gpt')) {
|
|
382
|
-
modelNames.add('chatgpt');
|
|
383
|
-
}
|
|
384
|
-
if (modelNames.has('chatgpt')) {
|
|
385
|
-
modelNames.add('gpt');
|
|
386
|
-
}
|
|
387
|
-
return Array.from(modelNames);
|
|
365
|
+
return Object.keys(config.models);
|
|
388
366
|
}
|
|
@@ -210,7 +210,7 @@ PUBLISH INTENT (CRITICAL)
|
|
|
210
210
|
2. If only 1 server → use it directly in playlistSettings.feedServer
|
|
211
211
|
3. If 2+ servers → ask user "Which feed server?" with numbered list (e.g., "1) https://feed.feralfile.com 2) http://localhost:8787")
|
|
212
212
|
4. After selection, set playlistSettings.feedServer = { baseUrl, apiKey } from selected server
|
|
213
|
-
5. Acknowledge in Settings bullets (e.g., "publish to: https://feed.
|
|
213
|
+
5. Acknowledge in Settings bullets (e.g., "publish to: https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1")
|
|
214
214
|
- User can request both device display AND publishing (e.g., "send to FF1 and publish to feed") → set both deviceName and feedServer
|
|
215
215
|
- Publishing happens automatically after playlist verification passes
|
|
216
216
|
|
|
@@ -493,20 +493,6 @@ function printMarkdownContent(content) {
|
|
|
493
493
|
function sleep(ms) {
|
|
494
494
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
495
495
|
}
|
|
496
|
-
function buildToolResponseMessages(toolCalls, responses) {
|
|
497
|
-
return toolCalls
|
|
498
|
-
.filter((toolCall) => toolCall.id)
|
|
499
|
-
.map((toolCall) => {
|
|
500
|
-
const content = toolCall.id && Object.prototype.hasOwnProperty.call(responses, toolCall.id)
|
|
501
|
-
? responses[toolCall.id]
|
|
502
|
-
: { error: `Unknown function: ${toolCall.function.name}` };
|
|
503
|
-
return {
|
|
504
|
-
role: 'tool',
|
|
505
|
-
tool_call_id: toolCall.id,
|
|
506
|
-
content: JSON.stringify(content),
|
|
507
|
-
};
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
496
|
async function processNonStreamingResponse(response) {
|
|
511
497
|
const message = response.choices[0]?.message;
|
|
512
498
|
if (!message) {
|
|
@@ -705,10 +691,13 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
705
691
|
// Get the list of configured devices
|
|
706
692
|
const { getConfiguredDevices } = await Promise.resolve().then(() => __importStar(require('../utilities/functions')));
|
|
707
693
|
const result = await getConfiguredDevices();
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
694
|
+
// Add tool result to messages and continue conversation
|
|
695
|
+
const toolResultMessage = {
|
|
696
|
+
role: 'tool',
|
|
697
|
+
tool_call_id: toolCall.id,
|
|
698
|
+
content: JSON.stringify(result),
|
|
699
|
+
};
|
|
700
|
+
const updatedMessages = [...messages, message, toolResultMessage];
|
|
712
701
|
// Continue the conversation with the device list
|
|
713
702
|
const followUpRequest = {
|
|
714
703
|
model: modelConfig.model,
|
|
@@ -752,13 +741,16 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
752
741
|
apiKey: server.apiKey,
|
|
753
742
|
})) ||
|
|
754
743
|
feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
744
|
+
// Add tool result to messages and continue conversation
|
|
745
|
+
const feedToolResultMessage = {
|
|
746
|
+
role: 'tool',
|
|
747
|
+
tool_call_id: followUpToolCall.id,
|
|
748
|
+
content: JSON.stringify({ servers: serverList }),
|
|
749
|
+
};
|
|
758
750
|
const feedUpdatedMessages = [
|
|
759
751
|
...updatedMessages,
|
|
760
752
|
followUpMessage,
|
|
761
|
-
|
|
753
|
+
feedToolResultMessage,
|
|
762
754
|
];
|
|
763
755
|
// Continue the conversation with the feed server list
|
|
764
756
|
const feedFollowUpRequest = {
|
|
@@ -894,8 +886,15 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
894
886
|
}
|
|
895
887
|
}
|
|
896
888
|
else {
|
|
897
|
-
|
|
898
|
-
const
|
|
889
|
+
// Unhandled tool call - add assistant message and tool response to messages
|
|
890
|
+
const toolResultMessage = {
|
|
891
|
+
role: 'tool',
|
|
892
|
+
tool_call_id: followUpToolCall.id,
|
|
893
|
+
content: JSON.stringify({
|
|
894
|
+
error: `Unknown function: ${followUpToolCall.function.name}`,
|
|
895
|
+
}),
|
|
896
|
+
};
|
|
897
|
+
const validMessages = [...updatedMessages, followUpMessage, toolResultMessage];
|
|
899
898
|
// AI is still asking for more information after the error
|
|
900
899
|
return {
|
|
901
900
|
approved: false,
|
|
@@ -922,10 +921,13 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
922
921
|
baseUrl: server.baseUrl,
|
|
923
922
|
apiKey: server.apiKey,
|
|
924
923
|
})) || feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
924
|
+
// Add tool result to messages and continue conversation
|
|
925
|
+
const toolResultMessage = {
|
|
926
|
+
role: 'tool',
|
|
927
|
+
tool_call_id: toolCall.id,
|
|
928
|
+
content: JSON.stringify({ servers: serverList }),
|
|
929
|
+
};
|
|
930
|
+
const updatedMessages = [...messages, message, toolResultMessage];
|
|
929
931
|
// Continue the conversation with the feed server list
|
|
930
932
|
const followUpRequest = {
|
|
931
933
|
model: modelConfig.model,
|
|
@@ -1029,14 +1031,17 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
1029
1031
|
// Validate and confirm the playlist
|
|
1030
1032
|
const confirmation = await confirmPlaylistForSending(args.filePath, args.deviceName);
|
|
1031
1033
|
if (!confirmation.success) {
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
+
// Add tool response message to make conversation valid
|
|
1035
|
+
const toolResultMessage = {
|
|
1036
|
+
role: 'tool',
|
|
1037
|
+
tool_call_id: toolCall.id,
|
|
1038
|
+
content: JSON.stringify({
|
|
1034
1039
|
success: false,
|
|
1035
1040
|
error: confirmation.error,
|
|
1036
1041
|
message: confirmation.message,
|
|
1037
|
-
},
|
|
1038
|
-
}
|
|
1039
|
-
const validMessages = [...messages, message,
|
|
1042
|
+
}),
|
|
1043
|
+
};
|
|
1044
|
+
const validMessages = [...messages, message, toolResultMessage];
|
|
1040
1045
|
// Check if this is a device selection needed case
|
|
1041
1046
|
if (confirmation.needsDeviceSelection) {
|
|
1042
1047
|
// Multiple devices available - ask user to choose
|
|
@@ -1120,14 +1125,17 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
1120
1125
|
const { verifyAddresses } = await Promise.resolve().then(() => __importStar(require('../utilities/functions')));
|
|
1121
1126
|
const verificationResult = await verifyAddresses({ addresses: args.addresses });
|
|
1122
1127
|
if (!verificationResult.valid) {
|
|
1123
|
-
|
|
1124
|
-
|
|
1128
|
+
// Add tool response message for invalid addresses
|
|
1129
|
+
const toolResultMessage = {
|
|
1130
|
+
role: 'tool',
|
|
1131
|
+
tool_call_id: toolCall.id,
|
|
1132
|
+
content: JSON.stringify({
|
|
1125
1133
|
valid: false,
|
|
1126
1134
|
errors: verificationResult.errors,
|
|
1127
1135
|
results: verificationResult.results,
|
|
1128
|
-
},
|
|
1129
|
-
}
|
|
1130
|
-
const validMessages = [...messages, message,
|
|
1136
|
+
}),
|
|
1137
|
+
};
|
|
1138
|
+
const validMessages = [...messages, message, toolResultMessage];
|
|
1131
1139
|
// Ask user to correct the addresses
|
|
1132
1140
|
return {
|
|
1133
1141
|
approved: false,
|
|
@@ -1136,13 +1144,16 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
1136
1144
|
messages: validMessages,
|
|
1137
1145
|
};
|
|
1138
1146
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1147
|
+
// All addresses are valid - continue to next step
|
|
1148
|
+
const toolResultMessage = {
|
|
1149
|
+
role: 'tool',
|
|
1150
|
+
tool_call_id: toolCall.id,
|
|
1151
|
+
content: JSON.stringify({
|
|
1141
1152
|
valid: true,
|
|
1142
1153
|
results: verificationResult.results,
|
|
1143
|
-
},
|
|
1144
|
-
}
|
|
1145
|
-
const validMessages = [...messages, message,
|
|
1154
|
+
}),
|
|
1155
|
+
};
|
|
1156
|
+
const validMessages = [...messages, message, toolResultMessage];
|
|
1146
1157
|
// Continue conversation after validation
|
|
1147
1158
|
const followUpRequest = {
|
|
1148
1159
|
model: modelConfig.model,
|
|
@@ -1187,14 +1198,12 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
1187
1198
|
})) ||
|
|
1188
1199
|
feedConfig.baseURLs.map((url) => ({ baseUrl: url, apiKey: feedConfig.apiKey }));
|
|
1189
1200
|
// Add tool result to messages and continue conversation
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
...feedToolResultMessages,
|
|
1197
|
-
];
|
|
1201
|
+
const feedToolResultMessage = {
|
|
1202
|
+
role: 'tool',
|
|
1203
|
+
tool_call_id: followUpToolCall.id,
|
|
1204
|
+
content: JSON.stringify({ servers: serverList }),
|
|
1205
|
+
};
|
|
1206
|
+
const feedUpdatedMessages = [...validMessages, followUpMessage, feedToolResultMessage];
|
|
1198
1207
|
// Continue the conversation with the feed server list
|
|
1199
1208
|
const feedFollowUpRequest = {
|
|
1200
1209
|
model: modelConfig.model,
|
|
@@ -1299,8 +1308,15 @@ async function processIntentParserRequest(userRequest, options = {}) {
|
|
|
1299
1308
|
};
|
|
1300
1309
|
}
|
|
1301
1310
|
else {
|
|
1302
|
-
|
|
1303
|
-
const
|
|
1311
|
+
// Unhandled tool call at top level
|
|
1312
|
+
const toolResultMessage = {
|
|
1313
|
+
role: 'tool',
|
|
1314
|
+
tool_call_id: toolCall.id,
|
|
1315
|
+
content: JSON.stringify({
|
|
1316
|
+
error: `Unknown function: ${toolCall.function.name}`,
|
|
1317
|
+
}),
|
|
1318
|
+
};
|
|
1319
|
+
const validMessages = [...messages, message, toolResultMessage];
|
|
1304
1320
|
return {
|
|
1305
1321
|
approved: false,
|
|
1306
1322
|
needsMoreInfo: true,
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -94,7 +94,7 @@ DP‑1 Feed API configuration.
|
|
|
94
94
|
|
|
95
95
|
- `feed.baseURLs` (string[]): Array of DP‑1 Feed Operator API v1 base URLs. The CLI queries all feeds in parallel.
|
|
96
96
|
- Legacy support: `feed.baseURL` (string) is still accepted and normalized to an array.
|
|
97
|
-
- Default: `https://feed.
|
|
97
|
+
- Default: `https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1` if not set.
|
|
98
98
|
- 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).
|
|
99
99
|
|
|
100
100
|
Endpoints used by the CLI:
|
|
@@ -105,7 +105,7 @@ Endpoints used by the CLI:
|
|
|
105
105
|
Environment variable alternative:
|
|
106
106
|
|
|
107
107
|
```env
|
|
108
|
-
FEED_BASE_URLS=https://feed.
|
|
108
|
+
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
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
## ff1Devices
|
|
@@ -150,7 +150,7 @@ Minimal `config.json` example (selected fields):
|
|
|
150
150
|
},
|
|
151
151
|
"feed": {
|
|
152
152
|
"baseURLs": [
|
|
153
|
-
"https://feed.
|
|
153
|
+
"https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1"
|
|
154
154
|
]
|
|
155
155
|
},
|
|
156
156
|
"ff1Devices": {
|
package/docs/EXAMPLES.md
CHANGED
|
@@ -23,7 +23,7 @@ npm run dev -- chat "Get 3 items from Social Codes and 2 from 0xdef" -v
|
|
|
23
23
|
|
|
24
24
|
# Switch model
|
|
25
25
|
npm run dev -- chat "your request" --model grok
|
|
26
|
-
npm run dev -- chat "your request" --model
|
|
26
|
+
npm run dev -- chat "your request" --model chatgpt
|
|
27
27
|
npm run dev -- chat "your request" --model gemini
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -44,7 +44,7 @@ cat examples/params-example.json | npm run dev -- build -o playlist.json
|
|
|
44
44
|
npm run dev -- chat "Build a playlist of my Tezos works from address tz1... plus 3 from Social Codes" -v -o playlist.json
|
|
45
45
|
|
|
46
46
|
# Switch model if desired
|
|
47
|
-
npm run dev -- chat "Build playlist from Ethereum address 0x... and 2 from Social Codes" --model
|
|
47
|
+
npm run dev -- chat "Build playlist from Ethereum address 0x... and 2 from Social Codes" --model chatgpt -v
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
### One‑shot complex prompt
|
|
@@ -67,7 +67,7 @@ The CLI recognizes publishing keywords like "publish", "publish to my feed", "pu
|
|
|
67
67
|
npm run dev -- chat "Build playlist from Ethereum contract 0xb932a70A57673d89f4acfFBE830E8ed7f75Fb9e0 with tokens 52932 and 52457; publish to my feed" -o playlist.json -v
|
|
68
68
|
|
|
69
69
|
# With feed selection (if multiple servers configured)
|
|
70
|
-
# The CLI will ask: "Which feed server? 1) https://feed.
|
|
70
|
+
# The CLI will ask: "Which feed server? 1) https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1 2) http://localhost:8787"
|
|
71
71
|
npm run dev -- chat "Get 3 from Social Codes and publish to feed" -v
|
|
72
72
|
|
|
73
73
|
# Publish existing playlist (defaults to ./playlist.json)
|
|
@@ -92,21 +92,18 @@ npm run dev -- chat "Get 5 from Social Codes, shuffle, display on 'Living Room',
|
|
|
92
92
|
### How It Works
|
|
93
93
|
|
|
94
94
|
**Mode 1: Build and Publish** (when sources are mentioned)
|
|
95
|
-
|
|
96
95
|
1. Intent parser detects "publish" keywords with sources/requirements
|
|
97
96
|
2. Calls `get_feed_servers` to retrieve configured servers
|
|
98
97
|
3. If 1 server → uses it automatically; if 2+ servers → asks user to pick
|
|
99
98
|
4. Builds playlist → verifies → publishes automatically
|
|
100
99
|
|
|
101
100
|
**Mode 2: Publish Existing File** (e.g., "publish playlist")
|
|
102
|
-
|
|
103
101
|
1. Intent parser detects "publish playlist" or similar phrases
|
|
104
102
|
2. Calls `get_feed_servers` to retrieve configured servers
|
|
105
103
|
3. If 1 server → uses it automatically; if 2+ servers → asks user to pick
|
|
106
104
|
4. Publishes the playlist from `./playlist.json` (or specified path)
|
|
107
105
|
|
|
108
106
|
Output shows:
|
|
109
|
-
|
|
110
107
|
- Playlist build progress (Mode 1 only)
|
|
111
108
|
- Device sending (if requested): `✓ Sent to device: Living Room`
|
|
112
109
|
- Publishing status: `✓ Published to feed server`
|
|
@@ -190,21 +187,18 @@ Select server (0-based index): 0
|
|
|
190
187
|
### Error Handling
|
|
191
188
|
|
|
192
189
|
**Validation failed:**
|
|
193
|
-
|
|
194
190
|
```
|
|
195
191
|
❌ Failed to publish playlist
|
|
196
192
|
Playlist validation failed: dpVersion: Required; id: Required
|
|
197
193
|
```
|
|
198
194
|
|
|
199
195
|
**File not found:**
|
|
200
|
-
|
|
201
196
|
```
|
|
202
197
|
❌ Failed to publish playlist
|
|
203
198
|
Playlist file not found: /path/to/playlist.json
|
|
204
199
|
```
|
|
205
200
|
|
|
206
201
|
**API error:**
|
|
207
|
-
|
|
208
202
|
```
|
|
209
203
|
❌ Failed to publish playlist
|
|
210
204
|
Failed to publish: {"error":"unauthorized","message":"Invalid API key"}
|
|
@@ -239,6 +233,7 @@ npm run dev -- config show
|
|
|
239
233
|
npm run dev -- config init
|
|
240
234
|
```
|
|
241
235
|
|
|
236
|
+
|
|
242
237
|
### Natural‑language one‑shot examples (proven)
|
|
243
238
|
|
|
244
239
|
- **ETH contract + token IDs (shuffle/mix, generic device)**
|
|
@@ -333,4 +328,4 @@ npm run dev -- config init
|
|
|
333
328
|
```bash
|
|
334
329
|
npm run dev -- chat "Compose a playlist from reas.eth (3 items); send to device" -o playlist-generic-device.json -v
|
|
335
330
|
npm run dev -- chat "Compose a playlist from reas.eth (3 items); send to 'Living Room'" -o playlist-named-device.json -v
|
|
336
|
-
```
|
|
331
|
+
```
|
package/docs/README.md
CHANGED
|
@@ -40,7 +40,7 @@ See the full configuration reference here: `./CONFIGURATION.md`.
|
|
|
40
40
|
"model": "grok-beta",
|
|
41
41
|
"supportsFunctionCalling": true
|
|
42
42
|
},
|
|
43
|
-
"
|
|
43
|
+
"chatgpt": {
|
|
44
44
|
"apiKey": "sk-your-openai-key-here",
|
|
45
45
|
"baseURL": "https://api.openai.com/v1",
|
|
46
46
|
"model": "gpt-4o",
|
|
@@ -57,7 +57,7 @@ See the full configuration reference here: `./CONFIGURATION.md`.
|
|
|
57
57
|
"playlist": {
|
|
58
58
|
"privateKey": "your_ed25519_private_key_base64_here"
|
|
59
59
|
},
|
|
60
|
-
"feed": { "baseURLs": ["https://feed.
|
|
60
|
+
"feed": { "baseURLs": ["https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1"] },
|
|
61
61
|
"ff1Devices": {
|
|
62
62
|
"devices": [
|
|
63
63
|
{
|
|
@@ -163,7 +163,7 @@ How it works (at a glance):
|
|
|
163
163
|
- If `deviceName` is present, the CLI will send the validated playlist to that FF1 device.
|
|
164
164
|
- If `feedServer` is present (via "publish to my feed"), the CLI will publish the playlist to the selected feed server.
|
|
165
165
|
|
|
166
|
-
Use `--model grok|
|
|
166
|
+
Use `--model grok|chatgpt|gemini` to switch models, or set `defaultModel` in `config.json`.
|
|
167
167
|
|
|
168
168
|
### Natural language publishing
|
|
169
169
|
|
package/docs/RELEASING.md
CHANGED
|
@@ -4,16 +4,40 @@ The curl installer downloads prebuilt binaries from GitHub Releases. Build one a
|
|
|
4
4
|
|
|
5
5
|
## Build a Release Asset (local)
|
|
6
6
|
|
|
7
|
+
**macOS / Linux:**
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
10
|
./scripts/release/build-asset.sh
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
**Windows (PowerShell):**
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
.\scripts\release\build-asset-windows.ps1
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This produces (names vary by OS/arch):
|
|
20
|
+
|
|
21
|
+
- `release/ff1-cli-darwin-arm64.tar.gz` (and `.sha256`) on macOS
|
|
22
|
+
- `release/ff1-cli-linux-x64.tar.gz` (and `.sha256`) on Linux
|
|
23
|
+
- `release/ff1-cli-windows-x64.zip` (and `.sha256`) on Windows
|
|
24
|
+
|
|
25
|
+
Run the appropriate script on each target platform and upload each pair to the GitHub release.
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
## GitHub Actions
|
|
28
|
+
|
|
29
|
+
- **Build** (`build.yml`): Trigger manually (Actions → Build → Run workflow) or on pull requests. Builds binaries on macOS, Linux, and Windows and uploads them as workflow artifacts for download.
|
|
30
|
+
- **Release** (`release.yml`): Triggered when you **publish a release** (create a release from the repo Releases page, or publish an existing draft). Builds the same binaries, then uploads them to that release. Pushing a tag alone does not run this; only creating/publishing a release does.
|
|
31
|
+
|
|
32
|
+
## Installer Redirect
|
|
33
|
+
|
|
34
|
+
`https://feralfile.com/ff1-cli-install` should redirect to:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
https://raw.githubusercontent.com/feral-file/ff1-cli/main/scripts/install.sh
|
|
38
|
+
```
|
|
15
39
|
|
|
16
|
-
The
|
|
40
|
+
The installer script then fetches the release assets from GitHub Releases.
|
|
17
41
|
|
|
18
42
|
## Environment Overrides
|
|
19
43
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ff1-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "CLI to fetch NFT information and build DP1 playlists using Grok API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -43,24 +43,16 @@
|
|
|
43
43
|
"author": "",
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@lancedb/lancedb": "^0.22.1",
|
|
47
|
-
"@mozilla/readability": "^0.6.0",
|
|
48
|
-
"@xenova/transformers": "^2.17.2",
|
|
49
46
|
"axios": "^1.6.2",
|
|
50
47
|
"chalk": "^4.1.2",
|
|
51
|
-
"cheerio": "^1.0.0-rc.12",
|
|
52
48
|
"commander": "^11.1.0",
|
|
53
49
|
"dotenv": "^16.3.1",
|
|
54
50
|
"dp1-js": "^1.1.0",
|
|
55
51
|
"fuzzysort": "^3.1.0",
|
|
56
|
-
"jsdom": "^27.0.0",
|
|
57
52
|
"openai": "^4.58.1",
|
|
58
|
-
"ora": "^5.4.1",
|
|
59
|
-
"puppeteer": "^24.22.3",
|
|
60
53
|
"viem": "^2.38.2"
|
|
61
54
|
},
|
|
62
55
|
"devDependencies": {
|
|
63
|
-
"@types/jsdom": "^27.0.0",
|
|
64
56
|
"@types/node": "^24.7.1",
|
|
65
57
|
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
|
66
58
|
"@typescript-eslint/parser": "^8.46.0",
|