nansen-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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/nansen-cli.svg)](https://www.npmjs.com/package/nansen-cli)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Tests](https://img.shields.io/badge/tests-342%20passing-brightgreen.svg)]()
5
+ [![Tests](https://img.shields.io/badge/tests-356%20passing-brightgreen.svg)]()
6
6
  [![Coverage](https://img.shields.io/badge/coverage-80%25-brightgreen.svg)]()
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
- ## Medium Priority
5
+ ## P1 - Should Have
6
6
 
7
- ### Response Caching
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nansen-cli",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Command-line interface for Nansen API - designed for AI agents",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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,7 +3,7 @@
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 =============
@@ -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
- const api = new NansenAPIClass(undefined, undefined, { retry: retryOptions });
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