@feralfile/cli 1.1.1 → 1.2.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.
Files changed (36) hide show
  1. package/README.md +26 -0
  2. package/dist/index.js +4 -2
  3. package/dist/package.json +84 -0
  4. package/dist/src/ai-orchestrator/index.js +14 -2
  5. package/dist/src/commands/build.js +3 -3
  6. package/dist/src/commands/chat.js +3 -3
  7. package/dist/src/commands/config.js +2 -2
  8. package/dist/src/commands/device.js +6 -6
  9. package/dist/src/commands/find.js +502 -0
  10. package/dist/src/commands/helpers/device-discovery.js +1 -1
  11. package/dist/src/commands/play.js +52 -11
  12. package/dist/src/commands/setup.js +1 -1
  13. package/dist/src/commands/status.js +2 -2
  14. package/dist/src/intent-parser/index.js +16 -5
  15. package/dist/src/intent-parser/utils.js +7 -2
  16. package/dist/src/main.js +61 -17
  17. package/dist/src/utilities/ab-marketplace.js +95 -0
  18. package/dist/src/utilities/feral-file-artwork.js +89 -0
  19. package/dist/src/utilities/ff-marketplace.js +115 -0
  20. package/dist/src/utilities/ff1-device.js +4 -4
  21. package/dist/src/utilities/functions.js +1 -1
  22. package/dist/src/utilities/fxhash-marketplace.js +146 -0
  23. package/dist/src/utilities/index.js +80 -1
  24. package/dist/src/utilities/marketplace-url.js +373 -0
  25. package/dist/src/utilities/neort-marketplace.js +102 -0
  26. package/dist/src/utilities/nft-indexer.js +22 -14
  27. package/dist/src/utilities/objkt-marketplace.js +77 -0
  28. package/dist/src/utilities/playlist-publisher.js +9 -42
  29. package/dist/src/utilities/raster-client.js +252 -0
  30. package/dist/src/utilities/user-agent.js +20 -0
  31. package/docs/CONFIGURATION.md +3 -1
  32. package/docs/EXAMPLES.md +23 -0
  33. package/docs/FUNCTION_CALLING.md +3 -1
  34. package/docs/README.md +11 -10
  35. package/docs/RELEASING.md +92 -22
  36. package/package.json +9 -1
package/README.md CHANGED
@@ -44,6 +44,32 @@ ff-cli config init
44
44
  ff-cli config validate
