@manturhub/cli 0.7.0 → 0.8.1

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/lib/archive.js ADDED
@@ -0,0 +1,146 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import {
3
+ cpSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readdirSync,
8
+ rmSync,
9
+ } from "node:fs";
10
+ import { basename, join, posix } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+
13
+ const MAX_FILES = 2000;
14
+ const MAX_UNPACKED_BYTES = 500 * 1024 * 1024;
15
+
16
+ export function validateSlug(slug, label = "ID") {
17
+ if (!slug || !/^[a-z0-9][a-z0-9._-]*$/i.test(slug)) {
18
+ throw new Error(`${label} 格式不合法: ${slug || "(空)"}`);
19
+ }
20
+ return slug;
21
+ }
22
+
23
+ function listArchive(zipPath) {
24
+ const commands = [
25
+ {
26
+ command: "unzip",
27
+ list: ["-Z1", zipPath],
28
+ verbose: ["-Z", "-l", zipPath],
29
+ summary: ["-Z", "-t", zipPath],
30
+ extract: (dest) => ["-o", "-q", zipPath, "-d", dest],
31
+ },
32
+ {
33
+ command: "tar",
34
+ list: ["-tf", zipPath],
35
+ verbose: ["-tvf", zipPath],
36
+ extract: (dest) => ["-xf", zipPath, "-C", dest],
37
+ },
38
+ ];
39
+ for (const tool of commands) {
40
+ try {
41
+ const output = execFileSync(tool.command, tool.list, { encoding: "utf8" });
42
+ const verbose = execFileSync(tool.command, tool.verbose, { encoding: "utf8" });
43
+ const summary = tool.summary
44
+ ? execFileSync(tool.command, tool.summary, { encoding: "utf8" })
45
+ : "";
46
+ return { tool, entries: output.split(/\r?\n/).filter(Boolean), verbose, summary };
47
+ } catch {
48
+ // Try the next installed extractor.
49
+ }
50
+ }
51
+ throw new Error("解压失败(需系统 tar 或 unzip 命令)");
52
+ }
53
+
54
+ function declaredUnpackedBytes(tool, verbose, summary) {
55
+ if (tool.command === "unzip") {
56
+ const match = summary.match(/([\d,]+)\s+bytes? uncompressed/i);
57
+ return match ? Number(match[1].replaceAll(",", "")) : Number.NaN;
58
+ }
59
+ let total = 0;
60
+ for (const line of verbose.split(/\r?\n/).filter(Boolean)) {
61
+ const bsd = line.match(/^\S+\s+\d+\s+\S+\s+\S+\s+(\d+)\s+/);
62
+ const gnu = line.match(/^\S+\s+\S+\s+(\d+)\s+/);
63
+ const size = Number((bsd || gnu)?.[1]);
64
+ if (!Number.isFinite(size)) return Number.NaN;
65
+ total += size;
66
+ }
67
+ return total;
68
+ }
69
+
70
+ export function validateArchiveEntries(entries, verbose = "", unpackedBytes = 0) {
71
+ if (!entries.length) throw new Error("安装包为空");
72
+ if (entries.length > MAX_FILES) throw new Error(`安装包文件过多(最多 ${MAX_FILES} 个)`);
73
+ if (!Number.isFinite(unpackedBytes) || unpackedBytes < 0) {
74
+ throw new Error("无法确认安装包解压后大小");
75
+ }
76
+ if (unpackedBytes > MAX_UNPACKED_BYTES) {
77
+ throw new Error(`安装包解压后过大(最大 ${MAX_UNPACKED_BYTES / 1024 / 1024} MB)`);
78
+ }
79
+ if (/^\s*[lh][rwx-]{9}\s/m.test(verbose)) {
80
+ throw new Error("安装包不得包含符号链接或硬链接");
81
+ }
82
+ for (const raw of entries) {
83
+ const name = raw.replaceAll("\\", "/");
84
+ const normalized = posix.normalize(name);
85
+ if (
86
+ name.includes("\0") ||
87
+ /[\x00-\x1f\x7f]/.test(name) ||
88
+ name.startsWith("/") ||
89
+ /^[a-z]:\//i.test(name) ||
90
+ normalized === ".." ||
91
+ normalized.startsWith("../")
92
+ ) {
93
+ throw new Error(`安装包包含不安全路径: ${raw}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ function inspectExtracted(dir, state = { files: 0, bytes: 0 }) {
99
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
100
+ if (/[\x00-\x1f\x7f]/.test(entry.name)) {
101
+ throw new Error(`安装包包含不安全文件名: ${JSON.stringify(entry.name)}`);
102
+ }
103
+ const path = join(dir, entry.name);
104
+ const stat = lstatSync(path);
105
+ if (stat.isSymbolicLink()) throw new Error(`安装包不得包含符号链接: ${entry.name}`);
106
+ if (stat.isDirectory()) inspectExtracted(path, state);
107
+ else if (stat.isFile()) {
108
+ state.files++;
109
+ state.bytes += stat.size;
110
+ if (state.files > MAX_FILES) throw new Error(`安装包文件过多(最多 ${MAX_FILES} 个)`);
111
+ if (state.bytes > MAX_UNPACKED_BYTES) {
112
+ throw new Error(`安装包解压后过大(最大 ${MAX_UNPACKED_BYTES / 1024 / 1024} MB)`);
113
+ }
114
+ } else {
115
+ throw new Error(`安装包包含不支持的文件类型: ${entry.name}`);
116
+ }
117
+ }
118
+ return state;
119
+ }
120
+
121
+ function rejectDestinationLinks(dir) {
122
+ if (!lstatSync(dir).isDirectory()) throw new Error(`安装目标不是目录: ${dir}`);
123
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
124
+ const path = join(dir, entry.name);
125
+ const stat = lstatSync(path);
126
+ if (stat.isSymbolicLink()) throw new Error(`安装目标包含符号链接: ${path}`);
127
+ if (stat.isDirectory()) rejectDestinationLinks(path);
128
+ }
129
+ }
130
+
131
+ export function extractZipSafely(zipPath, dest) {
132
+ const { tool, entries, verbose, summary } = listArchive(zipPath);
133
+ validateArchiveEntries(entries, verbose, declaredUnpackedBytes(tool, verbose, summary));
134
+ const staging = mkdtempSync(join(tmpdir(), "manturhub-extract-"));
135
+ try {
136
+ execFileSync(tool.command, tool.extract(staging), { stdio: ["ignore", "ignore", "ignore"] });
137
+ inspectExtracted(staging);
138
+ mkdirSync(dest, { recursive: true });
139
+ rejectDestinationLinks(dest);
140
+ for (const entry of readdirSync(staging)) {
141
+ cpSync(join(staging, entry), join(dest, basename(entry)), { recursive: true, force: true });
142
+ }
143
+ } finally {
144
+ rmSync(staging, { recursive: true, force: true });
145
+ }
146
+ }
package/lib/config.js CHANGED
@@ -11,7 +11,14 @@ import {
11
11
  // Local config lives at ~/.manturhub/config.json (chmod 600). Env vars win over it.
12
12
  const DIR = join(homedir(), ".manturhub");
13
13
  const FILE = join(DIR, "config.json");
14
- const DEFAULT_BASE = "https://hub.mantur.cn";
14
+ // Published CLI defaults to production; test/private deployments override with
15
+ // MANTURHUB_BASE or config.json. All generated links must go through getBaseUrl().
16
+ const DEFAULT_BASE = "https://hub.mantur.ai";
17
+ const LEGACY_DEFAULT_BASES = new Set([
18
+ "https://hub.mantur.cn",
19
+ "https://manturhub.leisurecat.cloud",
20
+ "https://api.ophub.com",
21
+ ]);
15
22
 
16
23
  export function loadConfig() {
17
24
  try {
@@ -23,7 +30,7 @@ export function loadConfig() {
23
30
 
24
31
  export function saveConfig(cfg) {
25
32
  if (!existsSync(DIR)) mkdirSync(DIR, { recursive: true });
26
- writeFileSync(FILE, JSON.stringify(cfg, null, 2));
33
+ writeFileSync(FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
27
34
  try {
28
35
  chmodSync(FILE, 0o600);
29
36
  } catch {
@@ -35,6 +42,24 @@ export function getKey() {
35
42
  return process.env.MANTURHUB_KEY || loadConfig().key || null;
36
43
  }
37
44
 
45
+ function validateBaseUrl(value) {
46
+ let url;
47
+ try {
48
+ url = new URL(value);
49
+ } catch {
50
+ throw new Error(`ManturHub 网关地址不合法: ${value}`);
51
+ }
52
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
53
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
54
+ throw new Error("MANTURHUB_BASE 必须使用 HTTPS(仅 localhost/127.0.0.1/::1 可使用 HTTP)");
55
+ }
56
+ if (url.username || url.password) throw new Error("MANTURHUB_BASE 不得包含用户名或密码");
57
+ return url.href.replace(/\/$/, "");
58
+ }
59
+
38
60
  export function getBaseUrl() {
39
- return process.env.MANTURHUB_BASE || loadConfig().baseUrl || DEFAULT_BASE;
61
+ if (process.env.MANTURHUB_BASE) return validateBaseUrl(process.env.MANTURHUB_BASE);
62
+ const storedBase = loadConfig().baseUrl?.replace(/\/$/, "");
63
+ if (storedBase && !LEGACY_DEFAULT_BASES.has(storedBase)) return validateBaseUrl(storedBase);
64
+ return DEFAULT_BASE;
40
65
  }
@@ -0,0 +1,45 @@
1
+ import { createWriteStream, rmSync } from "node:fs";
2
+ import { Readable, Transform } from "node:stream";
3
+ import { pipeline } from "node:stream/promises";
4
+
5
+ export const MAX_PACKAGE_BYTES = 100 * 1024 * 1024;
6
+
7
+ export function assertSecureDownloadUrl(value) {
8
+ let url;
9
+ try {
10
+ url = new URL(value);
11
+ } catch {
12
+ throw new Error("下载重定向地址不合法");
13
+ }
14
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
15
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
16
+ throw new Error("安装包下载地址必须使用 HTTPS");
17
+ }
18
+ return url.href;
19
+ }
20
+
21
+ export async function downloadResponseToFile(response, target, maxBytes = MAX_PACKAGE_BYTES) {
22
+ if (!response.ok) throw new Error(`安装包下载失败(HTTP ${response.status})`);
23
+ if (!response.body) throw new Error("安装包响应为空");
24
+ const declared = Number(response.headers.get("content-length"));
25
+ if (Number.isFinite(declared) && declared > maxBytes) {
26
+ await response.body.cancel();
27
+ throw new Error(`安装包过大(最大 ${Math.floor(maxBytes / 1024 / 1024)} MB)`);
28
+ }
29
+
30
+ let received = 0;
31
+ const limit = new Transform({
32
+ transform(chunk, _encoding, callback) {
33
+ received += chunk.length;
34
+ if (received > maxBytes) callback(new Error(`安装包过大(最大 ${Math.floor(maxBytes / 1024 / 1024)} MB)`));
35
+ else callback(null, chunk);
36
+ },
37
+ });
38
+ try {
39
+ await pipeline(Readable.fromWeb(response.body), limit, createWriteStream(target, { flags: "wx" }));
40
+ return received;
41
+ } catch (error) {
42
+ rmSync(target, { force: true });
43
+ throw error;
44
+ }
45
+ }
package/lib/login-link.js CHANGED
@@ -27,7 +27,10 @@ export async function loginViaBrowser() {
27
27
  // 1. 发起 CLI 会话(无鉴权)
28
28
  let session;
29
29
  try {
30
- const r = await fetch(base + "/api/v1/cli/session", { method: "POST" });
30
+ const r = await fetch(base + "/api/v1/cli/session", {
31
+ method: "POST",
32
+ signal: AbortSignal.timeout(15000),
33
+ });
31
34
  if (!r.ok) throw new Error("HTTP " + r.status);
32
35
  session = await r.json();
33
36
  } catch (e) {
@@ -36,24 +39,38 @@ export async function loginViaBrowser() {
36
39
  );
37
40
  process.exit(1);
38
41
  }
39
- const { device_code, user_code, verify_url, interval = 2, expires_in = 600 } = session;
42
+ const { device_code, user_code, verify_url, interval = 5, expires_in = 600 } = session;
43
+ if (!device_code || !user_code || !verify_url) {
44
+ console.error("\n 登录服务返回不完整,请稍后重试。\n");
45
+ process.exit(1);
46
+ }
47
+ let verifyUrl;
48
+ try {
49
+ verifyUrl = new URL(verify_url);
50
+ if (verifyUrl.origin !== new URL(base).origin) throw new Error("origin mismatch");
51
+ } catch {
52
+ console.error("\n 登录服务返回了不安全的授权地址,已停止。\n");
53
+ process.exit(1);
54
+ }
40
55
 
41
56
  console.log("\n 在浏览器里打开以下链接,登录后创建并授权 Key(已尝试自动打开):\n");
42
57
  console.log(" \x1b[36m" + verify_url + "\x1b[0m\n");
43
58
  console.log(
44
59
  " 核对码(请确认浏览器页面显示的码与此一致再创建): \x1b[1m" + user_code + "\x1b[0m\n"
45
60
  );
46
- openBrowser(verify_url);
61
+ openBrowser(verifyUrl.href);
47
62
  console.log(" 等待授权中…(在网页里给这次登录起个 Key 名称并点「创建」)");
48
63
 
49
64
  // 2. 轮询领 key
50
65
  const deadline = Date.now() + expires_in * 1000;
66
+ let waitSeconds = Math.max(1, Number(interval) || 5);
51
67
  while (Date.now() < deadline) {
52
- await sleep(interval * 1000);
68
+ await sleep(waitSeconds * 1000);
53
69
  let poll;
54
70
  try {
55
71
  const r = await fetch(
56
- base + "/api/v1/cli/poll?device_code=" + encodeURIComponent(device_code)
72
+ base + "/api/v1/cli/poll?device_code=" + encodeURIComponent(device_code),
73
+ { signal: AbortSignal.timeout(15000) }
57
74
  );
58
75
  poll = await r.json();
59
76
  if (r.status === 410 || poll.status === "expired") {
@@ -61,7 +78,17 @@ export async function loginViaBrowser() {
61
78
  process.exit(1);
62
79
  }
63
80
  } catch {
64
- continue; // 网络抖动,继续轮询
81
+ waitSeconds = Math.min(waitSeconds * 2, 30);
82
+ continue;
83
+ }
84
+ const status = poll.status || poll.error;
85
+ if (status === "slow_down") {
86
+ waitSeconds += 5;
87
+ continue;
88
+ }
89
+ if (status === "access_denied" || status === "denied") {
90
+ console.error("\n 已在浏览器拒绝本次授权。\n");
91
+ process.exit(1);
65
92
  }
66
93
  if (poll.status === "ready" && poll.key) {
67
94
  const cfg = loadConfig();
@@ -69,10 +96,12 @@ export async function loginViaBrowser() {
69
96
  saveConfig(cfg);
70
97
  const me = await apiFetch("/api/v1/me", { key: poll.key });
71
98
  if (me.ok) {
99
+ const balance = Number(me.json.balance);
100
+ const usd = Number.isFinite(balance) ? `$${(balance * 0.01).toFixed(2)} USD` : "-";
72
101
  console.log(
73
102
  `\n ✓ 授权成功,Key 已导入 ~/.manturhub/config.json。账号: ${
74
103
  me.json.email || "-"
75
- } 余额: ${me.json.balance ?? "-"} 馒头\n`
104
+ } 余额: ${usd}(${me.json.balance ?? "-"} 馒头)\n`
76
105
  );
77
106
  } else {
78
107
  console.log(
package/lib/params.js ADDED
@@ -0,0 +1,102 @@
1
+ function typeName(value) {
2
+ if (Array.isArray(value)) return "array";
3
+ if (value === null) return "null";
4
+ return typeof value;
5
+ }
6
+
7
+ function normalizeType(type) {
8
+ const t = String(type || "").toLowerCase();
9
+ if (t.endsWith("[]") || t.startsWith("array")) return "array";
10
+ if (t.includes("integer") || t === "int") return "integer";
11
+ if (t.includes("number") || t === "float" || t === "double") return "number";
12
+ if (t.includes("boolean") || t === "bool") return "boolean";
13
+ if (t.includes("object") || t === "json") return "object";
14
+ if (t.includes("string") || t === "url") return "string";
15
+ return null;
16
+ }
17
+
18
+ function coerceValue(value, expected, name) {
19
+ if (typeof value !== "string" || !expected || expected === "string") return value;
20
+ if (expected === "boolean") {
21
+ if (value === "true") return true;
22
+ if (value === "false") return false;
23
+ throw new Error(`参数 ${name} 必须是 true 或 false`);
24
+ }
25
+ if (expected === "integer" || expected === "number") {
26
+ const number = Number(value);
27
+ if (!Number.isFinite(number) || (expected === "integer" && !Number.isInteger(number))) {
28
+ throw new Error(`参数 ${name} 必须是${expected === "integer" ? "整数" : "数字"}`);
29
+ }
30
+ return number;
31
+ }
32
+ if (expected === "array" || expected === "object") {
33
+ try {
34
+ return JSON.parse(value);
35
+ } catch {
36
+ throw new Error(`参数 ${name} 必须是合法 JSON ${expected === "array" ? "数组" : "对象"}`);
37
+ }
38
+ }
39
+ return value;
40
+ }
41
+
42
+ function matchesType(value, expected) {
43
+ if (!expected) return true;
44
+ if (expected === "array") return Array.isArray(value);
45
+ if (expected === "integer") return Number.isInteger(value);
46
+ if (expected === "object") return value !== null && typeof value === "object" && !Array.isArray(value);
47
+ return typeof value === expected;
48
+ }
49
+
50
+ export function validateParams(body, schema, { coerceStrings = false } = {}) {
51
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
52
+ throw new Error("算子参数必须是 JSON 对象");
53
+ }
54
+ const fields = Array.isArray(schema?.fields) ? schema.fields : [];
55
+ if (!fields.length) return body;
56
+
57
+ const byName = new Map(fields.map((field) => [String(field.name), field]));
58
+ const unknown = Object.keys(body).filter((name) => !byName.has(name));
59
+ if (unknown.length) {
60
+ throw new Error(`未知参数: ${unknown.join(", ")}。请先运行 manturhub describe 查看精确字段`);
61
+ }
62
+ const missing = fields
63
+ .filter((field) => field.required && !Object.hasOwn(body, field.name))
64
+ .map((field) => field.name);
65
+ if (missing.length) throw new Error(`缺少必填参数: ${missing.join(", ")}`);
66
+
67
+ const result = { ...body };
68
+ for (const [name, value] of Object.entries(result)) {
69
+ const field = byName.get(name);
70
+ const expected = normalizeType(field.type);
71
+ const checked = coerceStrings ? coerceValue(value, expected, name) : value;
72
+ if (!matchesType(checked, expected)) {
73
+ throw new Error(`参数 ${name} 类型错误:需要 ${expected},收到 ${typeName(checked)}`);
74
+ }
75
+ if (Array.isArray(field.enum) && !field.enum.includes(checked)) {
76
+ throw new Error(`参数 ${name} 只能是: ${field.enum.join(" | ")}`);
77
+ }
78
+ result[name] = checked;
79
+ }
80
+ return result;
81
+ }
82
+
83
+ export function parseDynamicParams(tokens) {
84
+ const body = {};
85
+ for (let i = 0; i < tokens.length; i++) {
86
+ const token = tokens[i];
87
+ if (token === "--no-wait") continue;
88
+ if (!token?.startsWith("--")) throw new Error(`无法识别的参数: ${token}`);
89
+ const equals = token.indexOf("=");
90
+ if (equals > 2) {
91
+ body[token.slice(2, equals)] = token.slice(equals + 1);
92
+ continue;
93
+ }
94
+ const value = tokens[i + 1];
95
+ if (value === undefined || value.startsWith("--")) {
96
+ throw new Error(`参数 ${token} 缺少值`);
97
+ }
98
+ body[token.slice(2)] = value;
99
+ i++;
100
+ }
101
+ return body;
102
+ }
package/lib/setup.js CHANGED
@@ -1,11 +1,8 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join, dirname } from "node:path";
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
4
3
  import { getKey } from "./config.js";
5
4
  import { installSkill } from "./skill.js";
6
5
 
7
- const HUB = "https://hub.mantur.cn";
8
-
9
6
  // ───────────────────────── manturhub init ─────────────────────────
10
7
  // 往项目写一段 agent 引导,让 Claude Code / Codex / Cursor 知道用 manturhub CLI。
11
8
  const MARK_START = "<!-- manturhub:start -->";
@@ -18,10 +15,11 @@ function guideBlock() {
18
15
  本机已安装 \`manturhub\` CLI,可调用 ManturHub 算子广场的 AI 能力(文生图 / 视频生成 / Seedance提示词润色 / 语音合成 / 视频理解 / 短剧 / 文案等)与行业数据(漫剧·短剧热榜 / 爆量趋势 / 出海投放数据等市场洞察)。**当任务需要这类 AI 能力或行业数据时,优先用它——查热榜 / 行业数据别先上网页搜索,也不要自己造轮子。**
19
16
 
20
17
  - 列出可用算子:\`manturhub ls\`(按类:\`manturhub ls --cat image|video|audio|text|data\`)
21
- - 调用算子:\`manturhub run <算子ID> --json '{...入参...}'\`(异步算子会自动轮询到出结果,**别重复调用以免重复扣费**)
18
+ - 查实时价格:\`manturhub quote <算子ID>\`(不要在 Skill / 文档里写死价格)
19
+ - 调用算子:优先把参数写入 JSON 文件,再运行 \`manturhub run <算子ID> --json-file params.json\`;简短可信参数也可用 \`--json '{...}'\`(异步算子会轮询到结果,**别重复调用以免重复扣费**)
22
20
  - 上传本地文件:\`manturhub upload <文件>\` → 公网 URL(算子只接受公网 URL,本地路径非法)
23
21
  - 查余额:\`manturhub balance\`
24
- - 查某算子的精确入参字段:\`manturhub describe <算子ID>\`(字段名必须精确,拼错会被静默忽略;也可看 ${HUB}/marketplace/<算子ID>)
22
+ - 查某算子的精确入参字段:\`manturhub describe <算子ID>\`
25
23
  - Agent 套件(多角色团队工作区,如小说→短剧制作团队):\`manturhub suite ls\` 看清单,\`manturhub suite install <slug>\` 装为工作目录——用户想「组个团队干一类完整业务」时先看有没有现成套件
26
24
 
27
25
  若运行 \`manturhub\` 时 stderr 出现「有新版」提示,请主动告知用户可运行 \`npm i -g @manturhub/cli@latest\` 更新。
@@ -46,9 +44,9 @@ function upsertGuide(file) {
46
44
  }
47
45
 
48
46
  export function runInit() {
49
- // 1) 装全局「ManturHub 使用 skill」(Claude Code 用,让 agent 会用算子、少踩坑)
50
- const skillFile = installSkill();
51
- console.log(`✓ 已装 ManturHub 使用 skill:${skillFile}`);
47
+ // 1) 同时给 Claude Code Codex 装全局「ManturHub 使用 skill」。
48
+ const skillFiles = installSkill("all");
49
+ console.log(`✓ 已装 ManturHub 使用 skill:\n ${skillFiles.join("\n ")}`);
52
50
  console.log(` (教 agent:先 manturhub ls 找算子、describe 查字段、本地文件先 upload、异步 run 自动等结果、别重复调用)\n`);
53
51
  // 2) 写项目级 agent 引导
54
52
  const targets = ["AGENTS.md", "CLAUDE.md", ".cursorrules"];
@@ -59,86 +57,7 @@ export function runInit() {
59
57
  }
60
58
  console.log(
61
59
  `\nClaude Code 读 skill + CLAUDE.md,Codex 读 AGENTS.md,Cursor 读 .cursorrules。` +
62
- `\n重启客户端后,agent 即可在 shell 里用 manturhub 直接调算子(无需 MCP)。`
60
+ `\n重启客户端后,Agent 即可在 shell 里用 manturhub 直接调算子。`
63
61
  );
64
- if (!getKey()) console.log(`\n⚠ 还没配 Key,先跑:manturhub login --key sk-xxx`);
65
- }
66
-
67
- // ──────────────────────── manturhub mcp-install ────────────────────────
68
- // 把 manturhub MCP server 写进各 AI 客户端配置(自动合并,不覆盖已有其他 server)。
69
- // Key 由 manturhub login 存的 ~/.manturhub/config.json 自动读取,无需写进各配置。
70
- const SERVER_DEF = { command: "manturhub", args: ["mcp"] };
71
-
72
- function writeJsonMcp(file, label) {
73
- mkdirSync(dirname(file), { recursive: true });
74
- let cfg = {};
75
- if (existsSync(file)) {
76
- try {
77
- cfg = JSON.parse(readFileSync(file, "utf8"));
78
- } catch {
79
- console.log(` ⚠ ${label}: 现有文件不是合法 JSON,跳过 → ${file}`);
80
- return;
81
- }
82
- }
83
- cfg.mcpServers = cfg.mcpServers || {};
84
- cfg.mcpServers.manturhub = { ...SERVER_DEF };
85
- writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
86
- console.log(` ✓ ${label}: ${file}`);
87
- }
88
-
89
- function writeTomlCodex(file, label) {
90
- mkdirSync(dirname(file), { recursive: true });
91
- let content = existsSync(file) ? readFileSync(file, "utf8") : "";
92
- if (/\[mcp_servers\.manturhub\]/.test(content)) {
93
- console.log(` ✓ ${label}: 已存在 manturhub,未改动 → ${file}`);
94
- return;
95
- }
96
- const section = `\n[mcp_servers.manturhub]\ncommand = "manturhub"\nargs = ["mcp"]\n`;
97
- content = (content.trimEnd() + "\n" + section).replace(/^\n+/, "");
98
- writeFileSync(file, content);
99
- console.log(` ✓ ${label}: ${file}`);
100
- }
101
-
102
- const CLIENTS = {
103
- "claude-code": () =>
104
- writeJsonMcp(join(process.cwd(), ".mcp.json"), "Claude Code (项目 .mcp.json)"),
105
- "claude-desktop": () =>
106
- writeJsonMcp(
107
- join(homedir(), "Library/Application Support/Claude/claude_desktop_config.json"),
108
- "Claude Desktop"
109
- ),
110
- cursor: () => writeJsonMcp(join(homedir(), ".cursor/mcp.json"), "Cursor"),
111
- codex: () => writeTomlCodex(join(homedir(), ".codex/config.toml"), "Codex"),
112
- };
113
-
114
- export function runMcpInstall(client) {
115
- if (!client) {
116
- console.log(`把 ManturHub MCP 接进 AI 客户端(agent 自动发现算子工具,无需学命令)。\n`);
117
- console.log(`一键写入(自动合并,不动你已有的其他 MCP server):`);
118
- console.log(` manturhub mcp-install --client claude-code # 项目 .mcp.json`);
119
- console.log(` manturhub mcp-install --client codex # ~/.codex/config.toml`);
120
- console.log(` manturhub mcp-install --client cursor # ~/.cursor/mcp.json`);
121
- console.log(` manturhub mcp-install --client claude-desktop # Claude Desktop`);
122
- console.log(` manturhub mcp-install --client all # 全部`);
123
- console.log(`\nKey 由 \`manturhub login\` 的配置自动读取,无需写进各客户端配置。`);
124
- return;
125
- }
126
- const names = client === "all" ? Object.keys(CLIENTS) : [client];
127
- console.log("写入 MCP 配置:");
128
- for (const n of names) {
129
- const fn = CLIENTS[n];
130
- if (!fn) {
131
- console.log(` 未知客户端: ${n}(可选: ${Object.keys(CLIENTS).join(", ")}, all)`);
132
- continue;
133
- }
134
- fn();
135
- }
136
- // 随 MCP 一起装「使用 skill」——让 agent 不只拿到生工具,还有怎么用/避坑的 playbook。
137
- if (names.includes("claude-code")) {
138
- const skillFile = installSkill();
139
- console.log(` ✓ Claude Code 使用 skill: ${skillFile}`);
140
- console.log(` (教 agent 正确用算子:先 list_operators、本地文件先 upload_file、异步用 get_job_status 轮询别重扣费)`);
141
- }
142
- console.log(`\n完成。重启对应客户端即可看到 manturhub 的算子工具。`);
143
- if (!getKey()) console.log(`⚠ 还没配 Key,MCP 启动会报错。先跑:manturhub login --key sk-xxx`);
62
+ if (!getKey()) console.log(`\n⚠ 还没配 Key,先跑:manturhub login`);
144
63
  }