@stalfh233/omc-cli 0.4.99 → 0.4.101

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/skills.js CHANGED
@@ -81,6 +81,22 @@ function scanHeadings(lines) {
81
81
  }
82
82
  return found;
83
83
  }
84
+ /**
85
+ * Slice one heading's body using the documented rule: a section runs until the
86
+ * next heading of the same or a higher level. Shared by `--section` and search
87
+ * so a search hit can announce the exact bytes `--section` would return.
88
+ */
89
+ function sliceSectionText(lines, start, level) {
90
+ let end = lines.length;
91
+ for (let index = start + 1; index < lines.length; index += 1) {
92
+ const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
93
+ if (match && match[1].length <= level) {
94
+ end = index;
95
+ break;
96
+ }
97
+ }
98
+ return lines.slice(start, end).join("\n").trim() + "\n";
99
+ }
84
100
  /** Return one Markdown heading and its nested content without loading the full topic into the envelope. */
85
101
  export function readSkillSection(name, requested) {
86
102
  const document = readSkill(name);
@@ -121,16 +137,7 @@ export function readSkillSection(name, requested) {
121
137
  if (start < 0)
122
138
  return { document, section: "", headings };
123
139
  const startEntry = scanned.find((entry) => entry.index === start);
124
- const level = startEntry.level;
125
- let end = lines.length;
126
- for (let index = start + 1; index < lines.length; index += 1) {
127
- const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
128
- if (match && match[1].length <= level) {
129
- end = index;
130
- break;
131
- }
132
- }
133
- return { document, section: lines.slice(start, end).join("\n").trim() + "\n", headings };
140
+ return { document, section: sliceSectionText(lines, startEntry.index, startEntry.level), headings };
134
141
  }
135
142
  /**
136
143
  * The "shape" of a topic: every heading with the bytes it would cost to read.
@@ -147,17 +154,7 @@ export function readSkillOutline(name) {
147
154
  const lines = document.content.split(/\r?\n/);
148
155
  const headings = scanHeadings(lines);
149
156
  const sections = headings.map((entry) => {
150
- let end = lines.length;
151
- // Same slicing rule as readSkillSection: a section runs until the next
152
- // heading of the same or a higher level, so reported bytes match the read.
153
- for (let index = entry.index + 1; index < lines.length; index += 1) {
154
- const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
155
- if (match && match[1].length <= entry.level) {
156
- end = index;
157
- break;
158
- }
159
- }
160
- const body = `${lines.slice(entry.index, end).join("\n").trim()}\n`;
157
+ const body = sliceSectionText(lines, entry.index, entry.level);
161
158
  return { heading: entry.heading, level: entry.level, bytes: Buffer.byteLength(body, "utf8") };
162
159
  });
163
160
  const bytes = Buffer.byteLength(document.content, "utf8");
@@ -170,4 +167,75 @@ export function readSkillOutline(name) {
170
167
  const visible = indexLevel === undefined ? sections : sections.filter((entry) => entry.level === indexLevel);
171
168
  return { document, bytes, sections: visible.length > 0 ? visible : sections };
172
169
  }
170
+ /**
171
+ * Cross-topic full-text search (`omc help --search`).
172
+ *
173
+ * Why this exists: r18 spent 173.8 KB re-dumping five whole topics twice to grep
174
+ * for `40000`, because `omc help` is a bookshelf (one heading index per topic)
175
+ * and there was no way to ask a question. A search that returns only
176
+ * topic/heading would still cost a second call, so a hit carries its section
177
+ * body and the exact bytes `--section` would return.
178
+ *
179
+ * Matching is deliberately literal: case-insensitive substring, whitespace
180
+ * splits terms, `all` requires every term in the same section and `any`
181
+ * requires one. No stemming, no fuzzy matching — an agent searching an exact
182
+ * error code must never be steered to a similar-looking section.
183
+ */
184
+ export function searchSkills(query, options) {
185
+ const terms = query.trim().toLowerCase().split(/\s+/).filter((term) => term.length > 0);
186
+ if (terms.length === 0)
187
+ return { matches: [], total: 0 };
188
+ const found = [];
189
+ for (const entry of listSkills()) {
190
+ const document = readSkill(entry.name);
191
+ if (!document)
192
+ continue;
193
+ const lines = document.content.split(/\r?\n/);
194
+ const scanned = scanHeadings(lines);
195
+ // Same visible levels as the index: a whole-document H1 must not swallow
196
+ // every H2 hit into one giant match.
197
+ const levels = [...new Set(scanned.map((item) => item.level))].sort((left, right) => left - right);
198
+ const singleRoot = levels.length > 1 && scanned.filter((item) => item.level === levels[0]).length === 1;
199
+ const indexLevel = singleRoot ? levels[1] : levels[0];
200
+ const visible = scanned.filter((heading) => heading.level === indexLevel);
201
+ for (const [ordinal, heading] of visible.entries()) {
202
+ if (indexLevel === undefined)
203
+ continue;
204
+ const section = sliceSectionText(lines, heading.index, heading.level);
205
+ const lower = section.toLowerCase();
206
+ const hitTerms = terms.filter((term) => lower.includes(term));
207
+ if (hitTerms.length === 0)
208
+ continue;
209
+ if (options.mode === "all" && hitTerms.length < terms.length)
210
+ continue;
211
+ const matchLines = [];
212
+ for (let index = heading.index; index < lines.length && matchLines.length < 3; index += 1) {
213
+ const text = lines[index];
214
+ const lineTerms = hitTerms.filter((term) => text.toLowerCase().includes(term));
215
+ if (lineTerms.length === 0)
216
+ continue;
217
+ if (matchLines.length > 0 && /^(#{1,6})\s+/.test(text))
218
+ continue;
219
+ matchLines.push({ line: index + 1, text: text.trim(), terms: lineTerms });
220
+ }
221
+ const headingHit = hitTerms.some((term) => heading.heading.toLowerCase().includes(term));
222
+ const bytes = Buffer.byteLength(section, "utf8");
223
+ found.push({
224
+ topic: document.name,
225
+ heading: heading.heading,
226
+ sectionOrdinal: ordinal + 1,
227
+ matchLines,
228
+ sectionBytes: bytes,
229
+ section,
230
+ // Heading hits beat body hits; more distinct terms beat fewer; smaller
231
+ // sections beat larger ones (cheaper to carry, usually more specific).
232
+ score: (headingHit ? 1000 : 0) + hitTerms.length * 100 + Math.max(0, 100 - Math.round(bytes / 64)),
233
+ });
234
+ }
235
+ }
236
+ found.sort((left, right) => right.score - left.score
237
+ || left.topic.localeCompare(right.topic)
238
+ || left.sectionOrdinal - right.sectionOrdinal);
239
+ return { matches: found.slice(0, Math.max(1, options.limit)), total: found.length };
240
+ }
173
241
  //# sourceMappingURL=skills.js.map
package/dist/tools.js CHANGED
@@ -21,6 +21,8 @@ import { registerListViewLane } from "./lanes/list-view.js";
21
21
  import { registerReportLane } from "./lanes/report.js";
22
22
  import { registerApplyChangesLane } from "./lanes/apply-changes.js";
23
23
  import { executionMetadataFor } from "./execution-metadata.js";
24
+ import { attachRefusalRecovery } from "./refusal-guidance.js";
25
+ import { ToolRefusalError } from "./tool-types.js";
24
26
  export { ToolRefusalError, ToolUncertainError } from "./tool-types.js";
25
27
  export { makeToolContext };
26
28
  /**
@@ -59,6 +61,39 @@ export function buildToolRegistry() {
59
61
  if (metadata)
60
62
  tool.executionMetadata = metadata;
61
63
  }
64
+ // r19 ticket 06 — 拒绝即教学,收口在最深的一层:每个 handler 返回/抛出的
65
+ // refused 结果在这里统一附加 recovery(同源配方命令、正确形状、不要做什么)。
66
+ // 收口在 registry 而不是 CLI 出口,是因为 runToolCall、测试 harness、MCP
67
+ // client、组装器全部经过 handler——一处附加、处处生效。已自带 recovery 的
68
+ // 拒绝(write-lock-held)不会被覆盖(attachRefusalRecovery 幂等)。
69
+ for (const tool of tools) {
70
+ const inner = tool.handler;
71
+ const name = tool.name;
72
+ tool.handler = async (ctx, args, self) => {
73
+ try {
74
+ const result = await inner(ctx, args, self);
75
+ if (result && typeof result === "object") {
76
+ const status = result.status;
77
+ // attachRefusalRecovery decides for itself: refused envelopes get
78
+ // guidance, uncertain ones only when the dashboard boundary is
79
+ // pinned in the frozen detail (r19 p14).
80
+ if (status === "refused" || status === "uncertain") {
81
+ return attachRefusalRecovery(result, name, tools);
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+ catch (error) {
87
+ const payload = error.payload;
88
+ if (payload && typeof payload === "object" && payload.status === "refused") {
89
+ const enriched = attachRefusalRecovery(payload, name, tools);
90
+ if (enriched !== payload)
91
+ throw new ToolRefusalError(enriched);
92
+ }
93
+ throw error;
94
+ }
95
+ };
96
+ }
62
97
  return tools;
63
98
  }
64
99
  export { loadCoverageManifest };
package/dist/usage-log.js CHANGED
@@ -34,6 +34,21 @@ const USAGE_LOG_MAX_BYTES = 8 * 1024 * 1024;
34
34
  * Memos still read `logPath()` only — a fallback log is append-only telemetry,
35
35
  * never a source of "already did this" answers.
36
36
  */
37
+ /**
38
+ * r19 ticket 02: explicit opt-out for the WRITE side of the ledger.
39
+ *
40
+ * A maintainer script or an investigation subprocess that runs read-only
41
+ * discovery faces inside a sandbox used to append to that round's
42
+ * `.omc/usage.jsonl` — the r18 probe was read as 469 calls where the agent
43
+ * made 184 (19 polling reads + 105 investigation reads). "What did this round
44
+ * cost" must mean "what did the AGENT spend": `OMC_NO_USAGE=1` mutes this
45
+ * invocation's records (project log, user fallback, explicit OMC_USAGE_LOG —
46
+ * the switch beats them all). Only the write entry points consult this guard;
47
+ * reading (`omc usage`, idempotency memos) is untouched.
48
+ */
49
+ function recordingDisabled() {
50
+ return process.env.OMC_NO_USAGE === "1";
51
+ }
37
52
  function recordLogPath() {
38
53
  const explicit = process.env.OMC_USAGE_LOG;
39
54
  if (explicit)
@@ -74,6 +89,8 @@ function appendRecord(path, record) {
74
89
  }
75
90
  /** Append a `script-call` record: one line of `omc script` = one inner tool call. */
76
91
  export function recordScriptCall(input) {
92
+ if (recordingDisabled())
93
+ return;
77
94
  const target = recordLogPath();
78
95
  if (!target)
79
96
  return;
@@ -209,7 +226,7 @@ function commandOf(argv) {
209
226
  for (let index = 2; index < argv.length && values.length < 2; index += 1) {
210
227
  const token = argv[index];
211
228
  if (token.startsWith("--")) {
212
- if (["--out", "--max-output-bytes", "--args", "--args-file", "--env", "--environment", "--request-id", "--timeout-ms", "--wait", "--path", "--top"].includes(token))
229
+ if (["--out", "--max-output-bytes", "--args", "--args-file", "--env", "--environment", "--request-id", "--timeout-ms", "--wait", "--path", "--top", "--search", "--search-mode", "--limit"].includes(token))
213
230
  index += 1;
214
231
  continue;
215
232
  }
@@ -227,7 +244,7 @@ function facesOf(argv) {
227
244
  // `omc artifact get <id> --jq <path>` showed up as a bare `artifact get` with
228
245
  // empty argKeys, so the cost report could not tell a 200 B projection from a
229
246
  // 44 KB pull — precisely the read path these flags exist to make cheap.
230
- const faces = ["--full", "--compact", "--json", "--schema-only", "--param", "--example", "--section", "--sections", "--out", "--quiet", "--jq", "--summary", "--keys", "--path-only", "--extract", "--extract-keys", "--records"];
247
+ const faces = ["--full", "--compact", "--json", "--schema-only", "--param", "--example", "--section", "--sections", "--search", "--search-mode", "--out", "--quiet", "--jq", "--summary", "--keys", "--path-only", "--extract", "--extract-keys", "--records"];
231
248
  return faces.filter((face) => argv.includes(face));
232
249
  }
233
250
  const safeArgumentKeys = new Set(["schemaCode", "propertyCode", "code", "workflowCode", "ruleCode", "sheetCode", "id", "dataRuleType", "propertyType", "published", "handler"]);
@@ -270,6 +287,8 @@ function summarizeArgs(argv) {
270
287
  return summary;
271
288
  }
272
289
  export function beginUsage(argv) {
290
+ if (recordingDisabled())
291
+ return;
273
292
  const target = recordLogPath();
274
293
  if (!target)
275
294
  return;
package/dist/write.js CHANGED
@@ -124,7 +124,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
124
124
  const artifactRef = writeArtifact(ctx, `${tool.name}-${randomUUID().slice(0, 8)}.json`, artifact);
125
125
  return { status: executed.status, steps: executed.steps, failedStep: executed.failedStep, ...executed.envelope, ...(args.requestId ? { requestId: args.requestId } : {}), artifactFingerprint: artifactRef.detailSha256 };
126
126
  }
127
- const { performed, readback, steps } = executed;
127
+ const { performed, readback, steps, halfSuccess } = executed;
128
128
  // Readback gate (live 2026-10-17): a tool that reports `readback.present:false`
129
129
  // has NOT proven its own write — the platform accepted the request but the
130
130
  // object is absent from the authoritative face. "Called without error" must
@@ -177,7 +177,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
177
177
  });
178
178
  rollbackRef = { seq: entry.seq, hint: `omc rollback plan ${entry.seq}(--env ${entry.environment})` };
179
179
  }
180
- return { status: "ok", deliveryState, performed, readback, ...(readbackWarning ? { readbackWarning } : {}), ...(steps !== undefined ? { steps } : {}), ...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name) } : {}), ...(productionConfirmed ? { productionConfirmed: true } : {}), ...(rollbackRef ? { rollback: rollbackRef } : {}), summary: writeSummary(tool.name, args, { performed, readback }, "done"), detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256, artifactFingerprint: artifactRef.detailSha256 };
180
+ return { status: "ok", deliveryState, performed, readback, ...(readbackWarning ? { readbackWarning } : {}), ...(steps !== undefined ? { steps } : {}), ...(halfSuccess !== undefined ? { halfSuccess } : {}), ...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name) } : {}), ...(productionConfirmed ? { productionConfirmed: true } : {}), ...(rollbackRef ? { rollback: rollbackRef } : {}), summary: writeSummary(tool.name, args, { performed, readback }, "done"), detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256, artifactFingerprint: artifactRef.detailSha256 };
181
181
  });
