nansen-cli 1.43.1 → 1.44.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/src/cli.js CHANGED
@@ -12,8 +12,12 @@ import { buildLimitOrderCommands } from './limit-order.js';
12
12
  import { formatAlertsTable, buildAlertsCommands } from './commands/alerts.js';
13
13
  import { buildAgentCommands } from './commands/agent.js';
14
14
  import { buildMcpCommands } from './commands/mcp.js';
15
- import { buildResearchCommands, RESEARCH_HISTORICAL_SUBCOMMANDS } from './commands/research.js';
15
+ import { buildCompletionCommands } from './commands/completion.js';
16
+ import { buildResearchCommands, RESEARCH_HISTORICAL_SUBCOMMANDS, RESEARCH_SUBCOMMANDS } from './commands/research.js';
17
+ import { buildPagination, parseSort } from './query-options.js';
18
+ export { buildPagination, parseSort };
16
19
  import { resolveAddress, isEnsName } from './ens.js';
20
+ import { compareSemver } from './semver.js';
17
21
  import fs from 'fs';
18
22
  import { getUpdateNotification, getUpgradeNotice, scheduleUpdateCheck } from './update-check.js';
19
23
  import { getAuthStatus, runDoctorChecks, runConnectivityChecks, formatDoctorReport } from './doctor.js';
@@ -54,14 +58,6 @@ export function resolveBooleanOption(options, flags, key) {
54
58
  return undefined;
55
59
  }
56
60
 
57
- export function buildPagination(options) {
58
- if (!options.limit && !options.page) return undefined;
59
- return {
60
- page: Math.max(1, parseInt(options.page, 10) || 1),
61
- per_page: options.limit,
62
- };
63
- }
64
-
65
61
  // ============= Field Filtering =============
66
62
 
67
63
  /**
@@ -168,18 +164,15 @@ export function compactSchema(schema) {
168
164
  };
169
165
  }
170
166
 
171
- /**
172
- * Compare two semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
173
- */
174
- function compareSemver(a, b) {
175
- const parse = v => v.replace(/^v/, '').split('.').map(Number);
176
- const [aM, am, ap] = parse(a);
177
- const [bM, bm, bp] = parse(b);
178
- if (aM !== bM) return aM > bM ? 1 : -1;
179
- if (am !== bm) return am > bm ? 1 : -1;
180
- if (ap !== bp) return ap > bp ? 1 : -1;
181
- return 0;
182
- }
167
+ // Long options that never consume the next argument. The shell-completion
168
+ // generator needs the same list to tell an option's value apart from a
169
+ // subcommand, so it lives here rather than inline in parseArgs.
170
+ export const VALUELESS_FLAGS = new Set([
171
+ 'pretty', 'help', 'version', 'table', 'no-retry', 'cache', 'no-cache', 'stream',
172
+ 'enrich', 'full', 'human', 'enabled', 'disabled', 'expert', 'json', 'offline',
173
+ 'no-simulate', 'no-verify-outcome', 'no-revoke-excessive-allowance', 'dry-run',
174
+ 'send-api-key',
175
+ ]);
183
176
 
