feat: add track command group, split follow-wallet/kol/smartmoney from portfolio

- Add new `track` command group with three sub-commands:
  - `track follow-wallet`: monitor trade activity of followed wallets (critical auth)
  - `track kol`: KOL trade records with optional --chain and client-side --side filter
  - `track smartmoney`: Smart Money trade records with optional --chain and client-side --side filter
- Remove follow-wallet, kol, smartmoney sub-commands from portfolio
- Add getKol/getSmartMoney optional chain param in OpenApiClient
- Switch getFollowWallet to criticalRequest (signature auth required)
- Align track follow-wallet options to actual API params (remove unsupported flags)
- Add skills/gmgn-track/SKILL.md with sub-command guide, options, response fields
- Add IMPORTANT notice to all SKILL.md files to prevent AI from scraping gmgn.ai
- Add CRITICAL RULE section to CLAUDE.md to enforce CLI-only data access
- Update Readme.md and Readme.zh.md: add gmgn-track skill, Track CLI section, user tip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gumponchain
2026-03-27 14:19:57 +08:00
committed by GMGN.AI
co-authored by Claude Sonnet 4.6
parent 58849a8c61
commit 28062887d8
12 changed files with 265 additions and 119 deletions
+5 -3
View File
@@ -148,17 +148,19 @@ export class OpenApiClient {
}
async getFollowWallet(chain: string, extra: Record<string, string | number | string[]> = {}): Promise<unknown> {
return this.normalRequest("GET", "/v1/trade/follow_wallet", { chain, ...extra });
return this.criticalRequest("GET", "/v1/trade/follow_wallet", { chain, ...extra }, null);
}
async getKol(limit?: number): Promise<unknown> {
async getKol(chain?: string, limit?: number): Promise<unknown> {
const query: Record<string, string | number> = {};
if (chain) query["chain"] = chain;
if (limit != null) query["limit"] = limit;
return this.normalRequest("GET", "/v1/user/kol", query);
}
async getSmartMoney(limit?: number): Promise<unknown> {
async getSmartMoney(chain?: string, limit?: number): Promise<unknown> {
const query: Record<string, string | number> = {};
if (chain) query["chain"] = chain;
if (limit != null) query["limit"] = limit;
return this.normalRequest("GET", "/v1/user/smartmoney", query);
}
-58
View File
@@ -113,63 +113,5 @@ export function registerPortfolioCommands(program: Command): void {
printResult(data, opts.raw);
});
portfolio
.command("follow-wallet")
.description("Get follow-wallet trade records")
.requiredOption("--chain <chain>", "Chain: sol / bsc / base / eth")
.option("--wallet <address>", "Filter by wallet address")
.option("--base-token <address>", "Filter by base token address")
.option("--page-token <cursor>", "Pagination cursor")
.option("--limit <n>", "Page size (1200, default 100)", parseInt)
.option("--side <side>", "Trade direction filter")
.option("--cost <cost>", "Cost filter")
.option("--filter <tag...>", "Filter conditions, repeatable")
.option("--with-balance", "Include balance in response")
.option("--with-security", "Include security info in response")
.option("--min-amount-usd <n>", "Minimum trade amount (USD)", parseFloat)
.option("--max-amount-usd <n>", "Maximum trade amount (USD)", parseFloat)
.option("--is-gray", "Gray mode filter")
.option("--raw", "Output raw JSON")
.action(async (opts) => {
validateChain(opts.chain);
const extra: Record<string, string | number | string[]> = {};
if (opts.wallet) extra["wallet_address"] = opts.wallet;
if (opts.baseToken) extra["base_token"] = opts.baseToken;
if (opts.pageToken) extra["page_token"] = opts.pageToken;
if (opts.limit != null) extra["limit"] = opts.limit;
if (opts.side) extra["side"] = opts.side;
if (opts.cost) extra["cost"] = opts.cost;
if (opts.filter?.length) extra["filters"] = opts.filter;
if (opts.withBalance) extra["with_balance"] = "true";
if (opts.withSecurity) extra["with_security"] = "true";
if (opts.minAmountUsd != null) extra["min_amount_usd"] = opts.minAmountUsd;
if (opts.maxAmountUsd != null) extra["max_amount_usd"] = opts.maxAmountUsd;
if (opts.isGray) extra["is_gray"] = "true";
const client = new OpenApiClient(getConfig());
const data = await client.getFollowWallet(opts.chain, extra).catch(exitOnError);
printResult(data, opts.raw);
});
portfolio
.command("kol")
.description("Get KOL trade records (SOL chain)")
.option("--limit <n>", "Page size (1200, default 100)", parseInt)
.option("--raw", "Output raw JSON")
.action(async (opts) => {
const client = new OpenApiClient(getConfig());
const data = await client.getKol(opts.limit).catch(exitOnError);
printResult(data, opts.raw);
});
portfolio
.command("smartmoney")
.description("Get Smart Money trade records (SOL chain)")
.option("--limit <n>", "Page size (1200, default 100)", parseInt)
.option("--raw", "Output raw JSON")
.action(async (opts) => {
const client = new OpenApiClient(getConfig());
const data = await client.getSmartMoney(opts.limit).catch(exitOnError);
printResult(data, opts.raw);
});
}
+68
View File
@@ -0,0 +1,68 @@
import { Command } from "commander";
import { OpenApiClient } from "../client/OpenApiClient.js";
import { getConfig } from "../config.js";
import { exitOnError, printResult } from "../output.js";
import { validateChain } from "../validate.js";
export function registerTrackCommands(program: Command): void {
const track = program.command("track").description("On-chain tracking commands: follow-wallet trades, KOL trades, Smart Money trades");
track
.command("follow-wallet")
.description("Get follow-wallet trade records")
.requiredOption("--chain <chain>", "Chain: sol / bsc / base")
.option("--wallet <address>", "Filter by wallet address")
.option("--limit <n>", "Page size (1100, default 10)", parseInt)
.option("--side <side>", "Trade direction filter: buy / sell")
.option("--filter <tag...>", "Filter conditions, repeatable")
.option("--min-amount-usd <n>", "Minimum trade amount (USD)", parseFloat)
.option("--max-amount-usd <n>", "Maximum trade amount (USD)", parseFloat)
.option("--raw", "Output raw JSON")
.action(async (opts) => {
validateChain(opts.chain);
const extra: Record<string, string | number | string[]> = {};
if (opts.wallet) extra["wallet_address"] = opts.wallet;
if (opts.limit != null) extra["limit"] = opts.limit;
if (opts.side) extra["side"] = opts.side;
if (opts.filter?.length) extra["filters"] = opts.filter;
if (opts.minAmountUsd != null) extra["min_amount_usd"] = opts.minAmountUsd;
if (opts.maxAmountUsd != null) extra["max_amount_usd"] = opts.maxAmountUsd;
const client = new OpenApiClient(getConfig(true));
const data = await client.getFollowWallet(opts.chain, extra).catch(exitOnError);
printResult(data, opts.raw);
});
track
.command("kol")
.description("Get KOL trade records")
.option("--chain <chain>", "Chain: sol / bsc / base")
.option("--limit <n>", "Page size (1200, default 100)", parseInt)
.option("--side <side>", "Filter by trade direction: buy / sell (client-side filter)")
.option("--raw", "Output raw JSON")
.action(async (opts) => {
if (opts.chain) validateChain(opts.chain);
const client = new OpenApiClient(getConfig());
const data = await client.getKol(opts.chain, opts.limit).catch(exitOnError) as { list?: { side: string }[] };
if (opts.side && data?.list) {
data.list = data.list.filter((item) => item.side === opts.side);
}
printResult(data, opts.raw);
});
track
.command("smartmoney")
.description("Get Smart Money trade records")
.option("--chain <chain>", "Chain: sol / bsc / base")
.option("--limit <n>", "Page size (1200, default 100)", parseInt)
.option("--side <side>", "Filter by trade direction: buy / sell (client-side filter)")
.option("--raw", "Output raw JSON")
.action(async (opts) => {
if (opts.chain) validateChain(opts.chain);
const client = new OpenApiClient(getConfig());
const data = await client.getSmartMoney(opts.chain, opts.limit).catch(exitOnError) as { list?: { side: string }[] };
if (opts.side && data?.list) {
data.list = data.list.filter((item) => item.side === opts.side);
}
printResult(data, opts.raw);
});
}
+2
View File
@@ -8,6 +8,7 @@ import { Command } from "commander";
import { registerTokenCommands } from "./commands/token.js";
import { registerMarketCommands } from "./commands/market.js";
import { registerPortfolioCommands } from "./commands/portfolio.js";
import { registerTrackCommands } from "./commands/track.js";
import { registerSwapCommands } from "./commands/swap.js";
const proxy = process.env.HTTPS_PROXY ?? process.env.https_proxy
@@ -50,6 +51,7 @@ program
registerTokenCommands(program);
registerMarketCommands(program);
registerPortfolioCommands(program);
registerTrackCommands(program);
registerSwapCommands(program);
program.parseAsync().catch((err) => {