modelfunction-cli 0.5.1 → 0.5.2

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
@@ -5,7 +5,7 @@
5
5
  ## 安装
6
6
 
7
7
  ```bash
8
- npx -y modelfunction-cli@0.5.0 --version
8
+ npx -y modelfunction-cli@0.5.1 --version
9
9
  # 或
10
10
  npm i -D modelfunction-cli
11
11
  npx mf --version
@@ -81,7 +81,7 @@ INTERNAL_LLM_KEY=... mf eval --suite mf.suite.json \
81
81
  | `mf coverage` | 每工具用例数、参数覆盖、负例比例、疑似混淆对;有 `profiles` 时按组统计 |
82
82
  | `mf audit` | 一次静态审计:lint + 混淆族边界 + 用例覆盖 + 挂载检查,输出可转发的 Markdown/JSON;不调用模型 |
83
83
  | `mf lint` | 静态检查工具描述与参数 schema,以及 skills 的 name/description(不调用模型);重复规则自动合并成一条;有 `profiles` 时给出「这些组不要合并挂载」 |
84
- | `mf check` | 安装门禁:一个新能力 × 已装能力(`--manifest` + `--installed`),纯静态、返回 pass/warn、不阻断;也可 `import { check } from "modelfunction-cli"` |
84
+ | `mf check` | 安装门禁:一个或一包新能力 × 已装能力(`--manifest` / `--manifests` + `--installed`),纯静态、返回 pass/warn、不阻断;门禁语境只对混淆对/必填来源/结构性错误报 warn,其余降 info;也可 `import { check, checkMany } from "modelfunction-cli"` |
85
85
  | `mf compare` | 跨版本比较、干扰归因、混淆矩阵;两份报告都有分组时按组对比 |
86
86
  | `mf ablate` | 留一法:去掉某个工具再跑一遍,量化它是否干扰其他工具;`--profile` 限定在某个挂载组内 |
87
87
  | `mf cluster` | 聚类工具,建议挂载分组;参数形状拆散的同一族(如 `sessions_*`)列为「同族候选」;并输出「族内边界清单」(每族会互抢的对 + 每个工具可用的区分词);`--out` 把分组写成套件的 `profiles` |
@@ -126,19 +126,19 @@ mf eval --suite mf.suite.json --models internal-ds \
126
126
  "mcpServers": {
127
127
  "modelfunction": {
128
128
  "command": "npx",
129
- "args": ["-y", "modelfunction-cli@0.5.0", "mcp"]
129
+ "args": ["-y", "modelfunction-cli@0.5.1", "mcp"]
130
130
  }
131
131
  }
132
132
  }
133
133
  ```
134
134
 
135
- 暴露:`scan_tools`、`list_suite`、`coverage`、`add_cases`、`run_eval`(`run_eval` 支持 `profile` 与 `repeat` 参数:传 `all` 或组名按挂载组评分,`repeat` 取多数结果)。
135
+ 暴露:`scan_tools`、`list_suite`、`coverage`、`audit`、`lint`、`check`、`cluster`、`compare`、`add_cases`、`run_eval`(`run_eval` 支持 `profile`、`repeat`、`maxCost`、`progress`)。`lint`/`check`/`cluster`/`compare` 都是静态操作,不调用模型;`check` 就是安装门禁,Agent 可直接传入 manifest 与 installed。
136
136
 
137
137
  ## 契约与版本
138
138
 
139
139
  对外 JSON 字段、`check()` 返回结构、MCP 参数与退出码见 [SCHEMA.md](SCHEMA.md),自 0.4.3 起冻结为 v1:v1 内只做加法,重命名或删除字段需 major 并提前两个 minor 或 90 天弃用。`check()` 的类型声明随包发布。
140
140
 