182
182
  }
183
183
  /**
@@ -0,0 +1,41 @@
1
+ # 截图占位目录(尼西实战案例)
2
+
3
+ `omc-introduction.md` 的「15. 实战一:尼西双表单与流程搭建」章节里引用了 9 张截图。
4
+ 把截图按下面文件名放进本目录即可,文档中的占位说明可逐条删除。
5
+
6
+ ## 客户源需求(3 张)
7
+
8
+ | 文件名 | 内容 |
9
+ |---|---|
10
+ | `01-requirement-ft-sheet.png` | 客户 Excel 的 `FT` Sheet(字段清单 / 必填 / LotClass 显隐) |
11
+ | `02-requirement-fp-sheet.png` | 客户 Excel 的 `FP` Sheet |
12
+ | `03-requirement-flow.png` | 客户给的流程图(FT/FP,含 `SubConRootCause=1` 分支) |
13
+
14
+ ## 对话截图(6 张)
15
+
16
+ | 文件名 | 内容 |
17
+ |---|---|
18
+ | `04-chat-app-create.png` | 确认新建应用 + `app.create ok` |
19
+ | `05-chat-intent-apply.png` | `intent apply` 返回 `DELIVERED` + `equivalent: true`(89 个动作) |
20
+ | `06-chat-workflow-role.png` | 流程节点与角色路由(PIC→蔡依涛 / MOS→赵益 / AOSIT / AOSENG) |
21
+ | `07-chat-permission-matrix.png` | 节点字段权限矩阵(本节点 + 之前所有节点可见) |
22
+ | `08-chat-biz-integration.png` | `service.register` 注册 productType 服务 + `method.test` 返回 `PRODUCT_TYPE=MOS` |
23
+ | `09-chat-onlinejs-publish.png` | `onlinejs.compose` 返回 `published: true` |
24
+
25
+ ## 替换方式
26
+
27
+ 文档里每处占位是:
28
+
29
+ ```
30
+ > **[截图 01|客户源需求:FT 表单规格]**
31
+ > 占位文件:`docs/images/omc-nixi/01-requirement-ft-sheet.png`
32
+ > 建议内容:...
33
+ ```
34
+
35
+ 落地后改成标准图片引用即可:
36
+
37
+ ```markdown
38
+ ![客户源需求:FT 表单规格](images/omc-nixi/01-requirement-ft-sheet.png)
39
+ ```
40
+
41
+ 建议截图宽度 ≥ 1200px,避免中文文字模糊。