@stalfh233/omc-cli 0.6.19 → 0.6.20
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 +11 -0
- package/dist/cli.js +4 -4
- package/dist/config-store.js +20 -1
- package/dist/mcp-graph.js +108 -18
- package/dist/tool-types.js +14 -2
- package/package.json +3 -3
- package/skills/omc-refusals.md +5 -0
- package/skills/omc-start-here.md +5 -0
package/dist/cli-contract.js
CHANGED
|
@@ -386,6 +386,17 @@ export function classifyResult(result) {
|
|
|
386
386
|
const nextCommand = nextCommandFor(result) ?? (auth ? undefined : "omc help omc-refusals");
|
|
387
387
|
return { status: route.envelopeStatus, error: nextCommand === undefined ? error : { ...error, nextCommand }, exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.policyRefused };
|
|
388
388
|
}
|
|
389
|
+
// Cross-project environment isolation is a local policy gate. It is emitted
|
|
390
|
+
// as an error-shaped result by the call seam so ordinary ConfigurationError
|
|
391
|
+
// failures (unknown env, missing credentials, malformed config) retain their
|
|
392
|
+
// historical classification.
|
|
393
|
+
if (result.blocker === "cross-environment-policy" || /^跨环境访问被拒绝:/u.test(String(result.message ?? ""))) {
|
|
394
|
+
return {
|
|
395
|
+
status: "error",
|
|
396
|
+
error: withNext({ code: "policy-refused", message: String(result.message ?? "cross-environment access refused"), retryable: false }),
|
|
397
|
+
exitCode: CLI_EXIT_CODES.policyRefused,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
389
400
|
// r25 沙盒(2026-11-04,B1 的同类):`meta.get_status` 返回 `status:"failed"`、
|
|
390
401
|
// `rollback apply` 返回 `status:"stopped"`——两个都曾掉进 internal(exit 15):
|
|
391
402
|
// 调用方看到「CLI 内部故障」,其实是「配置/凭据读回失败」或「恢复链在 N 步停下」。
|
package/dist/cli.js
CHANGED
|
@@ -1666,7 +1666,7 @@ function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
|
|
|
1666
1666
|
}
|
|
1667
1667
|
catch (error) {
|
|
1668
1668
|
if (error instanceof GraphIndexAbsentError)
|
|
1669
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1669
|
+
return emitResult({ status: "refused", blocker: "graph-index-absent", message: `${error.message} 只读核查(不写索引):omc graph status --env <env>(当前配置根);MCP 面工具读的可能是另一配置根,比对 dbId/dbPath。`, nextCommand: "omc graph pull --env <env>" }, transport);
|
|
1670
1670
|
return emitThrown(error, transport);
|
|
1671
1671
|
}
|
|
1672
1672
|
}
|
|
@@ -1680,7 +1680,7 @@ function cmdGraphStatus(transport) {
|
|
|
1680
1680
|
}
|
|
1681
1681
|
catch (error) {
|
|
1682
1682
|
if (error instanceof GraphIndexAbsentError)
|
|
1683
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1683
|
+
return emitResult({ status: "refused", blocker: "graph-index-absent", message: `${error.message} 只读核查(不写索引):omc graph status --env <env>(当前配置根);MCP 面工具读的可能是另一配置根,比对 dbId/dbPath。`, nextCommand: "omc graph pull --env <env>" }, transport);
|
|
1684
1684
|
return emitThrown(error, transport);
|
|
1685
1685
|
}
|
|
1686
1686
|
}
|
|
@@ -1693,7 +1693,7 @@ function cmdGraphImpact(kind, code, transport) {
|
|
|
1693
1693
|
}
|
|
1694
1694
|
catch (error) {
|
|
1695
1695
|
if (error instanceof GraphIndexAbsentError)
|
|
1696
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1696
|
+
return emitResult({ status: "refused", blocker: "graph-index-absent", message: `${error.message} 只读核查(不写索引):omc graph status --env <env>(当前配置根);MCP 面工具读的可能是另一配置根,比对 dbId/dbPath。`, nextCommand: "omc graph pull --env <env>" }, transport);
|
|
1697
1697
|
return emitThrown(error, transport);
|
|
1698
1698
|
}
|
|
1699
1699
|
}
|
|
@@ -1704,7 +1704,7 @@ function cmdGraphSearch(term, transport) {
|
|
|
1704
1704
|
}
|
|
1705
1705
|
catch (error) {
|
|
1706
1706
|
if (error instanceof GraphIndexAbsentError)
|
|
1707
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1707
|
+
return emitResult({ status: "refused", blocker: "graph-index-absent", message: `${error.message} 只读核查(不写索引):omc graph status --env <env>(当前配置根);MCP 面工具读的可能是另一配置根,比对 dbId/dbPath。`, nextCommand: "omc graph pull --env <env>" }, transport);
|
|
1708
1708
|
return emitThrown(error, transport);
|
|
1709
1709
|
}
|
|
1710
1710
|
}
|
package/dist/config-store.js
CHANGED
|
@@ -14,6 +14,12 @@ export function removedProfileKeys(profile) {
|
|
|
14
14
|
return REMOVED_PROFILE_KEYS.filter((key) => raw[key] !== undefined);
|
|
15
15
|
}
|
|
16
16
|
export class ConfigurationError extends Error {
|
|
17
|
+
policyRefused;
|
|
18
|
+
constructor(message, options = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ConfigurationError";
|
|
21
|
+
this.policyRefused = options.policyRefused === true;
|
|
22
|
+
}
|
|
17
23
|
}
|
|
18
24
|
export function defaultPaths(projectDir = process.cwd()) {
|
|
19
25
|
const configRoot = process.env.OMC_CONFIG_HOME
|
|
@@ -290,12 +296,25 @@ export class ConfigStore {
|
|
|
290
296
|
const allowText = allowlist.length === 0
|
|
291
297
|
? `.omc-envs=(无或不存在;准入回退到 .omc-env ∪ 全局 default)`
|
|
292
298
|
: `.omc-envs=${allowlist.join(",")}`;
|
|
299
|
+
const configuredEnvironments = this.readConfig().environments;
|
|
300
|
+
const candidates = Object.entries(configuredEnvironments)
|
|
301
|
+
.filter(([, profile]) => profile.applicationCode === name);
|
|
302
|
+
// If several environment names point at the same application, recommend
|
|
303
|
+
// the one the caller is already working in before falling back to the
|
|
304
|
+
// machine-wide configuration order. This avoids steering an agent from a
|
|
305
|
+
// project-local `local` environment to an unrelated `prod` entry merely
|
|
306
|
+
// because `prod` appears first in omc.config.json.
|
|
307
|
+
const applicationHint = candidates.find(([candidate]) => candidate === bound)?.[0]
|
|
308
|
+
?? candidates.find(([candidate]) => allowlist.includes(candidate))?.[0]
|
|
309
|
+
?? candidates.find(([, profile]) => profile.isDefault)?.[0]
|
|
310
|
+
?? candidates[0]?.[0];
|
|
293
311
|
throw new ConfigurationError(`跨环境访问被拒绝:环境 "${name}" 不属于本项目(本项目允许:${own.join("、") || "(none)"})。` +
|
|
294
312
|
`解析到的项目根=${projectRoot};${boundText};${allowText}。` +
|
|
295
313
|
`准入由「当前工作目录」下的 .omc-envs(白名单)+ .omc-env(默认绑定)共同决定,不是由 --env 参数单独决定;` +
|
|
296
314
|
`在无声明的目录里 --env local 会落到机器全局 default(常见是 test)。` +
|
|
297
315
|
`请改到声明了该环境的项目根再跑,或先向用户报告并取得同意后执行 \`omc env allow ${name}\`(写入项目 .omc-envs),` +
|
|
298
|
-
`或临时设置 OMC_ALLOW_CROSS_ENV=1 单次放行。`
|
|
316
|
+
`或临时设置 OMC_ALLOW_CROSS_ENV=1 单次放行。` +
|
|
317
|
+
(applicationHint === undefined ? "" : `\n也许你指的是 --env ${applicationHint}(app=${name})。`), { policyRefused: true });
|
|
299
318
|
}
|
|
300
319
|
bindProjectEnvironment(name) {
|
|
301
320
|
const config = this.readConfig();
|
package/dist/mcp-graph.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import { createInterface } from "node:readline";
|
|
@@ -60,7 +61,7 @@ const EXPLORE_TOOL = {
|
|
|
60
61
|
"一次查全一个目标的引用与影响面(本地索引,毫秒级)。",
|
|
61
62
|
"输入 query 可为业务名(实体 name+code 子串)或规则 code+name 词首;也可直接给字段/模型/服务 code。kind 可选 field|model|service,不给则三种都查。",
|
|
62
63
|
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via}]}, impact:{kind:totals}, search:{matches:{model,field,service},rules}}。",
|
|
63
|
-
"references/impact 支持 offset+limit 分页;truncated 时按返回的 nextOffset
|
|
64
|
+
"references/impact 支持 offset+limit 分页;truncated 时按返回的 nextOffset 继续调用。实体分页结果带 nextCall;分页不会改变 resolved。字段同名时请用 schemaCode 消歧。",
|
|
64
65
|
"改/删/重命名任何字段、模型、服务之前必查;找\"关于 X 的规则\"拿 code 也走这里。",
|
|
65
66
|
"索引不含规则条件叶子与流程绑定;freshness=partial 时命中可能不完整(答案里有 gapWarning)。",
|
|
66
67
|
].join(" "),
|
|
@@ -69,6 +70,7 @@ const EXPLORE_TOOL = {
|
|
|
69
70
|
properties: {
|
|
70
71
|
query: { type: "string", description: "业务名(实体 name+code 子串)、字段/模型/服务 code,或规则检索词(规则 code+name 词首匹配)" },
|
|
71
72
|
kind: { type: "string", enum: [...KINDS], description: "限定一种引用种类;省略则 field/model/service 全查" },
|
|
73
|
+
schemaCode: { type: "string", description: "限定字段所属模型;也可用于校验模型。" },
|
|
72
74
|
env: { type: "string", description: "期望的环境名;与索引所属环境不一致时会拒绝(防跨环境误答)" },
|
|
73
75
|
offset: { type: "integer", minimum: 0, description: "引用/实体结果起始偏移;truncated 时使用 nextOffset" },
|
|
74
76
|
limit: { type: "integer", minimum: 1, maximum: MATCH_CAP, description: "每类结果最多返回多少条(默认 50)" },
|
|
@@ -78,7 +80,7 @@ const EXPLORE_TOOL = {
|
|
|
78
80
|
};
|
|
79
81
|
const STATUS_TOOL = {
|
|
80
82
|
name: "omc_graph_status",
|
|
81
|
-
description: "
|
|
83
|
+
description: "索引完整性与新鲜度(所有身份与新鲜度字段同层):environmentName、freshness(complete|partial)、模型/规则计数、pulledAtIso、ageMinutes、staleAfterMinutes、dbId、dbPath、coverage、gaps。超过阈值会给 warning;索引缺失会给出 pull 指引。",
|
|
82
84
|
inputSchema: { type: "object", properties: {} },
|
|
83
85
|
};
|
|
84
86
|
export function graphToolDefinitions() {
|
|
@@ -90,6 +92,57 @@ function toolResult(payload, isError = false) {
|
|
|
90
92
|
function toolError(message) {
|
|
91
93
|
return toolResult({ status: "error", message }, true);
|
|
92
94
|
}
|
|
95
|
+
function withMatchedOn(row, query) {
|
|
96
|
+
const name = row.name ?? "";
|
|
97
|
+
const code = row.code ?? row.schemaCode ?? "";
|
|
98
|
+
const on = name.includes(query) ? "name" : "code";
|
|
99
|
+
const value = on === "name" ? name : code;
|
|
100
|
+
const at = value.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
|
|
101
|
+
if (at < 0)
|
|
102
|
+
return { ...row, matchedOn: { on, fragment: query } };
|
|
103
|
+
let start = at;
|
|
104
|
+
let end = at + query.length;
|
|
105
|
+
while (start > 0 && /[\p{L}\p{N}]/u.test(value[start - 1]))
|
|
106
|
+
start--;
|
|
107
|
+
while (end < value.length && /[\p{L}\p{N}]/u.test(value[end]))
|
|
108
|
+
end++;
|
|
109
|
+
return { ...row, matchedOn: { on, fragment: value.slice(start, end) } };
|
|
110
|
+
}
|
|
111
|
+
function levenshtein(a, b) {
|
|
112
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
113
|
+
for (let i = 1; i <= a.length; i++) {
|
|
114
|
+
let corner = prev[0];
|
|
115
|
+
prev[0] = i;
|
|
116
|
+
for (let j = 1; j <= b.length; j++) {
|
|
117
|
+
const old = prev[j];
|
|
118
|
+
prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, corner + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
119
|
+
corner = old;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return prev[b.length];
|
|
123
|
+
}
|
|
124
|
+
function entityInventory() {
|
|
125
|
+
const db = new DatabaseSync(graphDbPath(), { readOnly: true });
|
|
126
|
+
try {
|
|
127
|
+
return [
|
|
128
|
+
...db.prepare("SELECT schema_code, name FROM models").all().map((r) => ({ kind: "model", schemaCode: r.schema_code, code: r.schema_code, name: r.name ?? "" })),
|
|
129
|
+
...db.prepare("SELECT schema_code, code, name FROM fields").all().map((r) => ({ kind: "field", schemaCode: r.schema_code, code: r.code, name: r.name ?? "" })),
|
|
130
|
+
...db.prepare("SELECT code, name FROM services").all().map((r) => ({ kind: "service", code: r.code, name: r.name ?? "" })),
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
db.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function suggestions(query) {
|
|
138
|
+
return entityInventory().map((row) => {
|
|
139
|
+
const q = query.toLocaleLowerCase();
|
|
140
|
+
const values = [row.code, row.name].map((v) => v.toLocaleLowerCase());
|
|
141
|
+
const prefix = values.some((v) => v.startsWith(q)) ? 0 : values.some((v) => v.includes(q)) ? 1 : 2;
|
|
142
|
+
const distance = Math.min(...values.map((v) => levenshtein(q, v)));
|
|
143
|
+
return { kind: row.kind, code: row.code, name: row.name, score: prefix * 1000 + distance };
|
|
144
|
+
}).sort((a, b) => a.score - b.score || a.code.localeCompare(b.code)).slice(0, 3);
|
|
145
|
+
}
|
|
93
146
|
function cap(rows, offset = 0, limit = MATCH_CAP) {
|
|
94
147
|
const page = rows.slice(offset, offset + limit);
|
|
95
148
|
return { rows: page, truncated: offset + page.length < rows.length, offset, limit, ...(offset + page.length < rows.length ? { nextOffset: offset + page.length } : {}), total: rows.length };
|
|
@@ -100,7 +153,9 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
100
153
|
try {
|
|
101
154
|
if (name === STATUS_TOOL.name) {
|
|
102
155
|
const status = readGraphStatus();
|
|
103
|
-
|
|
156
|
+
// Keep a tiny compatibility locator for older MCP clients; all identity,
|
|
157
|
+
// freshness, coverage and gaps fields live at the top level.
|
|
158
|
+
return toolResult({ status: "ok", ...status, ...status.index, index: { environmentName: status.index.environmentName } });
|
|
104
159
|
}
|
|
105
160
|
if (name === EXPLORE_TOOL.name) {
|
|
106
161
|
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
@@ -109,6 +164,9 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
109
164
|
const kind = args.kind === undefined ? undefined : String(args.kind);
|
|
110
165
|
if (kind !== undefined && !KINDS.includes(kind))
|
|
111
166
|
return toolError(`kind 必须是 ${KINDS.join("|")} 之一`);
|
|
167
|
+
const schemaCode = typeof args.schemaCode === "string" && args.schemaCode.trim() ? args.schemaCode.trim() : undefined;
|
|
168
|
+
if (schemaCode && kind !== "field" && kind !== "model")
|
|
169
|
+
return toolError("schemaCode 仅适用于 kind=field|model");
|
|
112
170
|
const status = readGraphStatus();
|
|
113
171
|
const index = status.index;
|
|
114
172
|
if (typeof args.env === "string" && args.env.trim() && args.env.trim() !== index.environmentName) {
|
|
@@ -119,14 +177,32 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
119
177
|
const limit = typeof args.limit === "number" && Number.isSafeInteger(args.limit) && args.limit > 0 && args.limit <= MATCH_CAP ? args.limit : MATCH_CAP;
|
|
120
178
|
const references = {};
|
|
121
179
|
const impact = {};
|
|
180
|
+
// Resolve from an unpaged search. The result page is presentation only;
|
|
181
|
+
// otherwise offset=2 can silently change the subject being inspected.
|
|
182
|
+
const search = searchGraphRules(query, limit, offset);
|
|
183
|
+
const resolvedSearch = searchGraphRules(query, MATCH_CAP, 0);
|
|
184
|
+
const loweredQuery = query.toLocaleLowerCase();
|
|
185
|
+
const allCandidates = entityInventory().filter((r) => (r.code.toLocaleLowerCase().includes(loweredQuery) || r.name.toLocaleLowerCase().includes(loweredQuery)) &&
|
|
186
|
+
(!schemaCode || (r.kind !== "service" && r.schemaCode === schemaCode)));
|
|
187
|
+
const typedCandidates = kind ? allCandidates.filter((r) => r.kind === kind) : allCandidates;
|
|
188
|
+
const fieldCandidates = kind === "field" ? typedCandidates : allCandidates.filter((r) => r.kind === "field");
|
|
189
|
+
const exactSame = fieldCandidates.filter((r) => r.code === query);
|
|
190
|
+
const ambiguous = kind === "field" && !schemaCode && exactSame.length > 1;
|
|
191
|
+
const resolvedEntity = ambiguous ? undefined : typedCandidates[0] ?? (kind === undefined ? allCandidates[0] : undefined);
|
|
192
|
+
const subjectByName = query !== (resolvedEntity?.code ?? "") && resolvedEntity;
|
|
193
|
+
const subjects = resolvedEntity
|
|
194
|
+
? [{ kind: resolvedEntity.kind, code: resolvedEntity.kind === "model" ? (resolvedEntity.schemaCode ?? resolvedEntity.code) : resolvedEntity.code }]
|
|
195
|
+
: [];
|
|
122
196
|
for (const one of kinds) {
|
|
123
|
-
const
|
|
124
|
-
const
|
|
197
|
+
const subject = subjects.find((s) => s.kind === one);
|
|
198
|
+
const ref = queryGraphReferences(one, subject?.code ?? query);
|
|
199
|
+
const scopedMatches = subject && one === "field" && schemaCode ? ref.matches.filter((row) => row.schemaCode === schemaCode) : ref.matches;
|
|
200
|
+
const capped = cap(scopedMatches, offset, limit);
|
|
125
201
|
// r35a-F4c(真机 2026-11-06):**MCP 面是第三个消费者**——CLI 的 references
|
|
126
202
|
// 与 impact 都带 coverage 了,这里若不显式透传,MCP 用户看到的仍是
|
|
127
203
|
// 「matches:[] + 一句旧 note」,照样会把「表单没扫」误读成「没人引用」。
|
|
128
204
|
references[one] = {
|
|
129
|
-
count:
|
|
205
|
+
count: scopedMatches.length,
|
|
130
206
|
rows: capped.rows,
|
|
131
207
|
offset: capped.offset,
|
|
132
208
|
limit: capped.limit,
|
|
@@ -134,35 +210,49 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
134
210
|
...(ref.coverage ? { coverage: ref.coverage } : {}),
|
|
135
211
|
...(ref.gapWarning ? { gapWarning: ref.gapWarning } : {}),
|
|
136
212
|
};
|
|
137
|
-
if (
|
|
138
|
-
const imp = impactGraph(one, query);
|
|
213
|
+
if (scopedMatches.length > 0 || kind !== undefined || subject !== undefined) {
|
|
214
|
+
const imp = impactGraph(one, subject?.code ?? query);
|
|
215
|
+
const scopedRules = subject && one === "field" && schemaCode ? imp.rules.filter((row) => row.schemaCode === schemaCode) : imp.rules;
|
|
139
216
|
impact[one] = {
|
|
140
|
-
totals: imp.totals,
|
|
141
|
-
rules: cap(
|
|
217
|
+
totals: scopedRules.length === imp.rules.length ? imp.totals : { ...imp.totals, rules: scopedRules.length },
|
|
218
|
+
rules: cap(scopedRules, offset, limit),
|
|
142
219
|
datarules: cap(imp.datarules, offset, limit),
|
|
143
220
|
listviews: cap(imp.listviews, offset, limit),
|
|
221
|
+
...(one === "model" ? {
|
|
222
|
+
fieldsReferenced: [...imp.rules.flatMap((r) => r.touches.field.map((code) => ({ code, ruleCode: r.ruleCode }))).reduce((m, x) => {
|
|
223
|
+
const item = m.get(x.code) ?? { code: x.code, touchedByRules: [] };
|
|
224
|
+
if (!item.touchedByRules.includes(x.ruleCode))
|
|
225
|
+
item.touchedByRules.push(x.ruleCode);
|
|
226
|
+
m.set(x.code, item);
|
|
227
|
+
return m;
|
|
228
|
+
}, new Map()).values()],
|
|
229
|
+
} : {}),
|
|
144
230
|
...(imp.coverage ? { coverage: imp.coverage } : {}),
|
|
145
231
|
};
|
|
146
232
|
}
|
|
147
233
|
}
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
search.rules[0] ? { kind: "rule", schemaCode: search.rules[0].schemaCode, code: search.rules[0].ruleCode } : undefined;
|
|
234
|
+
const decorate = (row) => withMatchedOn({ ...row, ...(row.schemaCode && row.code === undefined ? { code: row.schemaCode } : {}) }, query);
|
|
235
|
+
const decoratedMatches = { ...search.matches, model: { ...search.matches.model, items: search.matches.model.items.map(decorate) }, field: { ...search.matches.field, items: search.matches.field.items.map(decorate) }, service: { ...search.matches.service, items: search.matches.service.items.map(decorate) } };
|
|
236
|
+
const noEntityHit = search.matches.model.total === 0 && search.matches.field.total === 0 && search.matches.service.total === 0;
|
|
237
|
+
const composite = /\s|\+|,|\b(?:AND|OR)\b/i.test(query);
|
|
153
238
|
const searchNote = "实体检索:业务名/实体 name+code 使用子串匹配。\n规则检索:规则 code+name 使用词首匹配(中文子串不命中)。";
|
|
154
239
|
const entityTruncated = Object.values(search.matches).some((match) => match.truncated === true);
|
|
155
|
-
const
|
|
240
|
+
const nextCall = entityTruncated ? Object.fromEntries(Object.entries(search.matches).filter(([, m]) => m.truncated).map(([k, m]) => [k, { query, kind: k, offset: m.nextOffset, limit }])) : undefined;
|
|
241
|
+
const paginationNote = entityTruncated ? `\n结果已截断:请使用 nextCall 中对应调用继续。` : "";
|
|
156
242
|
return toolResult({
|
|
157
243
|
status: "ok",
|
|
158
244
|
query,
|
|
159
245
|
found: resolvedEntity !== undefined,
|
|
160
|
-
...(resolvedEntity ? { resolved: resolvedEntity } : {}),
|
|
246
|
+
...(resolvedEntity ? { resolved: { kind: resolvedEntity.kind, schemaCode: "schemaCode" in resolvedEntity ? resolvedEntity.schemaCode : "", code: resolvedEntity.code, matchedOn: { on: resolvedEntity.name.includes(query) ? "name" : "code", fragment: query } } } : {}),
|
|
247
|
+
...(ambiguous ? { ambiguous: true, candidates: exactSame.map((r) => ({ schemaCode: r.schemaCode, code: r.code, name: r.name })) } : {}),
|
|
161
248
|
...(kind !== undefined ? { kind } : {}),
|
|
162
249
|
index,
|
|
163
250
|
references,
|
|
164
251
|
impact,
|
|
165
|
-
search: { count: search.rules.length, rules: search.rules, matches:
|
|
252
|
+
search: { count: search.rules.length, rules: search.rules, matches: decoratedMatches, coverage: search.coverage },
|
|
253
|
+
...(nextCall ? { nextCall } : {}),
|
|
254
|
+
...(noEntityHit && composite ? { unsupportedQuerySyntax: true, hint: "组合检索暂不支持,请拆词单查后求交" } : {}),
|
|
255
|
+
...(noEntityHit ? { suggestions: suggestions(query), hint: "可尝试 suggestions 中的实体 code/name;引用明细请使用匹配 code 查 references/impact" } : {}),
|
|
166
256
|
searchNote: `${searchNote}${paginationNote}`,
|
|
167
257
|
...(search.note ? { searchNote: `${searchNote}\n${search.note}` } : {}),
|
|
168
258
|
// 旧句「索引不含规则条件叶子与流程绑定」漏了表单与流程权限(r35a-F4 实证
|
package/dist/tool-types.js
CHANGED
|
@@ -187,6 +187,14 @@ const CONTEXTUAL_KEY_GUIDANCE = Object.freeze({
|
|
|
187
187
|
precision: "precision→options.scale(数值小数位,不是顶层 precision)",
|
|
188
188
|
}),
|
|
189
189
|
});
|
|
190
|
+
// A key can be valid for a sibling tool while still being invalid here. That
|
|
191
|
+
// is a capability-routing problem, not a spelling problem, so do not present
|
|
192
|
+
// the misleading "no close key" sentence. Keep this explicit and narrow: an
|
|
193
|
+
// arbitrary key must continue to use the ordinary closest-key hint.
|
|
194
|
+
const SIBLING_TOOL_KEY_HINTS = Object.freeze({
|
|
195
|
+
"app.update": Object.freeze({ name: '"name" 是 app.create 的入参;app.update 不收它' }),
|
|
196
|
+
"model.create": Object.freeze({ fields: '"fields" 是 configure_model 的入参;model.create 不收它' }),
|
|
197
|
+
});
|
|
190
198
|
function closestSchemaKey(key, schemaKeys) {
|
|
191
199
|
if (schemaKeys.has(key))
|
|
192
200
|
return key;
|
|
@@ -249,6 +257,9 @@ export function parseToolInput(tool, input) {
|
|
|
249
257
|
const guidance = TWO_STEP_GUIDANCE[tool.name]?.[key] ?? CONTEXTUAL_KEY_GUIDANCE[tool.name]?.[key];
|
|
250
258
|
if (guidance !== undefined)
|
|
251
259
|
return guidance;
|
|
260
|
+
const sibling = SIBLING_TOOL_KEY_HINTS[tool.name]?.[key];
|
|
261
|
+
if (sibling !== undefined)
|
|
262
|
+
return sibling;
|
|
252
263
|
const semantic = KEY_SEMANTIC_HINTS[key];
|
|
253
264
|
const contextual = contextualClosestKey(tool.name, key, schemaKeys);
|
|
254
265
|
const closest = closestSchemaKey(key, schemaKeys);
|
|
@@ -259,14 +270,15 @@ export function parseToolInput(tool, input) {
|
|
|
259
270
|
// 未知键的下一步应指向**正确的那个参数**,不是整份 describe(4–17 KB)。
|
|
260
271
|
// 有近邻时 `--param <nearKey>` 是 150–900 B,且直接回答「本来想写哪个键」。
|
|
261
272
|
// 语境建议优先于编辑距离(r34a-F18:data.delete 的 id 指 requestId 是噪声)。
|
|
273
|
+
const siblingHints = unknown.map((key) => SIBLING_TOOL_KEY_HINTS[tool.name]?.[key]).filter((hint) => typeof hint === "string");
|
|
262
274
|
const nearestKey = contextualClosestKey(tool.name, unknown[0], schemaKeys) ?? closestSchemaKey(unknown[0], schemaKeys);
|
|
263
275
|
const paramCommand = nearestKey !== undefined ? `omc describe ${tool.name} --param ${nearestKey}` : `omc describe ${tool.name}`;
|
|
264
276
|
throw new ToolInputError({
|
|
265
277
|
status: "error",
|
|
266
278
|
blocker: "tool-input-unknown-keys",
|
|
267
279
|
tool: tool.name,
|
|
268
|
-
message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。${twoStep.length > 0 ? twoStep.join(" ") : `${guidanceLines.length > 0 ? guidanceLines.join(";") : `最接近的合法键:${suggestions.join(";")}`}。参数表:${paramCommand}`}`,
|
|
269
|
-
...(twoStep.length > 0 || guidanceLines.length > 0 ? { nextCommand: `omc describe ${tool.name}` } : { nextCommand: paramCommand }),
|
|
280
|
+
message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。${twoStep.length > 0 ? twoStep.join(" ") : siblingHints.length > 0 ? siblingHints.join(";") : `${guidanceLines.length > 0 ? guidanceLines.join(";") : `最接近的合法键:${suggestions.join(";")}`}。参数表:${paramCommand}`}`,
|
|
281
|
+
...(twoStep.length > 0 || guidanceLines.length > 0 || siblingHints.length > 0 ? { nextCommand: `omc describe ${tool.name}` } : { nextCommand: paramCommand }),
|
|
270
282
|
issues: unknown.map((key) => ({ path: [key], code: "unrecognized_key", message: issueHintFor(key) })),
|
|
271
283
|
});
|
|
272
284
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.20",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"commander": "14.0.3",
|
|
58
|
-
"omc-core": "0.6.
|
|
58
|
+
"omc-core": "0.6.20",
|
|
59
59
|
"proper-lockfile": "4.1.2",
|
|
60
60
|
"write-file-atomic": "6.0.0",
|
|
61
61
|
"zod": "^3.23.8",
|
|
@@ -73,6 +73,6 @@
|
|
|
73
73
|
"directory": "packages/omc-cli"
|
|
74
74
|
},
|
|
75
75
|
"optionalDependencies": {
|
|
76
|
-
"@stalfh233/omc-fake-platform": "0.6.
|
|
76
|
+
"@stalfh233/omc-fake-platform": "0.6.20"
|
|
77
77
|
}
|
|
78
78
|
}
|
package/skills/omc-refusals.md
CHANGED
|
@@ -11,6 +11,11 @@ Grouped by family. For a specific tool's exact wire and valid shapes, follow the
|
|
|
11
11
|
Read `result.blocker` (refused) or `result.status`+`result.reason` (uncertain).
|
|
12
12
|
`result.retryable` is advisory only — the family decides what to do.
|
|
13
13
|
|
|
14
|
+
`status:"refused"` means a local preflight or policy gate stopped the call:
|
|
15
|
+
it necessarily sent zero platform writes; there is no need to second-guess
|
|
16
|
+
whether anything was written. A platform/wire error (`wire-business` or an
|
|
17
|
+
`errcode`) is different: the outcome can be uncertain, so use readback.
|
|
18
|
+
|
|
14
19
|
## 1. Write permission declined
|
|
15
20
|
|
|
16
21
|
- `environment-write-disabled` — the environment profile has `writable:false`.
|
package/skills/omc-start-here.md
CHANGED
|
@@ -28,6 +28,11 @@ be treated as a second source. Read this, then follow the topics it names.
|
|
|
28
28
|
阶梯步骤已内联工具必填参数(`route.ladder[].tools`/`readTools`)与命中 recipe 的 `describe <tool> --example <id>`;据此直接执行——**不要再逐能力 `commands --lane`**,也不要先扫 `commands --json`/全部 `contract`/全部 `describe --full`。
|
|
29
29
|
方法论按 `route.readSections` 逐节读:对每条 `{topic, section}` 执行 `omc help <topic> --section "<section>"`(单节 0.4–3.4KB)。`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数);**不要 `--all` 读整篇**(5–14KB,读一次就留在后续每一轮窗口)。**带着具体问题(错误码/跨能力语义/某个词)直接 `omc help --search "<词>"`**——跨全部 topic 全文检索,命中节的正文随结果内联返回,一次调用就是答案;**严禁自己写 `for t in …; do omc help $t --all | grep …` 循环**(r18 实测 12 调用/176KB,检索一次 8KB 搞定)。
|
|
30
30
|
1b. **改/删/重命名既有对象前先看影响面(本地规则图索引)**:`omc graph pull --env <env>` 一次拉全平台建本地索引(增量、秒级、带锁与 gap 报告),之后 `omc graph references <service|model|field> <code>` 离线答「谁引用了它」——一调替代逐模型 `rule.get` 枚举(r30 实测影响评估手工枚举 8×rule.get,graph 一调可达);`impact <kind> <code>` 看爆炸半径、`search <词>` 找规则 code、`status` 看索引新鲜度。索引不含规则条件叶子与流程绑定,命中后仍以 `rule.get` 原文为准。无索引时会拒绝并指路 pull。宿主已接 MCP 时用 `omc mcp-graph --path <项目根>`(工具 `omc_graph_explore`/`omc_graph_status`)。
|
|
31
|
+
|
|
32
|
+
**图谱只覆盖单应用时怎么按应用盘点(只读)**:先用
|
|
33
|
+
`omc call app.list` 列出应用,再用
|
|
34
|
+
`omc call model.list --args '{"applicationCode":"<code>"}'` 按应用盘点模型。
|
|
35
|
+
graph 索引 coverage 默认只含配置应用;coverage 未命中不等于应用或模型不存在。
|
|
31
36
|
1c. **写后端 Java 源码(在线开发)走工作区而不是一条条 call**:`omc programming pull --env <env> --app <appCode>` 把业务规则源码拉进 `.omc-programming/`(git 可管),改文件后 `omc programming status` → `omc programming apply [--publish]`(逐文件走编程面治理);图形化规则转编码用 `omc programming adopt --schema <s> --rule <r>`;验证用 `omc programming test --schema <s> --rule <r>`(fixture 驱动:compile→publish→触发→断言→清理)。**平台运行时只执行已发布版**:只 save 不 publish 等于没改;publish 内部会编译并把规则切 CODING+enable。包路径/类名不得改。
|
|
32
37
|
2. 写前只需读 `omc-config-draft-review`(人审门)与 `omc-five-piece-flow`(发现→草稿→权限→执行→回读);`omc-business-configuration` 按命中的能力分节读(见 `next` 的 `readSections`),不要整篇预取。
|
|
33
38
|
3. 环境名以 `omc doctor` 的「环境=」或 `.omc-env` 为准;工具不确定先用默认 `describe`/`--example`,只有单个工具缺形状才用 `--full`;`describe` 带 `exampleHint`/`recipeHint` 时优先 `--example`。
|