nansen-cli 1.11.2 → 1.13.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/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/api.js +110 -1
- package/src/cli.js +117 -19
- package/src/schema.json +383 -196
- package/src/trading.js +5 -2
- package/src/transfer.js +1 -1
- package/src/wallet.js +95 -25
- package/src/x402.js +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#107](https://github.com/nansen-ai/nansen-cli/pull/107) [`5877c06`](https://github.com/nansen-ai/nansen-cli/commit/5877c06061c1d32998fa3ce011c16f4352fc22dc) Thanks [@marius-reed](https://github.com/marius-reed)! - Add 11 prediction market (Polymarket) endpoints under `nansen research pm`. Includes OHLCV, orderbook, top holders, trades, screeners, PnL, position detail, and categories. Supports `--market-id`, `--address`, `--sort-by`, `--query`, `--status` flags with pagination, sorting, and table output.
|
|
8
|
+
|
|
9
|
+
## 1.12.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#207](https://github.com/nansen-ai/nansen-cli/pull/207) [`73ca500`](https://github.com/nansen-ai/nansen-cli/commit/73ca5009c03ad541673165ca6b50f33ff4cc1673) Thanks [@TimNooren](https://github.com/TimNooren)! - Add --unsafe-no-password flag to wallet create for agent-friendly passwordless wallets.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#212](https://github.com/nansen-ai/nansen-cli/pull/212) [`726c29d`](https://github.com/nansen-ai/nansen-cli/commit/726c29d2676c8a37772299c6237b44890493dfa5) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Clarify empty input handling in parseAddressList with explicit early return
|
|
18
|
+
|
|
19
|
+
- [#218](https://github.com/nansen-ai/nansen-cli/pull/218) [`8c4dd71`](https://github.com/nansen-ai/nansen-cli/commit/8c4dd71ce215026e149a6b097540c46622f13d3a) Thanks [@TimNooren](https://github.com/TimNooren)! - fix: `nansen changelog --since <version>` now correctly filters changeset-format entries (## x.y.z) in addition to Keep a Changelog entries (## [x.y.z])
|
|
20
|
+
|
|
21
|
+
- [#209](https://github.com/nansen-ai/nansen-cli/pull/209) [`a6dc1ed`](https://github.com/nansen-ai/nansen-cli/commit/a6dc1ed9dc40ad3506e7debe09746d463a70c14d) Thanks [@0xlaveen](https://github.com/0xlaveen)! - fix: prevent --help from executing destructive commands (logout, schema, cache)
|
|
22
|
+
|
|
3
23
|
## 1.11.2
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ nansen wallet <subcommand> [options]
|
|
|
30
30
|
nansen schema [command] [--pretty] # full command reference (no API key needed)
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
**Research categories:** `smart-money` (`sm`), `token` (`tgm`), `profiler` (`prof`), `portfolio` (`port`), `search`, `perp`, `points`
|
|
33
|
+
**Research categories:** `smart-money` (`sm`), `token` (`tgm`), `profiler` (`prof`), `portfolio` (`port`), `prediction-market` (`pm`), `search`, `perp`, `points`
|
|
34
34
|
|
|
35
35
|
**Trade:** `quote`, `execute` — DEX swaps on Solana and Base.
|
|
36
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nansen-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "Command-line interface for Nansen API - designed for AI agents",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"lint": "eslint .",
|
|
25
25
|
"lint:fix": "eslint . --fix",
|
|
26
26
|
"changeset": "changeset",
|
|
27
|
-
"changeset:version": "changeset version",
|
|
27
|
+
"changeset:version": "changeset version && npm install --package-lock-only",
|
|
28
28
|
"changeset:publish": "changeset publish"
|
|
29
29
|
},
|
|
30
30
|
"keywords": [
|
package/src/api.js
CHANGED
|
@@ -582,7 +582,9 @@ export class NansenAPI {
|
|
|
582
582
|
// No API key and no payment wallet — guide the user to login rather than
|
|
583
583
|
// showing a confusing x402 payment dump they can't act on.
|
|
584
584
|
// TODO: full fix would skip x402 entirely when no apiKey is set — see PR #<this PR number>
|
|
585
|
-
message = 'No API key configured.
|
|
585
|
+
message = 'No API key configured. Two ways to authenticate:\n' +
|
|
586
|
+
' 1. API key: nansen login --api-key <key> (get key at https://app.nansen.ai/api)\n' +
|
|
587
|
+
' 2. x402 micropayment: nansen wallet create + fund with USDC (no API key needed)';
|
|
586
588
|
} else {
|
|
587
589
|
message = `x402 auto-payment failed: ${x402Err.message}`;
|
|
588
590
|
}
|
|
@@ -1101,6 +1103,113 @@ export class NansenAPI {
|
|
|
1101
1103
|
});
|
|
1102
1104
|
}
|
|
1103
1105
|
|
|
1106
|
+
// ============= Prediction Market Endpoints =============
|
|
1107
|
+
|
|
1108
|
+
async pmOhlcv(params = {}) {
|
|
1109
|
+
const { marketId, sort, pagination } = params;
|
|
1110
|
+
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1111
|
+
return this.request('/api/v1/prediction-market/ohlcv', {
|
|
1112
|
+
market_id: marketId,
|
|
1113
|
+
sort,
|
|
1114
|
+
pagination
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
async pmOrderbook(params = {}) {
|
|
1119
|
+
const { marketId, pagination } = params;
|
|
1120
|
+
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1121
|
+
return this.request('/api/v1/prediction-market/orderbook', {
|
|
1122
|
+
market_id: marketId,
|
|
1123
|
+
pagination
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async pmTopHolders(params = {}) {
|
|
1128
|
+
const { marketId, sort, pagination } = params;
|
|
1129
|
+
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1130
|
+
return this.request('/api/v1/prediction-market/top-holders', {
|
|
1131
|
+
market_id: marketId,
|
|
1132
|
+
sort,
|
|
1133
|
+
pagination
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async pmTradesByMarket(params = {}) {
|
|
1138
|
+
const { marketId, pagination } = params;
|
|
1139
|
+
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1140
|
+
return this.request('/api/v1/prediction-market/trades-by-market', {
|
|
1141
|
+
market_id: marketId,
|
|
1142
|
+
pagination
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async pmTradesByAddress(params = {}) {
|
|
1147
|
+
const { address, pagination } = params;
|
|
1148
|
+
// Polymarket runs exclusively on Polygon
|
|
1149
|
+
const validation = validateAddress(address, 'polygon');
|
|
1150
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1151
|
+
return this.request('/api/v1/prediction-market/trades-by-address', {
|
|
1152
|
+
address,
|
|
1153
|
+
pagination
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
async pmMarketScreener(params = {}) {
|
|
1158
|
+
const { sortBy = 'volume_24hr', query = '', status = '', pagination } = params;
|
|
1159
|
+
return this.request('/api/v1/prediction-market/market-screener', {
|
|
1160
|
+
sort_by: sortBy,
|
|
1161
|
+
query,
|
|
1162
|
+
status,
|
|
1163
|
+
pagination
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
async pmEventScreener(params = {}) {
|
|
1168
|
+
const { sortBy = 'volume_24hr', query = '', status = '', pagination } = params;
|
|
1169
|
+
return this.request('/api/v1/prediction-market/event-screener', {
|
|
1170
|
+
sort_by: sortBy,
|
|
1171
|
+
query,
|
|
1172
|
+
status,
|
|
1173
|
+
pagination
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
async pmPnlByMarket(params = {}) {
|
|
1178
|
+
const { marketId, pagination } = params;
|
|
1179
|
+
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1180
|
+
return this.request('/api/v1/prediction-market/pnl-by-market', {
|
|
1181
|
+
market_id: marketId,
|
|
1182
|
+
pagination
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
async pmPnlByAddress(params = {}) {
|
|
1187
|
+
const { address, pagination } = params;
|
|
1188
|
+
// Polymarket runs exclusively on Polygon
|
|
1189
|
+
const validation = validateAddress(address, 'polygon');
|
|
1190
|
+
if (!validation.valid) throw new NansenError(validation.error, validation.code);
|
|
1191
|
+
return this.request('/api/v1/prediction-market/pnl-by-address', {
|
|
1192
|
+
address,
|
|
1193
|
+
pagination
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async pmPositionDetail(params = {}) {
|
|
1198
|
+
const { marketId, pagination } = params;
|
|
1199
|
+
if (!marketId) throw new NansenError('market_id is required', ErrorCode.MISSING_PARAM);
|
|
1200
|
+
return this.request('/api/v1/prediction-market/position-detail', {
|
|
1201
|
+
market_id: marketId,
|
|
1202
|
+
pagination
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async pmCategories(params = {}) {
|
|
1207
|
+
const { pagination } = params;
|
|
1208
|
+
return this.request('/api/v1/prediction-market/categories', {
|
|
1209
|
+
pagination
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1104
1213
|
// ============= Points Endpoints =============
|
|
1105
1214
|
|
|
1106
1215
|
async pointsLeaderboard(params = {}) {
|
package/src/cli.js
CHANGED
|
@@ -55,15 +55,26 @@ export function filterFields(data, fields) {
|
|
|
55
55
|
const filtered = {};
|
|
56
56
|
for (const key of Object.keys(obj)) {
|
|
57
57
|
if (fieldSet.has(key)) {
|
|
58
|
+
// Explicitly requested — include as-is
|
|
58
59
|
filtered[key] = obj[key];
|
|
59
60
|
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
if (Array.isArray(obj[key])) {
|
|
62
|
+
// Only recurse into arrays whose elements are plain objects.
|
|
63
|
+
// Primitive arrays (e.g. tags: ["a","b"]) are dropped unless the
|
|
64
|
+
// key was explicitly requested (handled above).
|
|
65
|
+
const hasObjectElements = obj[key].length > 0 &&
|
|
66
|
+
typeof obj[key][0] === 'object' && obj[key][0] !== null;
|
|
67
|
+
if (hasObjectElements) {
|
|
68
|
+
const nested = obj[key].map(item => filterObject(item))
|
|
69
|
+
.filter(item => Object.keys(item).length > 0);
|
|
70
|
+
if (nested.length > 0) {
|
|
71
|
+
filtered[key] = nested;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Plain object — always recurse in case it wraps requested fields
|
|
76
|
+
const nested = filterObject(obj[key]);
|
|
77
|
+
if (nested !== null && nested !== undefined && Object.keys(nested).length > 0) {
|
|
67
78
|
filtered[key] = nested;
|
|
68
79
|
}
|
|
69
80
|
}
|
|
@@ -439,6 +450,37 @@ async function enrichTransfers(result, apiInstance, chain) {
|
|
|
439
450
|
return result;
|
|
440
451
|
}
|
|
441
452
|
|
|
453
|
+
// ============= Address Parsing =============
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Parse an --addresses option that may arrive as:
|
|
457
|
+
* - a pre-parsed array (arg parser split it)
|
|
458
|
+
* - a JSON array string: '["0x…","0x…"]'
|
|
459
|
+
* - a comma-separated string: "0x…,0x…"
|
|
460
|
+
* Non-array JSON values (objects, numbers, booleans) are rejected.
|
|
461
|
+
*/
|
|
462
|
+
export function parseAddressList(raw) {
|
|
463
|
+
if (Array.isArray(raw)) {
|
|
464
|
+
return raw.map(a => String(a).trim()).filter(Boolean);
|
|
465
|
+
}
|
|
466
|
+
if (!raw) return [];
|
|
467
|
+
|
|
468
|
+
const s = String(raw);
|
|
469
|
+
try {
|
|
470
|
+
const parsed = JSON.parse(s);
|
|
471
|
+
if (Array.isArray(parsed)) {
|
|
472
|
+
return parsed.map(a => String(a).trim()).filter(Boolean);
|
|
473
|
+
}
|
|
474
|
+
throw new NansenError(
|
|
475
|
+
'--addresses must be a comma-separated list or JSON array, got: ' + typeof parsed,
|
|
476
|
+
ErrorCode.INVALID_PARAMS
|
|
477
|
+
);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
if (e instanceof NansenError) throw e;
|
|
480
|
+
return s.split(',').map(a => a.trim()).filter(Boolean);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
442
484
|
// ============= Composite Functions =============
|
|
443
485
|
|
|
444
486
|
export async function batchProfile(api, params = {}) {
|
|
@@ -644,7 +686,7 @@ EXAMPLES:
|
|
|
644
686
|
nansen research profiler balance --address 0x... --chain ethereum
|
|
645
687
|
nansen trade quote --chain base --from ETH --to USDC --amount 1
|
|
646
688
|
|
|
647
|
-
Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
|
|
689
|
+
Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, zksync, mantle, ronin, sei, plasma, sonic, unichain, monad, hyperevm, iotaevm
|
|
648
690
|
Trade chains: solana, base
|
|
649
691
|
Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
|
|
650
692
|
|
|
@@ -780,6 +822,10 @@ export function buildCommands(deps = {}) {
|
|
|
780
822
|
},
|
|
781
823
|
|
|
782
824
|
'changelog': async (_args, _apiInstance, _flags, _options) => {
|
|
825
|
+
if (_flags.help || _flags.h) {
|
|
826
|
+
log('changelog — Show release history\n\nUsage:\n nansen changelog [--since <version>]\n\nOptions:\n --since <version> Show only entries for versions >= this version\n\nExamples:\n nansen changelog\n nansen changelog --since 1.10.0');
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
783
829
|
const changelogPath = new URL('../CHANGELOG.md', import.meta.url).pathname;
|
|
784
830
|
let content;
|
|
785
831
|
try {
|
|
@@ -795,10 +841,10 @@ export function buildCommands(deps = {}) {
|
|
|
795
841
|
const filtered = [];
|
|
796
842
|
let include = false;
|
|
797
843
|
for (const line of lines) {
|
|
798
|
-
// Match ## [x.y.z]
|
|
799
|
-
const match = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
|
|
844
|
+
// Match ## [x.y.z] (Keep a Changelog format) or ## x.y.z (changeset format)
|
|
845
|
+
const match = line.match(/^## \[(\d+\.\d+\.\d+)\]|^## (\d+\.\d+\.\d+)\b/);
|
|
800
846
|
if (match) {
|
|
801
|
-
const ver = match[1];
|
|
847
|
+
const ver = match[1] || match[2];
|
|
802
848
|
// Compare: include versions >= since, stop at versions < since
|
|
803
849
|
if (compareSemver(ver, since) >= 0) {
|
|
804
850
|
include = true;
|
|
@@ -945,7 +991,7 @@ export function buildCommands(deps = {}) {
|
|
|
945
991
|
'batch': () => {
|
|
946
992
|
let addresses = [];
|
|
947
993
|
if (options.addresses) {
|
|
948
|
-
addresses = options.addresses
|
|
994
|
+
addresses = parseAddressList(options.addresses);
|
|
949
995
|
} else if (options.file) {
|
|
950
996
|
const content = fs.readFileSync(options.file, 'utf8');
|
|
951
997
|
try {
|
|
@@ -976,13 +1022,13 @@ export function buildCommands(deps = {}) {
|
|
|
976
1022
|
return traceCounterparties(apiInstance, { address, chain, depth, width, days, delayMs });
|
|
977
1023
|
},
|
|
978
1024
|
'compare': () => {
|
|
979
|
-
const addrs = (options.addresses
|
|
1025
|
+
const addrs = parseAddressList(options.addresses);
|
|
980
1026
|
return compareWallets(apiInstance, { addresses: addrs, chain, days });
|
|
981
1027
|
},
|
|
982
1028
|
'help': () => ({
|
|
983
1029
|
commands: ['balance', 'labels', 'transactions', 'pnl', 'search', 'historical-balances', 'related-wallets', 'counterparties', 'pnl-summary', 'perp-positions', 'perp-trades', 'batch', 'trace', 'compare'],
|
|
984
1030
|
description: 'Wallet profiling endpoints',
|
|
985
|
-
example: 'nansen profiler
|
|
1031
|
+
example: 'nansen research profiler compare --addresses "0xABC...,0xDEF..." --chain ethereum'
|
|
986
1032
|
})
|
|
987
1033
|
};
|
|
988
1034
|
|
|
@@ -1182,12 +1228,48 @@ export function buildCommands(deps = {}) {
|
|
|
1182
1228
|
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
1183
1229
|
}
|
|
1184
1230
|
|
|
1231
|
+
return handlers[subcommand]();
|
|
1232
|
+
},
|
|
1233
|
+
|
|
1234
|
+
'prediction-market': async (args, apiInstance, flags, options) => {
|
|
1235
|
+
const subcommand = args[0] || 'help';
|
|
1236
|
+
const marketId = options['market-id'];
|
|
1237
|
+
const address = options.address;
|
|
1238
|
+
const sortBy = options['sort-by'];
|
|
1239
|
+
const query = options.query;
|
|
1240
|
+
const status = options.status;
|
|
1241
|
+
const sort = parseSort(options.sort);
|
|
1242
|
+
const pagination = buildPagination(options);
|
|
1243
|
+
|
|
1244
|
+
const handlers = {
|
|
1245
|
+
'ohlcv': () => apiInstance.pmOhlcv({ marketId, sort, pagination }),
|
|
1246
|
+
'orderbook': () => apiInstance.pmOrderbook({ marketId, pagination }),
|
|
1247
|
+
'top-holders': () => apiInstance.pmTopHolders({ marketId, sort, pagination }),
|
|
1248
|
+
'trades-by-market': () => apiInstance.pmTradesByMarket({ marketId, pagination }),
|
|
1249
|
+
'trades-by-address': () => apiInstance.pmTradesByAddress({ address, pagination }),
|
|
1250
|
+
'market-screener': () => apiInstance.pmMarketScreener({ sortBy, query, status, pagination }),
|
|
1251
|
+
'event-screener': () => apiInstance.pmEventScreener({ sortBy, query, status, pagination }),
|
|
1252
|
+
'pnl-by-market': () => apiInstance.pmPnlByMarket({ marketId, pagination }),
|
|
1253
|
+
'pnl-by-address': () => apiInstance.pmPnlByAddress({ address, pagination }),
|
|
1254
|
+
'position-detail': () => apiInstance.pmPositionDetail({ marketId, pagination }),
|
|
1255
|
+
'categories': () => apiInstance.pmCategories({ pagination }),
|
|
1256
|
+
'help': () => ({
|
|
1257
|
+
commands: ['ohlcv', 'orderbook', 'top-holders', 'trades-by-market', 'trades-by-address', 'market-screener', 'event-screener', 'pnl-by-market', 'pnl-by-address', 'position-detail', 'categories'],
|
|
1258
|
+
description: 'Polymarket prediction market analytics',
|
|
1259
|
+
example: 'nansen research pm market-screener --sort-by volume_24hr --limit 20'
|
|
1260
|
+
})
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
if (!handlers[subcommand]) {
|
|
1264
|
+
throw new NansenError(`Unknown subcommand: ${subcommand}. Available: ${Object.keys(handlers).filter(k => k !== 'help').join(', ')}`, ErrorCode.UNKNOWN);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1185
1267
|
return handlers[subcommand]();
|
|
1186
1268
|
}
|
|
1187
1269
|
};
|
|
1188
1270
|
|
|
1189
1271
|
// 'research' delegates to the category handlers defined above
|
|
1190
|
-
const RESEARCH_CATEGORIES = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points']);
|
|
1272
|
+
const RESEARCH_CATEGORIES = new Set(['smart-money', 'profiler', 'token', 'search', 'perp', 'portfolio', 'points', 'prediction-market']);
|
|
1191
1273
|
|
|
1192
1274
|
cmds['research'] = async (args, apiInstance, flags, options) => {
|
|
1193
1275
|
const rawCategory = args[0];
|
|
@@ -1263,11 +1345,12 @@ export const RESEARCH_CATEGORY_ALIASES = {
|
|
|
1263
1345
|
'tgm': 'token',
|
|
1264
1346
|
'sm': 'smart-money',
|
|
1265
1347
|
'prof': 'profiler',
|
|
1266
|
-
'port': 'portfolio'
|
|
1348
|
+
'port': 'portfolio',
|
|
1349
|
+
'pm': 'prediction-market'
|
|
1267
1350
|
};
|
|
1268
1351
|
|
|
1269
1352
|
// Generate help text for a specific subcommand using SCHEMA
|
|
1270
|
-
export function generateSubcommandHelp(command, subcommand) {
|
|
1353
|
+
export function generateSubcommandHelp(command, subcommand, prefix = null) {
|
|
1271
1354
|
const cmdSchema = SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command];
|
|
1272
1355
|
if (!cmdSchema) return null;
|
|
1273
1356
|
|
|
@@ -1294,7 +1377,8 @@ export function generateSubcommandHelp(command, subcommand) {
|
|
|
1294
1377
|
|
|
1295
1378
|
const exampleValues = { address: '0x...', token: '0x...', query: '"term"', symbol: 'BTC', date: '2024-01-01' };
|
|
1296
1379
|
const chain = subSchema.options?.chain?.default || 'solana';
|
|
1297
|
-
|
|
1380
|
+
const cmdPrefix = prefix || (DEPRECATED_TO_RESEARCH.has(command) ? `research ${command}` : command);
|
|
1381
|
+
let example = `nansen ${cmdPrefix} ${subcommand}`;
|
|
1298
1382
|
if (subSchema.options) {
|
|
1299
1383
|
for (const [name, opt] of Object.entries(subSchema.options)) {
|
|
1300
1384
|
if (opt.required) example += ` --${name} ${exampleValues[name] || '<val>'}`;
|
|
@@ -1362,7 +1446,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1362
1446
|
const category = RESEARCH_CATEGORY_ALIASES[subcommand] || subcommand;
|
|
1363
1447
|
const deepSub = subArgs[1];
|
|
1364
1448
|
if (deepSub) {
|
|
1365
|
-
const subHelp = generateSubcommandHelp(category, deepSub);
|
|
1449
|
+
const subHelp = generateSubcommandHelp(category, deepSub, `research ${subcommand}`);
|
|
1366
1450
|
if (subHelp) {
|
|
1367
1451
|
output(subHelp);
|
|
1368
1452
|
notify();
|
|
@@ -1408,6 +1492,20 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1408
1492
|
return { type: 'command-help', command };
|
|
1409
1493
|
}
|
|
1410
1494
|
}
|
|
1495
|
+
// Simple commands (logout, schema, cache) — show help instead of executing
|
|
1496
|
+
// Prevents destructive commands like logout from running when user just wants help
|
|
1497
|
+
if (commands[command]) {
|
|
1498
|
+
const simpleHelp = {
|
|
1499
|
+
'logout': 'nansen logout — Remove saved API key from ~/.nansen/config.json',
|
|
1500
|
+
'schema': 'nansen schema [command] [--pretty] — Show JSON schema for all commands (or a specific command)',
|
|
1501
|
+
'cache': 'nansen cache clear — Clear the API response cache',
|
|
1502
|
+
};
|
|
1503
|
+
if (simpleHelp[command]) {
|
|
1504
|
+
output(simpleHelp[command]);
|
|
1505
|
+
notify();
|
|
1506
|
+
return { type: 'command-help', command };
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1411
1509
|
// Commands with handlers (e.g. quote, execute) show their own usage
|
|
1412
1510
|
if (command === 'help' || !commands[command]) {
|
|
1413
1511
|
output(BANNER + HELP);
|