nansen-cli 1.1.0 → 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.
- package/README.md +13 -8
- package/TODO.md +11 -22
- package/package.json +1 -1
- package/src/api.js +108 -0
- package/src/cli.js +44 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/nansen-cli)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[]()
|
|
6
6
|
[]()
|
|
7
7
|
|
|
8
8
|
> **Built by agents, for agents.** We prioritize the best possible AI agent experience.
|
|
@@ -137,6 +137,15 @@ nansen schema smart-money --pretty
|
|
|
137
137
|
|
|
138
138
|
Returns command definitions, option types/defaults, supported chains, and smart money labels.
|
|
139
139
|
|
|
140
|
+
### `cache` - Cache Management
|
|
141
|
+
|
|
142
|
+
Manage the local response cache.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Clear all cached responses
|
|
146
|
+
nansen cache clear
|
|
147
|
+
```
|
|
148
|
+
|
|
140
149
|
## Options
|
|
141
150
|
|
|
142
151
|
| Option | Description |
|
|
@@ -144,6 +153,9 @@ Returns command definitions, option types/defaults, supported chains, and smart
|
|
|
144
153
|
| `--pretty` | Format JSON output for readability |
|
|
145
154
|
| `--table` | Format output as human-readable table |
|
|
146
155
|
| `--fields <list>` | Comma-separated fields to include (reduces response size) |
|
|
156
|
+
| `--cache` | Enable response caching |
|
|
157
|
+
| `--no-cache` | Bypass cache for this request |
|
|
158
|
+
| `--cache-ttl <s>` | Cache TTL in seconds (default: 300) |
|
|
147
159
|
| `--chain <chain>` | Blockchain to query |
|
|
148
160
|
| `--chains <json>` | Multiple chains as JSON array |
|
|
149
161
|
| `--limit <n>` | Number of results |
|
|
@@ -191,13 +203,6 @@ This CLI is built specifically for AI agents. Every design decision prioritizes
|
|
|
191
203
|
}
|
|
192
204
|
```
|
|
193
205
|
|
|
194
|
-
**Roadmap** (see [TODO.md](TODO.md)):
|
|
195
|
-
- ~~Rate limit handling with auto-retry~~ ✅
|
|
196
|
-
- ~~Structured error codes for programmatic handling~~ ✅
|
|
197
|
-
- ~~Schema discovery endpoint (`nansen schema`)~~ ✅
|
|
198
|
-
- ~~Field filtering to reduce response size~~ ✅
|
|
199
|
-
- Response caching and batch queries
|
|
200
|
-
|
|
201
206
|
## Examples
|
|
202
207
|
|
|
203
208
|
```bash
|
package/TODO.md
CHANGED
|
@@ -2,37 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
> **Built by agents, for agents.** We prioritize improvements that create the best possible AI agent experience.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## P1 - Should Have
|
|
6
6
|
|
|
7
|
-
###
|
|
8
|
-
- [ ] Add optional local cache (SQLite or file-based)
|
|
9
|
-
- [ ] Configurable TTL (default 60-300s)
|
|
10
|
-
- [ ] `--no-cache` flag to bypass
|
|
11
|
-
- [ ] `--cache-ttl` flag to override
|
|
12
|
-
|
|
13
|
-
### Batch Queries
|
|
14
|
-
- [ ] Support multiple addresses: `--addresses '[...]'`
|
|
15
|
-
- [ ] Support multiple tokens: `--tokens '[...]'`
|
|
16
|
-
- [ ] Reduce N calls to 1 call
|
|
17
|
-
|
|
18
|
-
## Test Quality
|
|
19
|
-
|
|
20
|
-
### Remaining Items
|
|
21
|
-
- [ ] Test config priority chain (ENV > ~/.nansen > local config.json) — needs `loadConfig()` exported
|
|
22
|
-
- [ ] Add snapshot tests for `--help` output
|
|
23
|
-
- [ ] Document magic test addresses (e.g. Binance hot wallet)
|
|
24
|
-
- [ ] Test Bitcoin address validation
|
|
25
|
-
- [ ] Test stdin pipe mode for API key input
|
|
7
|
+
### Test Cleanup
|
|
26
8
|
- [ ] Remove duplicated `parseArgs` in unit.test.js (now exported from cli.js)
|
|
27
9
|
- [ ] Reduce cli.test.js subprocess tests to ~10 smoke tests
|
|
28
10
|
|
|
29
|
-
## Nice to Have
|
|
30
|
-
|
|
31
11
|
### Streaming Output
|
|
32
12
|
- [ ] `--stream` flag for large result sets
|
|
33
13
|
- [ ] Output as JSON lines (newline-delimited JSON)
|
|
34
14
|
- [ ] Enable incremental processing by agents
|
|
35
15
|
|
|
16
|
+
## P2 - Nice to Have
|
|
17
|
+
|
|
18
|
+
### Test Coverage Gaps
|
|
19
|
+
- [ ] Test config priority chain (ENV > ~/.nansen > local) — needs `loadConfig()` exported
|
|
20
|
+
- [ ] Add snapshot tests for `--help` output
|
|
21
|
+
- [ ] Document magic test addresses (e.g. Binance hot wallet)
|
|
22
|
+
- [ ] Test Bitcoin address validation
|
|
23
|
+
- [ ] Test stdin pipe mode for API key input
|
|
24
|
+
|
|
36
25
|
### Shell Completions
|
|
37
26
|
- [ ] Bash completions
|
|
38
27
|
- [ ] Zsh completions
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -145,6 +145,93 @@ export function deleteConfig() {
|
|
|
145
145
|
return false;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// ============= Response Cache =============
|
|
149
|
+
|
|
150
|
+
const CACHE_DIR = path.join(CONFIG_DIR, 'cache');
|
|
151
|
+
const DEFAULT_CACHE_TTL = 300; // 5 minutes
|
|
152
|
+
|
|
153
|
+
import crypto from 'crypto';
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate cache key from endpoint and request body
|
|
157
|
+
*/
|
|
158
|
+
function getCacheKey(endpoint, body) {
|
|
159
|
+
const data = JSON.stringify({ endpoint, body });
|
|
160
|
+
return crypto.createHash('md5').update(data).digest('hex');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get cached response if valid
|
|
165
|
+
*/
|
|
166
|
+
export function getCachedResponse(endpoint, body, ttlSeconds = DEFAULT_CACHE_TTL) {
|
|
167
|
+
const cacheKey = getCacheKey(endpoint, body);
|
|
168
|
+
const cacheFile = path.join(CACHE_DIR, `${cacheKey}.json`);
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(cacheFile)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
176
|
+
const age = (Date.now() - cached.timestamp) / 1000;
|
|
177
|
+
|
|
178
|
+
if (ttlSeconds <= 0 || age > ttlSeconds) {
|
|
179
|
+
// Cache expired or TTL is 0, delete it
|
|
180
|
+
fs.unlinkSync(cacheFile);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { ...cached.data, _meta: { ...cached.data._meta, fromCache: true, cacheAge: Math.round(age) } };
|
|
185
|
+
} catch (e) {
|
|
186
|
+
// Invalid cache file, delete it
|
|
187
|
+
try { fs.unlinkSync(cacheFile); } catch {}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Save response to cache
|
|
194
|
+
*/
|
|
195
|
+
export function setCachedResponse(endpoint, body, data) {
|
|
196
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
197
|
+
fs.mkdirSync(CACHE_DIR, { mode: 0o700, recursive: true });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const cacheKey = getCacheKey(endpoint, body);
|
|
201
|
+
const cacheFile = path.join(CACHE_DIR, `${cacheKey}.json`);
|
|
202
|
+
|
|
203
|
+
const cached = {
|
|
204
|
+
timestamp: Date.now(),
|
|
205
|
+
endpoint,
|
|
206
|
+
data
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
fs.writeFileSync(cacheFile, JSON.stringify(cached), { mode: 0o600 });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Clear all cached responses
|
|
214
|
+
*/
|
|
215
|
+
export function clearCache() {
|
|
216
|
+
if (fs.existsSync(CACHE_DIR)) {
|
|
217
|
+
const files = fs.readdirSync(CACHE_DIR);
|
|
218
|
+
for (const file of files) {
|
|
219
|
+
if (file.endsWith('.json')) {
|
|
220
|
+
fs.unlinkSync(path.join(CACHE_DIR, file));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return files.length;
|
|
224
|
+
}
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get cache directory path
|
|
230
|
+
*/
|
|
231
|
+
export function getCacheDir() {
|
|
232
|
+
return CACHE_DIR;
|
|
233
|
+
}
|
|
234
|
+
|
|
148
235
|
// ============= Address Validation =============
|
|
149
236
|
|
|
150
237
|
const ADDRESS_PATTERNS = {
|
|
@@ -299,6 +386,10 @@ export class NansenAPI {
|
|
|
299
386
|
this.apiKey = apiKey;
|
|
300
387
|
this.baseUrl = baseUrl;
|
|
301
388
|
this.retryOptions = { ...DEFAULT_RETRY_OPTIONS, ...options.retry };
|
|
389
|
+
this.cacheOptions = {
|
|
390
|
+
enabled: options.cache?.enabled ?? false,
|
|
391
|
+
ttl: options.cache?.ttl ?? DEFAULT_CACHE_TTL
|
|
392
|
+
};
|
|
302
393
|
}
|
|
303
394
|
|
|
304
395
|
async request(endpoint, body = {}, options = {}) {
|
|
@@ -306,6 +397,17 @@ export class NansenAPI {
|
|
|
306
397
|
const { maxRetries, baseDelayMs, maxDelayMs, retryOnStatus } = this.retryOptions;
|
|
307
398
|
const shouldRetry = options.retry !== false; // Allow disabling retry per-request
|
|
308
399
|
|
|
400
|
+
// Check cache first (if enabled and not bypassed)
|
|
401
|
+
const useCache = options.cache !== false && this.cacheOptions.enabled;
|
|
402
|
+
const cacheTtl = options.cacheTtl ?? this.cacheOptions.ttl;
|
|
403
|
+
|
|
404
|
+
if (useCache) {
|
|
405
|
+
const cached = getCachedResponse(endpoint, body, cacheTtl);
|
|
406
|
+
if (cached) {
|
|
407
|
+
return cached;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
309
411
|
let lastError;
|
|
310
412
|
|
|
311
413
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
@@ -383,6 +485,12 @@ export class NansenAPI {
|
|
|
383
485
|
if (attempt > 0) {
|
|
384
486
|
data._meta = { ...(data._meta || {}), retriedAttempts: attempt };
|
|
385
487
|
}
|
|
488
|
+
|
|
489
|
+
// Cache successful response
|
|
490
|
+
if (useCache) {
|
|
491
|
+
setCachedResponse(endpoint, body, data);
|
|
492
|
+
}
|
|
493
|
+
|
|
386
494
|
return data;
|
|
387
495
|
}
|
|
388
496
|
|
package/src/cli.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* Extracted from index.js for coverage
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { NansenAPI, saveConfig, deleteConfig, getConfigFile } from './api.js';
|
|
6
|
+
import { NansenAPI, saveConfig, deleteConfig, getConfigFile, clearCache, getCacheDir } from './api.js';
|
|
7
7
|
import * as readline from 'readline';
|
|
8
8
|
|
|
9
9
|
// ============= Schema Definition =============
|
|
10
10
|
|
|
11
11
|
export const SCHEMA = {
|
|
12
|
-
version: '1.
|
|
12
|
+
version: '1.1.0',
|
|
13
13
|
commands: {
|
|
14
14
|
'smart-money': {
|
|
15
15
|
description: 'Smart Money analytics - track sophisticated market participants',
|
|
@@ -278,7 +278,7 @@ export function parseArgs(args) {
|
|
|
278
278
|
const key = arg.slice(2);
|
|
279
279
|
const next = args[i + 1];
|
|
280
280
|
|
|
281
|
-
if (key === 'pretty' || key === 'help' || key === 'table' || key === 'no-retry') {
|
|
281
|
+
if (key === 'pretty' || key === 'help' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache') {
|
|
282
282
|
result.flags[key] = true;
|
|
283
283
|
} else if (next && !next.startsWith('-')) {
|
|
284
284
|
// Try to parse as JSON first
|
|
@@ -439,6 +439,7 @@ COMMANDS:
|
|
|
439
439
|
login Save your API key (interactive)
|
|
440
440
|
logout Remove saved API key
|
|
441
441
|
schema Output JSON schema for all commands (for agent introspection)
|
|
442
|
+
cache Cache management (clear)
|
|
442
443
|
smart-money Smart Money analytics (netflow, dex-trades, holdings, dcas, historical-holdings)
|
|
443
444
|
profiler Wallet profiling (balance, labels, transactions, pnl, perp-positions, perp-trades)
|
|
444
445
|
token Token God Mode (screener, holders, flows, trades, pnl, perp-trades, perp-positions)
|
|
@@ -459,6 +460,9 @@ GLOBAL OPTIONS:
|
|
|
459
460
|
--symbol Token symbol (for perp endpoints)
|
|
460
461
|
--no-retry Disable automatic retry on rate limits/errors
|
|
461
462
|
--retries <n> Max retry attempts (default: 3)
|
|
463
|
+
--cache Enable response caching (default: off)
|
|
464
|
+
--no-cache Disable cache for this request
|
|
465
|
+
--cache-ttl <s> Cache TTL in seconds (default: 300)
|
|
462
466
|
|
|
463
467
|
EXAMPLES:
|
|
464
468
|
# Get Smart Money netflow on Solana
|
|
@@ -620,6 +624,35 @@ export function buildCommands(deps = {}) {
|
|
|
620
624
|
return SCHEMA;
|
|
621
625
|
},
|
|
622
626
|
|
|
627
|
+
'cache': async (args, apiInstance, flags, options) => {
|
|
628
|
+
const subcommand = args[0] || 'help';
|
|
629
|
+
|
|
630
|
+
const handlers = {
|
|
631
|
+
'clear': () => {
|
|
632
|
+
const count = clearCache();
|
|
633
|
+
log(`✓ Cleared ${count} cached responses`);
|
|
634
|
+
log(` Cache dir: ${getCacheDir()}`);
|
|
635
|
+
},
|
|
636
|
+
'help': () => {
|
|
637
|
+
log('Cache Management\n');
|
|
638
|
+
log('USAGE:');
|
|
639
|
+
log(' nansen cache clear Clear all cached responses\n');
|
|
640
|
+
log('CACHE OPTIONS (for any command):');
|
|
641
|
+
log(' --cache Enable caching for this session');
|
|
642
|
+
log(' --no-cache Bypass cache for this request');
|
|
643
|
+
log(' --cache-ttl <seconds> Set cache TTL (default: 300)');
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
if (!handlers[subcommand]) {
|
|
648
|
+
log(`Unknown cache subcommand: ${subcommand}`);
|
|
649
|
+
handlers['help']();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return handlers[subcommand]();
|
|
654
|
+
},
|
|
655
|
+
|
|
623
656
|
'smart-money': async (args, apiInstance, flags, options) => {
|
|
624
657
|
const subcommand = args[0] || 'help';
|
|
625
658
|
const chain = options.chain || 'solana';
|
|
@@ -763,7 +796,7 @@ export function buildCommands(deps = {}) {
|
|
|
763
796
|
}
|
|
764
797
|
|
|
765
798
|
// Commands that don't require API authentication
|
|
766
|
-
export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema'];
|
|
799
|
+
export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache'];
|
|
767
800
|
|
|
768
801
|
// Run CLI with given args (returns result, allows custom output/exit handlers)
|
|
769
802
|
export async function runCLI(rawArgs, deps = {}) {
|
|
@@ -820,7 +853,13 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
820
853
|
? { maxRetries: 0 }
|
|
821
854
|
: { maxRetries: options.retries !== undefined ? options.retries : 3 };
|
|
822
855
|
|
|
823
|
-
|
|
856
|
+
// Configure cache options
|
|
857
|
+
const cacheOptions = {
|
|
858
|
+
enabled: flags['cache'] && !flags['no-cache'],
|
|
859
|
+
ttl: options['cache-ttl'] !== undefined ? options['cache-ttl'] : 300
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions, cache: cacheOptions });
|
|
824
863
|
let result = await commands[command](subArgs, api, flags, options);
|
|
825
864
|
|
|
826
865
|
// Apply field filtering if --fields is specified
|