nansen-cli 1.3.3 → 1.5.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/CLAUDE.md +30 -6
- package/package.json +4 -4
- package/src/api.js +74 -3
- package/src/cli.js +278 -9
package/CLAUDE.md
CHANGED
|
@@ -178,14 +178,38 @@ Structured error codes for programmatic handling:
|
|
|
178
178
|
- **Beta endpoints** (`/api/beta/...`) may have different pagination
|
|
179
179
|
- **EVM vs Solana addresses** — validation auto-detects based on chain param
|
|
180
180
|
|
|
181
|
-
##
|
|
181
|
+
## Publishing (npm)
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
**⚠️ DO NOT manually run `npm version` or `npm publish`. CI handles everything.**
|
|
184
|
+
|
|
185
|
+
### How it works:
|
|
186
|
+
|
|
187
|
+
1. **Add a changeset** for user-facing changes:
|
|
188
|
+
```bash
|
|
189
|
+
npx changeset
|
|
190
|
+
# Or manually create .changeset/<name>.md
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
2. **Push to main** — CI runs tests
|
|
194
|
+
|
|
195
|
+
3. **CI creates a "Version Packages" PR** — This bumps version + updates CHANGELOG
|
|
196
|
+
|
|
197
|
+
4. **Merge the Version PR** — CI auto-publishes to npm
|
|
198
|
+
|
|
199
|
+
### Changeset format:
|
|
200
|
+
```markdown
|
|
201
|
+
---
|
|
202
|
+
"nansen-cli": minor
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
Description of changes (appears in CHANGELOG)
|
|
186
206
|
```
|
|
187
|
-
|
|
188
|
-
|
|
207
|
+
|
|
208
|
+
Choose: `patch` (bug fixes), `minor` (new features), `major` (breaking changes)
|
|
209
|
+
|
|
210
|
+
### If you mess up:
|
|
211
|
+
- Accidentally bumped version manually? `git revert` and add a changeset instead
|
|
212
|
+
- CI publish failed? Check GitHub Actions logs, likely needs `NPM_TOKEN` secret refresh
|
|
189
213
|
|
|
190
214
|
## PR Checklist
|
|
191
215
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nansen-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Command-line interface for Nansen API - designed for AI agents",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
|
-
"url": "git+https://github.com/
|
|
37
|
+
"url": "git+https://github.com/nansen-ai/nansen-cli.git"
|
|
38
38
|
},
|
|
39
39
|
"bugs": {
|
|
40
|
-
"url": "https://github.com/
|
|
40
|
+
"url": "https://github.com/nansen-ai/nansen-cli/issues"
|
|
41
41
|
},
|
|
42
|
-
"homepage": "https://github.com/
|
|
42
|
+
"homepage": "https://github.com/nansen-ai/nansen-cli#readme",
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=18.0.0"
|
|
45
45
|
},
|
package/src/api.js
CHANGED
|
@@ -19,6 +19,7 @@ export const ErrorCode = {
|
|
|
19
19
|
UNAUTHORIZED: 'UNAUTHORIZED', // 401 - Invalid or missing API key
|
|
20
20
|
FORBIDDEN: 'FORBIDDEN', // 403 - Valid key but insufficient permissions
|
|
21
21
|
CREDITS_EXHAUSTED: 'CREDITS_EXHAUSTED', // 403 - Insufficient API credits
|
|
22
|
+
PAYMENT_REQUIRED: 'PAYMENT_REQUIRED', // 402 - x402 payment required
|
|
22
23
|
|
|
23
24
|
// Rate Limiting
|
|
24
25
|
RATE_LIMITED: 'RATE_LIMITED', // 429 - Too many requests
|
|
@@ -87,6 +88,8 @@ function statusToErrorCode(status, data = {}) {
|
|
|
87
88
|
return ErrorCode.INVALID_PARAMS;
|
|
88
89
|
case 401:
|
|
89
90
|
return ErrorCode.UNAUTHORIZED;
|
|
91
|
+
case 402:
|
|
92
|
+
return ErrorCode.PAYMENT_REQUIRED;
|
|
90
93
|
case 403:
|
|
91
94
|
if (messageLower.includes('credit') || messageLower.includes('insufficient')) return ErrorCode.CREDITS_EXHAUSTED;
|
|
92
95
|
return ErrorCode.FORBIDDEN;
|
|
@@ -484,6 +487,16 @@ export class NansenAPI {
|
|
|
484
487
|
message = message.replace(/\.+$/, '') + '. This filter is not supported for this token/chain combination. Do not retry.';
|
|
485
488
|
} else if (code === ErrorCode.CREDITS_EXHAUSTED) {
|
|
486
489
|
message = message.replace(/\.+$/, '') + '. No retry will help. Check your Nansen dashboard for credit balance.';
|
|
490
|
+
} else if (code === ErrorCode.PAYMENT_REQUIRED) {
|
|
491
|
+
message = 'Payment required (x402). This endpoint requires on-chain payment.';
|
|
492
|
+
const paymentHeader = response.headers.get('payment-required');
|
|
493
|
+
if (paymentHeader) {
|
|
494
|
+
try {
|
|
495
|
+
data.paymentRequirements = JSON.parse(atob(paymentHeader));
|
|
496
|
+
} catch {
|
|
497
|
+
data.paymentRequiredRaw = paymentHeader;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
487
500
|
}
|
|
488
501
|
|
|
489
502
|
lastError = new NansenError(message, code, response.status, {
|
|
@@ -634,14 +647,23 @@ export class NansenAPI {
|
|
|
634
647
|
}
|
|
635
648
|
|
|
636
649
|
async addressPnl(params = {}) {
|
|
637
|
-
const { address, chain = 'ethereum' } = params;
|
|
650
|
+
const { address, chain = 'ethereum', date, days = 30, pagination } = params;
|
|
638
651
|
if (address) {
|
|
639
652
|
const validation = validateAddress(address, chain);
|
|
640
653
|
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
641
654
|
}
|
|
642
|
-
|
|
655
|
+
// Build date range
|
|
656
|
+
let dateRange = date;
|
|
657
|
+
if (!dateRange) {
|
|
658
|
+
const to = new Date().toISOString().split('T')[0];
|
|
659
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
660
|
+
dateRange = { from, to };
|
|
661
|
+
}
|
|
662
|
+
return this.request('/api/v1/profiler/address/pnl', {
|
|
643
663
|
address,
|
|
644
|
-
chain
|
|
664
|
+
chain,
|
|
665
|
+
date: dateRange,
|
|
666
|
+
pagination
|
|
645
667
|
});
|
|
646
668
|
}
|
|
647
669
|
|
|
@@ -938,6 +960,55 @@ export class NansenAPI {
|
|
|
938
960
|
});
|
|
939
961
|
}
|
|
940
962
|
|
|
963
|
+
async tokenInformation(params = {}) {
|
|
964
|
+
const { tokenAddress, chain = 'solana', timeframe = '24h' } = params;
|
|
965
|
+
if (tokenAddress) {
|
|
966
|
+
const validation = validateTokenAddress(tokenAddress, chain);
|
|
967
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
968
|
+
}
|
|
969
|
+
return this.request('/api/v1/tgm/token-information', {
|
|
970
|
+
token_address: tokenAddress,
|
|
971
|
+
chain,
|
|
972
|
+
timeframe
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// ============= Perp Endpoints =============
|
|
977
|
+
|
|
978
|
+
async perpScreener(params = {}) {
|
|
979
|
+
const { filters = {}, orderBy, pagination, days = 30 } = params;
|
|
980
|
+
const to = new Date().toISOString().split('T')[0];
|
|
981
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
982
|
+
return this.request('/api/v1/perp-screener', {
|
|
983
|
+
date: { from, to },
|
|
984
|
+
filters,
|
|
985
|
+
order_by: orderBy,
|
|
986
|
+
pagination
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async perpLeaderboard(params = {}) {
|
|
991
|
+
const { filters = {}, orderBy, pagination, days = 30 } = params;
|
|
992
|
+
const to = new Date().toISOString().split('T')[0];
|
|
993
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
994
|
+
return this.request('/api/v1/perp-leaderboard', {
|
|
995
|
+
date: { from, to },
|
|
996
|
+
filters,
|
|
997
|
+
order_by: orderBy,
|
|
998
|
+
pagination
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// ============= Points Endpoints =============
|
|
1003
|
+
|
|
1004
|
+
async pointsLeaderboard(params = {}) {
|
|
1005
|
+
const { tier, pagination } = params;
|
|
1006
|
+
return this.request('/api/v1/points/leaderboard', {
|
|
1007
|
+
tier,
|
|
1008
|
+
pagination
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
941
1012
|
// ============= Portfolio Endpoints =============
|
|
942
1013
|
|
|
943
1014
|
async portfolioDefiHoldings(params = {}) {
|
package/src/cli.js
CHANGED
|
@@ -90,7 +90,7 @@ export const SCHEMA = {
|
|
|
90
90
|
},
|
|
91
91
|
'pnl': {
|
|
92
92
|
description: 'PnL and trade performance',
|
|
93
|
-
options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' } },
|
|
93
|
+
options: { address: { type: 'string', required: true }, chain: { type: 'string', default: 'ethereum' }, date: { type: 'string', description: 'Date or date range (YYYY-MM-DD or {"from":"YYYY-MM-DD","to":"YYYY-MM-DD"})' }, days: { type: 'number', default: 30 }, limit: { type: 'number' } },
|
|
94
94
|
returns: ['token_address', 'token_symbol', 'realized_pnl_usd', 'unrealized_pnl_usd', 'total_pnl_usd']
|
|
95
95
|
},
|
|
96
96
|
'search': {
|
|
@@ -165,6 +165,15 @@ export const SCHEMA = {
|
|
|
165
165
|
'token': {
|
|
166
166
|
description: 'Token God Mode - deep analytics for any token',
|
|
167
167
|
subcommands: {
|
|
168
|
+
'info': {
|
|
169
|
+
description: 'Get detailed information for a specific token',
|
|
170
|
+
options: {
|
|
171
|
+
token: { type: 'string', required: true, description: 'Token address' },
|
|
172
|
+
chain: { type: 'string', default: 'solana' },
|
|
173
|
+
timeframe: { type: 'string', default: '24h', enum: ['5m', '10m', '1h', '6h', '24h', '7d', '30d'] }
|
|
174
|
+
},
|
|
175
|
+
returns: ['token_address', 'token_symbol', 'token_name', 'chain', 'price_usd', 'volume_usd', 'market_cap', 'holder_count', 'liquidity_usd']
|
|
176
|
+
},
|
|
168
177
|
'screener': {
|
|
169
178
|
description: 'Discover and filter tokens',
|
|
170
179
|
options: {
|
|
@@ -172,6 +181,7 @@ export const SCHEMA = {
|
|
|
172
181
|
chains: { type: 'array' },
|
|
173
182
|
timeframe: { type: 'string', default: '24h', enum: ['5m', '10m', '1h', '6h', '24h', '7d', '30d'] },
|
|
174
183
|
'smart-money': { type: 'boolean', description: 'Filter for Smart Money only' },
|
|
184
|
+
search: { type: 'string', description: 'Filter results by token symbol or name (client-side)' },
|
|
175
185
|
limit: { type: 'number' },
|
|
176
186
|
sort: { type: 'string' }
|
|
177
187
|
},
|
|
@@ -243,6 +253,44 @@ export const SCHEMA = {
|
|
|
243
253
|
returns: ['protocol', 'chain', 'position_type', 'token_symbol', 'balance', 'balance_usd']
|
|
244
254
|
}
|
|
245
255
|
}
|
|
256
|
+
},
|
|
257
|
+
'perp': {
|
|
258
|
+
description: 'Perpetual futures analytics',
|
|
259
|
+
subcommands: {
|
|
260
|
+
'screener': {
|
|
261
|
+
description: 'Screen perpetual futures contracts',
|
|
262
|
+
options: {
|
|
263
|
+
days: { type: 'number', default: 30 },
|
|
264
|
+
limit: { type: 'number' },
|
|
265
|
+
sort: { type: 'string' },
|
|
266
|
+
filters: { type: 'object' }
|
|
267
|
+
},
|
|
268
|
+
returns: ['token_symbol', 'volume_usd', 'open_interest', 'funding_rate', 'price_change_24h']
|
|
269
|
+
},
|
|
270
|
+
'leaderboard': {
|
|
271
|
+
description: 'Perpetual futures PnL leaderboard',
|
|
272
|
+
options: {
|
|
273
|
+
days: { type: 'number', default: 30 },
|
|
274
|
+
limit: { type: 'number' },
|
|
275
|
+
sort: { type: 'string' },
|
|
276
|
+
filters: { type: 'object' }
|
|
277
|
+
},
|
|
278
|
+
returns: ['address', 'address_label', 'realized_pnl', 'unrealized_pnl', 'total_pnl', 'trade_count', 'win_rate']
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
'points': {
|
|
283
|
+
description: 'Nansen Points analytics',
|
|
284
|
+
subcommands: {
|
|
285
|
+
'leaderboard': {
|
|
286
|
+
description: 'Points leaderboard',
|
|
287
|
+
options: {
|
|
288
|
+
tier: { type: 'string', description: 'Filter by tier' },
|
|
289
|
+
limit: { type: 'number' }
|
|
290
|
+
},
|
|
291
|
+
returns: ['rank', 'address', 'address_label', 'points', 'tier']
|
|
292
|
+
}
|
|
293
|
+
}
|
|
246
294
|
}
|
|
247
295
|
},
|
|
248
296
|
globalOptions: {
|
|
@@ -773,10 +821,16 @@ COMMANDS:
|
|
|
773
821
|
logout Remove saved API key
|
|
774
822
|
schema Output JSON schema for all commands (for agent introspection)
|
|
775
823
|
cache Cache management (clear)
|
|
776
|
-
smart-money Smart Money analytics (netflow, dex-trades, holdings, dcas, historical-holdings)
|
|
777
|
-
profiler Wallet profiling (balance, labels,
|
|
778
|
-
|
|
779
|
-
|
|
824
|
+
smart-money Smart Money analytics (netflow, dex-trades, perp-trades, holdings, dcas, historical-holdings)
|
|
825
|
+
profiler Wallet profiling (balance, labels, transactions, pnl, pnl-summary, search,
|
|
826
|
+
historical-balances, related-wallets, counterparties, perp-positions, perp-trades,
|
|
827
|
+
batch, trace, compare)
|
|
828
|
+
token Token God Mode (info, screener, holders, flows, dex-trades, pnl, who-bought-sold,
|
|
829
|
+
flow-intelligence, transfers, jup-dca, perp-trades, perp-positions,
|
|
830
|
+
perp-pnl-leaderboard)
|
|
831
|
+
portfolio Portfolio analytics (defi)
|
|
832
|
+
perp Perpetual futures analytics (screener, leaderboard)
|
|
833
|
+
points Nansen Points analytics (leaderboard)
|
|
780
834
|
help Show this help message
|
|
781
835
|
|
|
782
836
|
GLOBAL OPTIONS:
|
|
@@ -1043,7 +1097,10 @@ export function buildCommands(deps = {}) {
|
|
|
1043
1097
|
const date = parseDateOption(options.date, days);
|
|
1044
1098
|
return apiInstance.addressTransactions({ address, chain, filters, orderBy, pagination, days, date });
|
|
1045
1099
|
},
|
|
1046
|
-
'pnl': () =>
|
|
1100
|
+
'pnl': () => {
|
|
1101
|
+
const date = parseDateOption(options.date, days);
|
|
1102
|
+
return apiInstance.addressPnl({ address, chain, date, days, pagination });
|
|
1103
|
+
},
|
|
1047
1104
|
'search': () => apiInstance.entitySearch({ query: options.query }),
|
|
1048
1105
|
'historical-balances': () => apiInstance.addressHistoricalBalances({ address, chain, filters, orderBy, pagination, days }),
|
|
1049
1106
|
'related-wallets': () => apiInstance.addressRelatedWallets({ address, chain, orderBy, pagination }),
|
|
@@ -1122,7 +1179,31 @@ export function buildCommands(deps = {}) {
|
|
|
1122
1179
|
}
|
|
1123
1180
|
|
|
1124
1181
|
const handlers = {
|
|
1125
|
-
'
|
|
1182
|
+
'info': () => apiInstance.tokenInformation({ tokenAddress, chain, timeframe }),
|
|
1183
|
+
'screener': async () => {
|
|
1184
|
+
const search = options.search;
|
|
1185
|
+
// When searching, fetch more results to filter from (API has no server-side search)
|
|
1186
|
+
const searchPagination = search
|
|
1187
|
+
? { page: 1, per_page: Math.max(500, pagination?.per_page || 0) }
|
|
1188
|
+
: pagination;
|
|
1189
|
+
const result = await apiInstance.tokenScreener({ chains, timeframe, filters, orderBy, pagination: searchPagination });
|
|
1190
|
+
if (search) {
|
|
1191
|
+
const q = search.toLowerCase();
|
|
1192
|
+
const requestedLimit = pagination?.per_page || 100;
|
|
1193
|
+
const filterArr = (arr) => arr.filter(t =>
|
|
1194
|
+
(t.token_symbol && t.token_symbol.toLowerCase().includes(q)) ||
|
|
1195
|
+
(t.token_name && t.token_name.toLowerCase().includes(q)) ||
|
|
1196
|
+
(t.token_address && t.token_address.toLowerCase() === q)
|
|
1197
|
+
).slice(0, requestedLimit);
|
|
1198
|
+
// Handle nested response shapes: {data: [...]} or {data: {data: [...]}}
|
|
1199
|
+
if (Array.isArray(result?.data)) {
|
|
1200
|
+
return { ...result, data: filterArr(result.data) };
|
|
1201
|
+
} else if (result?.data?.data && Array.isArray(result.data.data)) {
|
|
1202
|
+
return { ...result, data: { ...result.data, data: filterArr(result.data.data) } };
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return result;
|
|
1206
|
+
},
|
|
1126
1207
|
'holders': () => apiInstance.tokenHolders({ tokenAddress, chain, labelType: onlySmartMoney ? 'smart_money' : 'all_holders', filters, orderBy, pagination }),
|
|
1127
1208
|
'flows': () => {
|
|
1128
1209
|
const date = parseDateOption(options.date, days);
|
|
@@ -1146,7 +1227,7 @@ export function buildCommands(deps = {}) {
|
|
|
1146
1227
|
'perp-positions': () => apiInstance.tokenPerpPositions({ tokenSymbol, filters, orderBy, pagination }),
|
|
1147
1228
|
'perp-pnl-leaderboard': () => apiInstance.tokenPerpPnlLeaderboard({ tokenSymbol, filters, orderBy, pagination, days }),
|
|
1148
1229
|
'help': () => ({
|
|
1149
|
-
commands: ['screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
|
|
1230
|
+
commands: ['info', 'screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
|
|
1150
1231
|
description: 'Token God Mode endpoints',
|
|
1151
1232
|
example: 'nansen token screener --chain solana --timeframe 24h --smart-money'
|
|
1152
1233
|
})
|
|
@@ -1184,6 +1265,51 @@ export function buildCommands(deps = {}) {
|
|
|
1184
1265
|
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
1185
1266
|
}
|
|
1186
1267
|
|
|
1268
|
+
return handlers[subcommand]();
|
|
1269
|
+
},
|
|
1270
|
+
|
|
1271
|
+
'perp': async (args, apiInstance, flags, options) => {
|
|
1272
|
+
const subcommand = args[0] || 'help';
|
|
1273
|
+
const filters = options.filters || {};
|
|
1274
|
+
const orderBy = parseSort(options.sort, options['order-by']);
|
|
1275
|
+
const pagination = options.limit ? { page: 1, per_page: options.limit } : undefined;
|
|
1276
|
+
const days = options.days ? parseInt(options.days) : 30;
|
|
1277
|
+
|
|
1278
|
+
const handlers = {
|
|
1279
|
+
'screener': () => apiInstance.perpScreener({ filters, orderBy, pagination, days }),
|
|
1280
|
+
'leaderboard': () => apiInstance.perpLeaderboard({ filters, orderBy, pagination, days }),
|
|
1281
|
+
'help': () => ({
|
|
1282
|
+
commands: ['screener', 'leaderboard'],
|
|
1283
|
+
description: 'Perpetual futures analytics endpoints',
|
|
1284
|
+
example: 'nansen perp screener --days 7 --limit 20'
|
|
1285
|
+
})
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
if (!handlers[subcommand]) {
|
|
1289
|
+
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
return handlers[subcommand]();
|
|
1293
|
+
},
|
|
1294
|
+
|
|
1295
|
+
'points': async (args, apiInstance, flags, options) => {
|
|
1296
|
+
const subcommand = args[0] || 'help';
|
|
1297
|
+
const tier = options.tier;
|
|
1298
|
+
const pagination = options.limit ? { page: 1, per_page: options.limit } : undefined;
|
|
1299
|
+
|
|
1300
|
+
const handlers = {
|
|
1301
|
+
'leaderboard': () => apiInstance.pointsLeaderboard({ tier, pagination }),
|
|
1302
|
+
'help': () => ({
|
|
1303
|
+
commands: ['leaderboard'],
|
|
1304
|
+
description: 'Nansen Points analytics endpoints',
|
|
1305
|
+
example: 'nansen points leaderboard --limit 100'
|
|
1306
|
+
})
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
if (!handlers[subcommand]) {
|
|
1310
|
+
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1187
1313
|
return handlers[subcommand]();
|
|
1188
1314
|
}
|
|
1189
1315
|
};
|
|
@@ -1192,6 +1318,120 @@ export function buildCommands(deps = {}) {
|
|
|
1192
1318
|
// Commands that don't require API authentication
|
|
1193
1319
|
export const NO_AUTH_COMMANDS = ['login', 'logout', 'help', 'schema', 'cache'];
|
|
1194
1320
|
|
|
1321
|
+
// Command aliases for convenience
|
|
1322
|
+
export const COMMAND_ALIASES = {
|
|
1323
|
+
'tgm': 'token', // Token God Mode
|
|
1324
|
+
'sm': 'smart-money', // Smart Money
|
|
1325
|
+
'prof': 'profiler', // Profiler
|
|
1326
|
+
'port': 'portfolio' // Portfolio
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
// Generate help text for a specific subcommand using SCHEMA
|
|
1330
|
+
export function generateSubcommandHelp(command, subcommand) {
|
|
1331
|
+
const cmdSchema = SCHEMA.commands[command];
|
|
1332
|
+
if (!cmdSchema) return null;
|
|
1333
|
+
|
|
1334
|
+
const subSchema = cmdSchema.subcommands?.[subcommand];
|
|
1335
|
+
if (!subSchema) return null;
|
|
1336
|
+
|
|
1337
|
+
const lines = [];
|
|
1338
|
+
lines.push(`\n${command} ${subcommand} - ${subSchema.description || 'No description'}\n`);
|
|
1339
|
+
|
|
1340
|
+
// Usage
|
|
1341
|
+
const requiredOpts = [];
|
|
1342
|
+
const optionalOpts = [];
|
|
1343
|
+
|
|
1344
|
+
if (subSchema.options) {
|
|
1345
|
+
for (const [name, opt] of Object.entries(subSchema.options)) {
|
|
1346
|
+
if (opt.required) {
|
|
1347
|
+
requiredOpts.push(name);
|
|
1348
|
+
} else {
|
|
1349
|
+
optionalOpts.push(name);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
let usage = `USAGE:\n nansen ${command} ${subcommand}`;
|
|
1355
|
+
if (requiredOpts.length) {
|
|
1356
|
+
usage += ' ' + requiredOpts.map(o => `--${o} <value>`).join(' ');
|
|
1357
|
+
}
|
|
1358
|
+
if (optionalOpts.length) {
|
|
1359
|
+
usage += ' [options]';
|
|
1360
|
+
}
|
|
1361
|
+
lines.push(usage);
|
|
1362
|
+
|
|
1363
|
+
// Required options
|
|
1364
|
+
if (requiredOpts.length) {
|
|
1365
|
+
lines.push('\nREQUIRED:');
|
|
1366
|
+
for (const name of requiredOpts) {
|
|
1367
|
+
const opt = subSchema.options[name];
|
|
1368
|
+
const desc = opt.description || `${opt.type}`;
|
|
1369
|
+
lines.push(` --${name.padEnd(16)} ${desc}`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Optional options
|
|
1374
|
+
if (optionalOpts.length) {
|
|
1375
|
+
lines.push('\nOPTIONS:');
|
|
1376
|
+
for (const name of optionalOpts) {
|
|
1377
|
+
const opt = subSchema.options[name];
|
|
1378
|
+
const defaultStr = opt.default !== undefined ? ` (default: ${opt.default})` : '';
|
|
1379
|
+
const desc = (opt.description || opt.type) + defaultStr;
|
|
1380
|
+
lines.push(` --${name.padEnd(16)} ${desc}`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Return fields
|
|
1385
|
+
if (subSchema.returns && subSchema.returns.length) {
|
|
1386
|
+
lines.push('\nRETURNS:');
|
|
1387
|
+
lines.push(` ${subSchema.returns.join(', ')}`);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Examples
|
|
1391
|
+
lines.push('\nEXAMPLES:');
|
|
1392
|
+
const chain = subSchema.options?.chain?.default || 'solana';
|
|
1393
|
+
|
|
1394
|
+
// Example values for common required options
|
|
1395
|
+
const exampleValues = {
|
|
1396
|
+
address: '0x123...',
|
|
1397
|
+
token: '0x123...',
|
|
1398
|
+
query: '"search term"',
|
|
1399
|
+
symbol: 'BTC',
|
|
1400
|
+
date: '2024-01-01'
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
// Build example based on required options
|
|
1404
|
+
let example = ` nansen ${command} ${subcommand}`;
|
|
1405
|
+
for (const name of requiredOpts) {
|
|
1406
|
+
const value = exampleValues[name] || '<value>';
|
|
1407
|
+
example += ` --${name} ${value}`;
|
|
1408
|
+
}
|
|
1409
|
+
if (subSchema.options?.chain && !requiredOpts.includes('chain')) {
|
|
1410
|
+
example += ` --chain ${chain}`;
|
|
1411
|
+
}
|
|
1412
|
+
example += ' --pretty';
|
|
1413
|
+
lines.push(example);
|
|
1414
|
+
|
|
1415
|
+
// Add a filtered example if filters are supported
|
|
1416
|
+
if (subSchema.options?.filters || subSchema.options?.labels) {
|
|
1417
|
+
let filterExample = ` nansen ${command} ${subcommand}`;
|
|
1418
|
+
for (const name of requiredOpts) {
|
|
1419
|
+
const value = exampleValues[name] || '<value>';
|
|
1420
|
+
filterExample += ` --${name} ${value}`;
|
|
1421
|
+
}
|
|
1422
|
+
if (subSchema.options?.chain && !requiredOpts.includes('chain')) {
|
|
1423
|
+
filterExample += ` --chain ${chain}`;
|
|
1424
|
+
}
|
|
1425
|
+
if (subSchema.options?.labels) {
|
|
1426
|
+
filterExample += ' --labels "Smart Trader"';
|
|
1427
|
+
}
|
|
1428
|
+
filterExample += ' --limit 10 --table';
|
|
1429
|
+
lines.push(filterExample);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
return lines.join('\n');
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1195
1435
|
// Run CLI with given args (returns result, allows custom output/exit handlers)
|
|
1196
1436
|
export async function runCLI(rawArgs, deps = {}) {
|
|
1197
1437
|
const {
|
|
@@ -1204,8 +1444,11 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1204
1444
|
|
|
1205
1445
|
const { _: positional, flags, options } = parseArgs(rawArgs);
|
|
1206
1446
|
|
|
1207
|
-
|
|
1447
|
+
// Resolve command aliases
|
|
1448
|
+
const rawCommand = positional[0] || 'help';
|
|
1449
|
+
const command = COMMAND_ALIASES[rawCommand] || rawCommand;
|
|
1208
1450
|
const subArgs = positional.slice(1);
|
|
1451
|
+
const subcommand = subArgs[0];
|
|
1209
1452
|
const pretty = flags.pretty || flags.p;
|
|
1210
1453
|
const table = flags.table || flags.t;
|
|
1211
1454
|
const stream = flags.stream || flags.s;
|
|
@@ -1224,6 +1467,32 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1224
1467
|
}
|
|
1225
1468
|
|
|
1226
1469
|
if (command === 'help' || flags.help || flags.h) {
|
|
1470
|
+
// Check for subcommand-specific help: nansen <command> <subcommand> --help
|
|
1471
|
+
if (flags.help || flags.h) {
|
|
1472
|
+
// First try subcommand help
|
|
1473
|
+
if (command && subcommand) {
|
|
1474
|
+
const subHelp = generateSubcommandHelp(command, subcommand);
|
|
1475
|
+
if (subHelp) {
|
|
1476
|
+
output(subHelp);
|
|
1477
|
+
notify();
|
|
1478
|
+
return { type: 'subcommand-help', command, subcommand };
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
// Then try command-level help (list subcommands)
|
|
1482
|
+
if (command && SCHEMA.commands[command]) {
|
|
1483
|
+
const cmdSchema = SCHEMA.commands[command];
|
|
1484
|
+
const lines = [`\n${command} - ${cmdSchema.description}\n`];
|
|
1485
|
+
lines.push('SUBCOMMANDS:');
|
|
1486
|
+
for (const [sub, subSchema] of Object.entries(cmdSchema.subcommands || {})) {
|
|
1487
|
+
lines.push(` ${sub.padEnd(20)} ${subSchema.description || ''}`);
|
|
1488
|
+
}
|
|
1489
|
+
lines.push(`\nFor detailed help: nansen ${command} <subcommand> --help`);
|
|
1490
|
+
output(lines.join('\n'));
|
|
1491
|
+
notify();
|
|
1492
|
+
return { type: 'command-help', command };
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
// Fallback to main help
|
|
1227
1496
|
output(BANNER + HELP);
|
|
1228
1497
|
notify();
|
|
1229
1498
|
return { type: 'help' };
|