@stalfh233/omc-cli 0.6.4 → 0.6.6
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/dist/cli-contract.js +18 -2
- package/dist/cli.js +36 -3
- package/dist/support.js +2 -2
- package/dist/tool-types.js +67 -3
- package/package.json +3 -3
package/dist/cli-contract.js
CHANGED
|
@@ -272,15 +272,31 @@ export function classifyResult(result) {
|
|
|
272
272
|
const suffix = candidates.length > 0 ? `;did you mean: ${candidates.join("、")}` : ";用 `omc commands --json --compact --lane <lane>` 或 `omc next` 定位工具";
|
|
273
273
|
return { status: "error", error: withNext({ code: "invalid-input", message: `unknown tool ${String(result.tool ?? "")}${suffix}`, retryable: false }), exitCode: CLI_EXIT_CODES.invalidInput };
|
|
274
274
|
}
|
|
275
|
-
|
|
275
|
+
// r25 探针实证(2026-11-04):`blocker: "invalid-input"` 有 8 处产出点
|
|
276
|
+
// (workflow/runtime-data/compound-writes),但这里只认 `tool-input-*`——于是
|
|
277
|
+
// 它们全落到函数末尾的 internal 分支:`error.code=internal` + exit 15,被调用方
|
|
278
|
+
// 读成"CLI 内部故障",而实际是**自己传错了参数**(探针原话:
|
|
279
|
+
// 「rejected ✓, but error.code=internal (should be invalid-input), and no nextCommand」)。
|
|
280
|
+
// 入参形状错误一律 exit 10,并给出下一步。
|
|
281
|
+
const inputShapeBlocker = result.blocker === "tool-input-invalid"
|
|
282
|
+
|| result.blocker === "tool-input-unknown-keys"
|
|
283
|
+
|| result.blocker === "invalid-input";
|
|
284
|
+
if (inputShapeBlocker) {
|
|
285
|
+
// `invalid-input` 也可能是 CLI 层参数校验(compound-writes 的 --schema 必填),
|
|
286
|
+
// 那时没有 tool 字段——别硬拼一条指向不存在的工具的 describe。
|
|
287
|
+
const toolName = typeof result.tool === "string" && result.tool ? result.tool : undefined;
|
|
288
|
+
const nextCommand = typeof result.nextCommand === "string"
|
|
289
|
+
? result.nextCommand
|
|
290
|
+
: toolName !== undefined ? `omc describe ${toolName}` : undefined;
|
|
276
291
|
return {
|
|
277
292
|
status: "error",
|
|
278
293
|
error: {
|
|
279
294
|
...withNext({ code: "invalid-input", message: String(result.message ?? "tool input is invalid"), retryable: false }),
|
|
280
|
-
nextCommand
|
|
295
|
+
...(nextCommand !== undefined ? { nextCommand } : {}),
|
|
281
296
|
},
|
|
282
297
|
exitCode: CLI_EXIT_CODES.invalidInput,
|
|
283
298
|
};
|
|
299
|
+
}
|
|
284
300
|
if (status === "uncertain")
|
|
285
301
|
return { status, error: withNext({ code: "uncertain", message: String(result.message ?? result.reason ?? "outcome is uncertain"), retryable: true }), exitCode: CLI_EXIT_CODES.uncertain };
|
|
286
302
|
if (status === "refused") {
|
package/dist/cli.js
CHANGED
|
@@ -43,6 +43,7 @@ else if (process.env.OMC_ASCII_STDOUT !== "0" && process.argv.some((arg) => arg
|
|
|
43
43
|
}
|
|
44
44
|
import { commandIndex, commandInventory, describeContract, describeContractFull, describeContractTool, describeExample, describeParam, describeSchemaOnly, describeTool, describeToolFull, filterTools } from "./discovery.js";
|
|
45
45
|
import { closestMatches } from "./closest-match.js";
|
|
46
|
+
import { KEY_ALIAS_HINTS } from "./tool-types.js";
|
|
46
47
|
import { repeatFullDescribe } from "./describe-cache.js";
|
|
47
48
|
import { buildCapabilityOverview, capabilityDetail, capabilityRouteIndex, compactCapabilities, executableCapabilityBundle, resolveCapabilityId, DISCOVERY_AUTHORITY } from "./capability-overview.js";
|
|
48
49
|
import { goalRoute } from "./goal-routes.js";
|
|
@@ -580,8 +581,9 @@ async function cmdCall(store, args) {
|
|
|
580
581
|
for (const key of Object.keys(parsedArgs)) {
|
|
581
582
|
if (schemaKeys.has(key) || recognized.has(key))
|
|
582
583
|
continue;
|
|
583
|
-
|
|
584
|
-
|
|
584
|
+
// r25 真机:`limt:5` 被就近指到 `filter`(共享 "fi" 就够过阈值),照提示改
|
|
585
|
+
// 会把「取 5 条」变成「按条件过滤」。短键走显式缩写表,语义才不跑偏。
|
|
586
|
+
ignoredReadKeys.push({ key, ...suggestArgumentKey(key, [...schemaKeys]) });
|
|
585
587
|
}
|
|
586
588
|
}
|
|
587
589
|
if (tool) {
|
|
@@ -655,7 +657,13 @@ async function cmdCall(store, args) {
|
|
|
655
657
|
}
|
|
656
658
|
// 读面未知键的非阻断回显:放在投影**之后**,否则会被 --jq 的窄投影吞掉。
|
|
657
659
|
if (ignoredReadKeys.length > 0 && typeof result === "object" && result !== null) {
|
|
658
|
-
const pairs = ignoredReadKeys.map((entry) =>
|
|
660
|
+
const pairs = ignoredReadKeys.map((entry) => {
|
|
661
|
+
if (entry.closest)
|
|
662
|
+
return `${entry.key}→${entry.closest}`;
|
|
663
|
+
if (entry.absentInTool)
|
|
664
|
+
return `${entry.key}(本工具没有 ${entry.absentInTool} 参数)`;
|
|
665
|
+
return entry.key;
|
|
666
|
+
});
|
|
659
667
|
result = {
|
|
660
668
|
...result,
|
|
661
669
|
ignoredArgs: {
|
|
@@ -679,6 +687,27 @@ function argvValues(args, name) {
|
|
|
679
687
|
}
|
|
680
688
|
return values;
|
|
681
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* 读面「拼错键」建议(r25 真机实证,2026-11-04):`limt:5` 曾按编辑距离被指到
|
|
692
|
+
* `filter`——两者共享 "fi" 片段就过阈值,但语义完全不同(取 5 条 vs 按条件过滤)。
|
|
693
|
+
* 短键(≤5 字符)靠距离算不准,这里先查显式缩写表(与 tool-types 的 KEY_ALIAS_HINTS
|
|
694
|
+
* 同源),命中且参数真实存在才用;否则退回 closestMatches。
|
|
695
|
+
*/
|
|
696
|
+
export function suggestArgumentKey(key, schemaKeys) {
|
|
697
|
+
const known = new Set(schemaKeys);
|
|
698
|
+
const aliases = KEY_ALIAS_HINTS[key.toLowerCase()] ?? [];
|
|
699
|
+
for (const candidate of aliases) {
|
|
700
|
+
if (known.has(candidate))
|
|
701
|
+
return { closest: candidate };
|
|
702
|
+
}
|
|
703
|
+
// 关键:若这个键明确是某个已知参数的缩写、而**本工具根本没有该参数**(实测
|
|
704
|
+
// `model.list` 就没有 `size`),不能退回最近匹配硬凑一个(`filter` 语义相反)——
|
|
705
|
+
// 返回 absentInTool,让回执直说「本工具没有这个参数」。
|
|
706
|
+
if (aliases.length > 0)
|
|
707
|
+
return { absentInTool: aliases[0] };
|
|
708
|
+
const closest = closestMatches(key, schemaKeys, 1)[0]?.name;
|
|
709
|
+
return closest !== undefined ? { closest } : {};
|
|
710
|
+
}
|
|
682
711
|
function resolvedDefaultEnvironment(store) {
|
|
683
712
|
try {
|
|
684
713
|
return store.resolveEnvironment(undefined).name;
|
|
@@ -1600,6 +1629,10 @@ function onboardingHelp() {
|
|
|
1600
1629
|
"方法论按需读:omc help <topic> 默认只回节索引(每节字节数);用 --section \"<标题>\" 取正文(可重复),确需整篇才 --all。找具体事实(错误码/字段名/命令名)不要整篇 dump:omc help --search \"<词>\"(跨 topic,命中节正文随结果返回)。",
|
|
1601
1630
|
"目标→工具一步定位:omc next \"<你要做什么>\"(离线、只读,给能力/说明/首批命令;不确定会明说并回退到 omc-requirement-analysis)。",
|
|
1602
1631
|
"大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --jq <path>(可重复;`[]` 映射数组,如 items[].fields.planNo)或 --extract/--extract-keys,省掉再跑一次 omc artifact get;artifact get 正文超 8KB 默认也只回形状(--jq/--keys 定向取,--full 才整包)。批量形状预检用 --args-list … --preflight(零 wire、零写入,一次报全)。",
|
|
1632
|
+
// r25 真机(2026-11-04):探针连跑同一读面 2–3 次想验证「第二次是否 unchanged」,
|
|
1633
|
+
// 结果三次都是全量,还去 describe 找 dedupe 参数(多花 3 轮)。读面确实不做跨调用
|
|
1634
|
+
// 结果去重——**明说**比留白强,省掉一次探索。
|
|
1635
|
+
"读面不做跨调用结果去重:同一读面同参数连跑 N 次就是 N 次真实读(不会回 unchanged,也没有 dedupe 开关)。要省就 --jq 定向取值或把结果落到自己的变量里复用;`unchanged:true` 只出现在写面回读缓存那一条路径。",
|
|
1603
1636
|
"工具发现:omc commands --json(含 capability id/lane/recipes;加 --compact 只回工具行、无目录散文)· omc capabilities(能力 id/lane/aliases/tools/依赖顺序)· omc capability <id> --executable(单能力完整执行包)。",
|
|
1604
1637
|
"单工具/契约:omc describe <tool>(schema+recipe+能力路由)· omc contract <lane>(wire 契约+能力路由);环境就绪:omc call discover_environment --env <env> --args '{}'。",
|
|
1605
1638
|
"写入两道门:对话层草稿确认(omc help omc-config-draft-review)+ 环境 writable 开关;生产环境每笔写需在 args 里带 confirmProduction:true。",
|
package/dist/support.js
CHANGED
|
@@ -126,8 +126,8 @@ export function applicationScopeHint(scope, noun) {
|
|
|
126
126
|
export function applicationScopeNotice(scope) {
|
|
127
127
|
const count = scope.applicationCount ?? 1;
|
|
128
128
|
const where = scope.source === "first-tenant-application" && count > 1
|
|
129
|
-
?
|
|
130
|
-
:
|
|
129
|
+
? `租户首个应用「${scope.code}」(租户共 ${count} 个应用)`
|
|
130
|
+
: `应用「${scope.code}」`;
|
|
131
131
|
return `本次只在${where}内扫描(source=${scope.source})——结果被应用边界裁剪,不代表租户全集;跨应用先 app.list 取 applicationCode 再传 applicationCode。`;
|
|
132
132
|
}
|
|
133
133
|
export function flattenTreeRows(rows) {
|
package/dist/tool-types.js
CHANGED
|
@@ -50,6 +50,23 @@ const KEY_SYNONYMS = Object.freeze({
|
|
|
50
50
|
code: ["schemaCode", "sheetCode", "workflowCode"],
|
|
51
51
|
ruleCode: ["code"],
|
|
52
52
|
});
|
|
53
|
+
/**
|
|
54
|
+
* r25 沙盒实证(2026-11-04):探针给 `configure_form` 传了 `publish:true`,
|
|
55
|
+
* 得到「未知键 publish。最接近的合法键:publish→(无相近键)」——写面 fail-closed
|
|
56
|
+
* 是对的,但**意图没被接住**:配置与发布本来就是两步,用户真正要的是
|
|
57
|
+
* `form.publish`。同类还有「配完字段就发布」(`field.publish`)与
|
|
58
|
+
* 「配完列表视图」(无需发布)。这里按工具给出可执行的两步指引,
|
|
59
|
+
* 比「无相近键」有用得多;代价是探针少花一轮(21→22 实测)。
|
|
60
|
+
*/
|
|
61
|
+
const TWO_STEP_GUIDANCE = Object.freeze({
|
|
62
|
+
configure_form: Object.freeze({
|
|
63
|
+
publish: "表单的配置与发布是两步:本工具只写草稿,发布用 `omc call form.publish --args '{\"schemaCode\":\"<模型>\",\"sheetCode\":\"<槽位>\"}'`(也可在本工具的成功回执后紧接着跑)。",
|
|
64
|
+
publishNow: "同上:本工具不负责发布,用 `omc call form.publish`。",
|
|
65
|
+
}),
|
|
66
|
+
configure_model: Object.freeze({
|
|
67
|
+
publish: "模型/字段的配置与发布是两步:字段建好后用 `omc call field.publish --args '{\"schemaCode\":\"<模型>\"}'` 发布;模型本身不需要单独发布。",
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
53
70
|
/**
|
|
54
71
|
* Per-tool input aliases: a synonym the caller may send INSTEAD of the canonical
|
|
55
72
|
* key. Applied in `parseToolInput` BEFORE validation (fill canonical from an
|
|
@@ -115,16 +132,46 @@ function keyRenameHint(issuePath, input, tool) {
|
|
|
115
132
|
return `(该对象在本工具里的键是 ${key},你传的是 ${synonyms.join("/")};键名对照见 omc-tool-selection 的 Key-name table)`;
|
|
116
133
|
}
|
|
117
134
|
/** Cheap closest-key suggestion: exact→prefix→shared-start bigrams, threshold keeps it honest. */
|
|
135
|
+
/**
|
|
136
|
+
* 常见参数缩写 → 规范键。r25 真机实证(2026-11-04):`limt:5` 被指到 `filter`
|
|
137
|
+
* (`limt` 与 `filter` 共享 "fi" 片段就够过阈值),照提示改会从"取 5 条"变成
|
|
138
|
+
* "按条件过滤",是**语义陷阱**;`fieldCodes` 被指到单数 `code`(真正要的是
|
|
139
|
+
* `codes`),照改会变成"只发一个字段"。短键与复数形态靠编辑距离算不准,
|
|
140
|
+
* 用显式小表兜住高频误拼,比调阈值可靠。
|
|
141
|
+
*/
|
|
142
|
+
export const KEY_ALIAS_HINTS = Object.freeze({
|
|
143
|
+
limt: ["size", "limit"],
|
|
144
|
+
limit: ["size"],
|
|
145
|
+
siz: ["size"],
|
|
146
|
+
offst: ["page", "offset"],
|
|
147
|
+
offset: ["page"],
|
|
148
|
+
fieldcodes: ["codes", "fieldCodes", "fields"],
|
|
149
|
+
codes: ["code"],
|
|
150
|
+
fliter: ["filter"],
|
|
151
|
+
filterr: ["filter"],
|
|
152
|
+
order: ["orders"],
|
|
153
|
+
orderby: ["orders"],
|
|
154
|
+
});
|
|
118
155
|
function closestSchemaKey(key, schemaKeys) {
|
|
119
156
|
if (schemaKeys.has(key))
|
|
120
157
|
return key;
|
|
158
|
+
// 0) 显式缩写表优先:命中且该键真实存在才用,避免"建议一个不存在的参数"。
|
|
159
|
+
for (const candidate of KEY_ALIAS_HINTS[key.toLowerCase()] ?? []) {
|
|
160
|
+
if (schemaKeys.has(candidate))
|
|
161
|
+
return candidate;
|
|
162
|
+
}
|
|
121
163
|
let best;
|
|
122
164
|
let bestScore = 0;
|
|
123
165
|
for (const candidate of schemaKeys) {
|
|
124
166
|
const lower = key.toLowerCase();
|
|
125
167
|
const candLower = candidate.toLowerCase();
|
|
126
168
|
let score = 0;
|
|
127
|
-
|
|
169
|
+
// 单复数/尾随 s 的差异要压过零散片段匹配:`fieldCodes`→`fieldCodes` 应胜过 →`code`。
|
|
170
|
+
if (candLower === lower)
|
|
171
|
+
score = 1;
|
|
172
|
+
else if (candLower === `${lower}s` || lower === `${candLower}s`)
|
|
173
|
+
score = 0.85;
|
|
174
|
+
else if (candLower.startsWith(lower) || lower.startsWith(candLower))
|
|
128
175
|
score = 0.9;
|
|
129
176
|
else if (candLower.includes(lower) || lower.includes(candLower))
|
|
130
177
|
score = 0.7;
|
|
@@ -152,12 +199,29 @@ export function parseToolInput(tool, input) {
|
|
|
152
199
|
const unknown = Object.keys(normalized).filter((key) => !schemaKeys.has(key));
|
|
153
200
|
if (unknown.length > 0) {
|
|
154
201
|
const suggestions = unknown.map((key) => `${key}→${closestSchemaKey(key, schemaKeys) ?? "(无相近键,omc describe <tool> 看参数表)"}`);
|
|
202
|
+
// r25 实证:`publish→(无相近键)` 等于没说——配置与发布是两步,用户要的是
|
|
203
|
+
// 另一个工具。有语义指引时优先给可执行的下一步。
|
|
204
|
+
const twoStep = unknown
|
|
205
|
+
.map((key) => TWO_STEP_GUIDANCE[tool.name]?.[key])
|
|
206
|
+
.filter((hint) => typeof hint === "string" && hint.length > 0);
|
|
207
|
+
// r25 真机实证:人读 `message` 已指向 `form.publish`,但 `issues[].message`
|
|
208
|
+
// 仍是 `unknown key; closest: none` —— 程序消费回执时会取 issues[],于是
|
|
209
|
+
// 漏掉发布指引(报告原文:「若用程序消费回执,会选 issues[],就会漏掉」)。
|
|
210
|
+
// 两条通道必须同源,否则等于修了一半。
|
|
211
|
+
const issueHintFor = (key) => {
|
|
212
|
+
const guidance = TWO_STEP_GUIDANCE[tool.name]?.[key];
|
|
213
|
+
if (guidance !== undefined)
|
|
214
|
+
return guidance;
|
|
215
|
+
const closest = closestSchemaKey(key, schemaKeys);
|
|
216
|
+
return closest === undefined ? "unknown key; no close match in this tool's params" : `unknown key; closest: ${closest}`;
|
|
217
|
+
};
|
|
155
218
|
throw new ToolInputError({
|
|
156
219
|
status: "error",
|
|
157
220
|
blocker: "tool-input-unknown-keys",
|
|
158
221
|
tool: tool.name,
|
|
159
|
-
message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}
|
|
160
|
-
|
|
222
|
+
message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。${twoStep.length > 0 ? twoStep.join(" ") : `最接近的合法键:${suggestions.join(";")}。参数表:omc describe ${tool.name}`}`,
|
|
223
|
+
...(twoStep.length > 0 ? { nextCommand: `omc describe ${tool.name}` } : {}),
|
|
224
|
+
issues: unknown.map((key) => ({ path: [key], code: "unrecognized_key", message: issueHintFor(key) })),
|
|
161
225
|
});
|
|
162
226
|
}
|
|
163
227
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@stalfh233/omc-fake-platform": "0.6.
|
|
55
|
+
"@stalfh233/omc-fake-platform": "0.6.6",
|
|
56
56
|
"commander": "14.0.3",
|
|
57
|
-
"omc-core": "0.6.
|
|
57
|
+
"omc-core": "0.6.6",
|
|
58
58
|
"proper-lockfile": "4.1.2",
|
|
59
59
|
"write-file-atomic": "6.0.0",
|
|
60
60
|
"zod": "^3.23.8",
|