184
177
  export function parseArgs(args) {
185
178
  const result = { _: [], flags: {}, options: {} };
@@ -191,7 +184,7 @@ export function parseArgs(args) {
191
184
  const key = arg.slice(2);
192
185
  const next = args[i + 1];
193
186
 
194
- if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich' || key === 'full' || key === 'human' || key === 'enabled' || key === 'disabled' || key === 'expert' || key === 'json' || key === 'offline' || key === 'no-simulate' || key === 'no-verify-outcome' || key === 'no-revoke-excessive-allowance' || key === 'dry-run') {
187
+ if (VALUELESS_FLAGS.has(key)) {
195
188
  result.flags[key] = true;
196
189
  } else if (next && (!next.startsWith('-') || /^-\d/.test(next))) {
197
190
  // Try to parse as JSON first (for objects/arrays/booleans),
@@ -460,22 +453,6 @@ export function parseDateOption(dateOption, days = 30) {
460
453
  return { from, to };
461
454
  }
462
455
 
463
- // Parse simple sort syntax: "field:direction" or "field" (defaults to DESC)
464
- export function parseSort(sortOption, orderByOption) {
465
- // If --order-by is provided, use it (full JSON control)
466
- if (orderByOption) return orderByOption;
467
-
468
- // If no --sort, return undefined
469
- if (!sortOption) return undefined;
470
-
471
- // Parse --sort field:direction or --sort field
472
- const parts = sortOption.split(':');
473
- const field = parts[0];
474
- const direction = (parts[1] || 'desc').toUpperCase();
475
-
476
- return [{ field, direction }];
477
- }
478
-
479
456
  // Enrich transfers with Nansen labels for from/to addresses
480
457
  async function enrichTransfers(result, apiInstance, chain) {
481
458
  const transfers = result?.data?.results || result?.transfers || result?.data || [];
@@ -746,6 +723,7 @@ COMMANDS:
746
723
  logout Remove saved API key
747
724
  doctor Diagnostics: auth, wallets, caches, connectivity (--offline --json)
748
725
  schema JSON schema for all commands (use "nansen schema <cmd>" for one)
726
+ completion Shell completions: bash, zsh, fish
749
727
  cache clear
750
728
  changelog --since <version> to filter
751
729
 
@@ -785,7 +763,7 @@ Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
785
763
  Docs: https://docs.nansen.ai
786
764
  Skills: npx skills add nansen-ai/nansen-cli (agent-optimised docs per command group)
787
765
 
788
- Telemetry: anonymous usage stats (commands, timing, errors). Perp order/close additionally send the order side and Hyperliquid order id. Disable: DO_NOT_TRACK=1
766
+ Telemetry: anonymous usage stats (commands, timing, errors). Perp order/close additionally send each leg's side, outcome, order id, shared submission id, and a SHA-256 wallet identifier. Raw wallet, price, size, and exchange error text are not sent. Disable: DO_NOT_TRACK=1
789
767
  `;
790
768
 
791
769
  // Usage text for the `trade` command group. Shared by the trade handler and the
@@ -1132,6 +1110,15 @@ export function buildCommands(deps = {}) {
1132
1110
  }
1133
1111
  const since = _options.since;
1134
1112
  if (since) {
1113
+ // compareSemver treats a missing trailing component as 0, so accept
1114
+ // "1", "1.43", and "1.43.0" alike here — but anything that isn't
1115
+ // digits-and-dots (e.g. "abc") needs a clear error instead of
1116
+ // silently comparing as if it were version 0.0.0, which would show
1117
+ // every entry rather than flag the typo.
1118
+ if (!/^v?\d+(\.\d+){0,2}$/.test(String(since))) {
1119
+ log(`Invalid --since value "${since}": expected a version like 1.43 or 1.43.0.`);
1120
+ return;
1121
+ }
1135
1122
  // Show only entries from the given version onwards
1136
1123
  const lines = content.split('\n');
1137
1124
  const filtered = [];
@@ -1530,7 +1517,7 @@ export function buildCommands(deps = {}) {
1530
1517
  };
1531
1518
 
1532
1519
  if (!handlers[subcommand]) {
1533
- return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
1520
+ throw new NansenError(`Unknown perp analytics subcommand: ${subcommand}. Available: screener, leaderboard`, ErrorCode.UNKNOWN);
1534
1521
  }
1535
1522
 
1536
1523
  return handlers[subcommand]();
@@ -1587,7 +1574,7 @@ export function buildCommands(deps = {}) {
1587
1574
  const maxUniqueTraders24h = options['max-unique-traders-24h'] != null ? Number(options['max-unique-traders-24h']) : undefined;
1588
1575
  const minVolume24hr = options['min-volume-24hr'] != null ? Number(options['min-volume-24hr']) : undefined;
1589
1576
  const maxVolume24hr = options['max-volume-24hr'] != null ? Number(options['max-volume-24hr']) : undefined;
1590
- const negRisk = options['neg-risk'] != null ? options['neg-risk'] === 'true' : undefined;
1577
+ const negRisk = resolveBooleanOption(options, flags, 'neg-risk');
1591
1578
  const minOpenInterest = options['min-open-interest'] != null ? Number(options['min-open-interest']) : undefined;
1592
1579
  const maxOpenInterest = options['max-open-interest'] != null ? Number(options['max-open-interest']) : undefined;
1593
1580
  const endDateBefore = options['end-date-before'];
@@ -1631,32 +1618,33 @@ export function buildCommands(deps = {}) {
1631
1618
  // it, so it has to be taken exactly once, here.
1632
1619
  const perpAnalytics = cmds['perp'];
1633
1620
 
1634
- const researchHistorical = buildResearchCommands(deps).research;
1621
+ const researchSub = buildResearchCommands(deps).research;
1635
1622
 
1636
1623
  cmds['research'] = async (args, apiInstance, flags, options) => {
1637
1624
  const rawCategory = args[0];
1638
1625
  if (!rawCategory || rawCategory === 'help') {
1639
1626
  return {
1640
1627
  categories: [...RESEARCH_CATEGORIES],
1628
+ subcommands: [...RESEARCH_SUBCOMMANDS],
1641
1629
  historical: [...RESEARCH_HISTORICAL_SUBCOMMANDS],
1642
1630
  aliases: RESEARCH_CATEGORY_ALIASES,
1643
1631
  description: 'Research and analytics commands',
1644
1632
  example: 'nansen research smart-money netflow --chain solana'
1645
1633
  };
1646
1634
  }
1647
- if (RESEARCH_HISTORICAL_SUBCOMMANDS.has(rawCategory)) {
1648
- return researchHistorical(args, apiInstance, flags, options);
1635
+ if (RESEARCH_SUBCOMMANDS.has(rawCategory)) {
1636
+ return researchSub(args, apiInstance, flags, options);
1649
1637
  }
1650
1638
  const category = RESEARCH_CATEGORY_ALIASES[rawCategory] || rawCategory;
1651
1639
  if (!RESEARCH_CATEGORIES.has(category)) {
1652
- throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES, ...RESEARCH_HISTORICAL_SUBCOMMANDS].join(', ')}`, ErrorCode.UNKNOWN);
1640
+ throw new NansenError(`Unknown research category: ${rawCategory}. Available: ${[...RESEARCH_CATEGORIES, ...RESEARCH_SUBCOMMANDS].join(', ')}`, ErrorCode.UNKNOWN);
1653
1641
  }
1654
1642
  // `research perp` reaches only the analytics half (screener/leaderboard) —
1655
- // the trading subcommands live at the top level. Routing its help through
1656
- // cmds['perp'] printed the trading help, advertising order/close/leverage
1657
- // from a command that can't run them.
1658
- if (category === 'perp' && (!args[1] || args[1] === 'help')) {
1659
- return perpAnalytics(['help'], apiInstance, flags, options);
1643
+ // the trading subcommands live at the top level. Use the captured handler
1644
+ // for every subcommand because cmds['perp'] is replaced below by the
1645
+ // combined top-level trading dispatcher.
1646
+ if (category === 'perp') {
1647
+ return perpAnalytics(args.slice(1), apiInstance, flags, options);
1660
1648
  }
1661
1649
  return cmds[category](args.slice(1), apiInstance, flags, options);
1662
1650
  };
@@ -1877,7 +1865,9 @@ export async function runCLI(rawArgs, deps = {}) {
1877
1865
  // contract covers the background update-check fetch and telemetry too,
1878
1866
  // not just the command's own requests.
1879
1867
  const isMcpUsage = command === 'mcp' && (subcommand !== 'verify' || flags.help || flags.h);
1880
- const isOfflineCommand = command === 'auth' || (command === 'doctor' && flags.offline) || isMcpUsage;
1868
+ // `completion` renders from the checked-in schema no network, and its
1869
+ // stdout is piped straight into a shell, so keep the update check out of it.
1870
+ const isOfflineCommand = command === 'auth' || (command === 'doctor' && flags.offline) || isMcpUsage || command === 'completion';
1881
1871
  const trackSucceeded = isOfflineCommand ? async () => {} : trackCommandSucceeded;
1882
1872
  const trackFailed = isOfflineCommand ? async () => {} : trackCommandFailed;
1883
1873
 
@@ -1899,7 +1889,7 @@ export async function runCLI(rawArgs, deps = {}) {
1899
1889
 
1900
1890
  // mcp prints its own output via `log`; runCLI callers inject their stdout
1901
1891
  // sink as `output`, so map it across (an explicit `log` dep still wins).
1902
- const commands = { ...buildCommands(deps), ...buildWalletCommands(deps), ...buildTradingCommands(deps), ...buildAlertsCommands(deps), ...buildAgentCommands(deps), ...buildMcpCommands({ ...deps, log: deps.log ?? output }), ...commandOverrides };
1892
+ const commands = { ...buildCommands(deps), ...buildWalletCommands(deps), ...buildTradingCommands(deps), ...buildAlertsCommands(deps), ...buildAgentCommands(deps), ...buildMcpCommands({ ...deps, log: deps.log ?? output }), ...buildCompletionCommands({ ...deps, log: deps.log ?? output }), ...commandOverrides };
1903
1893
 
1904
1894
  if (flags.version || flags.v) {
1905
1895
  output(VERSION);