@manturhub/cli 0.8.1 → 0.9.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 CHANGED
@@ -15,6 +15,8 @@ manturhub login
15
15
  printf '%s' "$YOUR_MANTURHUB_KEY" | manturhub login --key-stdin
16
16
  ```
17
17
 
18
+ CLI 默认连接生产站 `https://hub.mantur.ai`,Key 也必须在该生产站创建。`hub.mantur.cn` 是独立测试环境,其 Key 不能用于生产站。
19
+
18
20
  需要 Node.js ≥ 18。也可免安装运行:`npx -y @manturhub/cli <命令>`。
19
21
 
20
22
  ## 快速开始
@@ -29,7 +31,9 @@ manturhub quote op.text.commerce-copy
29
31
  manturhub run op.text.commerce-copy --json '{"product_info":"便携榨汁杯,USB 充电,300ml","scene":"product_title","tone":"lively"}'
30
32
  ```
31
33
 
32
- CLI 会在付费调用前按算子实时 schema 校验未知字段、必填项、类型和枚举,校验失败不会发起 invoke
34
+ CLI 会在付费调用前按算子实时 schema 校验未知字段、必填项、类型和枚举,校验失败不会发起 invoke。校验通过后会显示本次预计消耗和计费依据,获得确认才调用;完成后显示实际消耗和退款。批量参数也按整批请求试算。
35
+
36
+ 在 Agent 或脚本等非交互环境中,第一次运行只返回报价和 `quote_id`,不会调用或扣费;Agent 向用户确认后,使用提示中的 `--confirm <quote_id>` 执行。报价 5 分钟内有效且只能使用一次。
33
37
 
34
38
  ## 主要命令
35
39
 
@@ -43,7 +47,7 @@ CLI 会在付费调用前按算子实时 schema 校验未知字段、必填项
43
47
  | `manturhub upload <本地文件>` | 流式上传图片、音频或视频并输出公网 URL |
44
48
  | `manturhub status <poll_url>` | 查询 `run --no-wait` 返回的异步任务 |
45
49
  | `manturhub balance [--json]` | 查询余额及美元等值(1 馒头 = $0.01 USD) |
46
- | `manturhub skill ls [--json]` / `skill add <slug> --client codex` | 浏览和安装业务 Skill |
50
+ | `manturhub skill ls [--json]` / `skill add <slug> [--client codex]` | 浏览和安装业务 Skill;默认自动识别 Agent |
47
51
  | `manturhub recipe [关键词]` / `recipe get <ID>` | 搜索并查看已验证配方 |
48
52
  | `manturhub suite ls [--json]` / `suite install <slug>` | 安装多角色 Agent 套件,不复制 API Key |
49
53
  | `manturhub init` | 给当前项目写入 Agent 使用引导 |
package/bin/cli.js CHANGED
@@ -10,6 +10,7 @@ import { createReadStream, readFileSync, statSync } from "node:fs";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { dirname, join, basename, extname } from "node:path";
12
12
  import { parseDynamicParams, validateParams } from "../lib/params.js";
13
+ import { confirmCharge, formatMantou, printBillingResult } from "../lib/billing-confirm.js";
13
14
 
14
15
  // 本地文件 → MIME(presign 只接受 image/audio/video)
15
16
  const MIME_BY_EXT = {
@@ -67,8 +68,8 @@ function assertFlags(tokens, { value = [], boolean = [] } = {}) {
67
68
  function assertRunControlFlags(tokens) {
68
69
  for (let i = 0; i < tokens.length; i++) {
69
70
  const token = tokens[i];
70
- if (token === "--no-wait" || token.startsWith("--json=") || token.startsWith("--json-file=")) continue;
71
- if (token === "--json" || token === "--json-file") {
71
+ if (token === "--no-wait" || token.startsWith("--json=") || token.startsWith("--json-file=") || token.startsWith("--confirm=")) continue;
72
+ if (token === "--json" || token === "--json-file" || token === "--confirm") {
72
73
  const value = tokens[i + 1];
73
74
  if (value === undefined || value.startsWith("--")) throw new Error(`参数 ${token} 缺少值`);
74
75
  i++;
@@ -92,7 +93,7 @@ const HELP = `manturhub — ManturHub 算子广场 CLI v${VERSION}
92
93
  manturhub ls [--cat <分类>] [--json] 列出上线算子(无需登录)