141
- npm 已发布 `0.5.0`(凭证与本地反馈渠道);`0.5.1` 待发布,合并开发改动:usage `ci` 标记与 local/ci 统计、eval `--progress`/`--max-cost`、`mf audit` 组合审计、`scan --probe` 支持 streamable HTTP。示例固定 `@0.5.0`,发布后统一升级。
141
+ `0.5.1` 已发布到 npm:含凭证与本地反馈、usage `ci` 标记、eval `--progress`/`--max-cost`、`mf audit`、`scan --probe` streamable HTTP。本仓库 `0.5.2` 待发布:MCP 补齐 `lint`/`check`/`cluster`/`compare`,安装门禁支持 `--manifests` 一包批量检查,门禁 warn 只保留混淆对/必填来源/结构性错误(描述类降 info)。示例统一使用 `@0.5.1`。
142
142
 
143
143
  ## 数据边界
144
144
 
@@ -167,7 +167,7 @@ mf feedback --out out.json # 导出脱敏反馈包(聚合值,不含原始
167
167
 
168
168
  ```bash
169
169
  # 1. 把 mf 挂成 MCP server
170
- claude mcp add modelfunction -- npx -y modelfunction-cli@0.5.0 mcp
170
+ claude mcp add modelfunction -- npx -y modelfunction-cli@0.5.1 mcp
171
171
  ```
172
172
 
173
173
  ```text
package/SCHEMA.md CHANGED
@@ -174,9 +174,37 @@ import { check } from "modelfunction-cli";
174
174
  ```
175
175
 
176
176
  - `verdict` 只有 `pass` / `warn`,**没有 fail**;质量判定不阻断安装。
177
+ - **严重度策略(门禁语境)**:只有会改变模型行为的规则保持原等级——`desc-missing`、`param-type-missing`(error),`required-unsourced`、`pair-no-boundary`、`skill-pair-no-boundary`(warn);描述、命名、必填数量等质量规则降为 `info`,避免成熟工具集每次安装都告警。`mf lint` 不受影响,仍输出原始严重度。
177
178
  - 唯一抛错场景:`manifest.name` 缺失或为空。
178
179
  - 同步、无网络、无文件系统、无状态。类型声明见 `lib/check.d.ts`。
179
180
 
181
+ 批量(一次安装一包):
182
+
183
+ ```js
184
+ import { checkMany } from "modelfunction-cli";
185
+ const result = checkMany({ manifests: [/* ... */], installed: [/* ... */] });
186
+ ```
187
+
188
+ 返回 `{ verdict, summary, items, results[] }`:
189
+
190
+ ```json
191
+ {
192
+ "verdict": "warn",
193
+ "summary": { "error": 0, "warn": 1, "info": 0 },
194
+ "items": 2,
195
+ "results": [
196
+ { "name": "gitcrawl", "verdict": "warn", "summary": { "error": 0, "warn": 1, "info": 0 },
197
+ "findings": [ { "severity": "warn", "code": "skill-pair-no-boundary", "subject": "gitcrawl",
198
+ "with": "tag-duplicate-prs-issues", "message": "...", "suggestion": "..." } ],
199
+ "family": ["gitcrawl", "tag-duplicate-prs-issues"] },
200
+ { "name": "tag-duplicate-prs-issues", "verdict": "warn", "summary": { "error": 0, "warn": 1, "info": 0 },
201
+ "findings": [ /* 同一对,subject 为自身 */ ], "family": ["gitcrawl", "tag-duplicate-prs-issues"] }
202
+ ]
203
+ }
204
+ ```
205
+
206
+ `summary` 按 subject/with 去重(同一对只计一次);`results[]` 保留与该能力相关的全部发现(pair 会出现在双方)。`manifests` 为空、某项缺 name、名称重复时抛错。CLI:`mf check --manifests pack.json [--installed installed.json] [--json]`。
207
+
180
208
  ## 7. MCP 工具
181
209
 
182
210
  | 工具 | 参数 |
@@ -185,6 +213,10 @@ import { check } from "modelfunction-cli";
185
213
  | `list_suite` | `suite?` |
186
214
  | `coverage` | `suite?` |
