nansen-cli 1.17.0 → 1.18.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 (38) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +2 -1
  3. package/skills/nansen-alerts/SKILL.md +137 -0
  4. package/skills/nansen-alpha-discovery/SKILL.md +43 -0
  5. package/skills/nansen-batch-wallet/SKILL.md +26 -0
  6. package/skills/nansen-cross-chain-flow/SKILL.md +27 -0
  7. package/skills/nansen-dca-watch/SKILL.md +38 -0
  8. package/skills/nansen-defi-exposure/SKILL.md +37 -0
  9. package/skills/nansen-exit-signal/SKILL.md +39 -0
  10. package/skills/nansen-fund-watch/SKILL.md +35 -0
  11. package/skills/nansen-holder-quality/SKILL.md +38 -0
  12. package/skills/nansen-perp-scan/SKILL.md +32 -0
  13. package/skills/nansen-perp-trader/SKILL.md +39 -0
  14. package/skills/nansen-pm-deep-dive/SKILL.md +50 -0
  15. package/skills/nansen-pm-insider-scan/SKILL.md +62 -0
  16. package/skills/nansen-polymarket-trader/SKILL.md +43 -0
  17. package/skills/nansen-portfolio-history/SKILL.md +36 -0
  18. package/skills/nansen-prediction-market/SKILL.md +47 -0
  19. package/skills/nansen-profiler/SKILL.md +98 -0
  20. package/skills/nansen-search/SKILL.md +34 -0
  21. package/skills/nansen-sm-trend/SKILL.md +30 -0
  22. package/skills/nansen-smart-money/SKILL.md +71 -0
  23. package/skills/nansen-token/SKILL.md +90 -0
  24. package/skills/nansen-token-discovery/SKILL.md +54 -0
  25. package/skills/nansen-token-forensics/SKILL.md +40 -0
  26. package/skills/nansen-trade/SKILL.md +100 -0
  27. package/skills/nansen-wallet/SKILL.md +140 -0
  28. package/skills/nansen-wallet-analysis/SKILL.md +45 -0
  29. package/skills/nansen-wallet-attribution/REFERENCE.md +43 -0
  30. package/skills/nansen-wallet-attribution/SKILL.md +46 -0
  31. package/skills/nansen-wallet-migration/SKILL.md +183 -0
  32. package/skills/nansen-web-fetch/SKILL.md +50 -0
  33. package/skills/nansen-web-search/SKILL.md +39 -0
  34. package/src/api.js +76 -3
  35. package/src/cli.js +176 -14
  36. package/src/schema.json +164 -1
  37. package/src/telemetry.js +237 -0
  38. package/src/update-check.js +2 -2
@@ -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
  });