93
94
  manturhub describe <算子ID> [--json] 查看算子入参字段(无需登录)
94
95
  manturhub quote <算子ID> 查询实时计费公式(不要使用 Skill 内的历史价格)
95
- manturhub run <算子ID> --json '{}' 调用算子(异步算子自动轮询到出结果;--no-wait 只拿 job_id)
96
+ manturhub run <算子ID> --json '{}' 试算并确认费用后调用(异步算子自动轮询到出结果)
96
97
  manturhub run <算子ID> --json-file x.json 从文件读参数(prompt 来自配方/用户时更安全)
97
98
  manturhub upload <本地文件> 上传图片/音频/视频 → 公网 URL(喂算子前先转换本地文件)
98
99
  manturhub status <poll_url> 查异步任务状态(配合 run --no-wait)
@@ -100,7 +101,7 @@ const HELP = `manturhub — ManturHub 算子广场 CLI v${VERSION}
100
101
  manturhub init 装「ManturHub 使用 skill」+ 写 agent 引导(推荐:让 agent 会用算子)
101
102
  manturhub skill ls 列出平台 Skill(业务成品流程,如爆款复刻)
102
103
  manturhub skill add <slug> [--client claude-code|codex|all]
103
- 安装 Skill 到指定 Agent
104
+ 自动识别 Agent 并安装 Skill(可显式指定)
104
105
  manturhub suite ls 列出 Agent 套件(多角色团队工作区,给 Agent 配一整个团队)
105
106
  manturhub suite install <slug> 安装套件为工作目录 ./<slug>/(不复制 API Key)
106
107
  manturhub recipe [关键词] [--cat x] 搜配方(已验证创作的效果+可复现参数;分类 video/image/script)
@@ -161,7 +162,14 @@ async function main() {
161
162
  `✓ Key 已验证并保存。账号: ${r.json.email || "-"} 余额: ${usdFor(r.json.balance)}(${r.json.balance ?? "-"} 馒头)`
162
163
  );
163
164
  } else {
164
- console.error(`Key 验证失败(HTTP ${r.status}),未修改本地配置。请确认 key 是否正确、是否已激活。`);
165
+ const base = getBaseUrl();
166
+ const productionHint = new URL(base).hostname === "hub.mantur.ai"
167
+ ? " 对外用户请在 https://hub.mantur.ai 创建生产 Key;hub.mantur.cn 的测试 Key 不能用于生产。"
168
+ : "";
169
+ console.error(
170
+ `Key 验证失败(HTTP ${r.status}),未修改本地配置。当前连接:${base}。` +
171
+ `Key 必须由这个站点创建,请确认 Key 是否正确、是否已激活。${productionHint}`
172
+ );
165
173
  process.exit(1);
166
174
  }
167
175
  break;
@@ -338,9 +346,46 @@ async function main() {
338
346
  console.error(`参数校验失败: ${error.message}`);
339
347
  process.exit(1);
340
348
  }
349
+ let quoteId = getFlag("confirm");
350
+ if (!quoteId) {
351
+ const quote = await apiFetch(`/api/v1/operators/${encodeURIComponent(op)}/quote`, {
352
+ method: "POST",
353
+ body,
354
+ });
355
+ if (!quote.ok) {
356
+ console.error(`本次费用试算失败(HTTP ${quote.status}): ${JSON.stringify(quote.json)}`);
357
+ process.exit(1);
358
+ }
359
+ const estimated = Number(quote.json?.estimated_dumplings);
360
+ quoteId = quote.json?.quote_id;
361
+ if (Number.isFinite(estimated) && estimated > 0) {
362
+ if (process.stdin.isTTY && process.stderr.isTTY) {
363
+ if (!(await confirmCharge(quote.json))) {
364
+ console.error("已取消,未调用算子、未扣费。");
365
+ process.exit(2);
366
+ }
367
+ } else {
368
+ console.error(JSON.stringify({
369
+ error: "CONFIRMATION_REQUIRED",
370
+ message: `本次预计消耗 ${formatMantou(estimated)},请先取得用户确认`,
371
+ estimated_dumplings: estimated,
372
+ balance: quote.json?.balance,
373
+ formula: quote.json?.formula,
374
+ quote_id: quoteId,
375
+ retry_with: `--confirm ${quoteId}`,
376
+ }, null, 2));
377
+ process.exit(3);
378
+ }
379
+ }
380
+ }
341
381
  const r = await apiFetch(
342
382
  `/api/v1/operators/${encodeURIComponent(op)}/invoke`,
343
- { method: "POST", body, timeoutMs: 120000 }
383
+ {
384
+ method: "POST",
385
+ body,
386
+ timeoutMs: 120000,
387
+ headers: quoteId ? { "X-Mantur-Quote-Id": quoteId } : {},
388
+ }
344
389
  );
345
390
  // 异步算子(返回 poll_url)默认自动轮询到出结果;--no-wait 只拿 job_id。
346
391
  const pollUrl = r.ok && r.json && r.json.poll_url;
@@ -355,10 +400,12 @@ async function main() {
355
400
  ),
356
401
  });
357
402
  console.log(JSON.stringify(final, null, 2));
403
+ printBillingResult(final);
358
404
  const st = final && final.status;
359
405
  if (st === "failed" || st === "error" || (final && final._timeout)) process.exit(1);
360
406
  } else {
361
407
  console.log(JSON.stringify(r.json, null, 2));
408
+ printBillingResult(r.json);
362
409
  if (!r.ok) process.exit(1);
363
410
  }
364
411
  break;
@@ -469,7 +516,7 @@ async function main() {
469
516
  console.error(error.message);
470
517
  process.exit(1);
471
518
  }
472
- await skillAdd(args[2], getFlag("client", "claude-code"));
519
+ await skillAdd(args[2], getFlag("client"));
473
520
  }
474
521
  else {
475
522
  console.error("用法: manturhub skill ls | manturhub skill add <slug> [--client claude-code|codex|all]");
@@ -0,0 +1,88 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { determineAgent } from "@vercel/detect-agent";
6
+
7
+ const SUPPORTED_CLIENTS = new Set(["claude", "claude-code", "codex", "all"]);
8
+
9
+ export function normalizeClient(client) {
10
+ if (client === undefined || client === null || client === "") return null;
11
+ if (!SUPPORTED_CLIENTS.has(client)) {
12
+ throw new Error("不支持的 client。可用值: claude-code | codex | all");
13
+ }
14
+ return client === "claude" ? "claude-code" : client;
15
+ }
16
+
17
+ export function clientForAgentName(name) {
18
+ if (name === "claude" || name === "cowork") return "claude-code";
19
+ if (name === "codex") return "codex";
20
+ return null;
21
+ }
22
+
23
+ export function detectInstalledClients({ home = homedir(), exists = existsSync } = {}) {
24
+ const clients = [];
25
+ if (exists(join(home, ".claude"))) clients.push("claude-code");
26
+ if (exists(join(home, ".codex")) || exists("/etc/codex")) clients.push("codex");
27
+ return clients;
28
+ }
29
+
30
+ function displayClient(client) {
31
+ if (client === "all") return "Claude Code、Codex";
32
+ return client === "codex" ? "Codex" : "Claude Code";
33
+ }
34
+
35
+ async function promptForClient(input, output, installedClients) {
36
+ const detected = installedClients.length
37
+ ? `\n检测到: ${installedClients.map(displayClient).join("、")}`
38
+ : "";
39
+ output.write(`${detected}\n请选择 Skill 安装目标:\n 1) Codex\n 2) Claude Code\n 3) 两者\n`);
40
+ const rl = createInterface({ input, output });
41
+ try {
42
+ const answer = (await rl.question("请输入 1 / 2 / 3: ")).trim();
43
+ if (answer === "1") return "codex";
44
+ if (answer === "2") return "claude-code";
45
+ if (answer === "3") return "all";
46
+ throw new Error("已取消:请输入 1、2 或 3,或使用 --client 明确指定。");
47
+ } finally {
48
+ rl.close();
49
+ }
50
+ }
51
+
52
+ export async function resolveSkillClient(
53
+ client,
54
+ {
55
+ detectAgent = determineAgent,
56
+ installedClients,
57
+ input = process.stdin,
58
+ output = process.stdout,
59
+ } = {}
60
+ ) {
61
+ const explicit = normalizeClient(client);
62
+ if (explicit) return { client: explicit, detected: false };
63
+
64
+ const agent = await detectAgent();
65
+ const runtimeClient = agent.isAgent ? clientForAgentName(agent.agent?.name) : null;
66
+ if (runtimeClient) return { client: runtimeClient, detected: true };
67
+
68
+ const installed = installedClients ?? detectInstalledClients();
69
+ if (installed.length === 1) return { client: installed[0], detected: true };
70
+
71
+ if (input.isTTY && output.isTTY) {
72
+ return {
73
+ client: await promptForClient(input, output, installed),
74
+ detected: false,
75
+ };
76
+ }
77
+
78
+ if (installed.length > 1) {
79
+ throw new Error(
80
+ "检测到 Claude Code 和 Codex,但当前无法交互选择。请加 --client codex、--client claude-code 或 --client all。"
81
+ );
82
+ }
83
+ throw new Error(
84
+ "未识别到 Codex 或 Claude Code。请在 Agent 内重试,或加 --client codex|claude-code|all。"
85
+ );
86
+ }
87
+
88
+ export { displayClient };
package/lib/api.js CHANGED
@@ -3,7 +3,7 @@ import { getKey, getBaseUrl } from "./config.js";
3
3
  // Thin REST client against the ManturHub gateway. Public discovery calls may omit auth.
4
4
  export async function apiFetch(
5
5
  path,
6
- { method = "GET", body, key, auth = "required", timeoutMs = 30000 } = {}
6
+ { method = "GET", body, key, auth = "required", timeoutMs = 30000, headers = {} } = {}
7
7
  ) {
8
8
  const apiKey = key === undefined ? getKey() : key;
9
9
  if (auth === "required" && !apiKey) {
@@ -22,6 +22,7 @@ export async function apiFetch(
22
22
  headers: {
23
23
  ...(includeKey && apiKey ? { "x-api-key": apiKey } : {}),
24
24
  ...(body ? { "Content-Type": "application/json" } : {}),
25
+ ...headers,
25
26
  },
26
27
  body: body ? JSON.stringify(body) : undefined,
27
28
  signal: AbortSignal.timeout(timeoutMs),
@@ -0,0 +1,34 @@
1
+ import { createInterface } from "node:readline/promises";
2
+
3
+ export function formatMantou(value) {
4
+ const number = Number(value);
5
+ return Number.isFinite(number) ? `${number} 馒头($${(number * 0.01).toFixed(2)} USD)` : "未知";
6
+ }
7
+
8
+ export async function confirmCharge(quote, { input = process.stdin, output = process.stderr } = {}) {
9
+ output.write(`\n⚠️ 本次预计消耗:${formatMantou(quote.estimated_dumplings)}\n`);
10
+ if (quote.formula) output.write(`计费依据:${quote.formula}\n`);
11
+ if (Number.isFinite(Number(quote.balance))) output.write(`当前余额:${quote.balance} 馒头\n`);
12
+ const rl = createInterface({ input, output });
13
+ try {
14
+ const answer = (await rl.question("是否继续?[y/N] ")).trim().toLowerCase();
15
+ return answer === "y" || answer === "yes";
16
+ } finally {
17
+ rl.close();
18
+ }
19
+ }
20
+
21
+ export function printBillingResult(result, output = process.stderr) {
22
+ const billing = result?._billing;
23
+ if (!billing) return;
24
+ const estimated = Number(billing.estimated_dumplings);
25
+ const charged = Number(billing.charged_dumplings);
26
+ const refunded = Number(billing.refunded_dumplings);
27
+ if (billing.final) {
28
+ output.write(`\n✓ 本次实际消耗:${formatMantou(charged)}\n`);
29
+ if (Number.isFinite(refunded) && refunded > 0) output.write(` 已退款:${formatMantou(refunded)}\n`);
30
+ if (Number.isFinite(estimated) && estimated !== charged) output.write(` 调用前预计:${formatMantou(estimated)}\n`);
31
+ } else {
32
+ output.write(`\n⏳ 已预扣:${formatMantou(charged)},任务完成后以最终结算为准\n`);
33
+ }
34
+ }
package/lib/params.js CHANGED
@@ -85,6 +85,14 @@ export function parseDynamicParams(tokens) {
85
85
  for (let i = 0; i < tokens.length; i++) {
86
86
  const token = tokens[i];
87
87
  if (token === "--no-wait") continue;
88
+ if (token === "--confirm") {
89
+ if (tokens[i + 1] === undefined || tokens[i + 1].startsWith("--")) {
90
+ throw new Error("参数 --confirm 缺少值");
91
+ }
92
+ i++;
93
+ continue;
94
+ }
95
+ if (token?.startsWith("--confirm=")) continue;
88
96
  if (!token?.startsWith("--")) throw new Error(`无法识别的参数: ${token}`);
89
97
  const equals = token.indexOf("=");
90
98
  if (equals > 2) {
@@ -5,6 +5,7 @@ import { getKey, getBaseUrl } from "./config.js";
5
5
  import { extractZipSafely, validateSlug } from "./archive.js";
6
6
  import { apiFetch } from "./api.js";
7
7
  import { assertSecureDownloadUrl, downloadResponseToFile } from "./download.js";
8
+ import { displayClient, resolveSkillClient } from "./agent-target.js";
8
9
 
9
10
  // `manturhub skill ls` — 列出平台上线 Skill(公开元数据,无需 key;
10
11
  // 配了 key 则带上——管理员租户的 key 能看到 admin 专属 Skill)。
@@ -34,11 +35,11 @@ export async function skillLs({ json = false } = {}) {
34
35
  const ver = s.version ? `v${s.version}` : "";
35
36
  console.log(` ${slug} ${s.name || ""} ${cat} ${ver}`.trimEnd());
36
37
  }
37
- console.log(`\n用 \`manturhub skill add <slug> --client claude-code|codex|all\` 安装到指定 Agent。`);
38
+ console.log(`\n用 \`manturhub skill add <slug>\` 自动识别 Agent;也可用 \`--client\` 明确指定。`);
38
39
  }
