nansen-cli 1.17.0 → 1.19.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/package.json +5 -3
  3. package/skills/nansen-dca-tracker/SKILL.md +38 -0
  4. package/skills/nansen-defi-positions/SKILL.md +37 -0
  5. package/skills/nansen-exit-signals/SKILL.md +39 -0
  6. package/skills/nansen-fund-tracker/SKILL.md +35 -0
  7. package/skills/nansen-general-search/SKILL.md +34 -0
  8. package/skills/nansen-holder-analysis/SKILL.md +38 -0
  9. package/skills/nansen-perp-screener/SKILL.md +32 -0
  10. package/skills/nansen-perp-trader-profile/SKILL.md +39 -0
  11. package/skills/nansen-polymarket-deep-dive/SKILL.md +50 -0
  12. package/skills/nansen-polymarket-insider-scan/SKILL.md +62 -0
  13. package/skills/nansen-polymarket-trader-profile/SKILL.md +43 -0
  14. package/skills/nansen-portfolio-tracker/SKILL.md +36 -0
  15. package/skills/nansen-prediction-markets/SKILL.md +47 -0
  16. package/skills/nansen-sm-cross-chain-flows/SKILL.md +27 -0
  17. package/skills/nansen-smart-alerts/SKILL.md +137 -0
  18. package/skills/nansen-smart-money-alpha/SKILL.md +43 -0
  19. package/skills/nansen-smart-money-tracker/SKILL.md +71 -0
  20. package/skills/nansen-smart-money-trend/SKILL.md +30 -0
  21. package/skills/nansen-token-research/SKILL.md +90 -0
  22. package/skills/nansen-token-screener/SKILL.md +54 -0
  23. package/skills/nansen-token-transfer-analysis/SKILL.md +40 -0
  24. package/skills/nansen-trading/SKILL.md +100 -0
  25. package/skills/nansen-wallet-batch/SKILL.md +26 -0
  26. package/skills/nansen-wallet-clustering/REFERENCE.md +43 -0
  27. package/skills/nansen-wallet-clustering/SKILL.md +46 -0
  28. package/skills/nansen-wallet-deep-dive/SKILL.md +45 -0
  29. package/skills/nansen-wallet-keychain-migration/SKILL.md +183 -0
  30. package/skills/nansen-wallet-manager/SKILL.md +140 -0
  31. package/skills/nansen-wallet-profiler/SKILL.md +98 -0
  32. package/skills/nansen-web-fetcher/SKILL.md +50 -0
  33. package/skills/nansen-web-searcher/SKILL.md +39 -0
  34. package/src/api.js +76 -3
  35. package/src/cli.js +176 -14
  36. package/src/commands/alerts.js +717 -0
  37. package/src/schema.json +164 -1
  38. package/src/telemetry.js +237 -0
  39. package/src/update-check.js +2 -2
package/src/schema.json CHANGED
@@ -568,6 +568,129 @@
568
568
  }
569
569
  }
570
570
  },
