@stalfh233/omc-cli 0.5.2 → 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 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 规则全文 / status 完整性与新鲜度。候选定位器——不进 baseline/plan/写门禁");
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规则全文检索(code+name,FTS5,末词前缀匹配):找到\"关于 X 的规则\"拿 code,再走 references/impact。中文需给全词或词首(如 报名*)。\n").action(async (term, options) => {
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
- ? { note: tokens.length === 0 ? "检索词为空" : "没有命中规则(code+name 全文;引用检索用 references/impact <kind> <code>)" }
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
  };
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.5.2",
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.2",
55
+ "@stalfh233/omc-fake-platform": "0.5.3",
56
56
  "commander": "14.0.3",
57
- "omc-core": "0.5.2",
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",