45
45
  ```
46
46
 
47
+ ## Find an artwork
48
+
49
+ `ff-cli find` resolves a URL, raw `chain:contract:tokenId`, or a wallet address into a playable DP-1 playlist. The focus is computational and generative art; PFP collections and pre-ERC-721 contracts have limits noted below.
50
+
51
+ ```bash
52
+ # Most satisfying: paste a URL, play it on your FF1
53
+ ff-cli find https://www.artblocks.io/collection/ringers-by-dmitri-cherniak --play
54
+
55
+ # Tezos / hic et nunc via Objkt (the alias resolves to a KT1 contract)
56
+ ff-cli find https://objkt.com/tokens/hicetnunc/111068 --play
57
+
58
+ # Feral File artwork (public id can be hex or numeric — both forms resolve via /api/artworks)
59
+ ff-cli find https://feralfile.com/exhibitions/artwork/f0240e04d64717e319584957f6a83954b029254ad1260b6320472ea8c0c5b1cf --play
60
+
61
+ # Save without playing
62
+ ff-cli find ethereum:0xababababab20053426ad1c782de9ea8444358070:5001410 --output send-receive.json
63
+ ```
64
+
65
+ Sources: Art Blocks, Objkt, fxhash (canonical `/gentk/...`, live `/iteration/{slug}`, and project pages `/project/{slug}` / `/generative/{slug}`), OpenSea, SuperRare, Feral File, Neort (`/art/{id}`), raw on-chain coords, wallet addresses. Run `ff-cli find --help` for the full input list.
66
+
67
+ **Known limitations**
68
+
69
+ - **CryptoPunks (original)** don't index — the contract predates ERC-721 ([ff-indexer-v2#83](https://github.com/feral-file/ff-indexer-v2/issues/83)).
70
+ - **Mainstream PFPs** (Azuki, BAYC, Pudgy Penguins) build a single-item playlist instead of a series — Raster's curation is effectively the series allowlist.
71
+ - **Indexer 502s on burst load** are intermittent ([ff-indexer-v2#82](https://github.com/feral-file/ff-indexer-v2/issues/82)) — retry clears or use `--limit N`.
72
+
47
73
  ## Dev Quick Start
48
74
 
49
75
  **Set your LLM API key first (default Claude):** `export ANTHROPIC_API_KEY='sk-ant-your-api-key-here'`
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ const validate_1 = require("./src/commands/validate");
19
19
  const sign_1 = require("./src/commands/sign");
20
20
  const play_1 = require("./src/commands/play");
21
21
  const publish_1 = require("./src/commands/publish");
22
+ const find_1 = require("./src/commands/find");
22
23
  const build_1 = require("./src/commands/build");
23
24
  const config_1 = require("./src/commands/config");
24
25
  const ssh_1 = require("./src/commands/ssh");
@@ -32,11 +33,11 @@ try {
32
33
  catch {
33
34
  packageJsonPath = (0, path_1.resolve)((0, path_1.dirname)(__filename), 'package.json');
34
35
  }
35
- const { version: packageVersion } = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, 'utf8'));
36
+ const { version: packageVersion, description: packageDescription } = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, 'utf8'));
36
37
  const program = new commander_1.Command();
37
38
  program
38
39
  .name('ff-cli')
39
- .description('CLI for building DP-1 playlists of digital art using AI (Claude, Grok, ChatGPT, Gemini)')
40
+ .description(packageDescription)
40
41
  .version(packageVersion)
41
42
  .addHelpText('after', `\nQuick start:\n 1) ff-cli setup\n 2) ff-cli chat\n\nDocs: https://github.com/feral-file/ff-cli\n`);
42
43
  program.addCommand(setup_1.setupCommand);
@@ -47,6 +48,7 @@ program.addCommand(validate_1.validateCommand);
47
48
  program.addCommand(sign_1.signCommand);
48
49
  program.addCommand(play_1.playCommand);
49
50
  program.addCommand(publish_1.publishCommand);
51
+ program.addCommand(find_1.findCommand);
50
52
  program.addCommand(build_1.buildCommand);
51
53
  program.addCommand(config_1.configCommand);
