nansen-cli 1.37.0 → 1.39.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 +36 -0
- package/README.md +58 -2
- package/package.json +1 -1
- package/skills/nansen-wallet-batch/SKILL.md +1 -1
- package/skills/nansen-wallet-keychain-migration/SKILL.md +14 -12
- package/skills/nansen-wallet-profiler/SKILL.md +1 -1
- package/src/api.js +4 -3
- package/src/cli.js +55 -13
- package/src/doctor.js +480 -0
- package/src/keychain.js +46 -0
- package/src/response-meta.js +2 -2
- package/src/rpc-urls.js +67 -0
- package/src/schema.json +68 -1
- package/src/swap-simulation.js +477 -0
- package/src/telemetry.js +9 -2
- package/src/trade-validation.js +653 -0
- package/src/trading.js +530 -20
- package/src/update-check.js +2 -1
- package/src/walletconnect-trading.js +11 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.39.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#495](https://github.com/nansen-ai/nansen-cli/pull/495) [`3306897`](https://github.com/nansen-ai/nansen-cli/commit/3306897c1aaae594f4401fd4656b2451ab375d78) Thanks [@kome12](https://github.com/kome12)! - Add EVM swap-outcome verification to `trade execute`. Before broadcasting a swap on an EVM chain (Base), the CLI now simulates the transaction and confirms the wallet's balance changes match the quote — the input is spent within your maximum, at least the expected output is received, and no other token or NFT leaves the wallet — refusing to sign when they don't. This runs on top of the existing pre-broadcast checks and needs a simulation-capable RPC (`NANSEN_BASE_SIM_RPC`); when none is available it degrades with a warning rather than blocking the trade. Skip it with `--no-verify-outcome`. Solana is unaffected.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#495](https://github.com/nansen-ai/nansen-cli/pull/495) [`e8cf217`](https://github.com/nansen-ai/nansen-cli/commit/e8cf217feaa9e7c8f68f4f3c3c2a49adcda07101) Thanks [@kome12](https://github.com/kome12)! - Harden swap-outcome verification error handling: a revert reported by the simulation endpoint as a top-level JSON-RPC error (rather than a per-call status) now fails closed (blocks the swap) instead of degrading, and a non-2xx simulation response (e.g. HTTP 401 "Invalid API key") now degrades with the real status and message instead of a misleading "returned no call result" warning.
|
|
12
|
+
|
|
13
|
+
- [#499](https://github.com/nansen-ai/nansen-cli/pull/499) [`de0bcc5`](https://github.com/nansen-ai/nansen-cli/commit/de0bcc562bcd20a80edd3ab2f486870b80629c83) Thanks [@gulshngill](https://github.com/gulshngill)! - Fix `profiler labels`: call `/api/v1/profiler/address/labels` with its v1 request body — the beta endpoint previously used was removed from the Nansen API. `profiler batch --include labels` now returns the label array itself instead of the raw `{pagination, data}` envelope.
|
|
14
|
+
|
|
15
|
+
- [#506](https://github.com/nansen-ai/nansen-cli/pull/506) [`f407edb`](https://github.com/nansen-ai/nansen-cli/commit/f407edb19444d6b5a1a631d29b4c4fb9bd280708) Thanks [@gulshngill](https://github.com/gulshngill)! - Add a canonical MCP setup section to the README — endpoint `https://mcp.nansen.ai/ra/mcp`, `NANSEN-API-KEY` auth, per-client setup paths for Claude Code, Claude Tag, and generic or stdio-only clients, plus a pointer to the connection docs for Claude Desktop and Cursor — and point the out-of-credits and low-credit warnings at the credits tab of the billing page, `app.nansen.ai/api?tab=api`, instead of the bare `app.nansen.ai/api`.
|
|
16
|
+
|
|
17
|
+
- [#500](https://github.com/nansen-ai/nansen-cli/pull/500) [`9ccf8a2`](https://github.com/nansen-ai/nansen-cli/commit/9ccf8a20841a9ca01a2627ccf5de2575bf016a46) Thanks [@gulshngill](https://github.com/gulshngill)! - Document global pagination options in `nansen schema`.
|
|
18
|
+
|
|
19
|
+
## 1.38.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- [#486](https://github.com/nansen-ai/nansen-cli/pull/486) [`b752d81`](https://github.com/nansen-ai/nansen-cli/commit/b752d81336a5d9bda3ad85e62a4d42d98c069e58) Thanks [@gulshngill](https://github.com/gulshngill)! - Add `nansen auth status` and `nansen doctor`. `auth status` is fully offline: it reports whether an API key is configured and where it comes from (env var vs config file, masked), the active base URL, x402 wallet readiness, and OS keychain availability. `doctor` runs health checks over the whole setup — Node version, config file validity and permissions, wallet storage and password hygiene (flags the insecure `.credentials` file), keychain availability, Privy env credentials, caches, and telemetry — with an actionable fix per finding, plus a safe unauthenticated connectivity probe (no credits consumed; skip it with `--offline`). `--json` returns machine-readable checks.
|
|
24
|
+
|
|
25
|
+
- [#494](https://github.com/nansen-ai/nansen-cli/pull/494) [`67027e6`](https://github.com/nansen-ai/nansen-cli/commit/67027e6a0faefcc797ec9407f199bc985dbbfc56) Thanks [@kome12](https://github.com/kome12)! - Harden EVM swap signing: scope ERC-20 approvals to the trade amount instead of granting an unlimited allowance, and validate the swap target before signing (reject an empty/zero address, a non-contract target, or a target equal to the token being sold). As a result, ERC-20 sells on Base now include a per-swap approval transaction. Native ETH swaps and all Solana swaps are unaffected. Note: this scopes approvals granted from now on; a pre-existing unlimited approval from an earlier version is not automatically reduced.
|
|
26
|
+
|
|
27
|
+
Also tightens the input validation on the quote a swap is signed from. Every approval-signing path (local, Privy, WalletConnect) now shares one encoder that requires a well-formed 20-byte spender, keeps the approved amount bounded (never unlimited) and within the request cap, and produces fixed-width approval calldata. EVM execution now requires complete request intent persisted by the quote command and revalidates each quote against it (chain, wallet, token pair, mode, and amount), so the signed transaction remains bound to what was requested. A same-chain swap whose transaction is a bare ERC-20 transfer/approve rather than a routed swap is refused (bridge routes excluded).
|
|
28
|
+
|
|
29
|
+
The swap-target contract check now fails closed: it retries and, if it still can't confirm the target carries contract code, refuses to sign rather than proceeding on an unverified target.
|
|
30
|
+
|
|
31
|
+
EVM (Base) exactOut swaps now require an explicit maximum input (spend ceiling) via `--max-input` in base units of the sell token. The quote persists that `maxInputAmount`, and the execute path refuses to sign, approve, or broadcast any quote whose input exceeds it, for native and ERC-20 swaps across all three EVM signing paths. Solana exactOut is unaffected and does not require the flag (there is no ERC-20 approval to scope on that path). Quotes already above the cap are dropped at quote time (and, when none fit, a clear `MAX_INPUT_EXCEEDED` error is returned) rather than saved and rejected only at execute. Relatedly, a quote missing a field the request-intent check needs (sell/buy token address or the bound amount) is now rejected rather than skipped, and the exactOut output binding accepts more-than-requested output (only a shortfall is rejected, since the input is independently capped).
|
|
32
|
+
|
|
33
|
+
The execute path also binds the signer to the wallet the quote was built for: it now refuses to sign a quote whose persisted wallet doesn't match the current signer (e.g. the default wallet changed between quote and execute), since the quoted transaction is constructed for a specific sender.
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- [#494](https://github.com/nansen-ai/nansen-cli/pull/494) [`54b9d41`](https://github.com/nansen-ai/nansen-cli/commit/54b9d41fc9f99cd68bce416b95a129fe9e858981) Thanks [@kome12](https://github.com/kome12)! - Fix exactOut `--max-input` so it bounds the slippage-buffered approval, not the bare quote input. Previously an exactOut ERC-20 quote whose raw input equalled the cap (e.g. 1,000,000 at 3% slippage) passed the max-input filter and was saved, but execution scoped a larger approval (1,030,000) that the approval encoder then rejected for exceeding the cap — bricking the trade across local, Privy, and WalletConnect flows. Both the quote-time filter and the execute-time spend check now measure the same buffered amount the approval encoder does, so a quote that clears the cap can always be signed.
|
|
38
|
+
|
|
3
39
|
## 1.37.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -51,6 +51,61 @@ nansen schema [command] [--pretty] # full command reference (no API key neede
|
|
|
51
51
|
|
|
52
52
|
Run `nansen schema --pretty` for the full subcommand and field reference.
|
|
53
53
|
|
|
54
|
+
## MCP
|
|
55
|
+
|
|
56
|
+
Connect any MCP client to Nansen's streamable HTTP server:
|
|
57
|
+
|
|
58
|
+
- **Endpoint:** `https://mcp.nansen.ai/ra/mcp`
|
|
59
|
+
- **Authentication:** `NANSEN-API-KEY` header
|
|
60
|
+
- **API key:** [app.nansen.ai/auth/agent-setup](https://app.nansen.ai/auth/agent-setup)
|
|
61
|
+
|
|
62
|
+
**Claude Desktop and Cursor:** setup instructions for both — the Claude Desktop `.dxt` bundle and the Cursor install deep link — are in the connection docs: [docs.nansen.ai/mcp/connecting](https://docs.nansen.ai/mcp/connecting).
|
|
63
|
+
|
|
64
|
+
**One-command (Claude Code):**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
claude mcp add --transport http nansen https://mcp.nansen.ai/ra/mcp --header "NANSEN-API-KEY: <your-key>"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Manual (any streamable-HTTP client):** for example, add this to Cursor's `~/.cursor/mcp.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"nansen": {
|
|
76
|
+
"url": "https://mcp.nansen.ai/ra/mcp",
|
|
77
|
+
"headers": {
|
|
78
|
+
"NANSEN-API-KEY": "<your-key>"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Manual (stdio-only clients):** use `mcp-remote` as a bridge. Keep the header as one argument with no space after the colon:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"nansen": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": [
|
|
93
|
+
"-y",
|
|
94
|
+
"mcp-remote@latest",
|
|
95
|
+
"https://mcp.nansen.ai/ra/mcp",
|
|
96
|
+
"--header",
|
|
97
|
+
"NANSEN-API-KEY:${NANSEN_API_KEY}"
|
|
98
|
+
],
|
|
99
|
+
"env": {
|
|
100
|
+
"NANSEN_API_KEY": "<your-key>"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Claude Tag (Claude in Slack):** an admin must attach a plugin whose `.mcp.json` points at `https://mcp.nansen.ai/ra/mcp` and add a custom credential allowing the host `mcp.nansen.ai`. See the [Claude Tag custom-connections documentation](https://claude.com/docs/claude-tag/admins/connections/custom). Per-user fallback: use Claude Code or Claude Desktop.
|
|
108
|
+
|
|
54
109
|
## Trading
|
|
55
110
|
|
|
56
111
|
DEX swaps on `solana` and `base`. Two-step: quote then execute.
|
|
@@ -216,7 +271,7 @@ nansen research smart-money netflow --chain solana --fields token_symbol,net_flo
|
|
|
216
271
|
|
|
217
272
|
| Code | Action |
|
|
218
273
|
|------|--------|
|
|
219
|
-
| `CREDITS_EXHAUSTED` | Stop all API calls immediately. `details.credits.remaining` is your actual balance. Top up at [app.nansen.ai/api](https://app.nansen.ai/api). |
|
|
274
|
+
| `CREDITS_EXHAUSTED` | Stop all API calls immediately. `details.credits.remaining` is your actual balance. Top up at [app.nansen.ai/api?tab=api](https://app.nansen.ai/api?tab=api). |
|
|
220
275
|
| `UNAUTHORIZED` | Wrong or missing key. Re-auth. |
|
|
221
276
|
| `RATE_LIMITED` | Auto-retried by CLI. `details.rateLimit.resetSeconds` is how long the window needs to drain. |
|
|
222
277
|
| `UNSUPPORTED_FILTER` | Remove the filter and retry. |
|
|
@@ -238,7 +293,8 @@ Any field may be absent or `null`, meaning unknown — never assume zero. A low-
|
|
|
238
293
|
|---------|-----|
|
|
239
294
|
| `command not found` | `npm install -g nansen-cli` |
|
|
240
295
|
| Global install reports an older version | `npm i -g nansen-cli@latest --registry=https://registry.npmjs.org/ --prefer-online`, then check `which -a nansen` for stale binaries |
|
|
241
|
-
| `UNAUTHORIZED` after login | `
|
|
296
|
+
| `UNAUTHORIZED` after login | `nansen auth status` shows which key is active and where it comes from; re-run `nansen login` or set `NANSEN_API_KEY` |
|
|
297
|
+
| Anything else misbehaving | `nansen doctor` checks your whole setup (auth, wallets, caches, connectivity) with a fix per finding |
|
|
242
298
|
| Empty perp _research_ results | Use `--symbol BTC`, not `--token`. Perps are Hyperliquid-only. |
|
|
243
299
|
| `perp` _trading_ prints the usage banner | Trading needs `--coin BTC` (`--symbol` also works); see the Perpetuals section. |
|
|
244
300
|
| `UNSUPPORTED_FILTER` on token holders | Remove `--smart-money` — not all tokens have that data. |
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ allowed-tools: Bash(nansen:*)
|
|
|
19
19
|
ADDRESSES="0xaddr1,0xaddr2,0xaddr3,..." CHAIN=ethereum
|
|
20
20
|
nansen research profiler batch --addresses "$ADDRESSES" --chain $CHAIN --include labels,balance
|
|
21
21
|
# → .data.{total, completed, results[]: {address, chain, labels[], balance, error}}
|
|
22
|
-
# labels[]: {label, category ("smart_money","fund","social","behavioral","others"),
|
|
22
|
+
# labels[]: {label, category ("smart_money","fund","social","behavioral","others"), kind[]}
|
|
23
23
|
# balance: {data[]: {token_symbol, token_amount, price_usd, value_usd}}
|
|
24
24
|
```
|
|
25
25
|
Check .error per result — invalid addresses return an error message, not a crash. Skip those.
|
|
@@ -31,24 +31,26 @@ Use this skill when a user already has a nansen-cli wallet set up with the
|
|
|
31
31
|
## Detect current state
|
|
32
32
|
|
|
33
33
|
`wallet show` only displays addresses and does NOT load or check the password.
|
|
34
|
-
To detect the actual password situation, check
|
|
34
|
+
To detect the actual password situation, check the stored password source directly:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
# 1.
|
|
38
|
-
nansen
|
|
37
|
+
# 1. Where is the password stored? Offline, decrypts nothing, prints no secrets
|
|
38
|
+
nansen auth status --pretty # → x402.password.source: "env" | "keychain" | "file" | null
|
|
39
39
|
|
|
40
|
-
# 2.
|
|
41
|
-
|
|
42
|
-
ls -la ~/.nansen/wallets/.credentials 2>/dev/null && echo "FOUND: .credentials file (insecure)"
|
|
40
|
+
# 2. Full setup check — flags the insecure .credentials file with a fix
|
|
41
|
+
nansen doctor --offline
|
|
43
42
|
|
|
44
|
-
# 3.
|
|
45
|
-
nansen
|
|
43
|
+
# 3. Legacy pattern doctor does not cover: password written to ~/.nansen/.env
|
|
44
|
+
ls -la ~/.nansen/.env 2>/dev/null && echo "FOUND: ~/.nansen/.env (insecure)"
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
Interpret
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
- `
|
|
47
|
+
Interpret `x402.password.source`:
|
|
48
|
+
- `"file"` → password in `.credentials` file, needs migration (Path B)
|
|
49
|
+
- `"keychain"` → already secure, no migration needed
|
|
50
|
+
- `null` → password not persisted anywhere (Path C or D)
|
|
51
|
+
- `"env"` → `NANSEN_WALLET_PASSWORD` is set; check where it is being exported from (a `.env` file → Path A)
|
|
52
|
+
|
|
53
|
+
Do NOT use `nansen wallet export` to probe the password state — it prints private keys.
|
|
52
54
|
|
|
53
55
|
## Migration paths
|
|
54
56
|
|
|
@@ -92,7 +92,7 @@ nansen research profiler compare --addresses "0xabc,0xdef" --chain ethereum
|
|
|
92
92
|
|
|
93
93
|
- `pnl-summary` has no pagination support (returns aggregate stats, not a list).
|
|
94
94
|
- `perp-positions` has no pagination support.
|
|
95
|
-
- `labels`
|
|
95
|
+
- `labels` supports pagination — `--limit`/`--page` are honoured and the response is `{pagination: {page, per_page, is_last_page}, data[]: {label, category, kind[]}}`.
|
|
96
96
|
- `transactions` caps at per_page=100 (API limit).
|
|
97
97
|
- `trace` makes many API calls — use `--width` conservatively.
|
|
98
98
|
- `batch` accepts `--file <path>` with one address per line as alternative to `--addresses`.
|
package/src/api.js
CHANGED
|
@@ -393,7 +393,7 @@ function requireValidToken(tokenAddress, chain) {
|
|
|
393
393
|
if (!v.valid) throw new NansenError(v.error, v.code);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
function loadConfig() {
|
|
396
|
+
export function loadConfig() {
|
|
397
397
|
// Base config from files, then env vars override individual fields
|
|
398
398
|
let config = null;
|
|
399
399
|
|
|
@@ -908,8 +908,9 @@ export class NansenAPI {
|
|
|
908
908
|
async addressLabels(params = {}) {
|
|
909
909
|
const { address, chain = 'ethereum', pagination = { page: 1, per_page: 100 } } = params;
|
|
910
910
|
if (address) requireValidAddress(address, chain);
|
|
911
|
-
return this.request('/api/
|
|
912
|
-
|
|
911
|
+
return this.request('/api/v1/profiler/address/labels', {
|
|
912
|
+
address,
|
|
913
|
+
chain,
|
|
913
914
|
pagination
|
|
914
915
|
});
|
|
915
916
|
}
|
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { buildResearchCommands, RESEARCH_HISTORICAL_SUBCOMMANDS } from './comman
|
|
|
15
15
|
import { resolveAddress, isEnsName } from './ens.js';
|
|
16
16
|
import fs from 'fs';
|
|
17
17
|
import { getUpdateNotification, getUpgradeNotice, scheduleUpdateCheck } from './update-check.js';
|
|
18
|
+
import { getAuthStatus, runDoctorChecks, runConnectivityChecks, formatDoctorReport } from './doctor.js';
|
|
18
19
|
import { refreshCostMapIfStale, getCostForEndpoint, creditsCharged } from './cost-cache.js';
|
|
19
20
|
import { creditWarning, noticeWarnings } from './response-meta.js';
|
|
20
21
|
import { trackCommandSucceeded, trackCommandFailed } from './telemetry.js';
|
|
@@ -22,7 +23,7 @@ import { createRequire } from 'module';
|
|
|
22
23
|
import * as readline from 'readline';
|
|
23
24
|
|
|
24
25
|
const require = createRequire(import.meta.url);
|
|
25
|
-
const { version: VERSION } = require('../package.json');
|
|
26
|
+
const { version: VERSION, engines: ENGINES } = require('../package.json');
|
|
26
27
|
|
|
27
28
|
// ============= Schema Definition =============
|
|
28
29
|
|
|
@@ -189,7 +190,7 @@ export function parseArgs(args) {
|
|
|
189
190
|
const key = arg.slice(2);
|
|
190
191
|
const next = args[i + 1];
|
|
191
192
|
|
|
192
|
-
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') {
|
|
193
|
+
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') {
|
|
193
194
|
result.flags[key] = true;
|
|
194
195
|
} else if (next && (!next.startsWith('-') || /^-\d/.test(next))) {
|
|
195
196
|
// Try to parse as JSON first (for objects/arrays/booleans),
|
|
@@ -488,7 +489,9 @@ async function enrichTransfers(result, apiInstance, chain) {
|
|
|
488
489
|
for (const addr of addrs) {
|
|
489
490
|
try {
|
|
490
491
|
const labelsResult = await apiInstance.addressLabels({ address: addr, chain });
|
|
491
|
-
labelMap[addr] = labelsResult?.
|
|
492
|
+
labelMap[addr] = Array.isArray(labelsResult?.data)
|
|
493
|
+
? labelsResult.data.map(item => item.label)
|
|
494
|
+
: labelsResult?.labels || [];
|
|
492
495
|
} catch {
|
|
493
496
|
labelMap[addr] = [];
|
|
494
497
|
}
|
|
@@ -567,7 +570,10 @@ export async function batchProfile(api, params = {}) {
|
|
|
567
570
|
}
|
|
568
571
|
try {
|
|
569
572
|
if (include.includes('labels')) {
|
|
570
|
-
|
|
573
|
+
const labelsResult = await api.addressLabels({ address, chain });
|
|
574
|
+
entry.labels = Array.isArray(labelsResult?.data)
|
|
575
|
+
? labelsResult.data
|
|
576
|
+
: labelsResult?.labels || [];
|
|
571
577
|
}
|
|
572
578
|
if (include.includes('balance')) {
|
|
573
579
|
entry.balance = await api.addressBalance({ address, chain });
|
|
@@ -729,8 +735,10 @@ COMMANDS:
|
|
|
729
735
|
alerts list, create, update, toggle, delete
|
|
730
736
|
web search, fetch
|
|
731
737
|
account Show API key status, plan, and remaining credits
|
|
738
|
+
auth status — offline auth status: key source, wallets (no network)
|
|
732
739
|
login Save API key (--api-key <key>, --human, or NANSEN_API_KEY env var)
|
|
733
740
|
logout Remove saved API key
|
|
741
|
+
doctor Diagnostics: auth, wallets, caches, connectivity (--offline --json)
|
|
734
742
|
schema JSON schema for all commands (use "nansen schema <cmd>" for one)
|
|
735
743
|
cache clear
|
|
736
744
|
changelog --since <version> to filter
|
|
@@ -886,6 +894,31 @@ export function buildCommands(deps = {}) {
|
|
|
886
894
|
return apiInstance.getAccount();
|
|
887
895
|
},
|
|
888
896
|
|
|
897
|
+
'auth': async (args, _apiInstance, _flags, _options) => {
|
|
898
|
+
const subcommand = args[0] || 'status';
|
|
899
|
+
if (subcommand !== 'status') {
|
|
900
|
+
throw new NansenError(`Unknown auth subcommand: ${subcommand}. Available: status`, ErrorCode.UNKNOWN);
|
|
901
|
+
}
|
|
902
|
+
return getAuthStatus();
|
|
903
|
+
},
|
|
904
|
+
|
|
905
|
+
'doctor': async (_args, _apiInstance, flags, _options) => {
|
|
906
|
+
const checks = runDoctorChecks({ cliVersion: VERSION, engines: ENGINES });
|
|
907
|
+
if (!flags.offline) {
|
|
908
|
+
checks.push(...await runConnectivityChecks());
|
|
909
|
+
}
|
|
910
|
+
if (flags.json) {
|
|
911
|
+
return {
|
|
912
|
+
version: VERSION,
|
|
913
|
+
offline: Boolean(flags.offline),
|
|
914
|
+
checks,
|
|
915
|
+
errors: checks.filter(c => c.status === 'error').length,
|
|
916
|
+
warnings: checks.filter(c => c.status === 'warn').length,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
log(formatDoctorReport(checks, { cliVersion: VERSION, offline: Boolean(flags.offline) }));
|
|
920
|
+
},
|
|
921
|
+
|
|
889
922
|
'web': async (args, apiInstance, flags, options) => {
|
|
890
923
|
const subcommand = args[0] || 'help';
|
|
891
924
|
const subArgs = args.slice(1);
|
|
@@ -1815,10 +1848,17 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1815
1848
|
const stream = flags.stream || flags.s;
|
|
1816
1849
|
const csv = options.format === 'csv';
|
|
1817
1850
|
|
|
1851
|
+
// `auth` and `doctor --offline` promise zero network activity — that
|
|
1852
|
+
// contract covers the background update-check fetch and telemetry too,
|
|
1853
|
+
// not just the command's own requests.
|
|
1854
|
+
const isOfflineCommand = command === 'auth' || (command === 'doctor' && flags.offline);
|
|
1855
|
+
const trackSucceeded = isOfflineCommand ? async () => {} : trackCommandSucceeded;
|
|
1856
|
+
const trackFailed = isOfflineCommand ? async () => {} : trackCommandFailed;
|
|
1857
|
+
|
|
1818
1858
|
// Update check (read cached result + schedule background refresh)
|
|
1819
1859
|
const updateNotification = getUpdateNotification(VERSION);
|
|
1820
1860
|
const upgradeNotice = getUpgradeNotice(VERSION);
|
|
1821
|
-
scheduleUpdateCheck();
|
|
1861
|
+
if (!isOfflineCommand) scheduleUpdateCheck();
|
|
1822
1862
|
const notify = () => {
|
|
1823
1863
|
if (upgradeNotice) errorOutput(upgradeNotice);
|
|
1824
1864
|
if (updateNotification) errorOutput(updateNotification);
|
|
@@ -1839,7 +1879,9 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1839
1879
|
}
|
|
1840
1880
|
|
|
1841
1881
|
if (command === 'help' || flags.help || flags.h) {
|
|
1842
|
-
|
|
1882
|
+
// Help for an offline command still owes the zero-network contract: the
|
|
1883
|
+
// cost-map refresh fetches the OpenAPI spec and writes ~/.nansen/cost-map.json.
|
|
1884
|
+
if (!isOfflineCommand) await refreshCostMapIfStale();
|
|
1843
1885
|
// Check for subcommand-specific help: nansen <command> <subcommand> --help
|
|
1844
1886
|
if (flags.help || flags.h) {
|
|
1845
1887
|
// Handle 'research <category> <sub> --help' (3-level)
|
|
@@ -1972,7 +2014,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1972
2014
|
};
|
|
1973
2015
|
const formatted = formatOutput(errorData, { pretty, table });
|
|
1974
2016
|
output(formatted.text);
|
|
1975
|
-
await
|
|
2017
|
+
await trackFailed({ command: fullCommand, duration_ms: Date.now() - startTime, error_code: 'UNKNOWN_COMMAND', flags: usedFlags, chain });
|
|
1976
2018
|
exit(1);
|
|
1977
2019
|
return { type: 'error', data: errorData };
|
|
1978
2020
|
}
|
|
@@ -2025,7 +2067,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
2025
2067
|
|
|
2026
2068
|
// Commands that handle their own output return undefined
|
|
2027
2069
|
if (result === undefined) {
|
|
2028
|
-
await
|
|
2070
|
+
await trackSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
2029
2071
|
return { type: 'no-output', command };
|
|
2030
2072
|
}
|
|
2031
2073
|
|
|
@@ -2033,7 +2075,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
2033
2075
|
if (command === 'schema') {
|
|
2034
2076
|
const formatted = formatOutput(result, { pretty, table: false });
|
|
2035
2077
|
output(formatted.text);
|
|
2036
|
-
await
|
|
2078
|
+
await trackSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
2037
2079
|
return { type: 'schema', data: result };
|
|
2038
2080
|
}
|
|
2039
2081
|
|
|
@@ -2046,7 +2088,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
2046
2088
|
// Alerts list with --table uses custom table format
|
|
2047
2089
|
if (command === 'alerts' && subcommand === 'list' && table) {
|
|
2048
2090
|
output(formatAlertsTable(result));
|
|
2049
|
-
await
|
|
2091
|
+
await trackSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
2050
2092
|
return { type: 'success', data: result };
|
|
2051
2093
|
}
|
|
2052
2094
|
|
|
@@ -2057,14 +2099,14 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
2057
2099
|
if (streamOutput) {
|
|
2058
2100
|
output(streamOutput);
|
|
2059
2101
|
}
|
|
2060
|
-
await
|
|
2102
|
+
await trackSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
2061
2103
|
return { type: 'stream', data: result };
|
|
2062
2104
|
}
|
|
2063
2105
|
|
|
2064
2106
|
const successData = { success: true, data: result };
|
|
2065
2107
|
const formatted = formatOutput(successData, { pretty, table, csv });
|
|
2066
2108
|
output(formatted.text);
|
|
2067
|
-
await
|
|
2109
|
+
await trackSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
2068
2110
|
return { type: csv ? 'csv' : 'success', data: result };
|
|
2069
2111
|
} catch (error) {
|
|
2070
2112
|
// Unified error envelope across all command families (perp/bridge/trade):
|
|
@@ -2079,7 +2121,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
2079
2121
|
const formatted = formatOutput(errorData, { pretty, table, csv });
|
|
2080
2122
|
output(formatted.text);
|
|
2081
2123
|
}
|
|
2082
|
-
await
|
|
2124
|
+
await trackFailed({
|
|
2083
2125
|
command: fullCommand,
|
|
2084
2126
|
duration_ms: Date.now() - startTime,
|
|
2085
2127
|
error_code: error.code || 'UNKNOWN',
|