@manturhub/cli 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 +68 -0
- package/bin/cli.js +152 -0
- package/lib/api.js +27 -0
- package/lib/config.js +40 -0
- package/lib/mcp.js +108 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @manturhub/cli
|
|
2
|
+
|
|
3
|
+
ManturHub 算子广场命令行工具。两件事:
|
|
4
|
+
|
|
5
|
+
1. **命令行直调算子** —— 不写代码、不碰 MCP,终端一行调用 AI 算子。
|
|
6
|
+
2. **stdio MCP server** —— 给 Claude Code / Codex / Cursor / Claude Desktop 等当本地 MCP server。stdio 是所有 AI 客户端都稳定支持的传输,装上这个 CLI 就一行接入,绕开各家对远程传输支持参差的坑。
|
|
7
|
+
|
|
8
|
+
## 安装
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @manturhub/cli
|
|
12
|
+
# 或免安装直接用: npx -y @manturhub/cli <命令>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
需要 Node.js ≥ 18。
|
|
16
|
+
|
|
17
|
+
## 上手三步
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. 配 Key(去 https://manturhub.leisurecat.cloud/dashboard 拿)
|
|
21
|
+
manturhub login --key sk-你的key
|
|
22
|
+
|
|
23
|
+
# 2. 验证(列出全部上线算子)
|
|
24
|
+
manturhub ls
|
|
25
|
+
|
|
26
|
+
# 3. 调一个算子试试
|
|
27
|
+
manturhub run op.text.commerce-copy --json '{"product_info":"便携榨汁杯,USB充电,300ml","scene":"product_title","tone":"lively"}'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 把算子接进你的 AI(stdio,一行搞定)
|
|
31
|
+
|
|
32
|
+
**Codex** — `~/.codex/config.toml`:
|
|
33
|
+
```toml
|
|
34
|
+
[mcp_servers.manturhub]
|
|
35
|
+
command = "manturhub"
|
|
36
|
+
args = ["mcp"]
|
|
37
|
+
env = { MANTURHUB_KEY = "sk-你的key" }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Claude Desktop / Cursor / Cline**:
|
|
41
|
+
```json
|
|
42
|
+
{ "mcpServers": { "manturhub": { "command": "manturhub", "args": ["mcp"] } } }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
装好后,AI 就能直接 `list_operators` 看平台能力、`run_operator` 调用算子。只想要某个业务域?加 `--scope`:
|
|
46
|
+
```toml
|
|
47
|
+
args = ["mcp", "--scope", "drama"] # 只暴露剧本域算子
|
|
48
|
+
```
|
|
49
|
+
域可选: `manturhub`(全部) · `drama`(剧本) · `production`(制片) · `ecom-video`(电商视频)。
|
|
50
|
+
|
|
51
|
+
## 命令
|
|
52
|
+
|
|
53
|
+
| 命令 | 说明 |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `manturhub login --key sk-xxx` | 配置并验证 API Key(存 `~/.manturhub/config.json`,600 权限) |
|
|
56
|
+
| `manturhub mcp [--scope <域>]` | 启动 stdio MCP server(被 AI 客户端作为子进程拉起) |
|
|
57
|
+
| `manturhub ls [--cat <分类>]` | 列出上线算子 |
|
|
58
|
+
| `manturhub run <算子ID> --json '{}'` | 调用算子(也支持 `--字段 值` 形式) |
|
|
59
|
+
| `manturhub balance` | 查询馒头余额 |
|
|
60
|
+
|
|
61
|
+
环境变量: `MANTURHUB_KEY`(API Key,优先于配置文件) · `MANTURHUB_BASE`(网关地址)。
|
|
62
|
+
|
|
63
|
+
## 它是怎么工作的
|
|
64
|
+
|
|
65
|
+
`manturhub mcp` 不重新实现 MCP server,而是把客户端的 stdio JSON-RPC 消息转发到 ManturHub 已标准化的 Streamable HTTP MCP 端点(`/api/v1/mcp/<域>/mcp`),再把响应写回 stdout。工具发现、调用、计费全部在服务端完成,CLI 只做可靠的 stdio↔HTTP 桥接 —— 等于一个官方、可控的 mcp-remote。
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
ManturHub 算子广场 · https://manturhub.leisurecat.cloud
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { saveConfig, loadConfig } from "../lib/config.js";
|
|
3
|
+
import { apiFetch } from "../lib/api.js";
|
|
4
|
+
import { runMcpBridge } from "../lib/mcp.js";
|
|
5
|
+
|
|
6
|
+
const VERSION = "0.1.0";
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const cmd = args[0];
|
|
9
|
+
|
|
10
|
+
function getFlag(name, def) {
|
|
11
|
+
const i = args.indexOf(`--${name}`);
|
|
12
|
+
return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : def;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const HELP = `manturhub — ManturHub 算子广场 CLI v${VERSION}
|
|
16
|
+
|
|
17
|
+
用法:
|
|
18
|
+
manturhub login --key sk-xxx 配置 API Key(存 ~/.manturhub/config.json)
|
|
19
|
+
manturhub mcp [--scope <域>] 启动 stdio MCP server(给 Claude Code / Codex / Cursor 等)
|
|
20
|
+
域: manturhub(全部) | drama | production | ecom-video
|
|
21
|
+
manturhub ls [--cat <分类>] 列出上线算子(分类: text/image/video/audio/data)
|
|
22
|
+
manturhub run <算子ID> --json '{}' 调用算子(也可用 --字段 值 形式)
|
|
23
|
+
manturhub balance 查询馒头余额
|
|
24
|
+
manturhub help | --version
|
|
25
|
+
|
|
26
|
+
环境变量:
|
|
27
|
+
MANTURHUB_KEY API Key(优先于配置文件)
|
|
28
|
+
MANTURHUB_BASE 网关地址(默认 https://manturhub.leisurecat.cloud)
|
|
29
|
+
|
|
30
|
+
把算子接进你的 AI(stdio,所有客户端都稳):
|
|
31
|
+
Codex ~/.codex/config.toml:
|
|
32
|
+
[mcp_servers.manturhub]
|
|
33
|
+
command = "manturhub"
|
|
34
|
+
args = ["mcp"]
|
|
35
|
+
env = { MANTURHUB_KEY = "sk-xxx" }
|
|
36
|
+
|
|
37
|
+
Claude Desktop / Cursor:
|
|
38
|
+
{ "mcpServers": { "manturhub": { "command": "manturhub", "args": ["mcp"] } } }
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
switch (cmd) {
|
|
43
|
+
case "login": {
|
|
44
|
+
const key = getFlag("key");
|
|
45
|
+
if (!key) {
|
|
46
|
+
console.error("用法: manturhub login --key sk-xxx");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const cfg = loadConfig();
|
|
50
|
+
cfg.key = key;
|
|
51
|
+
saveConfig(cfg);
|
|
52
|
+
const r = await apiFetch("/api/v1/me", { key });
|
|
53
|
+
if (r.ok) {
|
|
54
|
+
console.log(
|
|
55
|
+
`✓ Key 已保存。账号: ${r.json.email || "-"} 余额: ${r.json.balance ?? "-"} 馒头`
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
console.log(
|
|
59
|
+
`✓ Key 已保存,但验证失败(HTTP ${r.status})。请确认 key 是否正确、是否已激活。`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case "mcp": {
|
|
66
|
+
await runMcpBridge({ scope: getFlag("scope", "manturhub") });
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case "ls": {
|
|
71
|
+
const cat = getFlag("cat");
|
|
72
|
+
const r = await apiFetch("/api/v1/operators?status=online");
|
|
73
|
+
if (!r.ok) {
|
|
74
|
+
console.error(`列表获取失败(HTTP ${r.status})`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
let ops = r.json.operators || r.json || [];
|
|
78
|
+
if (cat) ops = ops.filter((o) => o.cat === cat);
|
|
79
|
+
console.log(`ManturHub 上线算子(${ops.length} 个):\n`);
|
|
80
|
+
for (const o of ops) {
|
|
81
|
+
console.log(` ${o.id.padEnd(26)} ${o.name} [${o.cat}]`);
|
|
82
|
+
}
|
|
83
|
+
console.log(`\n用 \`manturhub run <算子ID> --json '{...}'\` 调用,参数见 ${"https://manturhub.leisurecat.cloud"}/marketplace/<算子ID>`);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case "run": {
|
|
88
|
+
const op = args[1];
|
|
89
|
+
if (!op || op.startsWith("--")) {
|
|
90
|
+
console.error("用法: manturhub run <算子ID> --json '{...}' 或 --字段 值");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
let body = {};
|
|
94
|
+
const jsonArg = getFlag("json");
|
|
95
|
+
if (jsonArg) {
|
|
96
|
+
try {
|
|
97
|
+
body = JSON.parse(jsonArg);
|
|
98
|
+
} catch {
|
|
99
|
+
console.error("--json 参数不是合法 JSON");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
for (let i = 2; i < args.length; i += 2) {
|
|
104
|
+
if (args[i] && args[i].startsWith("--") && args[i + 1] !== undefined) {
|
|
105
|
+
body[args[i].slice(2)] = args[i + 1];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const r = await apiFetch(
|
|
110
|
+
`/api/v1/operators/${encodeURIComponent(op)}/invoke`,
|
|
111
|
+
{ method: "POST", body }
|
|
112
|
+
);
|
|
113
|
+
console.log(JSON.stringify(r.json, null, 2));
|
|
114
|
+
if (!r.ok) process.exit(1);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case "balance": {
|
|
119
|
+
const r = await apiFetch("/api/v1/me");
|
|
120
|
+
if (!r.ok) {
|
|
121
|
+
console.error(`查询失败(HTTP ${r.status})`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
console.log(
|
|
125
|
+
`馒头余额: ${r.json.balance ?? "-"} 账号: ${r.json.email || "-"}`
|
|
126
|
+
);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case "--version":
|
|
131
|
+
case "-v":
|
|
132
|
+
console.log(VERSION);
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
case "help":
|
|
136
|
+
case "--help":
|
|
137
|
+
case "-h":
|
|
138
|
+
case undefined:
|
|
139
|
+
console.log(HELP);
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
default:
|
|
143
|
+
console.error(`未知命令: ${cmd}\n`);
|
|
144
|
+
console.log(HELP);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
main().catch((e) => {
|
|
150
|
+
console.error("错误:", e.message);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getKey, getBaseUrl } from "./config.js";
|
|
2
|
+
|
|
3
|
+
// Thin REST client against the ManturHub gateway. Every call carries x-api-key.
|
|
4
|
+
export async function apiFetch(path, { method = "GET", body, key } = {}) {
|
|
5
|
+
const apiKey = key || getKey();
|
|
6
|
+
if (!apiKey) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
"未配置 API Key。运行 `manturhub login --key sk-xxx`,或设置环境变量 MANTURHUB_KEY。"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
const res = await fetch(getBaseUrl() + path, {
|
|
12
|
+
method,
|
|
13
|
+
headers: {
|
|
14
|
+
"x-api-key": apiKey,
|
|
15
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
16
|
+
},
|
|
17
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
18
|
+
});
|
|
19
|
+
const text = await res.text();
|
|
20
|
+
let json;
|
|
21
|
+
try {
|
|
22
|
+
json = JSON.parse(text);
|
|
23
|
+
} catch {
|
|
24
|
+
json = text;
|
|
25
|
+
}
|
|
26
|
+
return { ok: res.ok, status: res.status, json };
|
|
27
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
chmodSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
|
|
11
|
+
// Local config lives at ~/.manturhub/config.json (chmod 600). Env vars win over it.
|
|
12
|
+
const DIR = join(homedir(), ".manturhub");
|
|
13
|
+
const FILE = join(DIR, "config.json");
|
|
14
|
+
const DEFAULT_BASE = "https://manturhub.leisurecat.cloud";
|
|
15
|
+
|
|
16
|
+
export function loadConfig() {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(FILE, "utf8"));
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function saveConfig(cfg) {
|
|
25
|
+
if (!existsSync(DIR)) mkdirSync(DIR, { recursive: true });
|
|
26
|
+
writeFileSync(FILE, JSON.stringify(cfg, null, 2));
|
|
27
|
+
try {
|
|
28
|
+
chmodSync(FILE, 0o600);
|
|
29
|
+
} catch {
|
|
30
|
+
/* best-effort on platforms without chmod */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getKey() {
|
|
35
|
+
return process.env.MANTURHUB_KEY || loadConfig().key || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getBaseUrl() {
|
|
39
|
+
return process.env.MANTURHUB_BASE || loadConfig().baseUrl || DEFAULT_BASE;
|
|
40
|
+
}
|
package/lib/mcp.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { getKey, getBaseUrl } from "./config.js";
|
|
3
|
+
|
|
4
|
+
// `manturhub mcp` — a stdio MCP server for clients (Claude Desktop / Codex /
|
|
5
|
+
// Cursor …) that only speak stdio or whose remote-transport support is immature.
|
|
6
|
+
//
|
|
7
|
+
// It does NOT reimplement the MCP server: it relays each newline-delimited
|
|
8
|
+
// JSON-RPC message from stdin to the remote ManturHub Streamable HTTP endpoint
|
|
9
|
+
// (already standard-compliant) and writes the HTTP response back to stdout.
|
|
10
|
+
// The remote handles initialize / tools/list / tools/call / scope. This is an
|
|
11
|
+
// official, controlled equivalent of mcp-remote.
|
|
12
|
+
export async function runMcpBridge({ scope = "manturhub" } = {}) {
|
|
13
|
+
const key = getKey();
|
|
14
|
+
if (!key) {
|
|
15
|
+
process.stderr.write(
|
|
16
|
+
"[manturhub mcp] 未配置 API Key。运行 `manturhub login --key sk-xxx`,或设 MANTURHUB_KEY。\n"
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const url = `${getBaseUrl()}/api/v1/mcp/${scope}/mcp`;
|
|
21
|
+
const rl = createInterface({ input: process.stdin });
|
|
22
|
+
|
|
23
|
+
// Track in-flight relays so we don't exit on stdin close while a fetch is
|
|
24
|
+
// still pending (real clients keep stdin open; piped input closes early).
|
|
25
|
+
let pending = 0;
|
|
26
|
+
let closed = false;
|
|
27
|
+
const maybeExit = () => {
|
|
28
|
+
if (closed && pending === 0) process.exit(0);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
rl.on("line", (line) => {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed) return;
|
|
34
|
+
let msg;
|
|
35
|
+
try {
|
|
36
|
+
msg = JSON.parse(trimmed);
|
|
37
|
+
} catch {
|
|
38
|
+
return; // exactly one JSON value per line; ignore noise
|
|
39
|
+
}
|
|
40
|
+
pending++;
|
|
41
|
+
relay(url, key, msg).finally(() => {
|
|
42
|
+
pending--;
|
|
43
|
+
maybeExit();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
rl.on("close", () => {
|
|
48
|
+
closed = true;
|
|
49
|
+
maybeExit();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function relay(url, key, msg) {
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
Accept: "application/json, text/event-stream",
|
|
60
|
+
"x-api-key": key,
|
|
61
|
+
"MCP-Protocol-Version": "2025-06-18",
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify(msg),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 202 Accepted = notification/response acknowledged, no body to relay.
|
|
67
|
+
if (res.status === 202) return;
|
|
68
|
+
|
|
69
|
+
const ctype = res.headers.get("content-type") || "";
|
|
70
|
+
const text = await res.text();
|
|
71
|
+
if (!text) return;
|
|
72
|
+
|
|
73
|
+
if (ctype.includes("text/event-stream")) {
|
|
74
|
+
// Defensive: if the remote ever streams SSE, pull JSON-RPC from data: lines.
|
|
75
|
+
for (const evt of text.split(/\n\n/)) {
|
|
76
|
+
const data = evt
|
|
77
|
+
.split("\n")
|
|
78
|
+
.filter((l) => l.startsWith("data:"))
|
|
79
|
+
.map((l) => l.slice(5).trim())
|
|
80
|
+
.join("");
|
|
81
|
+
if (data) writeFrame(data);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Normal case: remote returns a single application/json JSON-RPC response.
|
|
87
|
+
writeFrame(text);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// Transport failure → return a JSON-RPC error for this id (requests only).
|
|
90
|
+
if (msg.id !== undefined && msg.id !== null) {
|
|
91
|
+
writeFrame(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
jsonrpc: "2.0",
|
|
94
|
+
id: msg.id,
|
|
95
|
+
error: {
|
|
96
|
+
code: -32603,
|
|
97
|
+
message: "manturhub bridge: " + (err?.message || "upstream error"),
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// One JSON value per line (stdio framing). Collapse raw newlines so framing holds.
|
|
106
|
+
function writeFrame(jsonText) {
|
|
107
|
+
process.stdout.write(jsonText.replace(/\r?\n/g, " ") + "\n");
|
|
108
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@manturhub/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ManturHub 算子广场 CLI:命令行直调 AI 算子 + 给 Claude Code / Codex / Cursor 等当 stdio MCP server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"manturhub": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"lib",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"manturhub",
|
|
19
|
+
"mcp",
|
|
20
|
+
"ai",
|
|
21
|
+
"cli",
|
|
22
|
+
"operator",
|
|
23
|
+
"算子",
|
|
24
|
+
"stdio"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"homepage": "https://manturhub.leisurecat.cloud"
|
|
28
|
+
}
|