@vike-io/cli 0.5.0 → 0.7.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 +37 -0
- package/package.json +3 -1
- package/scripts/postinstall.js +36 -16
- package/skills/vike-token-screener/SKILL.md +82 -0
- package/skills/vike-wallet-batch/SKILL.md +56 -0
- package/skills/vike-wallet-compare/SKILL.md +56 -0
- package/skills/vike-wallet-trace/SKILL.md +75 -0
- package/skills/vike-wallet-transactions/SKILL.md +59 -0
- package/src/chains.js +86 -0
- package/src/commands/token.js +26 -0
- package/src/commands/wallet.js +81 -0
- package/src/index.js +24 -5
- package/src/telemetry.js +120 -0
- package/src/update-check.js +126 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes are documented here. Entries follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
4
|
|
|
5
|
+
## 0.7.0 — 5 new commands (transactions, compare, trace, batch, screener)
|
|
6
|
+
|
|
7
|
+
- **`vike wallet transactions <address>`** (alias `txs`) — raw per-wallet transfer list with `--direction in|out|all`, `--days`, `--min-usd`, `--token` filters.
|
|
8
|
+
- **`vike wallet compare <a> <b>`** — diff two wallets (shared counterparties + shared tokens within a window).
|
|
9
|
+
- **`vike wallet trace <address>`** — counterparty graph traversal, depth 1-5, width 1-10, with min-USD threshold.
|
|
10
|
+
- **`vike wallet batch <addr1> <addr2> ...`** — bulk label + 30d summary lookup for up to 100 addresses, preserves input order.
|
|
11
|
+
- **`vike token screener`** — token discovery filter ranked by volume / net inflow / holders / smart-money inflow.
|
|
12
|
+
- 5 new SKILL.md playbooks: `vike-wallet-transactions`, `vike-wallet-compare`, `vike-wallet-trace`, `vike-wallet-batch`, `vike-token-screener`.
|
|
13
|
+
|
|
14
|
+
Total: 38 skills (was 33), 34 MCP tools (was 29).
|
|
15
|
+
|
|
16
|
+
## 0.6.0 — infra hardening + observability + skill eval harness
|
|
17
|
+
|
|
18
|
+
Mostly internal-quality improvements; no new commands. Backbone for everything that follows.
|
|
19
|
+
|
|
20
|
+
**New infra**
|
|
21
|
+
- **Background update-check** — silent detached subprocess polls the registry once/day; next-run prints `Update available: X.Y.Z → A.B.C`. Honors `NO_UPDATE_NOTIFIER` / `CI` / `VIKE_NO_UPDATE_CHECK`.
|
|
22
|
+
- **First-run-after-upgrade notice** — one-time `Updated to X.Y.Z` print.
|
|
23
|
+
- **Telemetry skeleton** — anonymous UUID + 30-min session ID, fire-and-forget. NO sink endpoint wired yet (events drop silently); shape is stable so we can flip it on without refactoring. Opt-out via `DO_NOT_TRACK=1` or `VIKE_NO_TELEMETRY=1`.
|
|
24
|
+
- **Centralized chain registry** (`src/chains.js`) — `ALL_CHAINS`, `EVM_CHAINS`, `WALLET_CHAINS`, `CHAIN_ALIASES`, `normaliseChain()`, `isEvmAddress()`.
|
|
25
|
+
|
|
26
|
+
**Testing + release safety**
|
|
27
|
+
- Two-tier vitest config — `npm test` (unit/integration, <30s) vs `npm run test:e2e` (live MCP, up to 2 min).
|
|
28
|
+
- Four meta-tests added: shebang, pack-and-install (catches missing `files`), CLI help for every command, command coverage regression.
|
|
29
|
+
- Pretest changeset validator — hard-fails on wrong package name in changeset frontmatter; warns on missing changeset.
|
|
30
|
+
- Postinstall script hardened — global-install + TTY gates, always `exit 0`.
|
|
31
|
+
|
|
32
|
+
**Skill quality evals (preview)**
|
|
33
|
+
- `evals/` directory with a `uv run`-script harness: A/B-tests every question against `vike --help` only vs `vike --help + SKILL.md`. Scores per question; aggregates a Δ for each skill. If a skill doesn't move the score, it shouldn't ship. Run: `uv run --script evals/runner.py` (needs `ANTHROPIC_API_KEY`).
|
|
34
|
+
- 14 starter questions in `evals/questions.yaml`.
|
|
35
|
+
|
|
36
|
+
**Security**
|
|
37
|
+
- `SECURITY.md` published (report at security@vike.io; 48h ack, 7d critical-fix SLA).
|
|
38
|
+
- `CODEOWNERS` file (single-maintainer for now; security-sensitive paths called out).
|
|
39
|
+
- Dependabot weekly PRs for npm + GitHub Actions deps.
|
|
40
|
+
- `npm audit --audit-level=high` added to CI.
|
|
41
|
+
|
|
5
42
|
## 0.5.0 — initial public release
|
|
6
43
|
|
|
7
44
|
AI-agent CLI for vike.io on-chain analytics, perpetuals, options flow, and prediction-market data across 11 chains.
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.7.0",
|
|
7
7
|
"description": "AI-agent CLI for vike.io on-chain analytics, perps, options, and prediction markets across 11 chains",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "src/index.js",
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"postinstall": "node scripts/postinstall.js",
|
|
21
21
|
"start": "node src/index.js",
|
|
22
|
+
"pretest": "node scripts/check-changeset.js",
|
|
22
23
|
"test": "vitest run --passWithNoTests",
|
|
23
24
|
"test:watch": "vitest",
|
|
25
|
+
"test:e2e": "vitest run --config vitest.e2e.config.js",
|
|
24
26
|
"lint": "eslint .",
|
|
25
27
|
"lint:fix": "eslint . --fix",
|
|
26
28
|
"changeset": "changeset",
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
|
-
// Postinstall — print a friendly setup hint
|
|
2
|
-
//
|
|
1
|
+
// Postinstall — print a friendly setup hint on global install only.
|
|
2
|
+
//
|
|
3
|
+
// Three defensive guards:
|
|
4
|
+
// 1. Local installs (`npm i @vike-io/cli` inside a project) get nothing —
|
|
5
|
+
// the hint would be noise. Only global installs (`-g`) print.
|
|
6
|
+
// 2. Non-TTY environments (CI, scripts, docker build) get nothing —
|
|
7
|
+
// nothing in here is interactive, but defensively short-circuit so a
|
|
8
|
+
// future maintainer can't accidentally prompt and hang `npm ci`.
|
|
9
|
+
// 3. Any error is swallowed — postinstall must NEVER break `npm install`.
|
|
10
|
+
//
|
|
11
|
+
// Honors $CI and npm's --silent / loglevel=silent.
|
|
3
12
|
|
|
4
|
-
|
|
13
|
+
const isGlobal = String(process.env.npm_config_global || '') === 'true';
|
|
14
|
+
const hasTTY = Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
|
|
15
|
+
const isQuiet = process.env.CI ||
|
|
16
|
+
process.env.npm_config_loglevel === 'silent' ||
|
|
17
|
+
process.env.npm_config_loglevel === 'error';
|
|
18
|
+
|
|
19
|
+
if (!isGlobal || !hasTTY || isQuiet) {
|
|
5
20
|
process.exit(0);
|
|
6
21
|
}
|
|
7
22
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
try {
|
|
24
|
+
const lines = [
|
|
25
|
+
'',
|
|
26
|
+
' @vike-io/cli installed.',
|
|
27
|
+
'',
|
|
28
|
+
' Next steps:',
|
|
29
|
+
' vike init — interactive setup',
|
|
30
|
+
' vike doctor — verify your install',
|
|
31
|
+
' vike token search BTC — your first query',
|
|
32
|
+
'',
|
|
33
|
+
' Docs: https://vike.io/api',
|
|
34
|
+
'',
|
|
35
|
+
];
|
|
36
|
+
process.stdout.write(lines.join('\n'));
|
|
37
|
+
} catch {
|
|
38
|
+
// Postinstall must not fail an npm install. Swallow everything.
|
|
39
|
+
}
|
|
40
|
+
process.exit(0);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vike-token-screener
|
|
3
|
+
description: Token discovery filter — rank tokens by volume / net inflow / holder count / smart-money inflow within a window, with min-volume and min-holder filters. Use when the user asks "what tokens are trending today" / "find tokens with smart money accumulating" / "rising tokens this week" / token discovery from scratch.
|
|
4
|
+
allowed-tools: Bash(vike:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# vike-token-screener
|
|
8
|
+
|
|
9
|
+
## Command
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
vike token screener
|
|
13
|
+
[--chain ethereum|bsc|base]
|
|
14
|
+
[--window 24h|7d|30d|90d]
|
|
15
|
+
[--sort volume|net_inflow|holders|smart_inflow]
|
|
16
|
+
[--limit N] (max 100)
|
|
17
|
+
[--min-volume USD] (filter dust)
|
|
18
|
+
[--min-holders N] (require N+ unique holders)
|
|
19
|
+
[--smart-money-only] (restrict to smart-money cohort flows)
|
|
20
|
+
[--json]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Trending today by volume
|
|
27
|
+
vike token screener --window 24h --sort volume --limit 20
|
|
28
|
+
|
|
29
|
+
# Find tokens smart money is accumulating
|
|
30
|
+
vike token screener --window 7d --sort smart_inflow --smart-money-only
|
|
31
|
+
|
|
32
|
+
# Rising holders count (organic growth signal)
|
|
33
|
+
vike token screener --window 30d --sort holders --min-volume 100000
|
|
34
|
+
|
|
35
|
+
# Mid-cap tokens with real activity
|
|
36
|
+
vike token screener --min-volume 1000000 --min-holders 100 --sort net_inflow
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Output columns
|
|
40
|
+
|
|
41
|
+
`token_address`, `symbol` (when known), `name`, `volume_usd`, `net_flow_usd`, `in_usd`, `out_usd`, `unique_holders`, `tx_count`.
|
|
42
|
+
|
|
43
|
+
## Sort modes
|
|
44
|
+
|
|
45
|
+
| Sort | What it surfaces |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `volume` | Most-traded tokens (default) — captures attention-driven action |
|
|
48
|
+
| `net_inflow` | Tokens with biggest accumulation (in > out) — "what's being bought" |
|
|
49
|
+
| `holders` | Tokens gaining unique addresses — organic growth signal |
|
|
50
|
+
| `smart_inflow` | Tokens our smart-money cohort is buying — alpha signal |
|
|
51
|
+
|
|
52
|
+
## Reading
|
|
53
|
+
|
|
54
|
+
- **High volume + balanced in/out** = churn (DEX MM, arb bots). Low signal.
|
|
55
|
+
- **High volume + strong net_inflow** = real demand. Worth investigating.
|
|
56
|
+
- **Strong holder growth + flat volume** = small buys, distribution widening. Often early signal.
|
|
57
|
+
- **`smart_inflow` mode** + high values + low overall volume = cohort is positioned BEFORE retail. Highest-conviction signal in the tool.
|
|
58
|
+
|
|
59
|
+
## Filter tips
|
|
60
|
+
|
|
61
|
+
| Goal | Filters |
|
|
62
|
+
|---|---|
|
|
63
|
+
| "Rising memecoins" | `--window 24h --sort holders --min-holders 50 --min-volume 50000` |
|
|
64
|
+
| "Mid-cap alpha" | `--sort smart_inflow --min-volume 500000 --smart-money-only` |
|
|
65
|
+
| "What's getting dumped" | `--sort net_inflow` then reverse — bottom rows have biggest outflows |
|
|
66
|
+
|
|
67
|
+
## Anti-patterns
|
|
68
|
+
|
|
69
|
+
- Don't quote the symbol as authoritative if `symbol` is null/blank — token hasn't been mapped, identify by address instead
|
|
70
|
+
- Don't use `--window 24h --sort smart_inflow` for hot-new-token discovery — smart money usually positions BEFORE the 24h window; try `--window 7d` for that mode
|
|
71
|
+
- Don't omit `--min-holders` when sorting by holders — junk airdrops with 50,000 1-tx addresses dominate the top otherwise
|
|
72
|
+
- Don't omit `--min-volume` when sorting by net_inflow — tiny tokens with $100 net flow can rank high in % terms
|
|
73
|
+
|
|
74
|
+
## Cost note
|
|
75
|
+
|
|
76
|
+
3 credits per call. Cheaper than `wallet trace` (which is 4) but heavier than per-tool lookups. Use deliberately for discovery, not as a default.
|
|
77
|
+
|
|
78
|
+
## Pairs well with
|
|
79
|
+
|
|
80
|
+
- `vike token holders <addr>` — drill into the top holders of any flagged token
|
|
81
|
+
- `vike token transfers <addr>` — see WHO is buying/selling once a token is interesting
|
|
82
|
+
- `vike-smart-money-discovery` — composite playbook starting from this screen
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vike-wallet-batch
|
|
3
|
+
description: Bulk label + 30d summary lookup for up to 100 addresses in one call. Use when the user pastes a CSV of addresses, asks "enrich this list with labels", or wants to quickly screen many wallets for entity / risk / activity.
|
|
4
|
+
allowed-tools: Bash(vike:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# vike-wallet-batch
|
|
8
|
+
|
|
9
|
+
## Command
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
vike wallet batch <addr1> <addr2> <addr3> ...
|
|
13
|
+
[--chain ethereum|bsc|base]
|
|
14
|
+
[--json]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Caps at 100 addresses per call. For larger lists, split into chunks.
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
vike wallet batch 0xd8da... 0xabc... 0xdef... --json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Output
|
|
26
|
+
|
|
27
|
+
One row per INPUT address (preserves input order — useful when joining back to a source CSV):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
[
|
|
31
|
+
{ "wallet": "0xd8da...", "entity": null, "wallet_type": "user", "confidence": "Medium", "risk_flag": null, "net_30d_usd": 12000, "volume_30d_usd": 50000, "tx_30d": 42, "tokens_30d_count": 8, "found": true },
|
|
32
|
+
{ "wallet": "0xabc...", "entity": "Binance", "wallet_type": "cex", "confidence": "High", "risk_flag": null, "net_30d_usd": -5e9, "volume_30d_usd": 9e9, "tx_30d": 2000, "tokens_30d_count": 80, "found": true },
|
|
33
|
+
{ "wallet": "0xdef...", "entity": null, "wallet_type": null, "confidence": null, "risk_flag": null, "net_30d_usd": 0, "volume_30d_usd": 0, "tx_30d": 0, "tokens_30d_count": 0, "found": false }
|
|
34
|
+
]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`found: false` = address has no labels AND no 30d activity in our DB (might be brand-new wallet or one we haven't indexed).
|
|
38
|
+
|
|
39
|
+
## Reading
|
|
40
|
+
|
|
41
|
+
- Filter to `wallet_type = "user"` to find real-people addresses worth investigating further
|
|
42
|
+
- Any `risk_flag != null` → flag prominently in any user-facing report
|
|
43
|
+
- High `volume_30d_usd` but `tx_30d = 0` shouldn't happen — if it does, indicates stale aggregate; recompute
|
|
44
|
+
- `entity = "Binance"` + `wallet_type = "cex"` rows in a "smart money" list = false positives (CEX deposits aren't traders)
|
|
45
|
+
|
|
46
|
+
## Anti-patterns
|
|
47
|
+
|
|
48
|
+
- Don't run individual `vike wallet summary` in a loop for >5 addresses — this batch is ~20× faster
|
|
49
|
+
- Don't pass mixed-case addresses inconsistently — they're normalised but cleaner CSVs help readers
|
|
50
|
+
- Don't expect lifetime data — this is 30d window. For lifetime use the individual `wallet summary`
|
|
51
|
+
|
|
52
|
+
## Pairs well with
|
|
53
|
+
|
|
54
|
+
- `vike labels <addr>` — drill into any address showing high-confidence labels
|
|
55
|
+
- `vike wallet compare <a> <b>` — pick two interesting batch results and diff them
|
|
56
|
+
- `vike funds` — when batch reveals fund/MM/treasury wallets, get their flows
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vike-wallet-compare
|
|
3
|
+
description: Diff two wallets — find their shared counterparties (both transacted with these addresses) and shared tokens (both touched these tokens) within a window. Use when the user asks "are these two wallets related" / "what do X and Y have in common" / "find connections between wallet A and B".
|
|
4
|
+
allowed-tools: Bash(vike:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# vike-wallet-compare
|
|
8
|
+
|
|
9
|
+
## Command
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
vike wallet compare <address_a> <address_b>
|
|
13
|
+
[--chain ethereum|bsc|base]
|
|
14
|
+
[--window 24h|7d|30d|90d|180d|all]
|
|
15
|
+
[--limit N] (max 50)
|
|
16
|
+
[--json]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Output
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"address_a": "0x...",
|
|
24
|
+
"address_b": "0x...",
|
|
25
|
+
"window": "30d",
|
|
26
|
+
"shared_counterparties": [
|
|
27
|
+
{ "wallet": "0x...", "a_volume_usd": 50000, "b_volume_usd": 75000, "a_tx_count": 3, "b_tx_count": 5, "entity": "Binance", "wallet_type": "cex" },
|
|
28
|
+
...
|
|
29
|
+
],
|
|
30
|
+
"shared_tokens": [
|
|
31
|
+
{ "token_address": "0x...", "a_volume_usd": 12000, "b_volume_usd": 8000, "a_tx_count": 2, "b_tx_count": 4 },
|
|
32
|
+
...
|
|
33
|
+
],
|
|
34
|
+
"summary": { "shared_counterparty_count": 7, "shared_token_count": 12 }
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Reading
|
|
39
|
+
|
|
40
|
+
- Many shared counterparties + same entity types = likely operated by same person/group
|
|
41
|
+
- Same exchange (Binance hot wallet) as counterparty for both = trivial overlap, both users of that CEX
|
|
42
|
+
- Shared NICHE tokens (long-tail memes, low-vol) with similar tx counts = strong signal of coordination
|
|
43
|
+
- Shared infra contracts (Uniswap, 1inch) = trivial overlap, ignore
|
|
44
|
+
- Zero shared = wallets genuinely independent
|
|
45
|
+
|
|
46
|
+
## Anti-patterns
|
|
47
|
+
|
|
48
|
+
- **Don't infer common ownership from CEX overlap.** Millions of users deposit to the same Binance hot wallets. Shared CEX counterparty = nothing.
|
|
49
|
+
- **Don't claim coordination from common DEX router overlap** (Uniswap, 1inch routers). Almost every DEX user shares those.
|
|
50
|
+
- Filter mentally for `wallet_type IN ('user', 'unknown')` overlap if you want a meaningful relatedness signal.
|
|
51
|
+
|
|
52
|
+
## Pairs well with
|
|
53
|
+
|
|
54
|
+
- `vike labels <addr>` — drill into any interesting shared counterparty
|
|
55
|
+
- `vike wallet trace <addr>` — follow money outward from either wallet
|
|
56
|
+
- `vike wallet counterparties <addr>` — top partners per wallet individually
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vike-wallet-trace
|
|
3
|
+
description: Counterparty graph traversal from a root wallet outward. At each hop, expand to the top N counterparties by USD volume. Use when the user asks "where did funds from X go" / "follow the money 3 hops out" / "trace the network around this wallet" / wallet investigation.
|
|
4
|
+
allowed-tools: Bash(vike:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# vike-wallet-trace
|
|
8
|
+
|
|
9
|
+
## Command
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
vike wallet trace <address>
|
|
13
|
+
[--chain ethereum|bsc|base]
|
|
14
|
+
[--depth N] (1-5, default 2)
|
|
15
|
+
[--width N] (1-10, default 5)
|
|
16
|
+
[--window 24h|7d|30d|90d|all]
|
|
17
|
+
[--min-usd N] (default 10000 — prunes noise)
|
|
18
|
+
[--json]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Output
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"root": "0xabc...",
|
|
26
|
+
"depth": 2, "width": 5, "window": "30d", "min_usd": 10000,
|
|
27
|
+
"nodes": [
|
|
28
|
+
{ "address": "0xabc...", "depth": 0, "total_volume_usd": 0.0 },
|
|
29
|
+
{ "address": "0xdef...", "depth": 1, "total_volume_usd": 250000, "label": "Binance", "wallet_type": "cex" },
|
|
30
|
+
{ "address": "0x123...", "depth": 2, "total_volume_usd": 45000, "label": null, "wallet_type": null },
|
|
31
|
+
...
|
|
32
|
+
],
|
|
33
|
+
"edges": [
|
|
34
|
+
{ "from": "0xabc...", "to": "0xdef...", "volume_usd": 200000, "tx_count": 4 },
|
|
35
|
+
...
|
|
36
|
+
],
|
|
37
|
+
"stats": { "node_count": 23, "edge_count": 31, "max_depth_reached": 2 }
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Reading the graph
|
|
42
|
+
|
|
43
|
+
- **Root (depth 0)** = the wallet you traced from. `total_volume_usd` = 0 because it's where edges originate.
|
|
44
|
+
- **Depth 1** = direct counterparties. Most useful layer.
|
|
45
|
+
- **Depth 2-3** = "friend-of-friend" — useful for finding where funds end up.
|
|
46
|
+
- **Edge volume_usd** = the volume between those two specific addresses, NOT cumulative.
|
|
47
|
+
- **`wallet_type = "cex"`** at depth 1 = funds went to / came from an exchange. Often the END of the trail (anonymous from there).
|
|
48
|
+
- **Unlabeled wallet with high volume at depth 1-2** = potentially related wallet, investigate with `vike labels`.
|
|
49
|
+
|
|
50
|
+
## Parameter tuning
|
|
51
|
+
|
|
52
|
+
| Parameter | Effect | Use when |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `--depth 1` | Direct counterparties only | "Who does X trade with" — same as `wallet counterparties` |
|
|
55
|
+
| `--depth 2` | One degree of separation | Default. Reveals immediate networks. |
|
|
56
|
+
| `--depth 3+` | Wider net | Investigation / OSINT. Returns much more data; graph gets noisy. |
|
|
57
|
+
| `--width 3` | Top 3 per hop | Cleaner, focused on biggest flows |
|
|
58
|
+
| `--width 10` | Top 10 per hop | Wider scan, more noise |
|
|
59
|
+
| `--min-usd 50000` | High-value flows only | Strip out dust/airdrops |
|
|
60
|
+
|
|
61
|
+
## Cost note
|
|
62
|
+
|
|
63
|
+
Tool credits = 4 per call (heavier than other graph-less tools). Graph explodes exponentially with depth — depth 3 + width 10 = up to 1000 nodes per scan. Use `--depth 2 --width 5` as default.
|
|
64
|
+
|
|
65
|
+
## Anti-patterns
|
|
66
|
+
|
|
67
|
+
- Don't trace through CEX deposits. Once funds enter Binance/Coinbase, the trail goes opaque (CEX internal accounting, not on-chain). Note CEX nodes and stop following.
|
|
68
|
+
- Don't depth-5 by default. Graph blows up; results become noise.
|
|
69
|
+
- Don't ignore `min_usd` — without it, dust transfers create false-positive edges.
|
|
70
|
+
|
|
71
|
+
## Pairs well with
|
|
72
|
+
|
|
73
|
+
- `vike labels <addr>` — identify any interesting node
|
|
74
|
+
- `vike wallet compare <addr_a> <addr_b>` — confirm relatedness once trace surfaces candidates
|
|
75
|
+
- `vike wallet transactions <addr>` — drill into specific transfers between two nodes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vike-wallet-transactions
|
|
3
|
+
description: Raw per-wallet transfer list (individual transactions, not aggregates) with direction, day, USD, and token filters. Use when the user asks "show me recent transfers for X" / "last 100 transactions of Y" / "all USDC moves from this wallet" / any forensic audit query.
|
|
4
|
+
allowed-tools: Bash(vike:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# vike-wallet-transactions
|
|
8
|
+
|
|
9
|
+
## Command
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
vike wallet transactions <address> # alias: vike wallet txs
|
|
13
|
+
[--chain ethereum|bsc|base]
|
|
14
|
+
[--direction in|out|all]
|
|
15
|
+
[--days N] (max 365)
|
|
16
|
+
[--limit N] (max 500)
|
|
17
|
+
[--min-usd N]
|
|
18
|
+
[--token <addr>]
|
|
19
|
+
[--json]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Recent in+out transfers, last 7 days
|
|
26
|
+
vike wallet txs 0xd8da6bf26964af9d7eed9e03e53415d37aa96045 --days 7 --limit 20
|
|
27
|
+
|
|
28
|
+
# Only outgoing > $10k in last 30d
|
|
29
|
+
vike wallet txs 0xabc... --direction out --min-usd 10000
|
|
30
|
+
|
|
31
|
+
# Only USDC transfers
|
|
32
|
+
vike wallet txs 0xabc... --token 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Output columns
|
|
36
|
+
|
|
37
|
+
`time`, `tx_hash`, `from_address`, `to_address`, `token_address`, `value_usd`, `direction` (in/out), `block`.
|
|
38
|
+
|
|
39
|
+
Sorted descending by time (most recent first).
|
|
40
|
+
|
|
41
|
+
## Reading
|
|
42
|
+
|
|
43
|
+
- High `tx_count` over short window + low `value_usd` per tx = bot / MM / contract
|
|
44
|
+
- Mixed in/out with similar volumes = active trader
|
|
45
|
+
- Many `out` to one address = sweep / deposit pattern
|
|
46
|
+
- One huge `in` then nothing = airdrop receiver / one-time deposit
|
|
47
|
+
|
|
48
|
+
## Anti-patterns
|
|
49
|
+
|
|
50
|
+
- Don't use this for aggregated metrics (volume, PnL) — use `wallet summary` instead, it's faster
|
|
51
|
+
- Don't paginate by re-running with smaller windows — limit max 500, just bump `--limit`
|
|
52
|
+
- Don't use `--token` on every query if you want full activity; the filter restricts to one token
|
|
53
|
+
|
|
54
|
+
## Pairs well with
|
|
55
|
+
|
|
56
|
+
- `vike wallet summary <addr>` — aggregate metrics over same window
|
|
57
|
+
- `vike wallet counterparties <addr>` — who they trade with most
|
|
58
|
+
- `vike wallet trace <addr>` — graph-traverse outward from this wallet
|
|
59
|
+
- `vike labels <addr>` — identify the wallet itself
|
package/src/chains.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Central chain registry. Every command that takes a --chain flag should
|
|
2
|
+
// import EVM_CHAINS or ALL_CHAINS from here. Adding a chain = one edit.
|
|
3
|
+
|
|
4
|
+
export const EVM_CHAINS = [
|
|
5
|
+
'ethereum',
|
|
6
|
+
'bsc',
|
|
7
|
+
'base',
|
|
8
|
+
'arbitrum',
|
|
9
|
+
'polygon',
|
|
10
|
+
'optimism',
|
|
11
|
+
'hyperevm',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const NON_EVM_CHAINS = [
|
|
15
|
+
'bitcoin',
|
|
16
|
+
'solana',
|
|
17
|
+
'tron',
|
|
18
|
+
'xmr',
|
|
19
|
+
'zec',
|
|
20
|
+
'dash',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export const ALL_CHAINS = [...EVM_CHAINS, ...NON_EVM_CHAINS];
|
|
24
|
+
|
|
25
|
+
// Chains the wallet_* tools currently support (subset of EVM_CHAINS where
|
|
26
|
+
// we have indexed transfers + labels). Used by --chain validation in
|
|
27
|
+
// commands like `wallet summary` / `wallet counterparties` / etc.
|
|
28
|
+
export const WALLET_CHAINS = ['ethereum', 'bsc', 'base'];
|
|
29
|
+
|
|
30
|
+
// Chain abbreviation map: short → canonical. We accept both forms from
|
|
31
|
+
// CLI input and normalise to canonical before sending to MCP.
|
|
32
|
+
export const CHAIN_ALIASES = {
|
|
33
|
+
eth: 'ethereum',
|
|
34
|
+
ethereum: 'ethereum',
|
|
35
|
+
bsc: 'bsc',
|
|
36
|
+
bnb: 'bsc',
|
|
37
|
+
base: 'base',
|
|
38
|
+
arb: 'arbitrum',
|
|
39
|
+
arbitrum: 'arbitrum',
|
|
40
|
+
poly: 'polygon',
|
|
41
|
+
polygon: 'polygon',
|
|
42
|
+
op: 'optimism',
|
|
43
|
+
optimism: 'optimism',
|
|
44
|
+
hyper: 'hyperevm',
|
|
45
|
+
hyperevm: 'hyperevm',
|
|
46
|
+
btc: 'bitcoin',
|
|
47
|
+
bitcoin: 'bitcoin',
|
|
48
|
+
sol: 'solana',
|
|
49
|
+
solana: 'solana',
|
|
50
|
+
tron: 'tron',
|
|
51
|
+
trx: 'tron',
|
|
52
|
+
xmr: 'xmr',
|
|
53
|
+
monero: 'xmr',
|
|
54
|
+
zec: 'zec',
|
|
55
|
+
zcash: 'zec',
|
|
56
|
+
dash: 'dash',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function normaliseChain(input, fallback = 'ethereum') {
|
|
60
|
+
if (!input) return fallback;
|
|
61
|
+
const key = String(input).toLowerCase().trim();
|
|
62
|
+
return CHAIN_ALIASES[key] ?? input;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Address-format expectations per chain. Used by `vike doctor` and
|
|
66
|
+
// future input validation. EVM = 0x + 40 hex; Solana = base58 32-44 chars;
|
|
67
|
+
// BTC = various encodings; privacy chains = chain-specific.
|
|
68
|
+
export const ADDRESS_FORMATS = {
|
|
69
|
+
ethereum: '0x + 40 hex chars',
|
|
70
|
+
bsc: '0x + 40 hex chars',
|
|
71
|
+
base: '0x + 40 hex chars',
|
|
72
|
+
arbitrum: '0x + 40 hex chars',
|
|
73
|
+
polygon: '0x + 40 hex chars',
|
|
74
|
+
optimism: '0x + 40 hex chars',
|
|
75
|
+
hyperevm: '0x + 40 hex chars',
|
|
76
|
+
solana: 'base58, 32-44 chars',
|
|
77
|
+
bitcoin: 'bech32 / P2PKH / P2SH',
|
|
78
|
+
tron: 'base58 starting with T',
|
|
79
|
+
xmr: 'monero address (95 chars)',
|
|
80
|
+
zec: 'zcash transparent or shielded',
|
|
81
|
+
dash: 'P2PKH starting with X',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export function isEvmAddress(addr) {
|
|
85
|
+
return typeof addr === 'string' && /^0x[0-9a-fA-F]{40}$/.test(addr);
|
|
86
|
+
}
|
package/src/commands/token.js
CHANGED
|
@@ -58,6 +58,32 @@ export function registerToken(program) {
|
|
|
58
58
|
} catch (err) { emitError(err, opts); }
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
cmd
|
|
62
|
+
.command('screener')
|
|
63
|
+
.description('Token discovery filter — rank tokens by volume / inflow / holders / smart-money inflow')
|
|
64
|
+
.option('--chain <c>', 'ethereum | bsc | base', 'ethereum')
|
|
65
|
+
.option('--window <w>', '24h | 7d | 30d | 90d', '24h')
|
|
66
|
+
.option('--sort <s>', 'volume | net_inflow | holders | smart_inflow', 'volume')
|
|
67
|
+
.option('--limit <n>', 'Max tokens (max 100)', '20')
|
|
68
|
+
.option('--min-volume <usd>', 'Minimum window volume USD', '0')
|
|
69
|
+
.option('--min-holders <n>', 'Minimum unique holders', '0')
|
|
70
|
+
.option('--smart-money-only', 'Restrict flows to smart-money cohort')
|
|
71
|
+
.option('--json', 'Emit JSON')
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
try {
|
|
74
|
+
const r = await callTool('token_screener', {
|
|
75
|
+
chain: opts.chain,
|
|
76
|
+
window: opts.window,
|
|
77
|
+
sort: opts.sort,
|
|
78
|
+
limit: parseInt10(opts.limit, 20),
|
|
79
|
+
min_volume_usd: parseFloat(opts.minVolume) || 0,
|
|
80
|
+
min_holders: parseInt10(opts.minHolders, 0),
|
|
81
|
+
smart_money_only: Boolean(opts.smartMoneyOnly),
|
|
82
|
+
});
|
|
83
|
+
emit(r, opts);
|
|
84
|
+
} catch (err) { emitError(err, opts); }
|
|
85
|
+
});
|
|
86
|
+
|
|
61
87
|
cmd
|
|
62
88
|
.command('holders <token_address>')
|
|
63
89
|
.description('Top holders of a token by cumulative net inflow USD')
|
package/src/commands/wallet.js
CHANGED
|
@@ -18,6 +18,87 @@ export function registerWallet(program) {
|
|
|
18
18
|
} catch (err) { emitError(err, opts); }
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
cmd
|
|
22
|
+
.command('transactions <address>')
|
|
23
|
+
.alias('txs')
|
|
24
|
+
.description('Raw per-wallet transfer list with filters')
|
|
25
|
+
.option('--chain <c>', 'ethereum | bsc | base', 'ethereum')
|
|
26
|
+
.option('--direction <d>', 'in | out | all', 'all')
|
|
27
|
+
.option('--days <n>', 'Days back (max 365)', '30')
|
|
28
|
+
.option('--limit <n>', 'Max txs (max 500)', '50')
|
|
29
|
+
.option('--min-usd <n>', 'Filter dust below this USD value', '0')
|
|
30
|
+
.option('--token <addr>', 'Optional: restrict to one token address')
|
|
31
|
+
.option('--json', 'Emit JSON')
|
|
32
|
+
.action(async (address, opts) => {
|
|
33
|
+
try {
|
|
34
|
+
const r = await callTool('wallet_transactions', {
|
|
35
|
+
address,
|
|
36
|
+
chain: opts.chain,
|
|
37
|
+
direction: opts.direction,
|
|
38
|
+
days: parseInt10(opts.days, 30),
|
|
39
|
+
limit: parseInt10(opts.limit, 50),
|
|
40
|
+
min_usd: parseFloat(opts.minUsd) || 0,
|
|
41
|
+
token_address: opts.token,
|
|
42
|
+
});
|
|
43
|
+
emit(r, opts);
|
|
44
|
+
} catch (err) { emitError(err, opts); }
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
cmd
|
|
48
|
+
.command('compare <address_a> <address_b>')
|
|
49
|
+
.description('Diff two wallets — shared counterparties + shared tokens')
|
|
50
|
+
.option('--chain <c>', 'ethereum | bsc | base', 'ethereum')
|
|
51
|
+
.option('--window <w>', '24h | 7d | 30d | 90d | 180d | all', '30d')
|
|
52
|
+
.option('--limit <n>', 'Max shared items per category (max 50)', '10')
|
|
53
|
+
.option('--json', 'Emit JSON')
|
|
54
|
+
.action(async (a, b, opts) => {
|
|
55
|
+
try {
|
|
56
|
+
const r = await callTool('wallet_compare', {
|
|
57
|
+
address_a: a, address_b: b,
|
|
58
|
+
chain: opts.chain, window: opts.window,
|
|
59
|
+
limit: parseInt10(opts.limit, 10),
|
|
60
|
+
});
|
|
61
|
+
emit(r, opts);
|
|
62
|
+
} catch (err) { emitError(err, opts); }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
cmd
|
|
66
|
+
.command('trace <address>')
|
|
67
|
+
.description('Counterparty graph traversal from a root wallet')
|
|
68
|
+
.option('--chain <c>', 'ethereum | bsc | base', 'ethereum')
|
|
69
|
+
.option('--depth <n>', 'Hops to follow (1-5)', '2')
|
|
70
|
+
.option('--width <n>', 'Top counterparties per hop (1-10)', '5')
|
|
71
|
+
.option('--window <w>', '24h | 7d | 30d | 90d | all', '30d')
|
|
72
|
+
.option('--min-usd <n>', 'Minimum USD per transfer to follow', '10000')
|
|
73
|
+
.option('--json', 'Emit JSON')
|
|
74
|
+
.action(async (address, opts) => {
|
|
75
|
+
try {
|
|
76
|
+
const r = await callTool('wallet_trace', {
|
|
77
|
+
address,
|
|
78
|
+
chain: opts.chain,
|
|
79
|
+
depth: parseInt10(opts.depth, 2),
|
|
80
|
+
width: parseInt10(opts.width, 5),
|
|
81
|
+
window: opts.window,
|
|
82
|
+
min_usd: parseFloat(opts.minUsd) || 10000,
|
|
83
|
+
});
|
|
84
|
+
emit(r, opts);
|
|
85
|
+
} catch (err) { emitError(err, opts); }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
cmd
|
|
89
|
+
.command('batch <addresses...>')
|
|
90
|
+
.description('Bulk label + 30d summary lookup for many addresses (max 100)')
|
|
91
|
+
.option('--chain <c>', 'ethereum | bsc | base', 'ethereum')
|
|
92
|
+
.option('--json', 'Emit JSON')
|
|
93
|
+
.action(async (addresses, opts) => {
|
|
94
|
+
try {
|
|
95
|
+
const r = await callTool('wallet_batch_labels', {
|
|
96
|
+
addresses, chain: opts.chain,
|
|
97
|
+
});
|
|
98
|
+
emit(r, opts);
|
|
99
|
+
} catch (err) { emitError(err, opts); }
|
|
100
|
+
});
|
|
101
|
+
|
|
21
102
|
cmd
|
|
22
103
|
.command('counterparties <address>')
|
|
23
104
|
.description('Top trading partners of a wallet by combined transfer volume')
|
package/src/index.js
CHANGED
|
@@ -16,13 +16,27 @@ import { registerDoctor } from './commands/doctor.js';
|
|
|
16
16
|
import { registerSchema } from './commands/schema.js';
|
|
17
17
|
import { registerLogin, registerLogout } from './commands/auth.js';
|
|
18
18
|
import { readPackageVersion } from './util.js';
|
|
19
|
+
import {
|
|
20
|
+
getUpdateNotification,
|
|
21
|
+
getUpgradeNotice,
|
|
22
|
+
scheduleUpdateCheck,
|
|
23
|
+
} from './update-check.js';
|
|
24
|
+
|
|
25
|
+
const version = readPackageVersion();
|
|
26
|
+
|
|
27
|
+
// Print at-most-once notifications: upgrade-since-last-run, and update-available
|
|
28
|
+
// (the latter from the previous background check). Cheap, sync, exits if quiet.
|
|
29
|
+
const upgradeNotice = getUpgradeNotice(version);
|
|
30
|
+
const updateNotice = getUpdateNotification(version);
|
|
31
|
+
if (upgradeNotice) process.stderr.write(upgradeNotice);
|
|
32
|
+
if (updateNotice) process.stderr.write(updateNotice);
|
|
19
33
|
|
|
20
34
|
const program = new Command();
|
|
21
35
|
|
|
22
36
|
program
|
|
23
37
|
.name('vike')
|
|
24
38
|
.description('AI-agent CLI for vike.io on-chain analytics')
|
|
25
|
-
.version(
|
|
39
|
+
.version(version, '-v, --version', 'Show CLI version');
|
|
26
40
|
|
|
27
41
|
registerLogin(program);
|
|
28
42
|
registerLogout(program);
|
|
@@ -38,7 +52,12 @@ registerPolymarket(program);
|
|
|
38
52
|
registerLabels(program);
|
|
39
53
|
registerWeb(program);
|
|
40
54
|
|
|
41
|
-
program.parseAsync(process.argv)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
program.parseAsync(process.argv)
|
|
56
|
+
.catch((err) => {
|
|
57
|
+
console.error(`vike: ${err.message ?? err}`);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
// Fire-and-forget background check, detached subprocess. Survives our exit.
|
|
62
|
+
scheduleUpdateCheck();
|
|
63
|
+
});
|
package/src/telemetry.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Anonymous, opt-out telemetry skeleton.
|
|
2
|
+
//
|
|
3
|
+
// Design decisions:
|
|
4
|
+
// - Honors DO_NOT_TRACK=1 (industry standard) AND VIKE_NO_TELEMETRY=1
|
|
5
|
+
// - Honors CI / NODE_ENV=test (never phones home from CI)
|
|
6
|
+
// - Persistent UUIDv4 in ~/.vike/telemetry-id (regenerated on delete)
|
|
7
|
+
// - Rolling 30-min session ID — same session_id within a 30-min window
|
|
8
|
+
// - AbortController + 1000ms timeout, timer.unref() so we can't keep
|
|
9
|
+
// the process alive
|
|
10
|
+
// - Payload includes: flag NAMES (no values), latency, success/error_code,
|
|
11
|
+
// chain (if relevant). NEVER the actual data the user queried.
|
|
12
|
+
// - Fire-and-forget: errors are swallowed
|
|
13
|
+
//
|
|
14
|
+
// THE SINK IS NOT YET WIRED. `_send()` is a no-op until we have an
|
|
15
|
+
// endpoint. Keeping the API shape stable now so we can flip the switch
|
|
16
|
+
// later without a refactor.
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
22
|
+
|
|
23
|
+
const CONFIG_DIR = join(homedir(), '.vike');
|
|
24
|
+
const ID_FILE = join(CONFIG_DIR, 'telemetry-id');
|
|
25
|
+
const SESSION_FILE = join(CONFIG_DIR, 'session');
|
|
26
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
27
|
+
const ENDPOINT = process.env.VIKE_TELEMETRY_ENDPOINT || '';
|
|
28
|
+
const REQUEST_TIMEOUT_MS = 1000;
|
|
29
|
+
|
|
30
|
+
function disabled() {
|
|
31
|
+
return process.env.DO_NOT_TRACK === '1' ||
|
|
32
|
+
process.env.VIKE_NO_TELEMETRY === '1' ||
|
|
33
|
+
process.env.CI ||
|
|
34
|
+
process.env.NODE_ENV === 'test';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureDir() {
|
|
38
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
39
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getDeviceId() {
|
|
44
|
+
try {
|
|
45
|
+
if (existsSync(ID_FILE)) return readFileSync(ID_FILE, 'utf8').trim();
|
|
46
|
+
} catch { /* ignore */ }
|
|
47
|
+
const id = randomUUID();
|
|
48
|
+
try { ensureDir(); writeFileSync(ID_FILE, id, { mode: 0o600 }); } catch { /* ignore */ }
|
|
49
|
+
return id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getSessionId() {
|
|
53
|
+
let prev = null;
|
|
54
|
+
try {
|
|
55
|
+
if (existsSync(SESSION_FILE)) {
|
|
56
|
+
prev = JSON.parse(readFileSync(SESSION_FILE, 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
} catch { /* ignore */ }
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
// Reuse session if within TTL; cheaply update touched timestamp at most
|
|
61
|
+
// once per minute to avoid hammering the file on rapid commands.
|
|
62
|
+
if (prev && prev.id && now - (prev.touched || 0) < SESSION_TTL_MS) {
|
|
63
|
+
if (now - (prev.touched || 0) > 60_000) {
|
|
64
|
+
try { writeFileSync(SESSION_FILE, JSON.stringify({ id: prev.id, touched: now })); }
|
|
65
|
+
catch { /* ignore */ }
|
|
66
|
+
}
|
|
67
|
+
return prev.id;
|
|
68
|
+
}
|
|
69
|
+
const id = randomUUID();
|
|
70
|
+
try {
|
|
71
|
+
ensureDir();
|
|
72
|
+
writeFileSync(SESSION_FILE, JSON.stringify({ id, touched: now }));
|
|
73
|
+
} catch { /* ignore */ }
|
|
74
|
+
return id;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _send(event, payload) {
|
|
78
|
+
if (disabled() || !ENDPOINT) return;
|
|
79
|
+
// Fire-and-forget. No await; can't keep the process alive.
|
|
80
|
+
const ctrl = new AbortController();
|
|
81
|
+
const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
|
|
82
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
83
|
+
fetch(ENDPOINT, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
event,
|
|
88
|
+
device_id: getDeviceId(),
|
|
89
|
+
session_id: getSessionId(),
|
|
90
|
+
ts: Date.now(),
|
|
91
|
+
...payload,
|
|
92
|
+
}),
|
|
93
|
+
signal: ctrl.signal,
|
|
94
|
+
})
|
|
95
|
+
.catch(() => { /* fire-and-forget */ })
|
|
96
|
+
.finally(() => clearTimeout(timer));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function flagNames(opts) {
|
|
100
|
+
// Extract flag names from a commander options object. Values stripped.
|
|
101
|
+
if (!opts || typeof opts !== 'object') return [];
|
|
102
|
+
return Object.keys(opts).filter((k) => !k.startsWith('_') && k !== 'parent');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function trackCommandSucceeded(command, opts, { durationMs } = {}) {
|
|
106
|
+
_send('command_succeeded', {
|
|
107
|
+
command,
|
|
108
|
+
flags: flagNames(opts),
|
|
109
|
+
duration_ms: typeof durationMs === 'number' ? Math.round(durationMs) : null,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function trackCommandFailed(command, opts, { errorCode, durationMs } = {}) {
|
|
114
|
+
_send('command_failed', {
|
|
115
|
+
command,
|
|
116
|
+
flags: flagNames(opts),
|
|
117
|
+
error_code: errorCode || 'unknown',
|
|
118
|
+
duration_ms: typeof durationMs === 'number' ? Math.round(durationMs) : null,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Background update check — reads cached result synchronously on startup,
|
|
2
|
+
// kicks off the registry probe as a detached subprocess that survives
|
|
3
|
+
// process exit. Zero blocking I/O on the user's command.
|
|
4
|
+
//
|
|
5
|
+
// Pattern: print a one-liner the *next* time the user runs us, after the
|
|
6
|
+
// background check has cached the result. No "checking for updates..."
|
|
7
|
+
// spinner ever appears.
|
|
8
|
+
//
|
|
9
|
+
// Honors NO_UPDATE_NOTIFIER and CI (industry-standard opt-out).
|
|
10
|
+
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const CONFIG_DIR = join(homedir(), '.vike');
|
|
18
|
+
const CHECK_FILE = join(CONFIG_DIR, 'update-check.json');
|
|
19
|
+
const LAST_VERSION_FILE = join(CONFIG_DIR, 'last-version.json');
|
|
20
|
+
const PACKAGE_NAME = '@vike-io/cli';
|
|
21
|
+
const REGISTRY = 'https://registry.npmjs.org';
|
|
22
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
23
|
+
|
|
24
|
+
function suppressed() {
|
|
25
|
+
return process.env.NO_UPDATE_NOTIFIER === '1' ||
|
|
26
|
+
process.env.CI ||
|
|
27
|
+
process.env.NODE_ENV === 'test' ||
|
|
28
|
+
process.env.VIKE_NO_UPDATE_CHECK === '1';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readCache() {
|
|
32
|
+
if (!existsSync(CHECK_FILE)) return null;
|
|
33
|
+
try { return JSON.parse(readFileSync(CHECK_FILE, 'utf8')); } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeCache(latest) {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
39
|
+
writeFileSync(CHECK_FILE, JSON.stringify({ latest, checkedAt: Date.now() }, null, 2));
|
|
40
|
+
} catch { /* ignore */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cmpSemver(a, b) {
|
|
44
|
+
const pa = a.split('.').map(Number);
|
|
45
|
+
const pb = b.split('.').map(Number);
|
|
46
|
+
for (let i = 0; i < 3; i++) {
|
|
47
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
48
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Synchronous: read whatever the background process last cached.
|
|
54
|
+
// Returns a notification string or null. Call at startup.
|
|
55
|
+
export function getUpdateNotification(currentVersion) {
|
|
56
|
+
if (suppressed()) return null;
|
|
57
|
+
const cache = readCache();
|
|
58
|
+
if (!cache || !cache.latest) return null;
|
|
59
|
+
if (cmpSemver(cache.latest, currentVersion) > 0) {
|
|
60
|
+
return (
|
|
61
|
+
`\n Update available: ${currentVersion} → ${cache.latest}\n` +
|
|
62
|
+
` Run: npm install -g ${PACKAGE_NAME}\n`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Fire the check as a detached subprocess. Doesn't block process exit.
|
|
69
|
+
// Honors a 1-day cache window — re-checking on every command would
|
|
70
|
+
// hammer the npm registry.
|
|
71
|
+
export function scheduleUpdateCheck() {
|
|
72
|
+
if (suppressed()) return;
|
|
73
|
+
const cache = readCache();
|
|
74
|
+
if (cache && Date.now() - (cache.checkedAt || 0) < CHECK_INTERVAL_MS) return;
|
|
75
|
+
|
|
76
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
77
|
+
// Inline child script — no separate file needed, no dependency on
|
|
78
|
+
// package layout, survives `npm install -g` symlinking.
|
|
79
|
+
const code = `
|
|
80
|
+
const https = require('https');
|
|
81
|
+
const fs = require('fs');
|
|
82
|
+
const path = require('path');
|
|
83
|
+
const os = require('os');
|
|
84
|
+
const file = path.join(os.homedir(), '.vike', 'update-check.json');
|
|
85
|
+
https.get('${REGISTRY}/${PACKAGE_NAME}/latest', { timeout: 5000 }, (r) => {
|
|
86
|
+
let buf = '';
|
|
87
|
+
r.on('data', (c) => { buf += c; });
|
|
88
|
+
r.on('end', () => {
|
|
89
|
+
try {
|
|
90
|
+
const v = JSON.parse(buf).version;
|
|
91
|
+
if (!v) return;
|
|
92
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
93
|
+
fs.writeFileSync(file, JSON.stringify({ latest: v, checkedAt: Date.now() }));
|
|
94
|
+
} catch {}
|
|
95
|
+
});
|
|
96
|
+
}).on('error', () => {}).on('timeout', function () { this.destroy(); });
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const child = spawn(process.execPath, ['-e', code], {
|
|
101
|
+
detached: true,
|
|
102
|
+
stdio: 'ignore',
|
|
103
|
+
});
|
|
104
|
+
child.unref();
|
|
105
|
+
} catch { /* ignore */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// First-run-after-upgrade hook. Stores last-seen version; if it changes,
|
|
109
|
+
// prints a one-time "you upgraded" notice on the next command.
|
|
110
|
+
export function getUpgradeNotice(currentVersion) {
|
|
111
|
+
if (suppressed()) return null;
|
|
112
|
+
let last = null;
|
|
113
|
+
try {
|
|
114
|
+
if (existsSync(LAST_VERSION_FILE)) {
|
|
115
|
+
last = JSON.parse(readFileSync(LAST_VERSION_FILE, 'utf8')).version;
|
|
116
|
+
}
|
|
117
|
+
} catch { /* ignore */ }
|
|
118
|
+
try {
|
|
119
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
120
|
+
writeFileSync(LAST_VERSION_FILE, JSON.stringify({ version: currentVersion }));
|
|
121
|
+
} catch { /* ignore */ }
|
|
122
|
+
if (last && last !== currentVersion && cmpSemver(currentVersion, last) > 0) {
|
|
123
|
+
return `\n Updated to ${currentVersion} (was ${last}).\n`;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|