@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 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.5.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",
@@ -1,20 +1,40 @@
1
- // Postinstall — print a friendly setup hint when the user installs vike-cli.
2
- // Silent in CI environments to avoid log noise.
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
- if (process.env.CI || process.env.npm_config_loglevel === 'silent') {
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
- const lines = [
9
- '',
10
- ' @vike-io/cli installed.',
11
- '',
12
- ' Next steps:',
13
- ' vike init — interactive setup',
14
- ' vike doctor — verify your install',
15
- ' vike token search BTC — your first query',
16
- '',
17
- ' Docs: https://vike.io/api',
18
- '',
19
- ];
20
- process.stdout.write(lines.join('\n'));
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
+ }
@@ -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')
@@ -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(readPackageVersion(), '-v, --version', 'Show CLI 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).catch((err) => {
42
- console.error(`vike: ${err.message ?? err}`);
43
- process.exitCode = 1;
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
+ });
@@ -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
+ }