39
40
 
40
- // `manturhub skill add <slug>` — 下载并安全解压到指定 Agent 的用户级 skills 目录。
41
- export async function skillAdd(slug, client = "claude-code") {
41
+ // `manturhub skill add <slug>` — 自动识别 Agent,下载并安全解压到用户级 skills 目录。
42
+ export async function skillAdd(slug, client) {
42
43
  if (!slug) {
43
44
  console.error("用法: manturhub skill add <slug> (先 `manturhub skill ls` 看可用 Skill)");
44
45
  process.exit(1);
@@ -49,6 +50,16 @@ export async function skillAdd(slug, client = "claude-code") {
49
50
  console.error(error.message);
50
51
  process.exit(1);
51
52
  }
53
+ let target;
54
+ try {
55
+ target = await resolveSkillClient(client);
56
+ } catch (error) {
57
+ console.error(error.message);
58
+ process.exit(1);
59
+ }
60
+ client = target.client;
61
+ if (target.detected) console.log(`✓ 已识别 Agent:${displayClient(client)}`);
62
+
52
63
  const key = getKey();
53
64
  if (!key) {
54
65
  console.error("下载 Skill 需 API Key。运行 `manturhub login`,或设置环境变量 MANTURHUB_KEY。");
@@ -96,13 +107,8 @@ export async function skillAdd(slug, client = "claude-code") {
96
107
  console.error(`下载失败(HTTP ${res.status})`);
97
108
  process.exit(1);
98
109
  }
99
- const supported = new Set(["claude", "claude-code", "codex", "all"]);
100
- if (!supported.has(client)) {
101
- console.error("不支持的 client。可用值: claude-code | codex | all");
102
- process.exit(1);
103
- }
104
110
  const destinations = [];
105
- if (client === "claude" || client === "claude-code" || client === "all") {
111
+ if (client === "claude-code" || client === "all") {
106
112
  destinations.push(join(homedir(), ".claude", "skills", slug));
107
113
  }
108
114
  if (client === "codex" || client === "all") {
@@ -122,5 +128,5 @@ export async function skillAdd(slug, client = "claude-code") {
122
128
  }
123
129
  rmSync(tempDir, { recursive: true, force: true });
124
130
  for (const dest of destinations) console.log(`✓ 已安装 Skill「${slug}」→ ${dest}`);
125
- console.log(" 重启对应 Agent 后,用自然语言描述任务;Claude Code 也可用 /<slug> 触发。");
131
+ console.log(" 重启对应 Agent 后,用自然语言描述任务即可使用。");
126
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manturhub/cli",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "ManturHub 算子广场 CLI:通过 REST 发现和调用 AI 算子、浏览配方,并安装 Skill 与 Agent 套件",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,9 @@
9
9
  "engines": {
10
10
  "node": ">=18"
11
11
  },
12
+ "dependencies": {
13
+ "@vercel/detect-agent": "^1.2.1"
14
+ },
12
15
  "scripts": {
13
16
  "test": "node --test",
14
17
  "prepack": "npm test"