@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 +41 -0
- package/bin/shopeefans-init.js +13 -0
- package/lib/cli.js +133 -0
- package/lib/install-mcp-claude.js +87 -0
- package/lib/install-mcp-codex.js +117 -0
- package/lib/install-skill.js +103 -0
- package/lib/parse-args.js +53 -0
- package/package.json +33 -0
- package/skills/shopeefans/SKILL.md +50 -0
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=…)`.
|