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.
- package/CHANGELOG.md +29 -0
- package/package.json +2 -1
- package/skills/nansen-alerts/SKILL.md +137 -0
- package/skills/nansen-alpha-discovery/SKILL.md +43 -0
- package/skills/nansen-batch-wallet/SKILL.md +26 -0
- package/skills/nansen-cross-chain-flow/SKILL.md +27 -0
- package/skills/nansen-dca-watch/SKILL.md +38 -0
- package/skills/nansen-defi-exposure/SKILL.md +37 -0
- package/skills/nansen-exit-signal/SKILL.md +39 -0
- package/skills/nansen-fund-watch/SKILL.md +35 -0
- package/skills/nansen-holder-quality/SKILL.md +38 -0
- package/skills/nansen-perp-scan/SKILL.md +32 -0
- package/skills/nansen-perp-trader/SKILL.md +39 -0
- package/skills/nansen-pm-deep-dive/SKILL.md +50 -0
- package/skills/nansen-pm-insider-scan/SKILL.md +62 -0
- package/skills/nansen-polymarket-trader/SKILL.md +43 -0
- package/skills/nansen-portfolio-history/SKILL.md +36 -0
- package/skills/nansen-prediction-market/SKILL.md +47 -0
- package/skills/nansen-profiler/SKILL.md +98 -0
- package/skills/nansen-search/SKILL.md +34 -0
- package/skills/nansen-sm-trend/SKILL.md +30 -0
- package/skills/nansen-smart-money/SKILL.md +71 -0
- package/skills/nansen-token/SKILL.md +90 -0
- package/skills/nansen-token-discovery/SKILL.md +54 -0
- package/skills/nansen-token-forensics/SKILL.md +40 -0
- package/skills/nansen-trade/SKILL.md +100 -0
- package/skills/nansen-wallet/SKILL.md +140 -0
- package/skills/nansen-wallet-analysis/SKILL.md +45 -0
- package/skills/nansen-wallet-attribution/REFERENCE.md +43 -0
- package/skills/nansen-wallet-attribution/SKILL.md +46 -0
- package/skills/nansen-wallet-migration/SKILL.md +183 -0
- package/skills/nansen-web-fetch/SKILL.md +50 -0
- package/skills/nansen-web-search/SKILL.md +39 -0
- package/src/api.js +76 -3
- package/src/cli.js +176 -14
- package/src/schema.json +164 -1
- package/src/telemetry.js +237 -0
- package/src/update-check.js +2 -2
package/src/telemetry.js
ADDED
|
@@ -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
|
+
}
|
package/src/update-check.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import
|
|
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
|
});
|