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 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
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "feedServers": [
56
56
  {
57
- "baseUrl": "https://feed.feralfile.com/api/v1",
57
+ "baseUrl": "https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1",
58
58
  "apiKey": "YOUR_FEED_SERVER_API_KEY_OPTIONAL"
59
59
  },
60
60
  {
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 warning from jsdom dependency
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('1.0.1')
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')
@@ -49,7 +49,7 @@ function loadConfig() {
49
49
  maxTokens: parseInt(process.env.MAX_TOKENS || '4000', 10),
50
50
  supportsFunctionCalling: true,
51
51
  },
52
- gpt: {
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.feralfile.com/api/v1'],
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.feralfile.com/api/v1'];
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 = resolveModelName(config, modelName);
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 = resolveModelName(config, modelName);
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
- const modelNames = new Set(Object.keys(config.models));
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.feralfile.com/api/v1")
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
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
709
- [toolCall.id]: result,
710
- });
711
- const updatedMessages = [...messages, message, ...toolResultMessages];
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
- const feedToolResultMessages = buildToolResponseMessages(followUpMessage.tool_calls, {
756
- [followUpToolCall.id]: { servers: serverList },
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
- ...feedToolResultMessages,
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
- const toolResultMessages = buildToolResponseMessages(followUpMessage.tool_calls, {});
898
- const validMessages = [...updatedMessages, followUpMessage, ...toolResultMessages];
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
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
926
- [toolCall.id]: { servers: serverList },
927
- });
928
- const updatedMessages = [...messages, message, ...toolResultMessages];
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
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
1033
- [toolCall.id]: {
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, ...toolResultMessages];
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
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
1124
- [toolCall.id]: {
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, ...toolResultMessages];
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
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {
1140
- [toolCall.id]: {
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, ...toolResultMessages];
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 feedToolResultMessages = buildToolResponseMessages(followUpMessage.tool_calls, {
1191
- [followUpToolCall.id]: { servers: serverList },
1192
- });
1193
- const feedUpdatedMessages = [
1194
- ...validMessages,
1195
- followUpMessage,
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
- const toolResultMessages = buildToolResponseMessages(message.tool_calls, {});
1303
- const validMessages = [...messages, message, ...toolResultMessages];
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,
@@ -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.feralfile.com/api/v1` if not set.
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.feralfile.com/api/v1,https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1
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.feralfile.com/api/v1"
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 gpt
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 gpt -v
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.feralfile.com/api/v1 2) http://localhost:8787"
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
- "gpt": {
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.feralfile.com/api/v1"] },
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|gpt|gemini` to switch models, or set `defaultModel` in `config.json`.
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
- This produces:
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
- - `release/ff1-cli-darwin-x64.tar.gz`
14
- - `release/ff1-cli-darwin-x64.tar.gz.sha256`
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 exact filename depends on the OS/arch you build on. Run the script on each target platform (macOS + Linux, x64/arm64) and upload each pair to the GitHub release.
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.1",
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",