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 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
- ## Changesets
181
+ ## Publishing (npm)
182
182
 
183
- Every PR that changes user-facing behavior must include a changeset:
184
- ```bash
185
- npx changeset
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
- Choose `patch` for bug fixes, `minor` for new features, `major` for breaking changes.
188
- CI will not publish without a changeset.
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.3",
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/askeluv/nansen-cli.git"
37
+ "url": "git+https://github.com/nansen-ai/nansen-cli.git"
38
38
  },
39
39
  "bugs": {
40
- "url": "https://github.com/askeluv/nansen-cli/issues"
40
+ "url": "https://github.com/nansen-ai/nansen-cli/issues"
41
41
  },
42
- "homepage": "https://github.com/askeluv/nansen-cli#readme",
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
- return this.request('/api/v1/profiler/address/pnl-and-trade-performance', {
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, pnl, batch, trace, compare, counterparties)
778
- token Token God Mode (screener, holders, flows, trades, pnl, perp-trades, perp-positions)
779
- portfolio Portfolio analytics (defi-holdings)
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': () => apiInstance.addressPnl({ address, chain }),
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
- 'screener': () => apiInstance.tokenScreener({ chains, timeframe, filters, orderBy, pagination }),
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
- const command = positional[0] || 'help';
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' };