571
+ "alerts": {
572
+ "description": "Smart alert management \u2014 create, update, toggle, delete alerts",
573
+ "subcommands": {
574
+ "list": {
575
+ "description": "List all alerts",
576
+ "returns": [
577
+ "id",
578
+ "name",
579
+ "type",
580
+ "timeWindow",
581
+ "isEnabled",
582
+ "channels",
583
+ "data",
584
+ "description"
585
+ ]
586
+ },
587
+ "create": {
588
+ "description": "Create a new alert",
589
+ "options": {
590
+ "name": {
591
+ "type": "string",
592
+ "required": true,
593
+ "description": "Alert name"
594
+ },
595
+ "type": {
596
+ "type": "string",
597
+ "required": true,
598
+ "description": "Alert type (e.g. sm-token-flows, common-token-transfer)"
599
+ },
600
+ "chains": {
601
+ "type": "string",
602
+ "description": "Comma-separated chains (e.g. ethereum,solana). Merged into data."
603
+ },
604
+ "telegram": {
605
+ "type": "string",
606
+ "description": "Telegram chat ID for notifications"
607
+ },
608
+ "slack": {
609
+ "type": "string",
610
+ "description": "Slack webhook URL for notifications"
611
+ },
612
+ "discord": {
613
+ "type": "string",
614
+ "description": "Discord webhook URL for notifications"
615
+ },
616
+ "data": {
617
+ "type": "string",
618
+ "description": "Alert config JSON. --chains is merged on top."
619
+ },
620
+ "description": {
621
+ "type": "string",
622
+ "description": "Optional description"
623
+ },
624
+ "disabled": {
625
+ "type": "boolean",
626
+ "description": "Create alert in disabled state"
627
+ }
628
+ }
629
+ },
630
+ "update": {
631
+ "description": "Update an existing alert. Usage: nansen alerts update <id> [options]",
632
+ "options": {
633
+ "name": {
634
+ "type": "string",
635
+ "description": "Alert name"
636
+ },
637
+ "type": {
638
+ "type": "string",
639
+ "description": "Alert type"
640
+ },
641
+ "chains": {
642
+ "type": "string",
643
+ "description": "Comma-separated chains. Merged into data."
644
+ },
645
+ "telegram": {
646
+ "type": "string",
647
+ "description": "Telegram chat ID"
648
+ },
649
+ "slack": {
650
+ "type": "string",
651
+ "description": "Slack webhook URL"
652
+ },
653
+ "discord": {
654
+ "type": "string",
655
+ "description": "Discord webhook URL"
656
+ },
657
+ "data": {
658
+ "type": "string",
659
+ "description": "Alert config JSON. --chains merged on top."
660
+ },
661
+ "description": {
662
+ "type": "string",
663
+ "description": "Description"
664
+ },
665
+ "enabled": {
666
+ "type": "boolean",
667
+ "description": "Enable alert"
668
+ },
669
+ "disabled": {
670
+ "type": "boolean",
671
+ "description": "Disable alert"
672
+ }
673
+ }
674
+ },
675
+ "toggle": {
676
+ "description": "Enable or disable an alert. Usage: nansen alerts toggle <id> --enabled|--disabled",
677
+ "options": {
678
+ "enabled": {
679
+ "type": "boolean",
680
+ "description": "Enable alert"
681
+ },
682
+ "disabled": {
683
+ "type": "boolean",
684
+ "description": "Disable alert"
685
+ }
686
+ }
687
+ },
688
+ "delete": {
689
+ "description": "Delete an alert. Usage: nansen alerts delete <id>",
690
+ "options": {}
691
+ }
692
+ }
693
+ },
571
694
  "trade": {
572
695
  "description": "DEX trading commands",
573
696
  "subcommands": {
@@ -596,7 +719,7 @@
596
719
  },
597
720
  "wallet": {
598
721
  "type": "string",
599
- "description": "Wallet name (or \"walletconnect\"/\"wc\" for WalletConnect, EVM only). A configured wallet is required run `nansen wallet create` if you haven't set one up yet."
722
+ "description": "Wallet name (or \"walletconnect\"/\"wc\" for WalletConnect, EVM only). A configured wallet is required \u2014 run `nansen wallet create` if you haven't set one up yet."
600
723
  }
601
724
  },
602
725
  "prerequisites": [
@@ -663,6 +786,46 @@
663
786
  },
664
787
  "account": {
665
788
  "description": "Show API key status, plan, and remaining credits. Does not consume credits."
789
+ },
790
+ "web": {
791
+ "description": "Web search and fetch commands",
792
+ "subcommands": {
793
+ "search": {
794
+ "description": "Search the web for one or more queries in parallel",
795
+ "options": {
796
+ "query": {
797
+ "description": "Search query (can be repeated for multiple queries)"
798
+ },
799
+ "num-results": {
800
+ "description": "Number of results per query (1-20, default 10)",
801
+ "default": 10
802
+ }
803
+ },
804
+ "examples": [
805
+ "nansen web search \"bitcoin price\"",
806
+ "nansen web search \"solana news\" --num-results 5",
807
+ "nansen web search --query \"btc\" --query \"eth\"",
808
+ "nansen web search \"btc\" --query \"eth\" # positional + flag merged"
809
+ ]
810
+ },
811
+ "fetch": {
812
+ "description": "Fetch and analyze content from one or more URLs using AI",
813
+ "options": {
814
+ "url": {
815
+ "description": "URL to fetch (can be repeated for multiple URLs)"
816
+ },
817
+ "question": {
818
+ "description": "Question to answer about the URL content",
819
+ "required": true
820
+ }
821
+ },
822
+ "examples": [
823
+ "nansen web fetch https://nansen.ai --question \"What does Nansen do?\"",
824
+ "nansen web fetch --url https://a.com --url https://b.com --question \"Compare these\"",
825
+ "nansen web fetch https://a.com --url https://b.com --question \"Diff?\" # positional + flag merged"
826
+ ]
827
+ }
828
+ }
666
829
  }
667
830
  },