52
54
  program.addCommand(ssh_1.sshCommand);
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@feralfile/cli",
3
+ "version": "1.2.0",
4
+ "description": "CLI for building DP-1 playlists of digital art using AI (Claude, Grok, ChatGPT, Gemini)",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/feral-file/ff-cli.git"
8
+ },
9
+ "homepage": "https://github.com/feral-file/ff-cli#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/feral-file/ff-cli/issues"
12
+ },
13
+ "main": "dist/index.js",
14
+ "bin": {
15
+ "ff-cli": "dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "docs",
20
+ "config.json.example",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "clean": "node -e \"const fs=require('fs'); fs.rmSync('dist',{ recursive:true, force:true }); fs.rmSync('release',{ recursive:true, force:true });\"",
26
+ "build": "tsc",
27
+ "typecheck": "tsc --noEmit",
28
+ "bundle": "node build.js",
29
+ "bundle:dev": "node build.js --dev",
30
+ "release:asset": "./scripts/release/build-asset.sh",
31
+ "prepare": "husky",
32
+ "start": "node dist/index.js",
33
+ "dev": "tsx index.ts",
34
+ "smoke": "npm run build && node dist/index.js validate examples/sample-playlist-v11.json && node dist/index.js config validate",
35
+ "check": "npm run format:check && npm run lint && npm run test",
36
+ "verify": "npm run check && npm run smoke",
37
+ "prepublishOnly": "npm run verify",
38
+ "test": "node scripts/run-tests.js",
39
+ "lint": "eslint .",
40
+ "lint:fix": "eslint . --fix",
41
+ "format": "prettier --write \"**/*.{js,ts}\"",
42
+ "format:check": "prettier --check \"**/*.{js,ts}\""
43
+ },
44
+ "engines": {
45
+ "node": ">=22"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "keywords": [
51
+ "nft",
52
+ "playlist",
53
+ "dp1",
54
+ "grok",
55
+ "cli"
56
+ ],
57
+ "author": "",
58
+ "license": "MIT",
59
+ "dependencies": {
60
+ "axios": "^1.6.2",
61
+ "bonjour-service": "^1.3.0",
62
+ "chalk": "^4.1.2",
63
+ "commander": "^11.1.0",
64
+ "dotenv": "^16.3.1",
65
+ "dp1-js": "2.0.0",
66
+ "ff1": "^1.0.0",
67
+ "fuzzysort": "^3.1.0",
68
+ "openai": "^4.58.1",
69
+ "viem": "^2.38.2"
70
+ },
71
+ "devDependencies": {
72
+ "@types/node": "^24.7.1",
73
+ "@typescript-eslint/eslint-plugin": "^8.46.0",
74
+ "@typescript-eslint/parser": "^8.46.0",
75
+ "esbuild": "^0.25.10",
76
+ "eslint": "^9.36.0",
77
+ "eslint-config-prettier": "^10.1.8",
78
+ "eslint-plugin-prettier": "^5.5.4",
79
+ "husky": "^9.1.7",
80
+ "prettier": "^3.6.2",
81
+ "tsx": "^4.20.6",
82
+ "typescript": "^5.9.3"
83
+ }
84
+ }
@@ -116,7 +116,7 @@ const functionSchemas = [
116
116
  type: 'function',
117
117
  function: {
118
118
  name: 'query_requirement',
119
- description: 'Query data for a requirement. Supports build_playlist (blockchain NFTs), query_address (all NFTs from address), and fetch_feed (feed playlists) types.',
119
+ description: 'Query data for a requirement. Supports build_playlist (blockchain NFTs), feral_file_artwork (Feral File artwork IDs/URLs), query_address (all NFTs from address), and fetch_feed (feed playlists) types.',
120
120
  parameters: {
121
121
  type: 'object',
122
122
  properties: {
@@ -126,7 +126,7 @@ const functionSchemas = [
126
126
  properties: {
127
127
  type: {
128
128
  type: 'string',
129
- enum: ['build_playlist', 'fetch_feed', 'query_address'],
129
+ enum: ['build_playlist', 'feral_file_artwork', 'fetch_feed', 'query_address'],
130
130
  description: 'Type of requirement',
131
131
  },
132
132
  blockchain: {
@@ -144,6 +144,10 @@ const functionSchemas = [
144
144
  type: 'string',
145
145
  },
146
146
  },
147
+ artworkId: {
148
+ type: 'string',
149
+ description: 'Feral File public artwork id or /exhibitions/artwork/{id} URL (REQUIRED for feral_file_artwork)',
150
+ },
147
151
  ownerAddress: {
148
152
  type: 'string',
149
153
  description: 'Owner wallet address (0x... for Ethereum, tz... for Tezos) - REQUIRED for query_address',
@@ -457,6 +461,9 @@ function buildOrchestratorSystemPrompt(params) {
457
461
  if (req.type === 'fetch_feed') {
458
462
  return `${i + 1}. Fetch ${req.quantity || 5} items from playlist "${req.playlistName}"`;
459
463
  }
464
+ else if (req.type === 'feral_file_artwork') {
465
+ return `${i + 1}. Resolve Feral File artwork ${req.artworkId}`;
466
+ }
460
467
  else if (req.type === 'query_address') {
461
468
  const quantityText = req.quantity === 'all' ? 'all ' : req.quantity ? req.quantity + ' random ' : 'all ';
462
469
  return `${i + 1}. Query ${quantityText}tokens from address ${req.ownerAddress}`;
@@ -512,6 +519,8 @@ DECISION LOOP
512
519
  1) For each requirement in order:
513
520
  - build_playlist: call query_requirement(requirement, duration=${playlistSettings.durationPerItem || 10}).
514
521
  Returns array with minimal item data including id field. Collect the id values.
522
+ - feral_file_artwork: call query_requirement(requirement, duration=${playlistSettings.durationPerItem || 10}).
523
+ Returns array with minimal item data including id field. Collect the id values.
515
524
  - query_address:
516
525
  • if ownerAddress endsWith .eth/.tez → resolve_domains([domain]); if resolved → use returned address; if not → mark failed and continue.
517
526
  • if ownerAddress is 0x…/tz… → call query_requirement(requirement, duration=${playlistSettings.durationPerItem || 10}).
@@ -599,6 +608,9 @@ async function buildPlaylistWithAI(params, options = {}) {
599
608
  if (req.type === 'fetch_feed') {
600
609
  return `${i + 1}. Fetch ${req.quantity || 5} items from playlist "${req.playlistName}"`;
601
610
  }
611
+ else if (req.type === 'feral_file_artwork') {
612
+ return `${i + 1}. Resolve Feral File artwork:\n - artworkId: "${req.artworkId}"`;
613
+ }
602
614
  else if (req.type === 'query_address') {
603
615
  const quantityDesc = req.quantity === 'all'
604
616
  ? 'all tokens (with pagination)'
@@ -35,9 +35,9 @@ exports.buildCommand = new commander_1.Command('build')
35
35
  if (!stdin.trim()) {
36
36
  console.error(chalk_1.default.red('No parameters provided'));
37
37
  console.log(chalk_1.default.yellow('\nUsage:'));
38
- console.log(' ff1 build params.json');
39
- console.log(' cat params.json | ff1 build');
40
- console.log(' echo \'{"requirements":[...]}\' | ff1 build');
38
+ console.log(' ff-cli build params.json');
39
+ console.log(' cat params.json | ff-cli build');
40
+ console.log(' echo \'{"requirements":[...]}\' | ff-cli build');
41
41
  process.exit(1);
42
42
  }
43
43
  params = JSON.parse(stdin);
@@ -69,12 +69,12 @@ exports.chatCommand = new commander_1.Command('chat')
69
69
  validation.errors.forEach((error) => {
70
70
  console.error(chalk_1.default.red(` • ${error}`));
71
71
  });
72
- console.log(chalk_1.default.yellow('\nRun: ff1 setup\n'));
72
+ console.log(chalk_1.default.yellow('\nRun: ff-cli setup\n'));
73
73
  process.exit(1);
74
74
  }
75
75
  // Non-interactive mode: content was passed as a positional argument.
76
76
  if (content) {
77
- console.log(chalk_1.default.blue('\nFF1 Chat (non-interactive)\n'));
77
+ console.log(chalk_1.default.blue('\nff-cli chat (non-interactive)\n'));
78
78
  console.log(chalk_1.default.dim(`Model: ${modelName}\n`));
79
79
  console.log(chalk_1.default.yellow('Request:'), content);
80
80
  console.log();
@@ -103,7 +103,7 @@ exports.chatCommand = new commander_1.Command('chat')
103
103
  }
104
104
  }
105
105
  // Interactive mode.
106
- console.log(chalk_1.default.blue('\nFF1 Chat\n'));
106
+ console.log(chalk_1.default.blue('\nff-cli chat\n'));
107
107
  console.log(chalk_1.default.dim('Describe the playlist you want. Ctrl+C to exit.'));
108
108
  console.log(chalk_1.default.dim(`Model: ${modelName}\n`));
109
109
  console.log(chalk_1.default.dim('Examples:'));
@@ -9,7 +9,7 @@ const chalk_1 = __importDefault(require("chalk"));
9
9
  const config_1 = require("../config");
10
10
  // `config` is a single command with an action argument rather than a
11
11
  // commander subcommand group. Kept this way to preserve the existing
12
- // CLI surface (`ff1 config init|show|validate`) used in scripts.
12
+ // CLI surface (`ff-cli config init|show|validate`) used in scripts.
13
13
  exports.configCommand = new commander_1.Command('config')
14
14
  .description('Manage configuration')
15
15
  .argument('<action>', 'Action: init, show, or validate')
@@ -20,7 +20,7 @@ exports.configCommand = new commander_1.Command('config')
20
20
  const { userPath } = (0, config_1.getConfigPaths)();
21
21
  const configPath = await (0, config_1.createSampleConfig)(userPath);
22
22
  console.log(chalk_1.default.green(`Created ${configPath}`));
23
- console.log(chalk_1.default.yellow('\nNext: ff1 setup\n'));
23
+ console.log(chalk_1.default.yellow('\nNext: ff-cli setup\n'));
24
24
  }
25
25
  else if (action === 'show') {
26
26
  const config = (0, config_1.getConfig)();
@@ -24,14 +24,14 @@ deviceCommand
24
24
  const configPath = await (0, config_files_1.resolveExistingConfigPath)();
25
25
  if (!configPath) {
26
26
  console.log(chalk_1.default.red('config.json not found'));
27
- console.log(chalk_1.default.dim('Run: ff1 setup'));
27
+ console.log(chalk_1.default.dim('Run: ff-cli setup'));
28
28
  process.exit(1);
29
29
  }
30
30
  const config = await (0, config_files_1.readConfigFile)(configPath);
31
31
  const devices = config.ff1Devices?.devices || [];
32
32
  if (devices.length === 0) {
33
33
  console.log(chalk_1.default.yellow('\nNo devices configured'));
34
- console.log(chalk_1.default.dim('Run: ff1 device add'));
34
+ console.log(chalk_1.default.dim('Run: ff-cli device add'));
35
35
  console.log();
36
36
  return;
37
37
  }
@@ -82,7 +82,7 @@ deviceCommand
82
82
  const configPath = await (0, config_files_1.resolveExistingConfigPath)();
83
83
  if (!configPath) {
84
84
  console.log(chalk_1.default.red('config.json not found'));
85
- console.log(chalk_1.default.dim('Run: ff1 setup'));
85
+ console.log(chalk_1.default.dim('Run: ff-cli setup'));
86
86
  process.exit(1);
87
87
  }
88
88
  const config = await (0, config_files_1.readConfigFile)(configPath);
@@ -155,7 +155,7 @@ deviceCommand
155
155
  if (nameConflict) {
156
156
  if (options.name) {
157
157
  console.error(chalk_1.default.red(`\nError: device name "${deviceName}" is already used by another device (${nameConflict.host}).`));
158
- console.error(chalk_1.default.dim('Use a different name or run "ff1 device remove" first.'));
158
+ console.error(chalk_1.default.dim('Use a different name or run "ff-cli device remove" first.'));
159
159
  closePrompt();
160
160
  process.exit(1);
161
161
  }
@@ -241,14 +241,14 @@ deviceCommand
241
241
  const configPath = await (0, config_files_1.resolveExistingConfigPath)();
242
242
  if (!configPath) {
243
243
  console.log(chalk_1.default.red('config.json not found'));
244
- console.log(chalk_1.default.dim('Run: ff1 setup'));
244
+ console.log(chalk_1.default.dim('Run: ff-cli setup'));
245
245
  process.exit(1);
246
246
  }
247
247
  const config = await (0, config_files_1.readConfigFile)(configPath);
248
248
  const existingDevices = config.ff1Devices?.devices || [];
249
249
  if (existingDevices.length === 0) {
250
250
  console.log(chalk_1.default.yellow('\nNo devices configured'));
251
- console.log(chalk_1.default.dim('Run: ff1 device add\n'));
251
+ console.log(chalk_1.default.dim('Run: ff-cli device add\n'));
252
252
  process.exit(1);
253
253
  }
254
254
  let result;