@shopeefans/init 0.1.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/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @shopeefans/init
2
+
3
+ One command for **Codex** and **Claude Code**: install the `$shopeefans` skill and wire the remote MCP server.
4
+
5
+ ```bash
6
+ npx -y @shopeefans/init@latest --agent all --api-key sk_live_YOUR_KEY
7
+ ```
8
+
9
+ ## What it does
10
+
11
+ | Step | Codex | Claude Code |
12
+ |------|-------|-------------|
13
+ | Skill | `~/.codex/skills/shopeefans` (+ `~/.agents/skills`) | `~/.claude/skills/shopeefans` (+ `~/.agents/skills`) |
14
+ | MCP | `~/.codex/config.toml` → `[mcp_servers.shopeefans-ai]` + `http_headers` | `~/.claude.json` → `mcpServers.shopeefans-ai` |
15
+
16
+ Does **not** use `codex mcp add --header` (unsupported). Backs up existing config to `*.bak-shopeefans`.
17
+
18
+ ## Options
19
+
20
+ ```bash
21
+ npx -y @shopeefans/init@latest --agent codex --api-key sk_live_…
22
+ npx -y @shopeefans/init@latest --agent claude --api-key sk_live_…
23
+ npx -y @shopeefans/init@latest --skill-only --agent all
24
+ npx -y @shopeefans/init@latest --mcp-only --agent codex --api-key sk_live_…
25
+ npx -y @shopeefans/init@latest uninstall --agent all
26
+ ```
27
+
28
+ Get a key: https://ai.shopeefans.com/account/setup
29
+
30
+ ## After install
31
+
32
+ 1. Restart / reload MCP in Codex or Claude Code
33
+ 2. Type `$shopeefans` or ask: `Search phone cases in SG, top 5 by monthly sales`
34
+
35
+ ## Local dev (unpublished)
36
+
37
+ ```bash
38
+ node packages/init/bin/shopeefans-init.js --agent all --api-key sk_live_…
39
+ # or
40
+ npx -y /absolute/path/to/shopeefans-web/packages/init --agent codex --api-key sk_live_…
41
+ ```
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const path = require("node:path");
5
+ const { run } = require("../lib/cli");
6
+
7
+ run(process.argv.slice(2), {
8
+ packageRoot: path.join(__dirname, ".."),
9
+ version: require("../package.json").version,
10
+ }).catch((error) => {
11
+ process.stderr.write(`shopeefans-init error: ${error.message}\n`);
12
+ process.exitCode = 1;
13
+ });
package/lib/cli.js ADDED
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+
3
+ const path = require("node:path");
4
+ const { parseArgs } = require("./parse-args");
5
+ const { installSkills, uninstallSkills } = require("./install-skill");
6
+ const { installCodexMcp, uninstallCodexMcp } = require("./install-mcp-codex");
7
+ const {
8
+ installClaudeMcp,
9
+ uninstallClaudeMcp,
10
+ } = require("./install-mcp-claude");
11
+
12
+ const USAGE = `Usage: shopeefans-init [install|uninstall] [options]
13
+
14
+ Install ShopeeFans AI local skill + remote MCP (Codex / Claude Code).
15
+
16
+ Options:
17
+ --agent, -a codex | claude | all (default: all)
18
+ --api-key, -k sk_live_… required for MCP write
19
+ --origin https://ai.shopeefans.com MCP host (default)
20
+ --skill-only only install skill files
21
+ --mcp-only only write MCP config
22
+ --help, -h
23
+ --version, -v
24
+
25
+ Examples:
26
+ npx -y @shopeefans/init@latest --agent all --api-key sk_live_xxx
27
+ npx -y @shopeefans/init@latest --agent codex --api-key sk_live_xxx
28
+ npx -y @shopeefans/init@latest --agent claude --api-key sk_live_xxx
29
+ npx -y @shopeefans/init@latest uninstall --agent all
30
+ `;
31
+
32
+ /**
33
+ * @param {string[]} argv
34
+ * @param {{ packageRoot: string, version: string }} ctx
35
+ */
36
+ async function run(argv, ctx) {
37
+ const options = parseArgs(argv);
38
+ if (options.help) {
39
+ process.stdout.write(USAGE);
40
+ return;
41
+ }
42
+ if (options.version) {
43
+ process.stdout.write(`${ctx.version}\n`);
44
+ return;
45
+ }
46
+
47
+ const sourceSkillDir = path.join(ctx.packageRoot, "skills", "shopeefans");
48
+ const wantSkill = !options.mcpOnly;
49
+ const wantMcp = !options.skillOnly;
50
+ const agents =
51
+ options.agent === "all" ? ["codex", "claude"] : [options.agent];
52
+
53
+ if (options.action === "uninstall") {
54
+ if (wantSkill) {
55
+ const removed = await uninstallSkills({ agent: options.agent });
56
+ for (const p of removed) process.stdout.write(`removed skill ${p}\n`);
57
+ }
58
+ if (wantMcp) {
59
+ if (agents.includes("codex")) {
60
+ const r = await uninstallCodexMcp();
61
+ process.stdout.write(
62
+ r.removed
63
+ ? `removed Codex MCP from ${r.configPath}\n`
64
+ : `Codex MCP not found in ${r.configPath}\n`,
65
+ );
66
+ }
67
+ if (agents.includes("claude")) {
68
+ const r = await uninstallClaudeMcp();
69
+ process.stdout.write(
70
+ r.removed
71
+ ? `removed Claude MCP from ${r.configPath}\n`
72
+ : `Claude MCP not found in ${r.configPath}\n`,
73
+ );
74
+ }
75
+ }
76
+ process.stdout.write("Done. Restart Codex / Claude Code if they are open.\n");
77
+ return;
78
+ }
79
+
80
+ if (wantMcp && !options.apiKey) {
81
+ throw new Error(
82
+ "Missing --api-key sk_live_… (or pass --skill-only). Get a key at https://ai.shopeefans.com/account/setup",
83
+ );
84
+ }
85
+ if (wantMcp && !/^sk_live_[a-zA-Z0-9]+$/.test(options.apiKey)) {
86
+ process.stderr.write(
87
+ "warning: api key does not look like sk_live_… — continuing anyway\n",
88
+ );
89
+ }
90
+
91
+ process.stdout.write(`ShopeeFans init v${ctx.version}\n`);
92
+ process.stdout.write(`agent=${options.agent} origin=${options.origin}\n`);
93
+
94
+ if (wantSkill) {
95
+ const installed = await installSkills({
96
+ agent: options.agent,
97
+ sourceSkillDir,
98
+ version: ctx.version,
99
+ });
100
+ for (const p of installed) process.stdout.write(`skill → ${p}\n`);
101
+ }
102
+
103
+ if (wantMcp) {
104
+ if (agents.includes("codex")) {
105
+ const r = await installCodexMcp({
106
+ origin: options.origin,
107
+ apiKey: options.apiKey,
108
+ });
109
+ process.stdout.write(`Codex MCP → ${r.configPath}\n`);
110
+ if (r.backup) process.stdout.write(` backup ${r.backup}\n`);
111
+ }
112
+ if (agents.includes("claude")) {
113
+ const r = await installClaudeMcp({
114
+ origin: options.origin,
115
+ apiKey: options.apiKey,
116
+ });
117
+ process.stdout.write(`Claude MCP → ${r.configPath}\n`);
118
+ if (r.backup) process.stdout.write(` backup ${r.backup}\n`);
119
+ }
120
+ }
121
+
122
+ process.stdout.write("\nNext:\n");
123
+ process.stdout.write(" 1) Restart or reload MCP in Codex / Claude Code\n");
124
+ process.stdout.write(" 2) Invoke $shopeefans (or ask in natural language)\n");
125
+ process.stdout.write(
126
+ ' 3) Try: Search phone cases in SG, top 5 by monthly sales\n',
127
+ );
128
+ process.stdout.write(
129
+ "\nHandoff: ShopeeFans skill + MCP installed. Prefer shopeefans-ai tools; do not web-search for Shopee sales.\n",
130
+ );
131
+ }
132
+
133
+ module.exports = { run, USAGE };
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+
7
+ const SERVER = "shopeefans-ai";
8
+
9
+ /**
10
+ * Merge HTTP MCP server into Claude Code ~/.claude.json
11
+ * @param {object} config
12
+ * @param {{ url: string, apiKey: string }} opts
13
+ */
14
+ function upsertClaudeJson(config, { url, apiKey }) {
15
+ const next = { ...config };
16
+ const servers = { ...(next.mcpServers || {}) };
17
+ servers[SERVER] = {
18
+ type: "http",
19
+ url,
20
+ headers: {
21
+ Authorization: `Bearer ${apiKey}`,
22
+ },
23
+ };
24
+ next.mcpServers = servers;
25
+ return next;
26
+ }
27
+
28
+ /**
29
+ * @param {{ origin: string, apiKey: string, homeDir?: string, env?: NodeJS.ProcessEnv }} opts
30
+ */
31
+ async function installClaudeMcp({
32
+ origin,
33
+ apiKey,
34
+ homeDir = os.homedir(),
35
+ env = process.env,
36
+ }) {
37
+ const claudeHome = env.CLAUDE_HOME || path.join(homeDir, ".claude");
38
+ // Claude Code user config — mcpServers live on ~/.claude.json in current releases
39
+ const configPath =
40
+ env.CLAUDE_CONFIG_PATH || path.join(homeDir, ".claude.json");
41
+ await fs.promises.mkdir(claudeHome, { recursive: true });
42
+
43
+ let existing = {};
44
+ if (fs.existsSync(configPath)) {
45
+ const raw = await fs.promises.readFile(configPath, "utf8");
46
+ try {
47
+ existing = JSON.parse(raw);
48
+ } catch {
49
+ throw new Error(`Could not parse ${configPath} as JSON`);
50
+ }
51
+ }
52
+
53
+ const url = `${origin.replace(/\/$/, "")}/mcp`;
54
+ const next = upsertClaudeJson(existing, { url, apiKey });
55
+ const backup = `${configPath}.bak-shopeefans`;
56
+ if (fs.existsSync(configPath)) {
57
+ await fs.promises.copyFile(configPath, backup);
58
+ }
59
+ await fs.promises.writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`);
60
+ return { configPath, backup: fs.existsSync(backup) ? backup : null, url };
61
+ }
62
+
63
+ async function uninstallClaudeMcp({
64
+ homeDir = os.homedir(),
65
+ env = process.env,
66
+ } = {}) {
67
+ const configPath =
68
+ env.CLAUDE_CONFIG_PATH || path.join(homeDir, ".claude.json");
69
+ if (!fs.existsSync(configPath)) return { configPath, removed: false };
70
+ const existing = JSON.parse(await fs.promises.readFile(configPath, "utf8"));
71
+ if (!existing.mcpServers || !existing.mcpServers[SERVER]) {
72
+ return { configPath, removed: false };
73
+ }
74
+ delete existing.mcpServers[SERVER];
75
+ await fs.promises.writeFile(
76
+ configPath,
77
+ `${JSON.stringify(existing, null, 2)}\n`,
78
+ );
79
+ return { configPath, removed: true };
80
+ }
81
+
82
+ module.exports = {
83
+ SERVER,
84
+ upsertClaudeJson,
85
+ installClaudeMcp,
86
+ uninstallClaudeMcp,
87
+ };
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+
7
+ const SERVER = "shopeefans-ai";
8
+
9
+ function escapeTomlBasicString(value) {
10
+ return JSON.stringify(String(value));
11
+ }
12
+
13
+ /**
14
+ * Build / replace the shopeefans-ai block inside Codex config.toml
15
+ * @param {string} existing
16
+ * @param {{ url: string, apiKey: string }} opts
17
+ */
18
+ function upsertCodexToml(existing, { url, apiKey }) {
19
+ const block = [
20
+ `[mcp_servers.${SERVER}]`,
21
+ `url = ${escapeTomlBasicString(url)}`,
22
+ ``,
23
+ `[mcp_servers.${SERVER}.http_headers]`,
24
+ `Authorization = ${escapeTomlBasicString(`Bearer ${apiKey}`)}`,
25
+ ``,
26
+ ].join("\n");
27
+
28
+ const text = existing.replace(/\s+$/, "") + (existing.trim() ? "\n\n" : "");
29
+ // Remove previous shopeefans-ai mcp_servers sections (url + http_headers)
30
+ const without = text
31
+ .replace(
32
+ new RegExp(
33
+ `\\n*\\[mcp_servers\\.${SERVER}\\][\\s\\S]*?(?=\\n\\[|\\n*$)`,
34
+ "g",
35
+ ),
36
+ "\n",
37
+ )
38
+ .replace(
39
+ new RegExp(
40
+ `\\n*\\[mcp_servers\\.${SERVER}\\.http_headers\\][\\s\\S]*?(?=\\n\\[|\\n*$)`,
41
+ "g",
42
+ ),
43
+ "\n",
44
+ )
45
+ .replace(/\n{3,}/g, "\n\n")
46
+ .trimEnd();
47
+
48
+ return `${without}\n\n${block}`;
49
+ }
50
+
51
+ /**
52
+ * @param {{ origin: string, apiKey: string, homeDir?: string, env?: NodeJS.ProcessEnv }} opts
53
+ */
54
+ async function installCodexMcp({
55
+ origin,
56
+ apiKey,
57
+ homeDir = os.homedir(),
58
+ env = process.env,
59
+ }) {
60
+ const codexHome = env.CODEX_HOME || path.join(homeDir, ".codex");
61
+ const configPath = path.join(codexHome, "config.toml");
62
+ await fs.promises.mkdir(codexHome, { recursive: true });
63
+ let existing = "";
64
+ try {
65
+ existing = await fs.promises.readFile(configPath, "utf8");
66
+ } catch (error) {
67
+ if (!error || error.code !== "ENOENT") throw error;
68
+ }
69
+ const url = `${origin.replace(/\/$/, "")}/mcp`;
70
+ const next = upsertCodexToml(existing, { url, apiKey });
71
+ const backup = `${configPath}.bak-shopeefans`;
72
+ if (existing) {
73
+ await fs.promises.writeFile(backup, existing);
74
+ }
75
+ await fs.promises.writeFile(configPath, next.endsWith("\n") ? next : `${next}\n`);
76
+ return { configPath, backup: existing ? backup : null, url };
77
+ }
78
+
79
+ /**
80
+ * Best-effort: strip shopeefans-ai sections from config.toml
81
+ */
82
+ async function uninstallCodexMcp({
83
+ homeDir = os.homedir(),
84
+ env = process.env,
85
+ } = {}) {
86
+ const codexHome = env.CODEX_HOME || path.join(homeDir, ".codex");
87
+ const configPath = path.join(codexHome, "config.toml");
88
+ if (!fs.existsSync(configPath)) return { configPath, removed: false };
89
+ const existing = await fs.promises.readFile(configPath, "utf8");
90
+ const next = existing
91
+ .replace(
92
+ new RegExp(
93
+ `\\n*\\[mcp_servers\\.${SERVER}\\][\\s\\S]*?(?=\\n\\[|\\n*$)`,
94
+ "g",
95
+ ),
96
+ "\n",
97
+ )
98
+ .replace(
99
+ new RegExp(
100
+ `\\n*\\[mcp_servers\\.${SERVER}\\.http_headers\\][\\s\\S]*?(?=\\n\\[|\\n*$)`,
101
+ "g",
102
+ ),
103
+ "\n",
104
+ )
105
+ .replace(/\n{3,}/g, "\n\n")
106
+ .trimEnd();
107
+ if (next === existing.trimEnd()) return { configPath, removed: false };
108
+ await fs.promises.writeFile(configPath, `${next}\n`);
109
+ return { configPath, removed: true };
110
+ }
111
+
112
+ module.exports = {
113
+ SERVER,
114
+ upsertCodexToml,
115
+ installCodexMcp,
116
+ uninstallCodexMcp,
117
+ };
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+
7
+ const SKILL_DIR_NAME = "shopeefans";
8
+ const MANIFEST = ".shopeefans-install.json";
9
+
10
+ /**
11
+ * @param {{ agent: string, env?: NodeJS.ProcessEnv, homeDir?: string }} opts
12
+ */
13
+ function resolveSkillRoots({
14
+ agent = "all",
15
+ env = process.env,
16
+ homeDir = os.homedir(),
17
+ } = {}) {
18
+ const roots = {
19
+ codex: path.join(env.CODEX_HOME || path.join(homeDir, ".codex"), "skills"),
20
+ claude: path.join(env.CLAUDE_HOME || path.join(homeDir, ".claude"), "skills"),
21
+ // Shared discovery root used by Codex and other agents
22
+ agents: path.join(env.AGENTS_HOME || path.join(homeDir, ".agents"), "skills"),
23
+ };
24
+
25
+ if (agent === "codex") return [roots.codex, roots.agents];
26
+ if (agent === "claude") return [roots.claude, roots.agents];
27
+ return [roots.codex, roots.claude, roots.agents];
28
+ }
29
+
30
+ /**
31
+ * @param {{ sourceSkillDir: string, targetRoot: string, version: string }} opts
32
+ */
33
+ async function installSkillIntoRoot({ sourceSkillDir, targetRoot, version }) {
34
+ const target = path.join(targetRoot, SKILL_DIR_NAME);
35
+ const tmp = `${target}.tmp-${process.pid}`;
36
+ await fs.promises.mkdir(targetRoot, { recursive: true });
37
+ await fs.promises.rm(tmp, { recursive: true, force: true });
38
+ await fs.promises.cp(sourceSkillDir, tmp, { recursive: true });
39
+ const skillMd = path.join(tmp, "SKILL.md");
40
+ if (!fs.existsSync(skillMd)) {
41
+ throw new Error(`Missing SKILL.md in ${sourceSkillDir}`);
42
+ }
43
+ await fs.promises.writeFile(
44
+ path.join(tmp, MANIFEST),
45
+ `${JSON.stringify(
46
+ {
47
+ schemaVersion: 1,
48
+ package: "@shopeefans/init",
49
+ skill: SKILL_DIR_NAME,
50
+ version,
51
+ installedAt: new Date().toISOString(),
52
+ },
53
+ null,
54
+ 2,
55
+ )}\n`,
56
+ );
57
+ await fs.promises.rm(target, { recursive: true, force: true });
58
+ await fs.promises.rename(tmp, target);
59
+ return target;
60
+ }
61
+
62
+ /**
63
+ * @param {{ agent: string, sourceSkillDir: string, version: string }} opts
64
+ */
65
+ async function installSkills(opts) {
66
+ const roots = resolveSkillRoots({ agent: opts.agent });
67
+ const installed = [];
68
+ for (const root of roots) {
69
+ const target = await installSkillIntoRoot({
70
+ sourceSkillDir: opts.sourceSkillDir,
71
+ targetRoot: root,
72
+ version: opts.version,
73
+ });
74
+ installed.push(target);
75
+ }
76
+ return installed;
77
+ }
78
+
79
+ /**
80
+ * @param {{ agent: string }} opts
81
+ */
82
+ async function uninstallSkills({ agent }) {
83
+ const roots = resolveSkillRoots({ agent });
84
+ const removed = [];
85
+ for (const root of roots) {
86
+ const target = path.join(root, SKILL_DIR_NAME);
87
+ if (!fs.existsSync(target)) continue;
88
+ const manifest = path.join(target, MANIFEST);
89
+ // Only remove if we installed it (or legacy copy without manifest but named shopeefans)
90
+ if (fs.existsSync(manifest) || fs.existsSync(path.join(target, "SKILL.md"))) {
91
+ await fs.promises.rm(target, { recursive: true, force: true });
92
+ removed.push(target);
93
+ }
94
+ }
95
+ return removed;
96
+ }
97
+
98
+ module.exports = {
99
+ SKILL_DIR_NAME,
100
+ resolveSkillRoots,
101
+ installSkills,
102
+ uninstallSkills,
103
+ };
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+
3
+ const AGENTS = new Set(["codex", "claude", "all"]);
4
+
5
+ /**
6
+ * @param {string[]} args
7
+ */
8
+ function parseArgs(args = []) {
9
+ const result = {
10
+ action: "install",
11
+ agent: "all",
12
+ apiKey: "",
13
+ origin: "https://ai.shopeefans.com",
14
+ skillOnly: false,
15
+ mcpOnly: false,
16
+ help: false,
17
+ version: false,
18
+ };
19
+
20
+ for (let i = 0; i < args.length; i += 1) {
21
+ const arg = args[i];
22
+ if (arg === "install" || arg === "uninstall") {
23
+ result.action = arg;
24
+ } else if (arg === "--help" || arg === "-h") {
25
+ result.help = true;
26
+ } else if (arg === "--version" || arg === "-v") {
27
+ result.version = true;
28
+ } else if (arg === "--skill-only") {
29
+ result.skillOnly = true;
30
+ } else if (arg === "--mcp-only") {
31
+ result.mcpOnly = true;
32
+ } else if (arg === "--agent" || arg === "-a") {
33
+ const value = String(args[++i] || "").toLowerCase();
34
+ if (!AGENTS.has(value)) throw new Error(`Unsupported --agent: ${value}`);
35
+ result.agent = value;
36
+ } else if (arg === "--api-key" || arg === "-k") {
37
+ result.apiKey = String(args[++i] || "").trim();
38
+ if (!result.apiKey) throw new Error(`${arg} requires a value`);
39
+ } else if (arg === "--origin") {
40
+ result.origin = String(args[++i] || "").replace(/\/$/, "");
41
+ if (!result.origin) throw new Error("--origin requires a value");
42
+ } else {
43
+ throw new Error(`Unknown option: ${arg}`);
44
+ }
45
+ }
46
+
47
+ if (result.skillOnly && result.mcpOnly) {
48
+ throw new Error("Use only one of --skill-only / --mcp-only");
49
+ }
50
+ return result;
51
+ }
52
+
53
+ module.exports = { parseArgs, AGENTS };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@shopeefans/init",
3
+ "version": "0.1.0",
4
+ "description": "Install ShopeeFans AI skill + MCP for Codex and Claude Code",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "type": "commonjs",
8
+ "bin": {
9
+ "shopeefans-init": "bin/shopeefans-init.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "lib",
14
+ "skills",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "keywords": [
21
+ "shopeefans",
22
+ "mcp",
23
+ "codex",
24
+ "claude",
25
+ "shopee",
26
+ "skill"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/izhiliu/shopeefans-web.git",
31
+ "directory": "packages/init"
32
+ }
33
+ }
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: shopeefans
3
+ description: >-
4
+ ShopeeFans AI — Shopee product research via MCP (shopeefans-ai). Use when the
5
+ user types /shopeefans or $shopeefans, or asks about Shopee bestsellers,
6
+ competitors, product sales, keyword search, or market/category scouting in
7
+ SG, TH, MY, ID, PH, VN, TW, BR, MX. Never use web search for Shopee sales
8
+ numbers — always call shopeefans-ai tools.
9
+ ---
10
+
11
+ # ShopeeFans
12
+
13
+ Use the **shopeefans-ai** remote MCP tools only. Do not invent Shopee numbers.
14
+ Do not use web search / browse for Shopee sales or rankings.
15
+
16
+ **MCP URL:** `https://ai.shopeefans.com/mcp`
17
+ **Auth:** `Authorization: Bearer sk_live_…` (configured by `@shopeefans/init`)
18
+
19
+ If MCP tools are missing, tell the human to run:
20
+
21
+ ```bash
22
+ npx -y @shopeefans/init@latest --agent all --api-key sk_live_YOUR_KEY
23
+ ```
24
+
25
+ Then reload MCP / restart the client.
26
+
27
+ ## Tools
28
+
29
+ | Goal | Tool |
30
+ |------|------|
31
+ | Markets | `list_markets()` — 0 credits |
32
+ | Categories | `list_categories(market, parent_id?)` — 0 credits |
33
+ | Keyword search | `search_products(market, query, limit?, sort?)` — 1 credit |
34
+ | Monthly boards | `get_trending(market, board, period=month, category_id?, limit?)` — 1 credit |
35
+ | SKU detail | `get_product(market, product_id)` — 3 credits |
36
+
37
+ Boards: `best_sellers` | `rising` | `new_arrivals`.
38
+
39
+ ## Rules
40
+
41
+ - Sales fields only: **`sales30d`** and **`totalSales`**. Never invent daily/weekly sales.
42
+ - `search_products`: use **local language** for the market when possible.
43
+ - On `Insufficient credits`, point to https://ai.shopeefans.com/pricing — do not retry.
44
+ - Prefer tables: title, price, sales30d, totalSales, rating. Cite `data_as_of` when present.
45
+
46
+ ## Default workflows
47
+
48
+ **Search:** `search_products(market="SG", query="phone cases", limit=5, sort="sales30d")` → optional `get_product` on top hits.
49
+
50
+ **Category leaders:** `list_categories` → drill → `get_trending(..., board="best_sellers", period="month", category_id=…)`.