@manturhub/cli 0.9.11 → 0.10.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/README.md CHANGED
@@ -51,12 +51,32 @@ CLI 会在付费调用前按算子实时 schema 校验未知字段、必填项
51
51
  | `manturhub balance [--json]` | 查询余额及美元等值(1 馒头 = $0.01 USD) |
52
52
  | `manturhub skill ls [--json]` / `skill add <slug> [--client codex]` | 浏览并从当前 ManturHub 站点安装业务 Skill |
53
53
  | `manturhub skill outdated [--json]` / `skill update <slug> [--force]` | 比较本地与线上版本并更新;本地修改默认不覆盖 |
54
+ | `manturhub skill doctor <slug>` / `skill run <slug> --project <目录>` | 验证 Skill 安装与依赖;从指定项目目录启动或恢复工作流 |
55
+ | `manturhub skill exec <slug> <action> -- <参数>` | 给 Agent 使用的安全机器入口;只执行 Skill 包声明的白名单动作 |
54
56
  | `manturhub recipe [关键词]` / `recipe get <ID>` | 搜索并查看已验证配方 |
55
57
  | `manturhub suite ls [--json]` / `suite install <slug>` | 安装多角色 Agent 套件,不复制 API Key |
56
58
  | `manturhub init [--force]` | 从线上安装核心 `manturhub` Skill,并给当前项目写入 Agent 使用引导 |
57
59
 
58
60
  机器消费输出可在支持的查询命令上加 `--json`。进度和提示写入 stderr,JSON 结果写入 stdout。
59
61
 
62
+ ## Skill 本地运行契约
63
+
64
+ 需要确定性本地动作的 Skill 可在包根目录声明 `skill-runtime.json`:
65
+
66
+ ```json
67
+ {
68
+ "schema_version": 1,
69
+ "runtime": "node",
70
+ "minimum_cli_version": "0.10.0",
71
+ "entry": "runtime/runner.mjs",
72
+ "doctor_action": "doctor",
73
+ "run_action": "run",
74
+ "actions": ["doctor", "run", "validate-input"]
75
+ }
76
+ ```
77
+
78
+ CLI 只执行 `actions` 白名单内的动作,且入口必须是 Skill 包内的 `.mjs` 文件。执行前会验证当前站点、可信安装记录、整包 SHA-256、frontmatter 版本和最低 CLI 版本;本地内容被修改、入口越界或动作未声明时直接失败,不会寻找其他脚本或解释器。动作必须只向 stdout 写一个 JSON object,日志与进度写 stderr。
79
+
60
80
  环境变量:`MANTURHUB_KEY`(优先于配置文件) · `MANTURHUB_BASE`(测试或私有部署覆盖,默认 `https://hub.mantur.ai`)。
61
81
 
62
82
  ManturHub 算子广场:https://hub.mantur.ai
package/bin/cli.js CHANGED
@@ -7,6 +7,11 @@ import { suiteLs, suiteInstall } from "../lib/suite-install.js";
7
7
  import { loginViaBrowser } from "../lib/login-link.js";
8
8
  import { maybeNotifyUpdate } from "../lib/update-check.js";
9
9
  import { maybeNotifySkillUpdates } from "../lib/skill-update-check.js";
10
+ import {
11
+ executeSkillAction,
12
+ parseSkillExecutionArgs,
13
+ resolveInstalledSkillRuntime,
14
+ } from "../lib/skill-runtime.js";
10
15
  import { createReadStream, readFileSync, statSync } from "node:fs";
11
16
  import { fileURLToPath } from "node:url";
12
17
  import { dirname, join, basename, extname } from "node:path";