668
831
  "globalOptions": {
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Lightweight CLI telemetry.
3
+ *
4
+ * Sends anonymous usage events so we can understand which commands are used,
5
+ * how long they take, and where errors occur. Events are fire-and-forget —
6
+ * failures are silently ignored and never block the CLI.
7
+ *
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import crypto from 'crypto';
13
+ import os from 'os';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+
18
+ const { version: cliVersion } = JSON.parse(
19
+ fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
20
+ );
21
+
22
+ const TELEMETRY_URL =
23
+ 'https://bi-data-sources.nansen.ai/events-service-68ifmnpsx2uq7cgab8dw/v2/event';
24
+
25
+ const TIMEOUT_MS = 2000;
26
+
27
+ // ─── opt-out ──────────────────────────────────────────────
28
+
29
+ export const TELEMETRY_DISABLED =
30
+ process.env.DO_NOT_TRACK === '1' || process.env.NANSEN_NO_TELEMETRY === '1';
31
+
32
+ // ─── environment ──────────────────────────────────────────
33
+
34
+ /**
35
+ * Infer prod vs dev from NANSEN_BASE_URL env var.
36
+ * Only engineers pointing at a local/staging API will have this set.
37
+ */
38
+ function getEventSource() {
39
+ const baseUrl = process.env.NANSEN_BASE_URL || '';
40
+ return baseUrl && !baseUrl.includes('api.nansen.ai') ? 'cli_dev' : 'cli_prod';
41
+ }
42
+
43
+ // ─── system info ──────────────────────────────────────────
44
+
45
+ const SYSTEM_NAMES = { Darwin: 'macos', Linux: 'linux', Windows_NT: 'windows' };
46
+
47
+ function getSystemName() {
48
+ return SYSTEM_NAMES[os.type()] || os.type().toLowerCase();
49
+ }
50
+
51
+ // ─── identity ──────────────────────────────────────────────
52
+
53
+ const TELEMETRY_ID_FILE = path.join(
54
+ process.env.HOME || process.env.USERPROFILE || '',
55
+ '.nansen',
56
+ 'telemetry-id'
57
+ );
58
+
59
+ /**
60
+ * Get or create a persistent random anonymous_id stored in ~/.nansen/telemetry-id.
61
+ */
62
+ let _anonymousId;
63
+ export function getAnonymousId() {
64
+ if (_anonymousId === undefined) {
65
+ try {
66
+ _anonymousId = fs.readFileSync(TELEMETRY_ID_FILE, 'utf8').trim();
67
+ } catch {
68
+ _anonymousId = crypto.randomUUID();
69
+ try {
70
+ fs.mkdirSync(path.dirname(TELEMETRY_ID_FILE), { recursive: true });
71
+ fs.writeFileSync(TELEMETRY_ID_FILE, _anonymousId, 'utf8');
72
+ } catch { /* best-effort persist */ }
73
+ }
74
+ }
75
+ return _anonymousId;
76
+ }
77
+
78
+ // ─── session ───────────────────────────────────────────────
79
+
80
+ const SESSION_FILE = path.join(
81
+ process.env.HOME || process.env.USERPROFILE || '',
82
+ '.nansen',
83
+ 'session'
84
+ );
85
+
86
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
87
+
88
+ /**
89
+ * Get or create a session ID. The session rotates after 30 min of inactivity.
90
+ * Callers can override via NANSEN_SESSION_ID env var.
91
+ */
92
+ let _sessionId;
93
+ export function getSessionId() {
94
+ if (_sessionId !== undefined) return _sessionId;
95
+
96
+ if (process.env.NANSEN_SESSION_ID) {
97
+ _sessionId = process.env.NANSEN_SESSION_ID;
98
+ return _sessionId;
99
+ }
100
+
101
+ const now = Date.now();
102
+ try {
103
+ const raw = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
104
+ if (raw.id && raw.ts && now - raw.ts < SESSION_TIMEOUT_MS) {
105
+ _sessionId = raw.id;
106
+ // touch timestamp, but only if >1 min elapsed to reduce writes
107
+ if (now - raw.ts > 60_000) {
108
+ try { fs.writeFileSync(SESSION_FILE, JSON.stringify({ id: _sessionId, ts: now }), 'utf8'); } catch { /* best-effort touch */ }
109
+ }
110
+ return _sessionId;
111
+ }
112
+ } catch { /* missing or corrupt → new session */ }
113
+
114
+ _sessionId = crypto.randomUUID();
115
+ try {
116
+ fs.mkdirSync(path.dirname(SESSION_FILE), { recursive: true });
117
+ fs.writeFileSync(SESSION_FILE, JSON.stringify({ id: _sessionId, ts: now }), 'utf8');
118
+ } catch { /* best-effort */ }
119
+ return _sessionId;
120
+ }
121
+
122
+ // ─── send ──────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Send a telemetry event. Fire-and-forget — never throws.
126
+ */
127
+ function sendEvent(event) {
128
+ if (TELEMETRY_DISABLED) return;
129
+ const controller = new AbortController();
130
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
131
+ timer.unref();
132
+
133
+ fetch(TELEMETRY_URL, {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: JSON.stringify(event),
137
+ signal: controller.signal,
138
+ })
139
+ .catch(() => {}) // swallow errors
140
+ .finally(() => clearTimeout(timer));
141
+ }
142
+
143
+ // ─── context ───────────────────────────────────────────────
144
+
145
+ function buildContext() {
146
+ return {
147
+ client_type: 'nansen-cli',
148
+ client_version: cliVersion,
149
+ system_name: getSystemName(),
150
+ system_version: os.release(),
151
+ node_version: process.version,
152
+ };
153
+ }
154
+
155
+ // ─── public API ────────────────────────────────────────────
156
+
157
+ /**
158
+ * Convert a command string like "smart-money netflow" to a path like "/smart-money/netflow".
159
+ */
160
+ function commandToPath(command) {
161
+ return '/' + command.replace(/\s+/g, '/');
162
+ }
163
+
164
+ /**
165
+ * Track a CLI command that completed successfully.
166
+ *
167
+ * @param {object} opts
168
+ * @param {string} opts.command - Full command string, e.g. "smart-money netflow"
169
+ * @param {number} opts.duration_ms - Wall-clock execution time
170
+ * @param {boolean} [opts.from_cache] - Whether result was served from cache
171
+ * @param {string[]} [opts.flags] - Flag names used (no values), e.g. ["--chain", "--pretty"]
172
+ * @param {string|null} [opts.chain] - Chain name if specified, e.g. "ethereum", "solana"
173
+ */
174
+ export function trackCommandSucceeded({
175
+ command,
176
+ duration_ms,
177
+ from_cache = false,
178
+ flags = [],
179
+ chain = null,
180
+ }) {
181
+ sendEvent({
182
+ event: 'cli_command_succeeded',
183
+ event_source: getEventSource(),
184
+ event_id: crypto.randomUUID(),
185
+ user_id: null,
186
+ anonymous_id: getAnonymousId(),
187
+ session_id: getSessionId(),
188
+ timestamp: new Date().toISOString(),
189
+ path: commandToPath(command),
190
+ properties: {
191
+ latency: duration_ms / 1000,
192
+ from_cache,
193
+ flags,
194
+ ...(chain ? { chain } : {}),
195
+ },
196
+ context: buildContext(),
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Track a CLI command that failed.
202
+ *
203
+ * @param {object} opts
204
+ * @param {string} opts.command - Full command string
205
+ * @param {number} opts.duration_ms - Wall-clock execution time
206
+ * @param {string} opts.error_code - Structured error code (from ErrorCode or custom)
207
+ * @param {number|null} [opts.status] - HTTP status if the error came from the API
208
+ * @param {string[]} [opts.flags] - Flag names used
209
+ * @param {string|null} [opts.chain] - Chain name if specified
210
+ */
211
+ export function trackCommandFailed({
212
+ command,
213
+ duration_ms,
214
+ error_code,
215
+ status = null,
216
+ flags = [],
217
+ chain = null,
218
+ }) {
219
+ sendEvent({
220
+ event: 'cli_command_failed',
221
+ event_source: getEventSource(),
222
+ event_id: crypto.randomUUID(),
223
+ user_id: null,
224
+ anonymous_id: getAnonymousId(),
225
+ session_id: getSessionId(),
226
+ timestamp: new Date().toISOString(),
227
+ path: commandToPath(command),
228
+ properties: {
229
+ latency: duration_ms / 1000,
230
+ error_code,
231
+ status,
232
+ flags,
233
+ ...(chain ? { chain } : {}),
234
+ },
235
+ context: buildContext(),
236
+ });
237
+ }
@@ -7,7 +7,7 @@
7
7
 
8
8
  import fs from 'fs';
9
9
  import path from 'path';
10
- import { spawn } from 'child_process';
10
+ import childProcess from 'child_process';
11
11
  import { fileURLToPath } from 'url';
12
12
 
13
13
  const __filename = fileURLToPath(import.meta.url);
@@ -119,7 +119,7 @@ export function scheduleUpdateCheck() {
119
119
  req.setTimeout(5000, () => req.destroy());
120
120
  `;
121
121
 
122
- const child = spawn(process.execPath, ['-e', script], {
122
+ const child = childProcess.spawn(process.execPath, ['-e', script], {
123
123
  detached: true,
124
124
  stdio: 'ignore'
125
125
  });