187
215
  | `audit` | `suite?` |
216
+ | `lint` | `suite?` |
217
+ | `check` | `manifest`(对象)、`installed?`(数组或 `{tools,skills}`) |
218
+ | `cluster` | `suite?`、`threshold?`、`maxSize?` |
219
+ | `compare` | `before`、`after`(评分卡 JSON 路径) |
188
220
  | `add_cases` | `suite?`、`cases[]`、`source?`、`reviewed?`、`dryRun?` |
189
221
  | `run_eval` | `suite?`、`models?`、`cases?`、`profile?`、`repeat?`、`failUnder?`、`maxCost?`、`progress?` |
190
222
 
package/lib/check.d.ts CHANGED
@@ -23,6 +23,11 @@ export interface CheckInput {
23
23
  }
24
24
 
25
25
  export interface CheckFinding {
26
+ /**
27
+ * Install-gate view: only behavior-changing findings keep warn/error
28
+ * (selection conflicts, unsourced required params, schema errors).
29
+ * Description and naming rules are reported as info so mature tools pass.
30
+ */
26
31
  severity: Severity;
27
32
  code: string;
28
33
  subject: string;
@@ -41,6 +46,23 @@ export interface CheckResult {
41
46
  family?: string[];
42
47
  }
43
48
 
49
+ export interface CheckManyInput {
50
+ manifests: Capability[];
51
+ installed?: Capability[] | InstalledCapabilities;
52
+ }
53
+
54
+ export interface CheckItemResult extends CheckResult {
55
+ name: string;
56
+ }
57
+
58
+ export interface CheckManyResult {
59
+ verdict: Verdict;
60
+ /** Deduplicated by subject/with; per-item summaries are in results[]. */
61
+ summary: { error: number; warn: number; info: number };
62
+ items: number;
63
+ results: CheckItemResult[];
64
+ }
65
+
44
66
  /**
45
67
  * Static install gate: judge one new capability against installed ones.
46
68
  * No model call, no network, no file system, no state.
@@ -48,4 +70,13 @@ export interface CheckResult {
48
70
  */
49
71
  export declare function check(input: CheckInput): CheckResult;
50
72
 
73
+ /**
74
+ * Batch install gate: judge a pack of new capabilities against installed ones
75
+ * and against each other.
76
+ * @throws when manifests is empty, an item lacks a name, or names are duplicated.
77
+ */
78
+ export declare function checkMany(input: CheckManyInput): CheckManyResult;
79
+
51
80
  export declare function formatCheck(result: CheckResult): string;
81
+
82
+ export declare function formatCheckMany(result: CheckManyResult): string;
package/lib/check.js CHANGED
@@ -1,4 +1,4 @@
1
- // Install-gate entry point: judge one new capability against what is already
1
+ // Install-gate entry point: judge new capabilities against what is already
2
2
  // installed. Static only — no model call, no network, no file system, no state.
3
3
  // Quality findings are advisory: the verdict is pass/warn, never "block".
4
4
  import { lintSuite } from "./lint.js";
@@ -6,6 +6,41 @@ import { lintSuite } from "./lint.js";
6
6
  export function check({ manifest, installed = [] } = {}) {
7
7
  const target = normalizeEntry(manifest);
8
8
  if (!target) throw new Error('check: manifest 需要非空 "name"');
9
+ const { results } = evaluateTargets([target], installed);
10
+ const [result] = results;
11
+ return {
12
+ verdict: result.verdict,
13
+ summary: result.summary,
14
+ findings: result.findings,
15
+ ...(result.family ? { family: result.family } : {})
16
+ };
17
+ }
18
+
19
+ // Installers usually apply a pack at once: new skills must be checked against
20
+ // installed ones AND against each other.
21
+ export function checkMany({ manifests, installed = [] } = {}) {
22
+ if (!Array.isArray(manifests) || manifests.length === 0) {
23
+ throw new Error("checkMany: manifests 需要非空数组");
24
+ }
25
+ const targets = manifests.map((entry) => normalizeEntry(entry));
26
+ if (targets.some((target) => target === null)) {
27
+ throw new Error('checkMany: 每个 manifest 都需要非空 "name"');
28
+ }
29
+ const names = new Set();
30
+ for (const target of targets) {
31
+ if (names.has(target.name)) throw new Error(`checkMany: duplicate name "${target.name}"`);
32
+ names.add(target.name);
33
+ }
34
+ const { results } = evaluateTargets(targets, installed);
35
+ return {
36
+ verdict: results.some((item) => item.verdict === "warn") ? "warn" : "pass",
37
+ summary: uniqueSummary(results),
38
+ items: results.length,
39
+ results
40
+ };
41
+ }
42
+
43
+ function evaluateTargets(targets, installed) {
9
44
  const pool = [];
10
45
  if (Array.isArray(installed)) pool.push(...installed);
11
46
  else if (installed && typeof installed === "object") {
@@ -16,22 +51,47 @@ export function check({ manifest, installed = [] } = {}) {
16
51
  const tools = [];
17
52
  const skills = [];
18
53
  for (const entry of entries) (isToolLike(entry) ? tools : skills).push(entry);
19
- (isToolLike(target) ? tools : skills).push(target);
54
+ for (const target of targets) (isToolLike(target) ? tools : skills).push(target);
20
55
 
21
56
  const report = lintSuite({ tools, skills });
22
- const findings = report.findings
23
- .filter((finding) => touches(finding, target.name))
24
- .map((finding) => shape(finding, target.name));
25
- const summary = { error: 0, warn: 0, info: 0 };
26
- for (const finding of findings) summary[finding.severity] += 1;
27
- const family = familyOf(report.findings.filter((finding) => finding.pair), target.name);
57
+ const pairFindings = report.findings.filter((finding) => finding.pair);
58
+ const results = targets.map((target) => {
59
+ const findings = report.findings
60
+ .filter((finding) => touches(finding, target.name))
61
+ .map((finding) => shape(finding, target.name));
62
+ const summary = { error: 0, warn: 0, info: 0 };
63
+ for (const finding of findings) summary[finding.severity] += 1;
64
+ const family = familyOf(pairFindings, target.name);
65
+ return {
66
+ name: target.name,
67
+ verdict: summary.error + summary.warn > 0 ? "warn" : "pass",
68
+ summary,
69
+ findings,
70
+ ...(family.length > 1 ? { family } : {})
71
+ };
72
+ });
73
+ return { results };
74
+ }
28
75
 
29
- return {
30
- verdict: summary.error + summary.warn > 0 ? "warn" : "pass",
31
- summary,
32
- findings,
33
- ...(family.length > 1 ? { family } : {})
34
- };
76
+ // A pair finding touches both items, so the batch summary is deduped by
77
+ // subject/with; per-item summaries keep the same finding under each item.
78
+ function uniqueSummary(results) {
79
+ const seen = new Map();
80
+ for (const result of results) {
81
+ for (const finding of result.findings) {
82
+ const key = [
83
+ finding.code,
84
+ finding.severity,
85
+ finding.subject,
86
+ finding.with ?? "",
87
+ finding.param ?? ""
88
+ ].join("|");
89
+ if (!seen.has(key)) seen.set(key, finding);
90
+ }
91
+ }
92
+ const summary = { error: 0, warn: 0, info: 0 };
93
+ for (const finding of seen.values()) summary[finding.severity] += 1;
94
+ return summary;
35
95
  }
36
96
 
37
97
  function normalizeEntry(entry) {
@@ -61,10 +121,22 @@ function touches(finding, name) {
61
121
  return Array.isArray(finding.pair) && finding.pair.includes(name);
62
122
  }
63
123
 
124
+ // An install gate should only warn about things that change model behavior:
125
+ // selection conflicts, unsourceable required params, and hard schema errors.
126
+ // Description and naming rules stay informative — otherwise every mature
127
+ // gateway tool warns on install and the signal becomes noise.
128
+ const GATE_CRITICAL_CODES = new Set([
129
+ "desc-missing",
130
+ "param-type-missing",
131
+ "required-unsourced",
132
+ "pair-no-boundary",
133
+ "skill-pair-no-boundary"
134
+ ]);
135
+
64
136
  function shape(finding, name) {
65
137
  const other = (finding.pair ?? []).find((item) => item !== name);
66
138
  return {
67
- severity: finding.severity,
139
+ severity: GATE_CRITICAL_CODES.has(finding.code) ? finding.severity : "info",
68
140
  code: finding.code,
69
141
  subject: name,
70
142
  ...(finding.param ? { param: finding.param } : {}),
@@ -101,18 +173,41 @@ function familyOf(pairs, name) {
101
173
  return [...seen].sort();
102
174
  }
103
175
 
176
+ function findingLines(finding) {
177
+ const param = finding.param ? `.${finding.param}` : "";
178
+ const withWhat = finding.with ? ` × ${finding.with}` : "";
179
+ const lines = [
180
+ `[${finding.severity}] ${finding.code} ${finding.subject}${param}${withWhat}`,
181
+ ` ${finding.message}`
182
+ ];
183
+ if (finding.suggestion) lines.push(` ${finding.suggestion}`);
184
+ return lines;
185
+ }
186
+
104
187
  export function formatCheck(result) {
105
188
  const lines = [
106
189
  `verdict: ${result.verdict}(error ${result.summary.error} / warn ${result.summary.warn} / info ${result.summary.info})`
107
190
  ];
108
191
  if (result.findings.length === 0) lines.push("没有发现问题");
109
- for (const finding of result.findings) {
110
- const param = finding.param ? `.${finding.param}` : "";
111
- const withWhat = finding.with ? ` × ${finding.with}` : "";
112
- lines.push(`[${finding.severity}] ${finding.code} ${finding.subject}${param}${withWhat}`);
113
- lines.push(` ${finding.message}`);
114
- if (finding.suggestion) lines.push(` ${finding.suggestion}`);
115
- }
192
+ for (const finding of result.findings) lines.push(...findingLines(finding));
116
193
  if (result.family) lines.push("", `族:${result.family.join(", ")}`);
117
194
  return lines.join("\n");
118
195
  }
196
+
197
+ export function formatCheckMany(result) {
198
+ const lines = [
199
+ `verdict: ${result.verdict}(${result.items} 个新能力;去重后 error ${result.summary.error} / warn ${result.summary.warn} / info ${result.summary.info})`
200
+ ];
201
+ result.results.forEach((item, index) => {
202
+ lines.push(
203
+ "",
204
+ `[${index + 1}] ${item.name} ${item.verdict}(error ${item.summary.error} / warn ${item.summary.warn} / info ${item.summary.info})`
205
+ );
206
+ if (item.findings.length === 0) lines.push(" 没有发现问题");
207
+ for (const finding of item.findings) {
208
+ for (const line of findingLines(finding)) lines.push(` ${line}`);
209
+ }
210
+ if (item.family) lines.push(` 族:${item.family.join(", ")}`);
211
+ });
212
+ return lines.join("\n");
213
+ }
package/lib/mcp-server.js CHANGED
@@ -11,6 +11,10 @@ import { writeFileAtomic } from "./fsutil.js";
11
11
  import { probeStdioServer, probeHttpServer } from "./mcp-probe.js";
12
12
  import { addCases, computeCoverage } from "./cases.js";
13
13
  import { auditSuite } from "./audit.js";
14
+ import { check, checkMany } from "./check.js";
15
+ import { lintSuite } from "./lint.js";
16
+ import { clusterTools } from "./cluster.js";
17
+ import { compareScorecards } from "./compare.js";
14
18
 
15
19
  const MF_PATH = fileURLToPath(new URL("../mf.js", import.meta.url));
16
20
  const PACKAGE_JSON = new URL("../package.json", import.meta.url);
@@ -125,6 +129,78 @@ export async function startMcpServer() {
125
129
  )
126
130
  );
127
131
 
132
+ server.registerTool(
133
+ "lint",
134
+ {
135
+ title: "Lint suite",
136
+ description:
137
+ "静态检查 tools 与 skills 的描述、参数与工具对边界(不调用模型)。返回 counts / findings / profilePairs。",
138
+ inputSchema: { suite: z.string().optional().describe("套件路径,默认 ./mf.suite.json") }
139
+ },
140
+ async ({ suite }) =>
141
+ json(
142
+ lintSuite(
143
+ await loadSuite(suitePath(suite), { allowEmptyCases: true, allowEmptyTools: true })
144
+ )
145
+ )
146
+ );
147
+
148
+ server.registerTool(
149
+ "check",
150
+ {
151
+ title: "Install gate",
152
+ description:
153
+ "安装门禁:把新能力与已装能力做静态比较,返回 pass/warn、findings 与竞争族。传 manifest 检查单个;传 manifests 检查一包(新能力之间也会互比)。不阻断、不调用模型、无网络。",
154
+ inputSchema: {
155
+ manifest: z.any().optional().describe("新能力 {name, description, parameters?} 或 tool schema"),
156
+ manifests: z.array(z.any()).optional().describe("一次安装一包能力时的数组"),
157
+ installed: z.any().optional().describe("已装能力数组,或 {tools: [], skills: []}")
158
+ }
159
+ },
160
+ async ({ manifest, manifests, installed }) => {
161
+ if (manifests) return json(checkMany({ manifests, installed }));
162
+ return json(check({ manifest, installed }));
163
+ }
164
+ );
165
+
166
+ server.registerTool(
167
+ "cluster",
168
+ {
169
+ title: "Cluster tools",
170
+ description:
171
+ "聚类工具,给出挂载分组、跨组高风险与族内边界清单(每族要补哪句选择条件)。不调用模型。",
172
+ inputSchema: {
173
+ suite: z.string().optional().describe("套件路径,默认 ./mf.suite.json"),
174
+ threshold: z.number().min(0).max(1).optional().describe("聚类阈值,默认 0.5"),
175
+ maxSize: z.number().int().positive().optional().describe("单组最大工具数,默认 10")
176
+ }
177
+ },
178
+ async ({ suite, threshold, maxSize }) => {
179
+ const loaded = await loadSuite(suitePath(suite), { allowEmptyCases: true });
180
+ return json(clusterTools(loaded.tools, { threshold, maxSize }));
181
+ }
182
+ );
183
+
184
+ server.registerTool(
185
+ "compare",
186
+ {
187
+ title: "Compare scorecards",
188
+ description:
189
+ "比较两份评分卡 JSON:可比性、升降、干扰归因与混淆矩阵。只读文件,不调用模型。",
190
+ inputSchema: {
191
+ before: z.string().describe("旧评分卡 JSON 路径"),
192
+ after: z.string().describe("新评分卡 JSON 路径")
193
+ }
194
+ },
195
+ async ({ before, after }) => {
196
+ const [oldText, newText] = await Promise.all([
197
+ readFile(resolve(before), "utf8"),
198
+ readFile(resolve(after), "utf8")
199
+ ]);
200
+ return json(compareScorecards(JSON.parse(oldText), JSON.parse(newText)));
201
+ }
202
+ );
203
+
128
204
  server.registerTool(
129
205
  "add_cases",
130
206
  {
package/mf.js CHANGED
@@ -11,7 +11,7 @@ import { probeStdioServer, probeHttpServer } from "./lib/mcp-probe.js";
11
11
  import { loadAdapterTools } from "./lib/adapter-import.js";
12
12
  import { addCases, computeCoverage, formatCoverage } from "./lib/cases.js";
13
13
  import { lintSuite, formatLint } from "./lib/lint.js";
14
- import { check, formatCheck } from "./lib/check.js";
14
+ import { check, checkMany, formatCheck, formatCheckMany } from "./lib/check.js";
15
15
  import { auditSuite, formatAudit } from "./lib/audit.js";
16
16
  import { suiteFingerprint, hashText, hashOf } from "./lib/fingerprint.js";
17
17
  import { runExecutionChecks } from "./lib/eval-exec.js";
@@ -75,7 +75,7 @@ Usage:
75
75
  mf coverage [--suite <file>] [--json]
76
76
  mf audit [--suite <file>] [--out <file>] [--json] # 静态审计:lint + 分组边界 + 用例覆盖 + 挂载
77
77
  mf lint [--suite <file>] [--json] # 静态检查 tools 与 skills,不调用模型
78
- mf check --manifest <file> [--installed <file>] [--json] # 安装门禁:新能力 × 已装能力
78
+ mf check (--manifest <file> | --manifests <file>) [--installed <file>] [--json] # 安装门禁:新能力 × 已装能力(--manifests 支持一包)
79
79
  mf compare <before.json> <after.json> [--json]
80
80
  mf ablate --tool <name> [--suite <file>] [--profile <group>] [--models <ids>] [--max-cost <cny>] [--progress] [--json]
81
81
  mf cluster [--suite <file>] [--threshold <0.5>] [--max-size <10>] [--out <file>] [--json] # 建议挂载分组 + 同族候选
@@ -584,11 +584,24 @@ export async function runSandboxCheck(options, io = console) {
584
584
  // Advisory install gate: static only, never blocks. Exit code stays 0 for a
585
585
  // warn verdict so a platform can log it without failing the install.
586
586
  export async function runCheck(options, io = console) {
587
- if (!options.manifest) throw new Error("--manifest <file> 必填(新能力的 [name, description, parameters?])");
588
- const manifest = JSON.parse(await readFile(resolve(options.manifest), "utf8"));
589
587
  const installed = options.installed
590
588
  ? JSON.parse(await readFile(resolve(options.installed), "utf8"))
591
589
  : [];
590
+
591
+ if (options.manifests) {
592
+ const manifests = JSON.parse(await readFile(resolve(options.manifests), "utf8"));
593
+ const result = checkMany({ manifests, installed });
594
+ noteUsage({
595
+ checkVerdict: result.verdict,
596
+ checkFindings: Object.values(result.summary).reduce((sum, count) => sum + count, 0)
597
+ });
598
+ io.log(options.json ? JSON.stringify(result, null, 2) : formatCheckMany(result));
599
+ return 0;
600
+ }
601
+ if (!options.manifest) {
602
+ throw new Error("--manifest 或 --manifests <file> 必填([name, description, parameters?])");
603
+ }
604
+ const manifest = JSON.parse(await readFile(resolve(options.manifest), "utf8"));
592
605
  const result = check({ manifest, installed });
593
606
  noteUsage({ checkVerdict: result.verdict, checkFindings: result.findings.length });
594
607
  io.log(options.json ? JSON.stringify(result, null, 2) : formatCheck(result));
@@ -1009,6 +1022,7 @@ function parseArgs(argv) {
1009
1022
  dryRun: false,
1010
1023
  tool: null,
1011
1024
  manifest: null,
1025
+ manifests: null,
1012
1026
  installed: null,
1013
1027
  threshold: 0.5,
1014
1028
  maxSize: 10,
@@ -1077,6 +1091,7 @@ function parseArgs(argv) {
1077
1091
  case "--dry-run": options.dryRun = true; break;
1078
1092
  case "--tool": options.tool = value(); break;
1079
1093
  case "--manifest": options.manifest = resolve(value()); break;
1094
+ case "--manifests": options.manifests = resolve(value()); break;
1080
1095
  case "--installed": options.installed = resolve(value()); break;
1081
1096
  case "--threshold": {
1082
1097
  const raw = Number(value());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelfunction-cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "mf — function-calling accuracy scorecard for AI agents",