@@ -147,6 +152,12 @@ const HELP = `manturhub — ManturHub 算子广场 CLI v${VERSION}
147
152
  manturhub skill outdated 比较本地安装与当前站点的线上版本
148
153
  manturhub skill update <slug> [--client ...] [--force]
149
154
  从线上更新;本地改过时默认拒绝覆盖
155
+ manturhub skill doctor <slug> [--client ...] [-- <参数>]
156
+ 验证安装完整性并运行 Skill 声明的预检
157
+ manturhub skill run <slug> --project <目录> [--client ...] [-- <参数>]
158
+ 从项目目录启动或恢复 Skill 工作流
159
+ manturhub skill exec <slug> <action> [--client ...] [--project <目录>] [-- <参数>]
160
+ 运行 Skill 白名单内的机器动作
150
161
  manturhub suite ls 列出 Agent 套件(多角色团队工作区,给 Agent 配一整个团队)
151
162
  manturhub suite install <slug> 安装套件为工作目录 ./<slug>/(不复制 API Key)
152
163
  manturhub recipe [关键词] [--cat x] 搜配方(已验证创作的效果+可复现参数;分类 video/image/script)
@@ -325,6 +336,20 @@ async function main() {
325
336
  console.error(error.message);
326
337
  process.exit(1);
327
338
  }
339
+ if (operator.billing_mode === "internal_free") {
340
+ const free = {
341
+ operator_id: operator.id,
342
+ billing_mode: "internal_free",
343
+ estimated_dumplings: 0,
344
+ formula: "内部管理员只读查询,不扣馒头",
345
+ };
346
+ console.log(
347
+ args.includes("--json")
348
+ ? JSON.stringify(free, null, 2)
349
+ : `${operator.name}: 内部管理员只读查询,不扣馒头`
350
+ );
351
+ break;
352
+ }
328
353
  const r = await apiFetch(`/api/v1/operators/${encodeURIComponent(operator.id)}/quote`, { auth: "optional" });
329
354
  if (!r.ok) {
330
355
  console.error(`查询价格失败(HTTP ${r.status}): ${JSON.stringify(r.json)}`);
@@ -409,7 +434,7 @@ async function main() {
409
434
  process.exit(1);
410
435
  }
411
436
  let quoteId = getFlag("confirm");
412
- if (!quoteId) {
437
+ if (!quoteId && operator.billing_mode !== "internal_free") {
413
438
  const quote = await apiFetch(`/api/v1/operators/${encodeURIComponent(operatorId)}/quote`, {
414
439
  method: "POST",
415
440
  body,
@@ -623,11 +648,49 @@ async function main() {
623
648
  process.exit(1);
624
649
  }
625
650
  await skillUpdate(args[2], getFlag("client"), { force: hasFlag("force") });
651
+ } else if (sub === "doctor" || sub === "run" || sub === "exec") {
652
+ const slug = args[2];
653
+ if (!slug || slug.startsWith("--")) {
654
+ console.error(`用法: manturhub skill ${sub} <slug>${sub === "exec" ? " <action>" : ""}`);
655
+ process.exit(1);
656
+ }
657
+ let action;
658
+ let remaining;
659
+ if (sub === "exec") {
660
+ action = args[3];
661
+ if (!action || action.startsWith("--")) {
662
+ console.error("用法: manturhub skill exec <slug> <action> [--client ...] [--project <目录>] [-- <参数>]");
663
+ process.exit(1);
664
+ }
665
+ remaining = args.slice(4);
666
+ } else {
667
+ remaining = args.slice(3);
668
+ }
669
+ let options;
670
+ try {
671
+ options = parseSkillExecutionArgs(remaining, { requireProject: sub === "run" });
672
+ if (!action) {
673
+ const runtime = resolveInstalledSkillRuntime(slug, {
674
+ client: options.client,
675
+ cliVersion: VERSION,
676
+ });
677
+ action = sub === "doctor" ? runtime.manifest.doctor_action : runtime.manifest.run_action;
678
+ if (!action) throw new Error(`Skill「${slug}」没有声明 ${sub}_action`);
679
+ }
680
+ const exitCode = await executeSkillAction(slug, action, options.actionArgs, {
681
+ client: options.client,
682
+ project: options.project,
683
+ cliVersion: VERSION,
684
+ });
685
+ if (exitCode !== 0) process.exit(exitCode);
686
+ } catch (error) {
687
+ console.error(error.message);
688
+ process.exit(1);
689
+ }
626
690
  }
627
691
  else {
628
692
  console.error(
629
- "用法: manturhub skill ls | outdated | add <slug> | update <slug>"
630
- + " [--client claude-code|codex|all] [--force]"
693
+ "用法: manturhub skill ls | outdated | add <slug> | update <slug> | doctor <slug> | run <slug> | exec <slug> <action>"
631
694
  );
632
695
  process.exit(1);
633
696
  }
package/lib/api.js CHANGED
@@ -20,6 +20,7 @@ export async function apiFetch(
20
20
  fetch(url, {
21
21
  method,
22
22
  headers: {
23
+ "X-Mantur-Client": "cli",
23
24
  ...(includeKey && apiKey ? { "x-api-key": apiKey } : {}),
24
25
  ...(body ? { "Content-Type": "application/json" } : {}),
25
26
  ...headers,
@@ -0,0 +1,332 @@
1
+ import { spawn } from "node:child_process";
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ } from "node:fs";
8
+ import { isAbsolute, join, relative, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { validateSlug } from "./archive.js";
11
+ import { getBaseUrl } from "./config.js";
12
+ import {
13
+ compareSkillVersions,
14
+ getSkillRecord,
15
+ hashSkillDirectory,
16
+ loadSkillState,
17
+ readInstalledSkillMetadata,
18
+ skillDestination,
19
+ } from "./skill-state.js";
20
+
21
+ const MANIFEST_NAME = "skill-runtime.json";
22
+ const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
23
+ const SUPPORTED_CLIENTS = new Set(["codex", "claude-code"]);
24
+ const ACTION_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
25
+ const CLI_ENTRY = fileURLToPath(new URL("../bin/cli.js", import.meta.url));
26
+
27
+ function plainObject(value) {
28
+ return value !== null && typeof value === "object" && !Array.isArray(value);
29
+ }
30
+
31
+ function validateRecordedBaseUrl(value) {
32
+ let url;
33
+ try {
34
+ url = new URL(value);
35
+ } catch {
36
+ throw new Error(`Skill 安装记录的 ManturHub 环境不合法: ${value}`);
37
+ }
38
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
39
+ if (
40
+ (url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
41
+ || url.username
42
+ || url.password
43
+ || url.pathname !== "/"
44
+ || url.search
45
+ || url.hash
46
+ ) {
47
+ throw new Error(`Skill 安装记录的 ManturHub 环境不安全: ${value}`);
48
+ }
49
+ return url.href.replace(/\/$/, "");
50
+ }
51
+
52
+ function validateActionName(value, label = "action") {
53
+ if (typeof value !== "string" || !ACTION_PATTERN.test(value)) {
54
+ throw new Error(`${label} 格式不合法: ${value || "(空)"}`);
55
+ }
56
+ return value;
57
+ }
58
+
59
+ function validateEntryPath(skillDir, entry) {
60
+ if (
61
+ typeof entry !== "string"
62
+ || !entry
63
+ || entry.includes("\\")
64
+ || isAbsolute(entry)
65
+ || entry.split("/").some((part) => !part || part === "." || part === "..")
66
+ ) {
67
+ throw new Error("Skill runtime entry 必须是包内相对路径");
68
+ }
69
+ if (!entry.endsWith(".mjs")) {
70
+ throw new Error("Skill runtime entry 必须是 .mjs 文件");
71
+ }
72
+ const root = realpathSync(skillDir);
73
+ const target = resolve(root, entry);
74
+ const relation = relative(root, target);
75
+ if (!relation || relation.startsWith("..") || isAbsolute(relation)) {
76
+ throw new Error("Skill runtime entry 超出 Skill 包目录");
77
+ }
78
+ if (!existsSync(target) || !lstatSync(target).isFile()) {
79
+ throw new Error(`Skill runtime entry 不存在: ${entry}`);
80
+ }
81
+ const realTarget = realpathSync(target);
82
+ const realRelation = relative(root, realTarget);
83
+ if (realRelation.startsWith("..") || isAbsolute(realRelation)) {
84
+ throw new Error("Skill runtime entry 指向包外路径");
85
+ }
86
+ return realTarget;
87
+ }
88
+
89
+ export function readSkillRuntimeManifest(skillDir, cliVersion) {
90
+ const manifestPath = join(skillDir, MANIFEST_NAME);
91
+ let manifest;
92
+ try {
93
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
94
+ } catch (error) {
95
+ throw new Error(`Skill 缺少有效的 ${MANIFEST_NAME}: ${error.message}`);
96
+ }
97
+ if (!plainObject(manifest) || manifest.schema_version !== 1) {
98
+ throw new Error("Skill runtime manifest 的 schema_version 必须为 1");
99
+ }
100
+ if (manifest.runtime !== "node") {
101
+ throw new Error(`不支持的 Skill runtime: ${manifest.runtime || "(空)"}`);
102
+ }
103
+ if (!manifest.minimum_cli_version) {
104
+ throw new Error("Skill runtime manifest 缺少 minimum_cli_version");
105
+ }
106
+ const compatible = compareSkillVersions(cliVersion, manifest.minimum_cli_version);
107
+ if (compatible === null) {
108
+ throw new Error(`无法比较 CLI 版本: ${cliVersion} / ${manifest.minimum_cli_version}`);
109
+ }
110
+ if (compatible < 0) {
111
+ throw new Error(
112
+ `此 Skill 需要 ManturHub CLI >= ${manifest.minimum_cli_version},当前为 ${cliVersion}。`
113
+ + "请先运行: npm i -g @manturhub/cli@latest"
114
+ );
115
+ }
116
+ if (!Array.isArray(manifest.actions) || !manifest.actions.length || manifest.actions.length > 100) {
117
+ throw new Error("Skill runtime manifest 的 actions 必须是 1-100 个动作名");
118
+ }
119
+ const actions = new Set();
120
+ for (const action of manifest.actions) {
121
+ validateActionName(action, "Skill action");
122
+ if (actions.has(action)) throw new Error(`Skill runtime manifest 包含重复 action: ${action}`);
123
+ actions.add(action);
124
+ }
125
+ for (const field of ["doctor_action", "run_action"]) {
126
+ if (manifest[field] !== undefined) {
127
+ validateActionName(manifest[field], field);
128
+ if (!actions.has(manifest[field])) {
129
+ throw new Error(`${field} 未列入 actions 白名单: ${manifest[field]}`);
130
+ }
131
+ }
132
+ }
133
+ return {
134
+ ...manifest,
135
+ actions,
136
+ entryPath: validateEntryPath(skillDir, manifest.entry),
137
+ };
138
+ }
139
+
140
+ function validateTrackedInstallation(slug, client, baseUrl, state) {
141
+ const skillDir = skillDestination(slug, client);
142
+ if (!existsSync(skillDir)) return null;
143
+ if (lstatSync(skillDir).isSymbolicLink() || !lstatSync(skillDir).isDirectory()) {
144
+ throw new Error(`Skill「${slug}」安装目录不是可信普通目录,拒绝执行`);
145
+ }
146
+ const record = getSkillRecord(state, slug, client);
147
+ if (!record) {
148
+ throw new Error(
149
+ `本地 Skill「${slug}」[${client}] 没有可信安装记录。请用 manturhub skill update ${slug} --client ${client} --force 重新安装。`
150
+ );
151
+ }
152
+ if (record.base_url !== baseUrl) {
153
+ throw new Error(
154
+ `Skill「${slug}」来自 ${record.base_url},当前连接 ${baseUrl};为防止跨站运行已停止。`
155
+ );
156
+ }
157
+ const expectedPath = skillDestination(slug, client);
158
+ if (record.path && resolve(record.path) !== resolve(expectedPath)) {
159
+ throw new Error(`Skill「${slug}」安装记录路径异常`);
160
+ }
161
+ const actualHash = hashSkillDirectory(skillDir);
162
+ if (!record.content_sha256 || actualHash !== record.content_sha256) {
163
+ throw new Error(
164
+ `Skill「${slug}」[${client}] 内容哈希与安装记录不一致,拒绝执行。请检查本地修改或重新安装。`
165
+ );
166
+ }
167
+ const metadata = readInstalledSkillMetadata(skillDir);
168
+ if (metadata.name !== slug || metadata.version !== record.version) {
169
+ throw new Error(`Skill「${slug}」frontmatter 与可信安装记录不一致,拒绝执行`);
170
+ }
171
+ return { client, skillDir, record, contentSha256: actualHash };
172
+ }
173
+
174
+ export function resolveInstalledSkillRuntime(slug, { client, cliVersion }) {
175
+ validateSlug(slug, "Skill ID");
176
+ if (client && !SUPPORTED_CLIENTS.has(client)) {
177
+ throw new Error(`不支持的 Skill 客户端: ${client}`);
178
+ }
179
+ const state = loadSkillState();
180
+ if (client) {
181
+ const record = getSkillRecord(state, slug, client);
182
+ if (!record) validateTrackedInstallation(slug, client, getBaseUrl(), state);
183
+ const baseUrl = process.env.MANTURHUB_BASE
184
+ ? getBaseUrl()
185
+ : validateRecordedBaseUrl(record?.base_url);
186
+ const selected = validateTrackedInstallation(slug, client, baseUrl, state);
187
+ if (!selected) throw new Error(`未安装 Skill「${slug}」[${client}]`);
188
+ return {
189
+ ...selected,
190
+ baseUrl,
191
+ manifest: readSkillRuntimeManifest(selected.skillDir, cliVersion),
192
+ };
193
+ }
194
+
195
+ const installedClients = [...SUPPORTED_CLIENTS].filter((candidate) =>
196
+ existsSync(skillDestination(slug, candidate))
197
+ );
198
+ if (!installedClients.length) {
199
+ throw new Error(`未安装 Skill「${slug}」。请先运行: manturhub skill add ${slug}`);
200
+ }
201
+ if (installedClients.length > 1) {
202
+ const records = installedClients.map((candidate) => getSkillRecord(state, slug, candidate));
203
+ if (
204
+ records.some((record) => !record)
205
+ || new Set(records.map((record) => `${record.base_url}:${record.content_sha256}`)).size !== 1
206
+ ) {
207
+ throw new Error(`Skill「${slug}」存在多个不一致安装,请用 --client codex 或 --client claude-code 明确选择`);
208
+ }
209
+ }
210
+ const selectedClient = installedClients.includes("codex") ? "codex" : installedClients[0];
211
+ const selectedRecord = getSkillRecord(state, slug, selectedClient);
212
+ if (!selectedRecord) validateTrackedInstallation(slug, selectedClient, getBaseUrl(), state);
213
+ const baseUrl = process.env.MANTURHUB_BASE
214
+ ? getBaseUrl()
215
+ : validateRecordedBaseUrl(selectedRecord?.base_url);
216
+ const selected = validateTrackedInstallation(slug, selectedClient, baseUrl, state);
217
+ return {
218
+ ...selected,
219
+ baseUrl,
220
+ manifest: readSkillRuntimeManifest(selected.skillDir, cliVersion),
221
+ };
222
+ }
223
+
224
+ function collectProcessOutput(child) {
225
+ return new Promise((resolvePromise, rejectPromise) => {
226
+ let stdout = "";
227
+ let stdoutBytes = 0;
228
+ let failed = false;
229
+ child.stdout.on("data", (chunk) => {
230
+ stdoutBytes += chunk.length;
231
+ if (stdoutBytes > MAX_OUTPUT_BYTES) {
232
+ failed = true;
233
+ child.kill();
234
+ rejectPromise(new Error("Skill action stdout 超过 10 MB 限制"));
235
+ return;
236
+ }
237
+ stdout += chunk.toString("utf8");
238
+ });
239
+ child.stderr.on("data", (chunk) => process.stderr.write(chunk));
240
+ child.on("error", rejectPromise);
241
+ child.on("close", (code, signal) => {
242
+ if (!failed) resolvePromise({ code, signal, stdout });
243
+ });
244
+ });
245
+ }
246
+
247
+ export async function executeSkillAction(
248
+ slug,
249
+ action,
250
+ actionArgs,
251
+ { client, project, cliVersion }
252
+ ) {
253
+ validateActionName(action);
254
+ const runtime = resolveInstalledSkillRuntime(slug, { client, cliVersion });
255
+ if (!runtime.manifest.actions.has(action)) {
256
+ throw new Error(`Skill「${slug}」未声明 action: ${action}`);
257
+ }
258
+ let projectDir = process.cwd();
259
+ if (project) {
260
+ const requested = resolve(project);
261
+ if (!existsSync(requested) || !lstatSync(requested).isDirectory()) {
262
+ throw new Error(`项目目录不存在或不是目录: ${project}`);
263
+ }
264
+ projectDir = realpathSync(requested);
265
+ }
266
+ const child = spawn(process.execPath, [runtime.manifest.entryPath, action, ...actionArgs], {
267
+ cwd: projectDir,
268
+ env: {
269
+ ...process.env,
270
+ MANTURHUB_BASE: runtime.baseUrl,
271
+ MANTURHUB_CLI_ENTRY: CLI_ENTRY,
272
+ MANTURHUB_SKILL_DIR: runtime.skillDir,
273
+ MANTURHUB_SKILL_SLUG: slug,
274
+ MANTURHUB_SKILL_ACTION: action,
275
+ MANTURHUB_PROJECT: projectDir,
276
+ },
277
+ stdio: ["inherit", "pipe", "pipe"],
278
+ });
279
+ const result = await collectProcessOutput(child);
280
+ let payload;
281
+ try {
282
+ payload = JSON.parse(result.stdout);
283
+ } catch {
284
+ throw new Error(`Skill action「${action}」没有返回合法 JSON object`);
285
+ }
286
+ if (!plainObject(payload)) {
287
+ throw new Error(`Skill action「${action}」必须返回 JSON object`);
288
+ }
289
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
290
+ if (result.signal) throw new Error(`Skill action「${action}」被信号 ${result.signal} 终止`);
291
+ return Number.isInteger(result.code) ? result.code : 1;
292
+ }
293
+
294
+ export function parseSkillExecutionArgs(tokens, { requireProject = false } = {}) {
295
+ let client;
296
+ let project;
297
+ const actionArgs = [];
298
+ for (let i = 0; i < tokens.length; i++) {
299
+ const token = tokens[i];
300
+ if (token === "--") {
301
+ actionArgs.push(...tokens.slice(i + 1));
302
+ break;
303
+ }
304
+ if (token === "--client" || token === "--project") {
305
+ const value = tokens[++i];
306
+ if (!value || value.startsWith("--")) throw new Error(`参数 ${token} 缺少值`);
307
+ if (token === "--client") {
308
+ if (client !== undefined) throw new Error("选项 --client 不能重复");
309
+ client = value;
310
+ } else {
311
+ if (project !== undefined) throw new Error("选项 --project 不能重复");
312
+ project = value;
313
+ }
314
+ continue;
315
+ }
316
+ if (token.startsWith("--client=")) {
317
+ if (client !== undefined) throw new Error("选项 --client 不能重复");
318
+ client = token.slice("--client=".length);
319
+ if (!client) throw new Error("参数 --client 缺少值");
320
+ continue;
321
+ }
322
+ if (token.startsWith("--project=")) {
323
+ if (project !== undefined) throw new Error("选项 --project 不能重复");
324
+ project = token.slice("--project=".length);
325
+ if (!project) throw new Error("参数 --project 缺少值");
326
+ continue;
327
+ }
328
+ actionArgs.push(token);
329
+ }
330
+ if (requireProject && !project) throw new Error("manturhub skill run 必须提供 --project <目录>");
331
+ return { client, project, actionArgs };
332
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manturhub/cli",
3
- "version": "0.9.11",
3
+ "version": "0.10.1",
4
4
  "description": "ManturHub 算子广场 CLI:通过 REST 发现和调用 AI 算子、浏览配方,并安装 Skill 与 Agent 套件",
5
5
  "type": "module",
6
6
  "bin": {