@stalfh233/omc-cli 0.5.1 → 0.5.3
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.js +14 -2
- package/dist/graph.js +8 -1
- package/dist/lanes/model.js +10 -27
- package/dist/mcp-graph.js +222 -0
- package/dist/model-purge.js +2 -2
- package/dist/model-record.js +40 -0
- package/dist/rule-support.js +5 -11
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -1839,7 +1839,7 @@ async function main() {
|
|
|
1839
1839
|
});
|
|
1840
1840
|
// ADR-0008: local derived rule-graph index — CLI management face, not a call
|
|
1841
1841
|
// tool (so it stays out of `commands --json`/coverage by construction).
|
|
1842
|
-
const graphCommand = command.command("graph").description("本地派生规则图索引(ADR-0008):pull 全量重建(有界并发)/ references 离线引用 / impact 影响面 / search
|
|
1842
|
+
const graphCommand = command.command("graph").description("本地派生规则图索引(ADR-0008):pull 全量重建(有界并发)/ references 离线引用 / impact 影响面 / search 规则 code+name 检索(非规则体)/ status 完整性与新鲜度。候选定位器——不进 baseline/plan/写门禁");
|
|
1843
1843
|
graphCommand.command("pull").option("--env <environment>").option("--environment <environment>").option("--concurrency <n>", "并发 wire 读数(1–32,默认 16):拉取已流水线化,墙钟 ≈ max(模型面,规则面)/并发 × 延迟;过高可能触发平台限流形成缺口").option("--max-output-bytes <bytes>").addHelpText("after", "\n只读远端(model.list → 每模型 rule.list+datarule.list → 每规则 rule.get,统一任务池流水线并发)+ 本地原子重建;不写环境、不占环境写锁。新鲜度随结果回报(complete|partial),查询走 graph references / impact / search。\n").action(async (options) => {
|
|
1844
1844
|
const parsed = options.concurrency === undefined ? undefined : Number.parseInt(String(options.concurrency), 10);
|
|
1845
1845
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphPull(options.environment ?? options.env, parsed, transport));
|
|
@@ -1850,12 +1850,24 @@ async function main() {
|
|
|
1850
1850
|
graphCommand.command("impact <kind> <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n影响面(离线):跳1=引用该编码的规则(同 references),跳2=这些规则还读写哪些 service/model/field——改前看爆炸半径。截断上限 50 规则×每类 20 码。\n").action(async (kind, code, options) => {
|
|
1851
1851
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphImpact(kind, code, transport));
|
|
1852
1852
|
});
|
|
1853
|
-
graphCommand.command("search <term...>").option("--max-output-bytes <bytes>").addHelpText("after", "\n
|
|
1853
|
+
graphCommand.command("search <term...>").option("--max-output-bytes <bytes>").addHelpText("after", "\n检索规则的 code+name(FTS5,末词前缀匹配;**不含规则体/配置内容**)。找到\"关于 X 的规则\"拿 code,再走 references/impact。中文必须是全词或词首通配(如 报名* / 增加预算*)——子串不命中:FTS5 把「增加预算」当一个 token,搜「预算」为 0。\n").action(async (term, options) => {
|
|
1854
1854
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphSearch(term.join(" "), transport));
|
|
1855
1855
|
});
|
|
1856
1856
|
graphCommand.command("status").option("--max-output-bytes <bytes>").action(async (options) => {
|
|
1857
1857
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphStatus(transport));
|
|
1858
1858
|
});
|
|
1859
|
+
// MCP stdio server (host registration: cpdocs installer's omc-graph entry;
|
|
1860
|
+
// top-level verb like `codegraph serve --mcp`, not a graph subcommand).
|
|
1861
|
+
// hidden like the graph lane — a transport, not a user verb. Only JSON-RPC
|
|
1862
|
+
// may reach stdout from here; envelopes/banners never do.
|
|
1863
|
+
command
|
|
1864
|
+
.command("mcp-graph", { hidden: true })
|
|
1865
|
+
.description("以 MCP stdio 服务器运行规则图检索(工具:omc_graph_explore / omc_graph_status)")
|
|
1866
|
+
.option("--path <dir>", "项目根目录(定位 .omc/graph/graph.db;MCP 注册时传绝对路径,不信任宿主 cwd)")
|
|
1867
|
+
.action(async (options) => {
|
|
1868
|
+
const { runMcpGraphServer } = await import("./mcp-graph.js");
|
|
1869
|
+
commandExitCode = await runMcpGraphServer(options.path);
|
|
1870
|
+
});
|
|
1859
1871
|
// `--jq` expressions the shell split arrive as several tokens; re-join them.
|
|
1860
1872
|
await command.parseAsync(joinSplitFlagValues(stripOutputFlags(argv), ["--jq"]));
|
|
1861
1873
|
return commandExitCode;
|
package/dist/graph.js
CHANGED
|
@@ -644,7 +644,14 @@ export function searchGraphRules(term, limit = 20) {
|
|
|
644
644
|
rules: matches,
|
|
645
645
|
index: summary,
|
|
646
646
|
...(matches.length === 0
|
|
647
|
-
? {
|
|
647
|
+
? {
|
|
648
|
+
note: tokens.length === 0
|
|
649
|
+
? "检索词为空"
|
|
650
|
+
// 实测摩擦(2026-10-30,zlt测试):中文子串 0 命中后没有下一步提示,
|
|
651
|
+
// 调用方在「换个词再试」里打转。把两个出口都写进 note:CJK 词首
|
|
652
|
+
// 通配 + 找引用走 references/impact。
|
|
653
|
+
: "没有命中规则。索引只覆盖规则的 code+name(不含规则体);且只匹配**词首**——FTS5 把「增加预算」当一个词,「预算」匹配不到它。请给全词(增加预算)、词首(增加 或 增加*),或英文 code 前缀。找\"谁引用了 X\"请走 references/impact <kind> <code>。",
|
|
654
|
+
}
|
|
648
655
|
: {}),
|
|
649
656
|
...(gapped ? { gapWarning: `索引有 ${summary.gaps.count} 个扫描缺口;命中列表可能不完整` } : {}),
|
|
650
657
|
};
|
package/dist/lanes/model.js
CHANGED
|
@@ -14,6 +14,7 @@ import { writeTool } from "../write.js";
|
|
|
14
14
|
import { registerPackageTool } from "../package-tool.js";
|
|
15
15
|
import { defineTool, ToolInputError, ToolRefusalError } from "../tool-types.js";
|
|
16
16
|
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
17
|
+
import { readModelRecord } from "../model-record.js";
|
|
17
18
|
import { MODEL_CREATE_BODY_DEFAULTS, MODEL_LANE_CONTRACT } from "./model-contract.js";
|
|
18
19
|
const MODEL_LANE = MODEL_LANE_CONTRACT.lane;
|
|
19
20
|
const MODEL_TOOLS = MODEL_LANE_CONTRACT.tools;
|
|
@@ -48,7 +49,7 @@ function modelRollbackSpec(toolName) {
|
|
|
48
49
|
target: (args) => `model:${String(args.schemaCode ?? args.code ?? "")}`,
|
|
49
50
|
read: async (session, args) => {
|
|
50
51
|
const resolved = await resolveModelTreeNode(session, String(args.schemaCode ?? args.code));
|
|
51
|
-
const record =
|
|
52
|
+
const record = await readModelRecord(session, String(args.schemaCode ?? args.code));
|
|
52
53
|
if (!resolved.node && !record)
|
|
53
54
|
return null;
|
|
54
55
|
return { node: resolved.node ?? null, record: record ?? null };
|
|
@@ -386,7 +387,7 @@ export function registerModelLane(tools) {
|
|
|
386
387
|
// bizmodels/get_all is keyed by the query, so a non-null object IS the
|
|
387
388
|
// model (an absent one answers a business errcode — wireCall throws and
|
|
388
389
|
// we fall through to create).
|
|
389
|
-
const record =
|
|
390
|
+
const record = await readModelRecord(session, args.code);
|
|
390
391
|
if (record !== null && typeof record === "object") {
|
|
391
392
|
throw new ToolRefusalError({
|
|
392
393
|
status: "refused",
|
|
@@ -461,7 +462,7 @@ export function registerModelLane(tools) {
|
|
|
461
462
|
async function modelDeleteDryRun(session, args) {
|
|
462
463
|
const record = await (async () => {
|
|
463
464
|
try {
|
|
464
|
-
return (
|
|
465
|
+
return readModelRecord(session, args.schemaCode);
|
|
465
466
|
}
|
|
466
467
|
catch (error) {
|
|
467
468
|
if (error instanceof WireError && error.kind === "business")
|
|
@@ -510,17 +511,7 @@ export function registerModelLane(tools) {
|
|
|
510
511
|
writeTool(tools, "model.delete", MODEL_LANE, MODEL_TOOLS["model.delete"].scope, MODEL_TOOLS["model.delete"].description, { schemaCode: z.string().min(1), applicationCode: z.string().optional() }, async (session, args) => {
|
|
511
512
|
// authoritative model record (bizmodels/get_all); the app-tree node id
|
|
512
513
|
// may lag behind and is cleaned best-effort.
|
|
513
|
-
const readRecord =
|
|
514
|
-
try {
|
|
515
|
-
const result = await wireCall(session, { path: ENDPOINTS.modelGet, query: { schemaCode: args.schemaCode } });
|
|
516
|
-
return result.data ?? null;
|
|
517
|
-
}
|
|
518
|
-
catch (error) {
|
|
519
|
-
if (error instanceof WireError && error.kind === "business")
|
|
520
|
-
return null;
|
|
521
|
-
throw error;
|
|
522
|
-
}
|
|
523
|
-
};
|
|
514
|
+
const readRecord = () => readModelRecord(session, args.schemaCode);
|
|
524
515
|
const record = await readRecord();
|
|
525
516
|
// Ticket 68: resolve the app-tree node across applications (the tree row
|
|
526
517
|
// id IS the AppFunction id physically_delete needs). Never fall back to
|
|
@@ -673,17 +664,7 @@ export function registerModelLane(tools) {
|
|
|
673
664
|
// every existing model in that single request after prechecking each
|
|
674
665
|
// target's inbound references — a referenced model refuses the whole
|
|
675
666
|
// batch with per-target evidence and ZERO side effects.
|
|
676
|
-
const readRecord =
|
|
677
|
-
try {
|
|
678
|
-
const result = await wireCall(session, { path: ENDPOINTS.modelGet, query: { schemaCode } });
|
|
679
|
-
return result.data ?? null;
|
|
680
|
-
}
|
|
681
|
-
catch (error) {
|
|
682
|
-
if (error instanceof WireError && error.kind === "business")
|
|
683
|
-
return null;
|
|
684
|
-
throw error;
|
|
685
|
-
}
|
|
686
|
-
};
|
|
667
|
+
const readRecord = (schemaCode) => readModelRecord(session, schemaCode);
|
|
687
668
|
const applicationCode = await resolveApplicationCode(session, args.applicationCode).catch(() => "");
|
|
688
669
|
const targets = [...new Set(args.schemaCodes)];
|
|
689
670
|
const alreadyAbsent = [];
|
|
@@ -885,8 +866,10 @@ export function registerModelLane(tools) {
|
|
|
885
866
|
// NOT migrated by this endpoint (probe evidence).
|
|
886
867
|
const after = await resolveModelTreeNode(session, args.schemaCode);
|
|
887
868
|
const nodeAfter = after.node;
|
|
888
|
-
|
|
889
|
-
|
|
869
|
+
// Informational only: the app-tree node is the authoritative face for a
|
|
870
|
+
// rename, so an unreadable record face must not fail the whole call.
|
|
871
|
+
const record = await readModelRecord(session, args.schemaCode).catch(() => null);
|
|
872
|
+
const recordName = record?.name ?? null;
|
|
890
873
|
const nodeName = typeof nodeAfter?.name === "string" ? nodeAfter.name : null;
|
|
891
874
|
return {
|
|
892
875
|
performed: { errcode: performed.errcode },
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { GraphIndexAbsentError, graphDbPath, impactGraph, queryGraphReferences, readGraphStatus, searchGraphRules, } from "./graph.js";
|
|
6
|
+
/**
|
|
7
|
+
* Stdio MCP server for the local rule-graph index (ADR-0008).
|
|
8
|
+
*
|
|
9
|
+
* Wire format is the MCP standard — newline-delimited JSON-RPC 2.0 — hand
|
|
10
|
+
* rolled on purpose: the precedent in this stack (CodeGraph) ships its own
|
|
11
|
+
* transport too, and omc-cli keeps a single-digit dependency budget. The
|
|
12
|
+
* protocol surface needed by Claude Code / Codex / WorkBuddy is small:
|
|
13
|
+
* initialize → tools/list → tools/call (+ping), everything else is -32601.
|
|
14
|
+
*
|
|
15
|
+
* Why a server at all (friction evidence, 2026-10-30 zlt测试 session):
|
|
16
|
+
* 1. CLI return shapes had to be guessed (`rules` mis-read as `matches`);
|
|
17
|
+
* MCP tools are self-describing via input/output examples in descriptions.
|
|
18
|
+
* 2. CJK terms passed through a Windows console got mangled — a process-bound
|
|
19
|
+
* JSON pipe is UTF-8 end to end.
|
|
20
|
+
* 3. Iterative explore→refine→explore paid a full CLI boot per question.
|
|
21
|
+
*
|
|
22
|
+
* Read-only by construction: the handlers only query the local SQLite index;
|
|
23
|
+
* `pull` deliberately stays a CLI verb (it reads the platform for ~a minute).
|
|
24
|
+
*/
|
|
25
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
26
|
+
const ownVersion = () => {
|
|
27
|
+
try {
|
|
28
|
+
return createRequire(import.meta.url)("../package.json").version;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return "0.0.0";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const SERVER_INFO = { name: "omc-graph", version: ownVersion() };
|
|
35
|
+
const KINDS = ["field", "model", "service"];
|
|
36
|
+
const MATCH_CAP = 50;
|
|
37
|
+
const ERROR_CODES = { parse: -32700, invalidRequest: -32600, methodNotFound: -32601, invalidParams: -32602, internal: -32603 };
|
|
38
|
+
function respond(id, result) {
|
|
39
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: id ?? null, result })}\n`);
|
|
40
|
+
}
|
|
41
|
+
function respondError(id, code, message) {
|
|
42
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: id ?? null, error: { code, message } })}\n`);
|
|
43
|
+
}
|
|
44
|
+
const INSTRUCTIONS_INDEXED = [
|
|
45
|
+
"规则/配置图检索(本地索引,先于逐平台扫描使用):",
|
|
46
|
+
"- 改名/删除/重构一个 字段(field)/模型(model)/服务(service) 前,先用 omc_graph_explore 查引用与影响面;查\"谁引用了 X\"也走它。",
|
|
47
|
+
"- 索引只覆盖:业务规则(methodMapping/targetObjectCode)、数据规则(propertyCodes 与公式 {字段})、视图(列/排序/条件)。**不含**规则条件叶子与流程绑定。",
|
|
48
|
+
"- search 语义:只匹配规则 code+name 的**词首**(中文子串不命中:搜「预算」匹配不到「增加预算」)。",
|
|
49
|
+
"答案带 freshness/pulledAtIso——索引是快照,平台侧改动后需 CLI 重建:omc graph pull --env <env>(只读远端,约 1 分钟)。",
|
|
50
|
+
"索引绑定单一环境(environmentName);查别的环境先重新 pull。",
|
|
51
|
+
].join("\n");
|
|
52
|
+
const INSTRUCTIONS_NO_INDEX = [
|
|
53
|
+
"规则/配置图检索(omc-graph):当前项目还没有本地索引(<项目>/.omc/graph/graph.db 不存在),工具暂时无法回答。",
|
|
54
|
+
"先请用户执行 CLI:omc graph pull --env <环境名>(只读远端,约 1 分钟;环境名以 omc doctor 的「环境=」或 .omc-env 为准),完成后再来调用。",
|
|
55
|
+
"不要在无索引状态下反复重试本工具。",
|
|
56
|
+
].join("\n");
|
|
57
|
+
const EXPLORE_TOOL = {
|
|
58
|
+
name: "omc_graph_explore",
|
|
59
|
+
description: [
|
|
60
|
+
"一次查全一个目标的引用与影响面(本地索引,毫秒级)。",
|
|
61
|
+
"输入 query=字段/模型/服务的 code(或规则检索词);kind 可选 field|model|service,不给则三种都查。",
|
|
62
|
+
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via}]}, impact:{kind:totals}, search:{rules}}。",
|
|
63
|
+
"改/删/重命名任何字段、模型、服务之前必查;找\"关于 X 的规则\"拿 code 也走这里。",
|
|
64
|
+
"索引不含规则条件叶子与流程绑定;freshness=partial 时命中可能不完整(答案里有 gapWarning)。",
|
|
65
|
+
].join(" "),
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
query: { type: "string", description: "字段/模型/服务的 code,或规则检索词(code+name 词首匹配)" },
|
|
70
|
+
kind: { type: "string", enum: [...KINDS], description: "限定一种引用种类;省略则 field/model/service 全查" },
|
|
71
|
+
env: { type: "string", description: "期望的环境名;与索引所属环境不一致时会拒绝(防跨环境误答)" },
|
|
72
|
+
},
|
|
73
|
+
required: ["query"],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const STATUS_TOOL = {
|
|
77
|
+
name: "omc_graph_status",
|
|
78
|
+
description: "索引完整性与新鲜度:environmentName、freshness(complete|partial)、模型/规则计数、pulledAtIso、gaps。回答前先看一眼,索引缺失会给出 pull 指引。",
|
|
79
|
+
inputSchema: { type: "object", properties: {} },
|
|
80
|
+
};
|
|
81
|
+
export function graphToolDefinitions() {
|
|
82
|
+
return [EXPLORE_TOOL, STATUS_TOOL];
|
|
83
|
+
}
|
|
84
|
+
function toolResult(payload, isError = false) {
|
|
85
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }], ...(isError ? { isError: true } : {}) };
|
|
86
|
+
}
|
|
87
|
+
function toolError(message) {
|
|
88
|
+
return toolResult({ status: "error", message }, true);
|
|
89
|
+
}
|
|
90
|
+
function cap(rows) {
|
|
91
|
+
return { rows: rows.slice(0, MATCH_CAP), truncated: rows.length > MATCH_CAP };
|
|
92
|
+
}
|
|
93
|
+
/** Shared handler core — exported so tests drive it without a stdio pair. */
|
|
94
|
+
export async function executeGraphTool(name, rawArgs) {
|
|
95
|
+
const args = (rawArgs ?? {});
|
|
96
|
+
try {
|
|
97
|
+
if (name === STATUS_TOOL.name) {
|
|
98
|
+
const status = readGraphStatus();
|
|
99
|
+
return toolResult({ status: "ok", ...status });
|
|
100
|
+
}
|
|
101
|
+
if (name === EXPLORE_TOOL.name) {
|
|
102
|
+
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
103
|
+
if (!query)
|
|
104
|
+
return toolError("query 不能为空");
|
|
105
|
+
const kind = args.kind === undefined ? undefined : String(args.kind);
|
|
106
|
+
if (kind !== undefined && !KINDS.includes(kind))
|
|
107
|
+
return toolError(`kind 必须是 ${KINDS.join("|")} 之一`);
|
|
108
|
+
const status = readGraphStatus();
|
|
109
|
+
const index = status.index;
|
|
110
|
+
if (typeof args.env === "string" && args.env.trim() && args.env.trim() !== index.environmentName) {
|
|
111
|
+
return toolError(`索引属于环境「${index.environmentName}」;要查「${args.env.trim()}」请先 omc graph pull --env ${args.env.trim()} 重建索引`);
|
|
112
|
+
}
|
|
113
|
+
const kinds = kind === undefined ? [...KINDS] : [kind];
|
|
114
|
+
const references = {};
|
|
115
|
+
const impact = {};
|
|
116
|
+
for (const one of kinds) {
|
|
117
|
+
const ref = queryGraphReferences(one, query);
|
|
118
|
+
const capped = cap(ref.matches);
|
|
119
|
+
references[one] = { count: ref.matches.length, ...capped, ...(ref.gapWarning ? { gapWarning: ref.gapWarning } : {}) };
|
|
120
|
+
if (ref.matches.length > 0 || kind !== undefined) {
|
|
121
|
+
const imp = impactGraph(one, query);
|
|
122
|
+
impact[one] = {
|
|
123
|
+
totals: imp.totals,
|
|
124
|
+
rules: cap(imp.rules),
|
|
125
|
+
datarules: cap(imp.datarules),
|
|
126
|
+
listviews: cap(imp.listviews),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const search = searchGraphRules(query);
|
|
131
|
+
return toolResult({
|
|
132
|
+
status: "ok",
|
|
133
|
+
query,
|
|
134
|
+
...(kind !== undefined ? { kind } : {}),
|
|
135
|
+
index,
|
|
136
|
+
references,
|
|
137
|
+
impact,
|
|
138
|
+
search: { count: search.rules.length, rules: search.rules },
|
|
139
|
+
...(search.note ? { searchNote: search.note } : {}),
|
|
140
|
+
note: "索引不含规则条件叶子与流程绑定;改这些面仍需平台侧核查",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return toolError(`未知工具:${name}`);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error instanceof GraphIndexAbsentError) {
|
|
147
|
+
return toolError(`本地索引不存在(${graphDbPath()})。先执行:omc graph pull --env <环境名>(只读远端,约 1 分钟),完成后再调用`);
|
|
148
|
+
}
|
|
149
|
+
return toolError(error instanceof Error ? error.message : String(error));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function handleMessage(message) {
|
|
153
|
+
const id = message.id;
|
|
154
|
+
if (id === undefined || id === null)
|
|
155
|
+
return; // notification — no response
|
|
156
|
+
if (message.jsonrpc !== "2.0" || typeof message.method !== "string") {
|
|
157
|
+
respondError(id, ERROR_CODES.invalidRequest, "invalid JSON-RPC 2.0 request");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const { method } = message;
|
|
161
|
+
if (method === "initialize") {
|
|
162
|
+
const params = (message.params ?? {});
|
|
163
|
+
const requested = typeof params.protocolVersion === "string" && params.protocolVersion ? params.protocolVersion : PROTOCOL_VERSION;
|
|
164
|
+
respond(id, {
|
|
165
|
+
protocolVersion: requested,
|
|
166
|
+
capabilities: { tools: {} },
|
|
167
|
+
serverInfo: SERVER_INFO,
|
|
168
|
+
instructions: existsSync(graphDbPath()) ? INSTRUCTIONS_INDEXED : INSTRUCTIONS_NO_INDEX,
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (method === "ping") {
|
|
173
|
+
respond(id, {});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (method === "tools/list") {
|
|
177
|
+
respond(id, { tools: graphToolDefinitions() });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (method === "tools/call") {
|
|
181
|
+
const params = (message.params ?? {});
|
|
182
|
+
if (typeof params.name !== "string") {
|
|
183
|
+
respondError(id, ERROR_CODES.invalidParams, "params.name must be a string");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
respond(id, await executeGraphTool(params.name, params.arguments));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
respondError(id, ERROR_CODES.methodNotFound, `method not found: ${method}`);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Run the stdio server. Resolves when stdin closes (the host went away).
|
|
193
|
+
* `projectRoot` (from --path) pins the index location exactly like the
|
|
194
|
+
* codegraph registration does — never trust the host's cwd.
|
|
195
|
+
*/
|
|
196
|
+
export async function runMcpGraphServer(projectRoot) {
|
|
197
|
+
if (projectRoot)
|
|
198
|
+
process.chdir(resolve(projectRoot));
|
|
199
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
200
|
+
lines.on("line", (line) => {
|
|
201
|
+
const text = line.trim();
|
|
202
|
+
if (!text)
|
|
203
|
+
return;
|
|
204
|
+
let parsed;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(text);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
respondError(null, ERROR_CODES.parse, "parse error");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
void handleMessage(parsed).catch((error) => {
|
|
213
|
+
respondError(idOf(parsed), ERROR_CODES.internal, error instanceof Error ? error.message : String(error));
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
await new Promise((resolve) => lines.on("close", () => resolve()));
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
function idOf(message) {
|
|
220
|
+
return message.id ?? null;
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=mcp-graph.js.map
|
package/dist/model-purge.js
CHANGED
|
@@ -27,6 +27,7 @@ import { CLI_EXIT_CODES, classifyResult, envelope, refusalText, requestId } from
|
|
|
27
27
|
import { writeEnvelope } from "./cli-output.js";
|
|
28
28
|
import { ENDPOINTS } from "./endpoints.js";
|
|
29
29
|
import { wireCall, wireRows } from "./wire.js";
|
|
30
|
+
import { readModelRecord } from "./model-record.js";
|
|
30
31
|
import { SessionManager } from "./session-manager.js";
|
|
31
32
|
import { ConfigStore, defaultPaths } from "./config-store.js";
|
|
32
33
|
async function sessionFor(environment) {
|
|
@@ -43,8 +44,7 @@ export async function planPurge(options, injected) {
|
|
|
43
44
|
const session = injected ?? (await sessionFor(options.environment));
|
|
44
45
|
let modelExists = true;
|
|
45
46
|
try {
|
|
46
|
-
|
|
47
|
-
modelExists = record !== null && record !== undefined;
|
|
47
|
+
modelExists = (await readModelRecord(session, options.schemaCode)) !== null;
|
|
48
48
|
}
|
|
49
49
|
catch {
|
|
50
50
|
modelExists = false;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ENDPOINTS } from "./endpoints.js";
|
|
2
|
+
import { wireCall, wireRows, WireError } from "./wire.js";
|
|
3
|
+
/**
|
|
4
|
+
* Authoritative biz-model record, across platform versions.
|
|
5
|
+
*
|
|
6
|
+
* 8.6 serves `/api/api/app/bizmodels/get_all` (BizSchemaController resolves
|
|
7
|
+
* getBizSchemaBySchemaCode and answers 10011 on a miss). 8.0/8.1 deployments
|
|
8
|
+
* answer **HTTP 404** for that path (field report 2026-10-30, 8.1.108 /
|
|
9
|
+
* 上海电建) and expose the same rows through `/bizmodels/list_schema`, filtered
|
|
10
|
+
* by `schemaCode`. Pinning the 8.6 path made model.*, rule.* and the purge
|
|
11
|
+
* precheck die with a raw 404 before any of their own logic ran.
|
|
12
|
+
*
|
|
13
|
+
* Reads the primary face first; only a 404 (endpoint absent) falls back, so a
|
|
14
|
+
* genuine 401/500/transport failure still surfaces as itself. Returns null when
|
|
15
|
+
* the model is not there (business "not found" on either face).
|
|
16
|
+
*/
|
|
17
|
+
export async function readModelRecord(session, schemaCode) {
|
|
18
|
+
try {
|
|
19
|
+
const result = await wireCall(session, { path: ENDPOINTS.modelGet, query: { schemaCode } });
|
|
20
|
+
return result.data ?? null;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof WireError && error.kind === "business")
|
|
24
|
+
return null;
|
|
25
|
+
const endpointAbsent = error instanceof WireError && error.kind === "http-status" && error.diagnostic?.httpStatus === 404;
|
|
26
|
+
if (!endpointAbsent)
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.modelList, query: { schemaCode } })).data);
|
|
31
|
+
const match = rows.find((row) => String(row.schemaCode ?? row.code ?? "") === schemaCode);
|
|
32
|
+
return match ?? null;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof WireError && error.kind === "business")
|
|
36
|
+
return null;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=model-record.js.map
|
package/dist/rule-support.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { wireCall, wireRows, WireError } from "./wire.js";
|
|
3
3
|
import { ENDPOINTS } from "./endpoints.js";
|
|
4
|
+
import { readModelRecord } from "./model-record.js";
|
|
4
5
|
import { ToolRefusalError } from "./tool-types.js";
|
|
5
6
|
import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "./json-column.js";
|
|
6
7
|
import { RULE_BIZ_RULE_TYPE_DESCRIPTION, RULE_CREATE_BODY, RULE_ROUTES_DESCRIPTION, ruleNodeGraphDescription, schedulerSettingDescription, } from "./lanes/businessrule-contract.js";
|
|
@@ -602,17 +603,10 @@ export async function assertRuleSchemaExists(session, schemaCode) {
|
|
|
602
603
|
// SCHEMA_NOT_FOUNT_ERR (10011 业务模型不存在) on miss — the same facade
|
|
603
604
|
// primitive the rule faces rely on. The fake answers ok+null for an absent
|
|
604
605
|
// schema, so BOTH signals decode as a miss.
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
catch (error) {
|
|
611
|
-
if (error instanceof WireError && error.kind === "business" && error.errcode === 10011)
|
|
612
|
-
missing = true;
|
|
613
|
-
else
|
|
614
|
-
throw error;
|
|
615
|
-
}
|
|
606
|
+
// Version-tolerant: 8.1 has no `/bizmodels/get_all` (404) and serves the same
|
|
607
|
+
// rows from `/bizmodels/list_schema`; see model-record.ts.
|
|
608
|
+
const record = await readModelRecord(session, schemaCode);
|
|
609
|
+
const missing = record === null;
|
|
616
610
|
if (missing) {
|
|
617
611
|
throw new ToolRefusalError({
|
|
618
612
|
status: "refused",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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.5.
|
|
55
|
+
"@stalfh233/omc-fake-platform": "0.5.3",
|
|
56
56
|
"commander": "14.0.3",
|
|
57
|
-
"omc-core": "0.5.
|
|
57
|
+
"omc-core": "0.5.3",
|
|
58
58
|
"proper-lockfile": "4.1.2",
|
|
59
59
|
"write-file-atomic": "6.0.0",
|
|
60
60
|
"zod": "^3.23.8",
|