@stalfh233/omc-cli 0.4.78 → 0.4.80
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/README.md +67 -27
- package/dist/cli.js +87 -21
- package/dist/goal-routes.js +1 -1
- package/dist/script.js +19 -1
- package/dist/skills.js +76 -15
- package/dist/usage-log.js +222 -34
- package/package.json +3 -3
- package/skills/omc-start-here.md +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
(x=327、158×40、y=36/116/196…),缺失路由坐标按节点**边中点**推导,缺失
|
|
19
19
|
`routeCondition`/`matchType` 补 `true`/`ANY`;已可渲染的布局与分支图原样保留
|
|
20
20
|
——调用方无需手算几何。
|
|
21
|
+
- **读面一步取值**——大结果落到 artifact 时,`omc call <tool> … --extract <path>` / `--extract-keys`
|
|
22
|
+
直接在**同一次调用**里取值,不必再跑一次 `artifact get`;路径支持 `.rules[code=Create].name`
|
|
23
|
+
这类数组选择器。
|
|
24
|
+
- **脚本面可读写**——表单在线 JS(`onlinejs.get/compose`)与**按钮 JS**
|
|
25
|
+
(`listview.get --buttonCode` / `listview.configure --buttonScripts`、
|
|
26
|
+
`form.get --buttonScripts` / `form.publish --buttonScripts`)都能一次读写,
|
|
27
|
+
不必翻 artifact、也不必重发整张按钮表。
|
|
28
|
+
- **应用目录(分组)**——`folder.tree/create/update/delete` + `model.move`;
|
|
29
|
+
应用树是嵌套 `children`,读面已递归展平并给 `path`/`depth`。
|
|
21
30
|
- **方法论即可读**——`skills/` 下的方法论通过 `omc help <topic>` 按需读取;agent 不必去翻仓库文件。
|
|
22
31
|
- **凭据零回显**——凭据(0600 存储或环境变量)不会出现在任何输出、产物或错误里。
|
|
23
32
|
- **不确定性语义**——传输失败时冻结(`status: "uncertain", frozen: true`),
|
|
@@ -51,12 +60,12 @@ omc call discover_environment --env <env> --args '{}' # 环境就绪(真实
|
|
|
51
60
|
`describe` 与 `contract` 重复所选能力路由,切换面不必再宽扫 topic 或契约。
|
|
52
61
|
`omc eval:discovery-comparison` 输出相对 ticket 01 基线的调用数/字节数对比。
|
|
53
62
|
|
|
54
|
-
## 工具面(
|
|
63
|
+
## 工具面(142 个工具)
|
|
55
64
|
|
|
56
65
|
| 类别 | 数量 | 说明 |
|
|
57
66
|
|---|---|---|
|
|
58
|
-
| 写工具 |
|
|
59
|
-
| 读工具 |
|
|
67
|
+
| 写工具 | 87 | 高层编排 5(`configure_*` + `apply_changes`)+ 低层原子写 82 |
|
|
68
|
+
| 读工具 | 50 | `*.list` / `*.get` / `*.load` / `*.tree` / `discover_environment` |
|
|
60
69
|
| 治理/元工具 | 5 | `get_status`、`doctor`、`coverage.manifest`、`setup.*` |
|
|
61
70
|
|
|
62
71
|
**全集与逐工具参数**:`omc commands --json`(name/lane/kind/requiredArgs/writeGate/timeout)、
|
|
@@ -73,22 +82,27 @@ omc call discover_environment --env <env> --args '{}' # 环境就绪(真实
|
|
|
73
82
|
| `setup.start` | 配置引导入口:返回待填字段与当前非密值 |
|
|
74
83
|
| `setup.commit` | 幂等提交环境配置(写 `omc.config.json` + 0600 凭据库),随后跑连通性+登录+版本族校验 |
|
|
75
84
|
|
|
76
|
-
### 读工具(
|
|
85
|
+
### 读工具(50)
|
|
77
86
|
|
|
78
87
|
| 域(lane) | 工具 |
|
|
79
88
|
|---|---|
|
|
80
89
|
| 环境 `environment-governance` | `discover_environment`、`environment.fingerprint` |
|
|
81
90
|
| 应用 `app-management` | `app.list` |
|
|
91
|
+
| 应用目录 `app-tree-folders` | `folder.tree`(递归展平嵌套 `children`,给 `path`/`depth`;`foldersOnly` 只回目录) |
|
|
82
92
|
| 数据字典 `dictionary-management` | `dictionary.list`、`dictionary.type`、`dictionary.names`、`dictionary.get`、`dictionary.records`、`dictionary.parent`、`dictionary.classes` |
|
|
93
|
+
| 业务模型 `model-management` | `model.list`、`model.inspect`(一次拿全字段/表单/视图/规则/流程摘要) |
|
|
83
94
|
| 字段 `field-families` | `field.list` |
|
|
84
|
-
| 表单 `form-publish-chain` | `form.get` |
|
|
95
|
+
| 表单 `form-publish-chain` | `form.get`(紧凑面直接回 `formButtons`/`formButtonCodes`;`--buttonScripts` 回按钮 JS 原文与 sha) |
|
|
96
|
+
| 列表视图 `W1-list-view` | `listview.get`(`--buttonCode`/`--buttons` 直接回按钮 JS 的 `scriptData`/sha)、`listview.list` |
|
|
85
97
|
| 字段级规则 `G1-datarule` | `datarule.list` |
|
|
86
98
|
| 业务规则 `B1-businessrule` | `rule.list`、`rule.get` |
|
|
87
|
-
| 流程 / 待办 `W7-S29-greenfield` | `workflow.get_draft
|
|
99
|
+
| 流程 / 待办 / 组织 `W7-S29-greenfield` | `workflow.get_draft`(每节点 `formButtons.enabled/disabled`)、`workflow.get_published`、`workflow.validate`、`workflow.instanceActivity`、`workitem.list`、`org.role.list`、`org.role.users`、`org.role.userScope`、`org.department.list`、`org.department.users`、`org.user.search`、`org.user.info`、`org.user.departments` |
|
|
100
|
+
| 在线脚本 `online-js` | `onlinejs.get`(草稿/发布面 `customScript` 原文 + SHA-256) |
|
|
88
101
|
| 运行数据 `M2-runtime-data` | `data.list`、`data.load` |
|
|
89
|
-
|
|
|
102
|
+
| 报表 `R1-report` | `report.tree`、`report.get`、`report.chartdata`、`report.datasource.list` |
|
|
103
|
+
| 业务集成 `B2-registration` | `service.list`、`service.get`、`method.get`、`service.category.list`、`service.dbconnpool.list`、`service.adapter_config`、`service.table_list`、`service.table_sql`、`service.export`、`service.check` |
|
|
90
104
|
|
|
91
|
-
## 能力总览(
|
|
105
|
+
## 能力总览(16 项)
|
|
92
106
|
|
|
93
107
|
`omc capabilities` 的静态快照;跑 `omc capabilities` / `omc capability <id>` 取实时投影。
|
|
94
108
|
「依赖」= `requires → precedes`,即能力之间的配置顺序。
|
|
@@ -96,53 +110,75 @@ omc call discover_environment --env <env> --args '{}' # 环境就绪(真实
|
|
|
96
110
|
| 能力 | 标题 | lane | 覆盖 | intent | 依赖 |
|
|
97
111
|
|---|---|---|---|---|---|
|
|
98
112
|
| `environment` | 环境治理 | `environment-governance` | executable | none | - → - |
|
|
99
|
-
| `app` | 应用 | `app-management` | executable | none | - →
|
|
113
|
+
| `app` | 应用 | `app-management` | executable | none | - → folder |
|
|
114
|
+
| `folder` | 应用目录(分组) | `app-tree-folders` | executable | none | app → - |
|
|
100
115
|
| `dictionary` | 数据字典 | `dictionary-management` | executable | none | - → - |
|
|
101
116
|
| `model` | 业务模型 | `model-management` | executable | partial | - → field, workflow |
|
|
102
|
-
| `field` |
|
|
117
|
+
| `field` | 字段(含子表、计算字段) | `field-families` | executable | partial | model → form, listview, datarule, businessrule, data |
|
|
103
118
|
| `form` | 默认表单 | `form-publish-chain` | executable | partial | field → onlinejs |
|
|
104
119
|
| `listview` | 列表视图 | `W1-list-view` | partial | partial | field → - |
|
|
105
120
|
| `datarule` | 字段级控件规则 | `G1-datarule` | partial | partial | field → - |
|
|
106
|
-
| `businessrule` |
|
|
121
|
+
| `businessrule` | 业务规则(图形引擎,含定时任务) | `B1-businessrule` | executable | partial | field → rule-lifecycle |
|
|
107
122
|
| `rule-lifecycle` | 规则生命周期 | `S25-rule-lifecycle` | executable | none | businessrule → - |
|
|
108
123
|
| `workflow` | 流程模板 | `W7-S29-greenfield` | executable | partial | model → - |
|
|
109
124
|
| `onlinejs` | 表单在线 JS | `online-js` | executable | partial | form → - |
|
|
110
125
|
| `bizservice` | 业务集成 | `B2-registration` | executable | partial | - → - |
|
|
126
|
+
| `report` | 报表 | `R1-report` | partial | partial | - → - |
|
|
111
127
|
| `data` | 运行数据 | `M2-runtime-data` | executable | none | field → - |
|
|
112
128
|
|
|
113
129
|
- **覆盖**:`executable` 全量可用;`partial` 有取证边界(见 `omc capability <id>` 的 notes)。
|
|
114
130
|
- **intent**:`partial` 表示该能力可声明式(`omc intent`)覆盖一部分——通常 create/update;
|
|
115
131
|
`none` 表示只走 `omc call`。删除类、方法面、分类/连接池等仍走 `omc call` 逃生舱。
|
|
116
132
|
|
|
133
|
+
## 近期能力(0.4.74 → 0.4.80)
|
|
134
|
+
|
|
135
|
+
| 版本 | 新增 / 修复 |
|
|
136
|
+
|---|---|
|
|
137
|
+
| 0.4.80 | **省 token 与用量账本**:`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数),`--all` 才读整篇;`omc artifact get` 正文 >8KB 默认只回形状(`--full` 才整包);`omc usage` 默认只回汇总(`byTool`/`redundancy`/`estimatedTokens`),`--records` 才展开;`omc script` 的**每行工具调用单独记账**(`script-call` + `parentId`);未绑定环境的目录回落 `$OMC_CONFIG_HOME/usage/<cwd-hash>.jsonl` |
|
|
138
|
+
| 0.4.79 | README 对齐 0.4.79 实际工具面(142 工具 / 16 能力) |
|
|
139
|
+
| 0.4.78 | **按钮 JS 直接读写**:`listview.get --buttonCode/--buttons`、`listview.configure --buttonScripts`、`form.get --buttonScripts`、`form.publish --buttonScripts`,外加 `buttons[].script` 简写 |
|
|
140
|
+
| 0.4.77 | **应用目录(分组)**:`folder.tree/create/update/delete` + `model.move`(`bizmodels/update` 换 parentId,跨应用目录单独报错) |
|
|
141
|
+
| 0.4.76 | **读面一键取值**:`call --extract <path>`(可重复)/`--extract-keys`,省掉一次 `artifact get`;`form.draft fromReadback:true` 允许平台原样布局回写;`--jq` 支持数组选择器 `[code=X]` |
|
|
142
|
+
| 0.4.75 | 节点级事件 `workflow.bindRule {activityCode,nodeEvent}`;`workflow.update` merge 拓扑保护(边数收缩即拒);`workflow.setButtons`;`rule.run` / `form.runButton`;表单按钮 `options.customButtonJson` |
|
|
143
|
+
| 0.4.74 | 组织写面 15 工具(角色组/角色/成员/管理范围/部门/用户) |
|
|
144
|
+
|
|
145
|
+
> **脚本面只有三处**:表单在线 JS(`onlinejs.*`)、按钮 JS(本轮补齐读写)、自定义页面 JS
|
|
146
|
+
> (`h_custom_page`,CLI 未覆盖)。**列表视图本身没有 JS 面**(`h_biz_query` 无脚本列)。
|
|
147
|
+
|
|
117
148
|
## 写工具做什么
|
|
118
149
|
|
|
119
150
|
写工具分三层用法,**全部走同一条受治理管道**。
|
|
120
151
|
|
|
121
|
-
### 1. 高层编排(
|
|
152
|
+
### 1. 高层编排(5 个,一次调用 = 一个变更包)
|
|
122
153
|
|
|
123
154
|
| 工具 | 作用 |
|
|
124
155
|
|---|---|
|
|
125
156
|
| `configure_model` | 建模型 → 建 1..N 个字段 → 模型级 `field.publish`,`publishForm:true` 时再 `form.draft` + `form.publish`,一步到达**结构可用形态**。字段族门逐字段在写入前拒绝。 |
|
|
126
157
|
| `configure_form` | 给模型配默认表单:`fieldCodes` 决定绑定哪些已发布字段(按序;省略=全量;未知 code 在建表前拒绝 `form-fieldcodes-unknown`),再发布。 |
|
|
127
158
|
| `configure_rule` | 保存并启用一条业务规则(graph 规则引擎)——委托 `rule.save` 的 upsert + `rule.enable`。 |
|
|
159
|
+
| `configure_workflow` | 建流程模板 → 写活动/路由 → 发布 → 绑定事件规则,一步到达可发起形态(节点级事件用 `workflow.bindRule {activityCode,nodeEvent}`)。 |
|
|
128
160
|
| `apply_changes` | **批量入口**:一次提交异构变更清单 `changes:[{tool,args}]`(只收低层原子写工具),按序执行、逐步权威读回、**首败即停**并报已完成步骤与失败点;每步传输超时按 `uncertain` 冻结。含**整批时间预算** `totalTimeoutMs`(缺省 8 分钟):超预算即停并返回 `uncertain` + `progressFile` + `resumeFromIndex`,绝不盲重试。 |
|
|
129
161
|
|
|
130
|
-
### 2. 低层原子写(
|
|
162
|
+
### 2. 低层原子写(82 个,按域)
|
|
131
163
|
|
|
132
164
|
| 域(lane) | 工具 |
|
|
133
165
|
|---|---|
|
|
134
|
-
| 模型 `model-management` | `model.create`、`model.update`(应用树节点改名补丁:bizSchemaCode/id/name/parentId 四键必需,modelType
|
|
135
|
-
|
|
|
136
|
-
|
|
|
166
|
+
| 模型 `model-management` | `model.create`、`model.update`(应用树节点改名补丁:bizSchemaCode/id/name/parentId 四键必需,modelType 恒拒)、`model.move`(换目录,只改菜单节点归属)、`model.setSummary`、`model.delete`、`model.deleteMany` |
|
|
167
|
+
| 应用目录 `app-tree-folders` | `folder.create`(省略 `parentId` = 顶层;同父同名默认拒)、`folder.update`(改名/挂载)、`folder.delete`(非递归,有子节点即拒) |
|
|
168
|
+
| 字段 `field-families` | `field.create`、`field.update`、`field.delete`、`field.publish`(模型级一次发布 + 逐字段读回比对) |
|
|
169
|
+
| 表单 `form-publish-chain` | `configure_form`、`form.draft`(`buttons`/`buttonScripts`/`fromReadback`)、`form.publish`(`buttons`/`buttonScripts`)、`form.runButton`(表单按钮执行,`runtime/form/executeCustomButton`) |
|
|
137
170
|
| 字段级规则 `G1-datarule` | `datarule.create`、`datarule.update`、`datarule.delete` |
|
|
138
|
-
| 业务规则图 `B1-businessrule`
|
|
171
|
+
| 业务规则图 `B1-businessrule` | `rule.save`、`rule.node.update` |
|
|
172
|
+
| 规则生命周期 `S25-rule-lifecycle` | `configure_rule`、`rule.enable`、`rule.disable`、`rule.disableMany`、`rule.delete`、`rule.run`(自定义规则手动执行/按钮验收) |
|
|
139
173
|
| 运行时数据 `M2-runtime-data` | `data.save`、`data.update`、`data.submit`、`data.delete` |
|
|
140
|
-
| 流程 `W7-S29-greenfield` | `workflow.create`、`workflow.update`、`workflow.publish`、`workflow.bindRule
|
|
141
|
-
| 在线脚本 `online-js` | `onlinejs.compose
|
|
142
|
-
| 列表视图 `W1-list-view` | `listview.create`、`listview.configure
|
|
174
|
+
| 流程 / 组织 `W7-S29-greenfield` | `configure_workflow`、`workflow.create`、`workflow.update`、`workflow.publish`、`workflow.bindRule`(流程级 `handler` 或节点级 `activityCode+nodeEvent`)、`workflow.setButtons`(节点「表单操作」勾选)、`workflow.delete`;组织:`org.roleGroup.create/delete`、`org.role.create/update/delete`、`org.role.addUsers/removeUsers/setUserScope`、`org.department.create/update/delete`、`org.user.create/update/delete/transferDepartment` |
|
|
175
|
+
| 在线脚本 `online-js` | `onlinejs.compose`(表单在线 JS;平台模板保真 + SHA-256 读回) |
|
|
176
|
+
| 列表视图 `W1-list-view` | `listview.create`、`listview.configure`(`buttons[].script` / `buttonScripts` 直接写按钮 JS)、`listview.delete` |
|
|
143
177
|
| 应用 `app-management` | `app.create`、`app.update`、`app.delete` |
|
|
144
|
-
| 数据字典 `dictionary-management` | `dictionary.save
|
|
145
|
-
|
|
|
178
|
+
| 数据字典 `dictionary-management` | `dictionary.save`、`dictionary.status`、`dictionary.usage`、`dictionary.class.save`、`dictionary.class.delete` |
|
|
179
|
+
| 报表 `R1-report` | `report.create`、`report.update`、`report.configure`、`report.datasource.link`、`report.delete` |
|
|
180
|
+
| 后端服务注册 `B2-registration` | `service.register/update/delete/import`、`method.create/update/delete/test`、`service.category.create/update/delete`、`service.dbconnpool.create/update/delete` |
|
|
181
|
+
| 批量编排(lane-less) | `apply_changes` |
|
|
146
182
|
|
|
147
183
|
### 3. 每个写调用都经过的治理管道
|
|
148
184
|
|
|
@@ -186,10 +222,13 @@ omc intent readback intent.yaml --env <env> # 解码回 intent 形状做等价
|
|
|
186
222
|
|
|
187
223
|
| 命令 | 作用 |
|
|
188
224
|
|---|---|
|
|
189
|
-
| `omc artifact get <id
|
|
225
|
+
| `omc artifact get <id> [--summary\|--keys\|--jq <path>\|--full]` | 取回大读回的 `detailFile` 工件(0600,校验 sha256)。**正文超 8KB 默认只回形状**(`outline:true` + 顶层键与体积 + `keysCommand`/`fullCommand`);`--jq`/`--keys`/`--summary` 定向取,`--full` 才整包 |
|
|
226
|
+
| `omc help <topic>` | 方法论 topic:**默认只回节索引**(`{heading, bytes}`,约 1KB);`--section "<标题>"`(可重复/逗号多节/序号/唯一前缀)取正文;`--all` 取整篇 |
|
|
227
|
+
| `omc call <tool> … --extract <path>` | **一步取值**:在本次结果的 artifact 内解析路径并直接回值(可重复给多个路径);`--extract-keys` 列顶层键与体积。路径支持 `.rules[code=Create].name` 这种数组选择器,免掉一次 `artifact get` |
|
|
190
228
|
| `omc setup [--list\|--edit <env>\|--remove <env>]` | 命名环境的增删改查 |
|
|
191
229
|
| `omc doctor [--environment <env>]` | 只报告的健康链 |
|
|
192
230
|
| `omc bench [--live-sample] [--env <env>]` | 工具面/wire 面的双面埋点跑分(离线 fake 或 live 采样) |
|
|
231
|
+
| `omc usage [--top <n>] [--records]` | 用量账本:默认只回汇总(调用数/字节/`redundancy` 重复浪费/`byTool` 排名/`estimatedTokens`≈字节/4);`--records` 才回原始记录。数据源 `.omc/usage.jsonl`(每条 `omc` 调用一行,`omc script` 的每行工具调用另记 `script-call`,带 `parentId`) |
|
|
193
232
|
| `npm run eval:discovery-cost --workspace omc-cli` | 三组离线 discovery 成本基线(调用次数、stdout 字节、拒绝/重试、CloudPivot 写入) |
|
|
194
233
|
|
|
195
234
|
## 覆盖边界
|
|
@@ -204,7 +243,7 @@ omc intent readback intent.yaml --env <env> # 解码回 intent 形状做等价
|
|
|
204
243
|
|
|
205
244
|
```
|
|
206
245
|
coverage/ m1-coverage-manifest-v1.json(能力基线)
|
|
207
|
-
skills/
|
|
246
|
+
skills/ 13 篇方法论 topic —— 以 `omc help <topic>` 暴露(见「方法论 topics」)
|
|
208
247
|
docs/ tool-surface-map.md —— 工具面地图
|
|
209
248
|
src/ args/support/plan/read/write/package/context(治理管道)
|
|
210
249
|
lanes/(各域适配器)· fake/(离线夹具)
|
|
@@ -246,7 +285,7 @@ src/ args/support/plan/read/write/package/context(治理管道)
|
|
|
246
285
|
|
|
247
286
|
## npm 分发
|
|
248
287
|
|
|
249
|
-
本包(`@stalfh233/omc-cli`,可执行命令仍是 `omc`)通过公共 npm registry 分发。`omc-core` 是 `omc-cli` 的内部依赖,发布时先发 `omc-core`,再发 `omc-cli`;两者版本保持一致。当前版本为 `0.4.
|
|
288
|
+
本包(`@stalfh233/omc-cli`,可执行命令仍是 `omc`)通过公共 npm registry 分发。`omc-core` 是 `omc-cli` 的内部依赖,发布时先发 `omc-core`,再发 `omc-cli`;两者版本保持一致。当前版本为 `0.4.78`,尚未宣称 `1.0.0` 稳定契约。
|
|
250
289
|
|
|
251
290
|
### 使用已发布包
|
|
252
291
|
|
|
@@ -264,7 +303,8 @@ omc help omc-business-configuration
|
|
|
264
303
|
|
|
265
304
|
```bash
|
|
266
305
|
npm run verify
|
|
267
|
-
npm publish --workspace
|
|
306
|
+
npm publish --workspace omc-core
|
|
307
|
+
npm publish --workspace packages/omc-fake-platform
|
|
268
308
|
npm publish --workspace packages/omc-cli
|
|
269
309
|
```
|
|
270
310
|
|
|
@@ -276,7 +316,7 @@ stdout 是机器解析的 JSON,编码按消费方选择:**终端**(`isTTY`
|
|
|
276
316
|
|
|
277
317
|
### 版本策略
|
|
278
318
|
|
|
279
|
-
- `0.
|
|
319
|
+
- `0.4.x`:继续演进 CLI/覆盖清单/配置契约,允许新增能力和必要的行为修复。
|
|
280
320
|
- `1.0.0`:等 CLI 面、intent 面、覆盖清单和分发安装流程稳定后再发布;发布前应增加迁移说明和兼容性检查。
|
|
281
321
|
|
|
282
322
|
### 分发后自检
|
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { readArgsFile, readStdinArgs, runToolCall, validateAgentPaths } from "./
|
|
|
14
14
|
import { environmentWritable, requiresProductionConfirmation } from "./environment-policy.js";
|
|
15
15
|
import { CLI_EXIT_CODES, classifyResult, envelope, errorEnvelope, requestId } from "./cli-contract.js";
|
|
16
16
|
import { configureOutput, humanSummary, stripOutputFlags, writeEnvelope } from "./cli-output.js";
|
|
17
|
-
import { beginUsage, completeUsage, recordUsageOutput, usageReport } from "./usage-log.js";
|
|
17
|
+
import { beginUsage, completeUsage, ensureProjectIgnore, recordUsageOutput, usageReport } from "./usage-log.js";
|
|
18
18
|
import { artifactFetchHint, artifactRoot, boundResult, emitInvalid, emitResult, emitTransportResult, outputBudget, readArtifact, withTransport, } from "./command-transport.js";
|
|
19
19
|
import { CLI_DEFAULT_OUTPUT_BYTES, assertSafeOutputPath, pruneArtifacts } from "./artifacts.js";
|
|
20
20
|
import { projectJsonPath, projectResultExtract } from "./path-projection.js";
|
|
@@ -36,7 +36,7 @@ import { absencePointer } from "./capability-absences.js";
|
|
|
36
36
|
import { intentConfigRoot, readIntentFile, validateIntentText } from "./intent/cli.js";
|
|
37
37
|
import { intentSchemaBlock, intentExample, intentExampleIds } from "./intent/schema.js";
|
|
38
38
|
import { planIntent, readbackIntent, applyIntent, intentWarnings } from "./intent/plan-apply.js";
|
|
39
|
-
import { listSkills, readSkill, readSkillSection } from "./skills.js";
|
|
39
|
+
import { listSkills, readSkill, readSkillOutline, readSkillSection } from "./skills.js";
|
|
40
40
|
import { parseToolInput, ToolInputError } from "./tool-types.js";
|
|
41
41
|
import { WireError, wireCall } from "./wire.js";
|
|
42
42
|
import { ENDPOINTS } from "./endpoints.js";
|
|
@@ -115,8 +115,17 @@ async function cmdSetup(store, args) {
|
|
|
115
115
|
if (username && password)
|
|
116
116
|
store.writeCredential(name, { username, password });
|
|
117
117
|
const bind = (await ask("将本项目(.omc-env)绑定到该环境?[y/N]:")).toLowerCase() === "y";
|
|
118
|
-
if (bind)
|
|
118
|
+
if (bind) {
|
|
119
119
|
store.bindProjectEnvironment(name);
|
|
120
|
+
// Binding opts the directory into `.omc/` (artifacts + usage log), so keep
|
|
121
|
+
// that directory out of version control. Only an existing .gitignore is
|
|
122
|
+
// touched: creating VCS files unasked is worse than one warning line.
|
|
123
|
+
const ignore = ensureProjectIgnore();
|
|
124
|
+
if (ignore === "appended")
|
|
125
|
+
console.error("已把 .omc/ 追加到 .gitignore(用量账本与产物不纳入版本控制)");
|
|
126
|
+
else if (ignore === "no-gitignore")
|
|
127
|
+
console.error("提示:本项目没有 .gitignore;建议添加一行 .omc/(否则 .omc/ 的用量账本可能被提交)");
|
|
128
|
+
}
|
|
120
129
|
console.error(`环境 ${name} 已保存(幂等,可随时重跑 omc setup)。凭据存储模式:${store.credentialsMode()}`);
|
|
121
130
|
console.error(`安全建议:优先使用专用集成账号(只读+目标应用写权限)而非系统管理员账号;密码仅存于本机 ~/.config/omc/credentials(0600),请勿在对话或文档中传递。`);
|
|
122
131
|
return 0;
|
|
@@ -460,6 +469,12 @@ function parseDurationMs(text) {
|
|
|
460
469
|
const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : unit === "d" ? 86_400_000 : 604_800_000;
|
|
461
470
|
return amount * factor;
|
|
462
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Above this size `omc artifact get` returns the outline, not the body. The
|
|
474
|
+
* transport cap (64 KB) is far too generous for a working context: one 27 KB
|
|
475
|
+
* template re-read three times is 81 KB of the session.
|
|
476
|
+
*/
|
|
477
|
+
const ARTIFACT_INLINE_CEILING_BYTES = 8192;
|
|
463
478
|
function formatBytes(bytes) {
|
|
464
479
|
if (bytes < 1024)
|
|
465
480
|
return `${bytes}B`;
|
|
@@ -531,14 +546,37 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
531
546
|
}
|
|
532
547
|
// r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
|
|
533
548
|
// large ones (up to 55 KB) pulled whole workflow templates just to read one
|
|
534
|
-
// nested key.
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
549
|
+
// nested key. Round 10 still spent 247 KB (22.7% of the session) on 28
|
|
550
|
+
// `artifact get` calls, re-fetching 10–27 KB bodies — the nudge below was
|
|
551
|
+
// advisory and did not change behaviour.
|
|
552
|
+
//
|
|
553
|
+
// 0.4.80: shape first, the same rule as `omc help`. Above the inline ceiling
|
|
554
|
+
// the default is the outline (top-level keys + their sizes) plus runnable
|
|
555
|
+
// projections; `--full` returns the whole body exactly as before. Explicit
|
|
556
|
+
// projections (--summary/--keys/--jq/--path-only) are handled above and are
|
|
557
|
+
// never affected.
|
|
558
|
+
if (options.full !== true && artifact.content.length > ARTIFACT_INLINE_CEILING_BYTES) {
|
|
559
|
+
const record = content && typeof content === "object" && !Array.isArray(content) ? content : undefined;
|
|
560
|
+
const keys = record
|
|
561
|
+
? Object.entries(record)
|
|
562
|
+
.map(([key, value]) => ({ key, bytes: Buffer.byteLength(JSON.stringify(value) ?? "null", "utf8"), kind: Array.isArray(value) ? `array[${value.length}]` : value === null ? "null" : typeof value }))
|
|
563
|
+
.sort((left, right) => right.bytes - left.bytes)
|
|
564
|
+
: [];
|
|
565
|
+
return emitTransportResult({
|
|
566
|
+
status: "ok",
|
|
567
|
+
artifactId: id,
|
|
568
|
+
sha256: artifact.sha256,
|
|
569
|
+
bytes: artifact.content.length,
|
|
570
|
+
outline: true,
|
|
571
|
+
...(record ? { keys } : { kind: typeof content }),
|
|
572
|
+
hint: `正文 ${Math.round(artifact.content.length / 1024)}KB 超过内联上限 ${ARTIFACT_INLINE_CEILING_BYTES}B,本次只回形状(顶层键 + 体积)。要某几个键:--keys <a,b>;要某个路径:--jq '<path>'(支持 .rules[code=Create].name 这类数组选择器);确实需要整包:--full。`,
|
|
573
|
+
keysCommand: keys.length > 0 ? `omc artifact get ${id} --keys ${keys.slice(0, 3).map((entry) => entry.key).join(",")}` : `omc artifact get ${id} --summary`,
|
|
574
|
+
fullCommand: `omc artifact get ${id} --full`,
|
|
575
|
+
}, transport);
|
|
576
|
+
}
|
|
577
|
+
const result = { status: "ok", artifactId: id, sha256: artifact.sha256, content };
|
|
540
578
|
if (Buffer.byteLength(JSON.stringify(result), "utf8") > transport.maxOutputBytes)
|
|
541
|
-
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length,
|
|
579
|
+
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, fetch: artifactFetchHint(id) }, transport);
|
|
542
580
|
return emitTransportResult(result, transport);
|
|
543
581
|
}
|
|
544
582
|
catch (error) {
|
|
@@ -692,8 +730,8 @@ function cmdDiscovery(kind, transport, value, options = {}) {
|
|
|
692
730
|
}
|
|
693
731
|
return emitTransportResult({ status: "ok", ...result }, transport);
|
|
694
732
|
}
|
|
695
|
-
function cmdUsage(transport) {
|
|
696
|
-
return emitTransportResult(usageReport(), transport);
|
|
733
|
+
function cmdUsage(transport, options = {}) {
|
|
734
|
+
return emitTransportResult(usageReport(undefined, { records: options.records === true, top: options.top, resolveLikeWriter: true }), transport);
|
|
697
735
|
}
|
|
698
736
|
/**
|
|
699
737
|
* Round 3, module A — `omc next "<goal>"`: the goal → route solver. Read-only,
|
|
@@ -853,7 +891,7 @@ function cmdIntentSchema(transport, example, exampleRequested) {
|
|
|
853
891
|
* like `git help <concept>`. The command list is the "what"; these topics are
|
|
854
892
|
* the "how".
|
|
855
893
|
*/
|
|
856
|
-
function cmdHelp(topic, commandNames, transport, section, sections = false) {
|
|
894
|
+
function cmdHelp(topic, commandNames, transport, section, sections = false, all = false) {
|
|
857
895
|
const topics = listSkills();
|
|
858
896
|
const commands = [...commandNames].sort();
|
|
859
897
|
// Several sections in one call: `--section a --section b` or `--section a,b`.
|
|
@@ -943,8 +981,31 @@ function cmdHelp(topic, commandNames, transport, section, sections = false) {
|
|
|
943
981
|
console.error(`help: ${message}`);
|
|
944
982
|
return emitInvalid(transport, { message });
|
|
945
983
|
}
|
|
946
|
-
|
|
947
|
-
|
|
984
|
+
if (all) {
|
|
985
|
+
console.error(`omc help ${document.name} --all(${Buffer.byteLength(document.content, "utf8")} B)`);
|
|
986
|
+
return emitTransportResult({ status: "ok", topic: document }, transport);
|
|
987
|
+
}
|
|
988
|
+
// Shape first. Round 10 paid 35,872 B per full read of one topic, seven times
|
|
989
|
+
// in a single session, to use one or two headings. The index names every
|
|
990
|
+
// heading AND its price, so the caller can pick a section (or ask for --all)
|
|
991
|
+
// instead of buying the whole document by default.
|
|
992
|
+
const outline = readSkillOutline(topic);
|
|
993
|
+
if (outline === undefined)
|
|
994
|
+
return emitInvalid(transport, { message: `未知 topic "${topic}";先用 omc help topics 查看可用 topic` });
|
|
995
|
+
console.error(`omc help ${topic}:${outline.sections.length} 节 / 全文 ${outline.bytes} B(默认只回索引;--section 取正文,--all 取全文)`);
|
|
996
|
+
return emitTransportResult({
|
|
997
|
+
status: "ok",
|
|
998
|
+
topic: outline.document.name,
|
|
999
|
+
bytes: outline.bytes,
|
|
1000
|
+
sectionCount: outline.sections.length,
|
|
1001
|
+
// Each section carries its price inline. `{"heading":"入口","bytes":282}`
|
|
1002
|
+
// is cheaper than parallel arrays and reads better than a heading→bytes map;
|
|
1003
|
+
// the heading names are what `--section` accepts.
|
|
1004
|
+
sections: outline.sections.map((entry) => ({ heading: entry.heading, bytes: entry.bytes })),
|
|
1005
|
+
hint: "默认只回节索引:先按各节 bytes 挑要读的节,再用 --section 取正文(可重复或逗号一次多节,也可用节序号或唯一前缀)。确需整篇才 --all。",
|
|
1006
|
+
sectionCommand: `omc help ${outline.document.name} --section "<heading>"`,
|
|
1007
|
+
fullCommand: `omc help ${outline.document.name} --all`,
|
|
1008
|
+
}, transport);
|
|
948
1009
|
}
|
|
949
1010
|
/**
|
|
950
1011
|
* The text every agent should see when it lands on `omc --help`, `omc` with no
|
|
@@ -955,8 +1016,9 @@ function cmdHelp(topic, commandNames, transport, section, sections = false) {
|
|
|
955
1016
|
function onboardingHelp() {
|
|
956
1017
|
return [
|
|
957
1018
|
"首次使用、或不确定从哪开始:omc help omc-start-here(随包权威 topic 路线);被拒或状态异常:omc help omc-refusals。",
|
|
1019
|
+
"方法论按需读:omc help <topic> 默认只回节索引(每节字节数);用 --section \"<标题>\" 取正文(可重复),确需整篇才 --all。",
|
|
958
1020
|
"目标→工具一步定位:omc next \"<你要做什么>\"(离线、只读,给能力/说明/首批命令;不确定会明说并回退到 omc-requirement-analysis)。",
|
|
959
|
-
"大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --extract <path>(可重复)或 --extract-keys,省掉再跑一次 omc artifact get
|
|
1021
|
+
"大结果取值一步到位:读面回 detailArtifactId 时,用 omc call <tool> … --extract <path>(可重复)或 --extract-keys,省掉再跑一次 omc artifact get;artifact get 正文超 8KB 默认也只回形状(--jq/--keys 定向取,--full 才整包)。",
|
|
960
1022
|
"工具发现:omc commands --json(含 capability id/lane/recipes;加 --compact 只回工具行、无目录散文)· omc capabilities(能力 id/lane/aliases/tools/依赖顺序)· omc capability <id> --executable(单能力完整执行包)。",
|
|
961
1023
|
"单工具/契约:omc describe <tool>(schema+recipe+能力路由)· omc contract <lane>(wire 契约+能力路由);环境就绪:omc call discover_environment --env <env> --args '{}'。",
|
|
962
1024
|
"写入两道门:对话层草稿确认(omc help omc-config-draft-review)+ 环境 writable 开关;生产环境每笔写需在 args 里带 confirmProduction:true。",
|
|
@@ -1035,7 +1097,7 @@ async function main() {
|
|
|
1035
1097
|
command.outputHelp();
|
|
1036
1098
|
return CLI_EXIT_CODES.ok;
|
|
1037
1099
|
}
|
|
1038
|
-
command.command("help [topic]").option("--section [heading]", "按标题取该节内容;可重复或用逗号一次取多节:--section a --section b / --section a,b(不带值则列出该 topic 的节标题)", (value, previous) => [...previous, value], []).option("--sections").option("--max-output-bytes <bytes>").action(async (topic, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdHelp(topic, command.commands.map((entry) => entry.name()), transport, options.section, options.sections === true)); });
|
|
1100
|
+
command.command("help [topic]").option("--section [heading]", "按标题取该节内容;可重复或用逗号一次取多节:--section a --section b / --section a,b(不带值则列出该 topic 的节标题)", (value, previous) => [...previous, value], []).option("--sections").option("--all", "回整篇正文(默认只回节索引:标题 + 各节字节)").option("--max-output-bytes <bytes>").action(async (topic, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdHelp(topic, command.commands.map((entry) => entry.name()), transport, options.section, options.sections === true, options.all === true)); });
|
|
1039
1101
|
command.command("setup").option("--list").option("--edit <environment>").option("--remove <environment>").action(async (options) => {
|
|
1040
1102
|
commandExitCode = await cmdSetup(new ConfigStore(defaultPaths()), [...(options.list ? ["--list"] : []), ...(options.edit ? ["--edit", options.edit] : []), ...(options.remove ? ["--remove", options.remove] : [])]);
|
|
1041
1103
|
});
|
|
@@ -1169,8 +1231,8 @@ async function main() {
|
|
|
1169
1231
|
console.error(`已清理 ${result.removed} 个读回产物,释放 ${formatBytes(result.freedBytes)};保留 ${result.kept} 个(${formatBytes(result.keptBytes)})`);
|
|
1170
1232
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
1171
1233
|
});
|
|
1172
|
-
artifactCommand.command("get <id>").option("--jq <path>").option("--path-only").option("--summary").option("--keys [a,b]").option("--max-output-bytes <bytes>").action(async (id, options) => {
|
|
1173
|
-
commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(options.jq ? { jq: options.jq } : {}), pathOnly: options.pathOnly === true, summary: options.summary === true || options.keys === true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}) }));
|
|
1234
|
+
artifactCommand.command("get <id>").option("--jq <path>").option("--path-only").option("--summary").option("--keys [a,b]").option("--full", "整包返回正文(默认:正文超过 8KB 时只回形状 outline + fetch,按需 --jq/--keys 定向取值)").option("--max-output-bytes <bytes>").action(async (id, options) => {
|
|
1235
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(options.jq ? { jq: options.jq } : {}), pathOnly: options.pathOnly === true, summary: options.summary === true || options.keys === true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
|
|
1174
1236
|
});
|
|
1175
1237
|
command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
|
|
1176
1238
|
command.command("commands").option("--json").option("--compact").option("--filter <keyword>").option("--lane <id>").option("--max-output-bytes <bytes>").action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("commands", transport, undefined, { json: options.json === true, compact: options.compact === true, ...(options.filter ? { filter: options.filter } : {}), ...(options.lane ? { lane: options.lane } : {}) })); });
|
|
@@ -1185,8 +1247,12 @@ async function main() {
|
|
|
1185
1247
|
command.command("next <goal>").option("--env <environment>").option("--max-output-bytes <bytes>").action(async (goal, options) => {
|
|
1186
1248
|
commandExitCode = await withTransport(options, request, (transport) => cmdNext(goal, transport));
|
|
1187
1249
|
});
|
|
1188
|
-
command.command("usage").option("--max-output-bytes <bytes>").action(async (options) => {
|
|
1189
|
-
|
|
1250
|
+
command.command("usage").option("--records").option("--top <n>").option("--max-output-bytes <bytes>").addHelpText("after", "\n用量账本(.omc/usage.jsonl):默认只回汇总(调用数/字节/重复浪费/按工具 Top N);--records 才回原始记录。\n").action(async (options) => {
|
|
1251
|
+
const parsedTop = options.top === undefined ? undefined : Number.parseInt(String(options.top), 10);
|
|
1252
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdUsage(transport, {
|
|
1253
|
+
records: options.records === true,
|
|
1254
|
+
...(Number.isFinite(parsedTop) && parsedTop > 0 ? { top: parsedTop } : {}),
|
|
1255
|
+
}));
|
|
1190
1256
|
});
|
|
1191
1257
|
command.command("capability <id>").option("--executable").option("--max-output-bytes <bytes>").action(async (id, options) => {
|
|
1192
1258
|
commandExitCode = await withTransport(options, request, (transport) => cmdCapability(id, transport, options.executable === true));
|
package/dist/goal-routes.js
CHANGED
|
@@ -286,7 +286,7 @@ function sectionBytes(pointer) {
|
|
|
286
286
|
const resolved = readSkillSection(pointer.topic, pointer.section);
|
|
287
287
|
return resolved ? Buffer.byteLength(resolved.section, "utf8") : 0;
|
|
288
288
|
}
|
|
289
|
-
const READ_HINT = '按 readCommands 一次读完(每条命令用可重复的 `--section` 取该 topic 的所有相关节,等价于逐节 `omc help <topic> --section "<section>"` 但只花一次往返);单节字节数见 readSectionBudget
|
|
289
|
+
const READ_HINT = '按 readCommands 一次读完(每条命令用可重复的 `--section` 取该 topic 的所有相关节,等价于逐节 `omc help <topic> --section "<section>"` 但只花一次往返);单节字节数见 readSectionBudget,按预算自选。`omc help <topic>` 默认只回节索引(每节带字节数,约 1KB),不要再用 `--all` 读整篇(5–14KB,读一次就留在后续每一轮)。';
|
|
290
290
|
function orderedCapabilities(ids, index) {
|
|
291
291
|
const selected = new Set(ids);
|
|
292
292
|
const nodes = [...index.nodeById.values()];
|
package/dist/script.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ConfigStore, defaultPaths } from "./config-store.js";
|
|
|
4
4
|
import { SessionManager } from "./session-manager.js";
|
|
5
5
|
import { makeToolContext } from "./tools.js";
|
|
6
6
|
import { runToolCall } from "./call.js";
|
|
7
|
+
import { currentUsageId, recordScriptCall } from "./usage-log.js";
|
|
7
8
|
export function parseScript(text) {
|
|
8
9
|
const lines = [];
|
|
9
10
|
const raw = text.split("\n");
|
|
@@ -36,8 +37,14 @@ export function parseScript(text) {
|
|
|
36
37
|
export async function runScript(lines, options = {}) {
|
|
37
38
|
const print = options.print ?? ((line) => console.log(line));
|
|
38
39
|
const context = options.context ?? makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
40
|
+
// The outer `script` invocation is already recorded by completeUsage; these
|
|
41
|
+
// inner records are what make a batch measurable at all. Before this, all 23
|
|
42
|
+
// `script -` calls in round 10 logged stdoutBytes=0 and the actual work was
|
|
43
|
+
// invisible (the outer record says "script -", the log says nothing else).
|
|
44
|
+
const parentId = currentUsageId();
|
|
39
45
|
let failed = false;
|
|
40
46
|
for (const [index, line] of lines.entries()) {
|
|
47
|
+
const startedMs = Date.now();
|
|
41
48
|
const { exitCode, result } = await runToolCall(line.tool, {
|
|
42
49
|
args: line.args,
|
|
43
50
|
environment: line.env ?? options.defaultEnv,
|
|
@@ -46,7 +53,18 @@ export async function runScript(lines, options = {}) {
|
|
|
46
53
|
context,
|
|
47
54
|
print: () => undefined,
|
|
48
55
|
});
|
|
49
|
-
|
|
56
|
+
const rendered = JSON.stringify({ index, tool: line.tool, exitCode, result });
|
|
57
|
+
recordScriptCall({
|
|
58
|
+
...(parentId ? { parentId } : {}),
|
|
59
|
+
index,
|
|
60
|
+
tool: line.tool,
|
|
61
|
+
...(line.args ? { args: line.args } : {}),
|
|
62
|
+
exitCode,
|
|
63
|
+
result,
|
|
64
|
+
stdoutBytes: Buffer.byteLength(rendered, "utf8"),
|
|
65
|
+
durationMs: Date.now() - startedMs,
|
|
66
|
+
});
|
|
67
|
+
print(rendered);
|
|
50
68
|
if (exitCode !== 0) {
|
|
51
69
|
failed = true;
|
|
52
70
|
if (options.continueOnError !== true) {
|
package/dist/skills.js
CHANGED
|
@@ -52,16 +52,45 @@ export function readSkill(name) {
|
|
|
52
52
|
return undefined;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Headings that are really headings. The naive `^#{1,6}` scan also matched
|
|
57
|
+
* shell/JS comments inside fenced code blocks — `# --env 在单环境项目...` in
|
|
58
|
+
* omc-business-configuration was listed as an H1 section, which both polluted
|
|
59
|
+
* the section list and (since it was a second H1) hid every H2 from the index.
|
|
60
|
+
* Fenced blocks (``` and ~~~) are skipped, per CommonMark: a closing fence is
|
|
61
|
+
* the same character and at least as long as the opening one.
|
|
62
|
+
*/
|
|
63
|
+
function scanHeadings(lines) {
|
|
64
|
+
const found = [];
|
|
65
|
+
let fence;
|
|
66
|
+
for (const [index, line] of lines.entries()) {
|
|
67
|
+
const fenceMatch = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
68
|
+
if (fenceMatch) {
|
|
69
|
+
const marker = fenceMatch[1];
|
|
70
|
+
if (fence === undefined)
|
|
71
|
+
fence = { char: marker[0], length: marker.length };
|
|
72
|
+
else if (marker[0] === fence.char && marker.length >= fence.length)
|
|
73
|
+
fence = undefined;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (fence !== undefined)
|
|
77
|
+
continue;
|
|
78
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
79
|
+
if (match)
|
|
80
|
+
found.push({ index, level: match[1].length, heading: match[2] });
|
|
81
|
+
}
|
|
82
|
+
return found;
|
|
83
|
+
}
|
|
55
84
|
/** Return one Markdown heading and its nested content without loading the full topic into the envelope. */
|
|
56
85
|
export function readSkillSection(name, requested) {
|
|
57
86
|
const document = readSkill(name);
|
|
58
87
|
if (!document)
|
|
59
88
|
return undefined;
|
|
60
89
|
const lines = document.content.split(/\r?\n/);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
const scanned = scanHeadings(lines);
|
|
91
|
+
// The section LIST is what `--section` with no value returns: every real
|
|
92
|
+
// heading (any level), in document order.
|
|
93
|
+
const headings = scanned.map((entry) => entry.heading);
|
|
65
94
|
const normalized = requested.trim().toLowerCase();
|
|
66
95
|
// A requested section may be given by its ordered number ("8") instead of the
|
|
67
96
|
// full heading ("8. Canonical configuration order(规范配置顺序)") — the
|
|
@@ -80,25 +109,19 @@ export function readSkillSection(name, requested) {
|
|
|
80
109
|
}
|
|
81
110
|
return false;
|
|
82
111
|
};
|
|
83
|
-
let start =
|
|
84
|
-
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
85
|
-
return match !== null && matches(match[2].trim().toLowerCase());
|
|
86
|
-
});
|
|
112
|
+
let start = scanned.find((entry) => matches(entry.heading.trim().toLowerCase()))?.index ?? -1;
|
|
87
113
|
// Forgiving fallback: a unique prefix/substring match ("流程" for "流程字段权限").
|
|
88
114
|
// A non-unique partial matches nothing (precision over recall) and the
|
|
89
115
|
// not-found error lists the real headings, so the caller can retry once.
|
|
90
116
|
if (start < 0 && normalized.length >= 2) {
|
|
91
|
-
const partial =
|
|
92
|
-
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
93
|
-
return match !== null && match[2].trim().toLowerCase().includes(normalized);
|
|
94
|
-
});
|
|
117
|
+
const partial = scanned.filter((entry) => entry.heading.trim().toLowerCase().includes(normalized));
|
|
95
118
|
if (partial.length === 1)
|
|
96
|
-
start =
|
|
119
|
+
start = partial[0].index;
|
|
97
120
|
}
|
|
98
121
|
if (start < 0)
|
|
99
122
|
return { document, section: "", headings };
|
|
100
|
-
const
|
|
101
|
-
const level =
|
|
123
|
+
const startEntry = scanned.find((entry) => entry.index === start);
|
|
124
|
+
const level = startEntry.level;
|
|
102
125
|
let end = lines.length;
|
|
103
126
|
for (let index = start + 1; index < lines.length; index += 1) {
|
|
104
127
|
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
|
|
@@ -109,4 +132,42 @@ export function readSkillSection(name, requested) {
|
|
|
109
132
|
}
|
|
110
133
|
return { document, section: lines.slice(start, end).join("\n").trim() + "\n", headings };
|
|
111
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The "shape" of a topic: every heading with the bytes it would cost to read.
|
|
137
|
+
*
|
|
138
|
+
* Round 10 measured a single `help omc-business-configuration` at 35,872 B and
|
|
139
|
+
* seven such full reads in one session; the caller only ever needed one or two
|
|
140
|
+
* headings. An index costs ~600 B and names the price of each part, so the
|
|
141
|
+
* caller chooses instead of paying for the whole document by default.
|
|
142
|
+
*/
|
|
143
|
+
export function readSkillOutline(name) {
|
|
144
|
+
const document = readSkill(name);
|
|
145
|
+
if (!document)
|
|
146
|
+
return undefined;
|
|
147
|
+
const lines = document.content.split(/\r?\n/);
|
|
148
|
+
const headings = scanHeadings(lines);
|
|
149
|
+
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`;
|
|
161
|
+
return { heading: entry.heading, level: entry.level, bytes: Buffer.byteLength(body, "utf8") };
|
|
162
|
+
});
|
|
163
|
+
const bytes = Buffer.byteLength(document.content, "utf8");
|
|
164
|
+
// A topic whose whole body sits under a single H1 (the title) would produce a
|
|
165
|
+
// one-entry index — useless. Descend to the next level in that case, so the
|
|
166
|
+
// index always names the parts a caller can actually choose between.
|
|
167
|
+
const levels = [...new Set(headings.map((entry) => entry.level))].sort((left, right) => left - right);
|
|
168
|
+
const singleRoot = levels.length > 1 && headings.filter((entry) => entry.level === levels[0]).length === 1;
|
|
169
|
+
const indexLevel = singleRoot ? levels[1] : levels[0];
|
|
170
|
+
const visible = indexLevel === undefined ? sections : sections.filter((entry) => entry.level === indexLevel);
|
|
171
|
+
return { document, bytes, sections: visible.length > 0 ? visible : sections };
|
|
172
|
+
}
|
|
112
173
|
//# sourceMappingURL=skills.js.map
|
package/dist/usage-log.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, closeSync, statSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, closeSync, renameSync, statSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
5
|
let pending;
|
|
5
6
|
/**
|
|
6
7
|
* The project session marker: a working directory that opted into the usage log
|
|
@@ -10,6 +11,11 @@ let pending;
|
|
|
10
11
|
export function projectSessionActive() {
|
|
11
12
|
return logPath() !== undefined;
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The project-scoped log: only present when the working directory opted in with
|
|
16
|
+
* `.omc-env` (or `OMC_USAGE_LOG`). Session-scoped caches and idempotency memos
|
|
17
|
+
* key off THIS path so repo tests and the golden master stay stable.
|
|
18
|
+
*/
|
|
13
19
|
function logPath() {
|
|
14
20
|
const explicit = process.env.OMC_USAGE_LOG;
|
|
15
21
|
if (explicit)
|
|
@@ -17,6 +23,83 @@ function logPath() {
|
|
|
17
23
|
const projectEnv = resolve(process.cwd(), ".omc-env");
|
|
18
24
|
return existsSync(projectEnv) ? resolve(process.cwd(), ".omc", "usage.jsonl") : undefined;
|
|
19
25
|
}
|
|
26
|
+
const USAGE_LOG_MAX_BYTES = 8 * 1024 * 1024;
|
|
27
|
+
/**
|
|
28
|
+
* Where invocation records are WRITTEN. Measurement must not silently stop just
|
|
29
|
+
* because a directory never ran `omc env use`: fall back to a per-directory file
|
|
30
|
+
* under the config root (`$OMC_CONFIG_HOME/usage/<cwd-hash>.jsonl`, default
|
|
31
|
+
* `~/.config/omc/usage/`). Tests isolate with OMC_CONFIG_HOME, so the fallback
|
|
32
|
+
* never leaks into the developer's real home during `npm test`.
|
|
33
|
+
*
|
|
34
|
+
* Memos still read `logPath()` only — a fallback log is append-only telemetry,
|
|
35
|
+
* never a source of "already did this" answers.
|
|
36
|
+
*/
|
|
37
|
+
function recordLogPath() {
|
|
38
|
+
const explicit = process.env.OMC_USAGE_LOG;
|
|
39
|
+
if (explicit)
|
|
40
|
+
return { path: resolve(explicit), scope: "project" };
|
|
41
|
+
const project = logPath();
|
|
42
|
+
if (project)
|
|
43
|
+
return { path: project, scope: "project" };
|
|
44
|
+
if (process.env.OMC_USAGE_DISABLE === "1")
|
|
45
|
+
return undefined;
|
|
46
|
+
// The fallback writes into the user's config root. Keep it out of the test
|
|
47
|
+
// suite and out of scripted non-interactive runs unless explicitly asked:
|
|
48
|
+
// a measurement side effect must never be the reason a fixture directory
|
|
49
|
+
// suddenly has an extra file (or the developer's home grows during npm test).
|
|
50
|
+
if (process.env.NODE_TEST_CONTEXT !== undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (process.env.OMC_USAGE_USER_FALLBACK === "0")
|
|
53
|
+
return undefined;
|
|
54
|
+
const configRoot = process.env.OMC_CONFIG_HOME ? resolve(process.env.OMC_CONFIG_HOME) : join(homedir(), ".config", "omc");
|
|
55
|
+
const cwd = resolve(process.cwd());
|
|
56
|
+
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 12);
|
|
57
|
+
return { path: join(configRoot, "usage", `${hash}.jsonl`), scope: "user" };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `.omc/` holds artifacts and this log; committing it leaks business data. The
|
|
61
|
+
* append is explicit-only (`omc setup` binding a project); `projectIgnoreState`
|
|
62
|
+
* is what the read side reports, so `omc usage` never edits the working tree.
|
|
63
|
+
*/
|
|
64
|
+
function appendRecord(path, record) {
|
|
65
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
66
|
+
try {
|
|
67
|
+
if (existsSync(path) && statSync(path).size > USAGE_LOG_MAX_BYTES)
|
|
68
|
+
renameSync(path, `${path}.1`);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Rotation is best-effort: a failed rename must not drop the record.
|
|
72
|
+
}
|
|
73
|
+
appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
|
|
74
|
+
}
|
|
75
|
+
/** Append a `script-call` record: one line of `omc script` = one inner tool call. */
|
|
76
|
+
export function recordScriptCall(input) {
|
|
77
|
+
const target = recordLogPath();
|
|
78
|
+
if (!target)
|
|
79
|
+
return;
|
|
80
|
+
const envelope = input.result ?? {};
|
|
81
|
+
const error = envelope.error && typeof envelope.error === "object" ? envelope.error : undefined;
|
|
82
|
+
const resultEnvelope = envelope.result && typeof envelope.result === "object" ? envelope.result : undefined;
|
|
83
|
+
const blocker = typeof resultEnvelope?.blocker === "string" ? resultEnvelope.blocker : typeof envelope.blocker === "string" ? envelope.blocker : undefined;
|
|
84
|
+
const status = typeof envelope.status === "string" ? envelope.status : input.exitCode === 0 ? "ok" : "error";
|
|
85
|
+
appendRecord(target.path, {
|
|
86
|
+
kind: "script-call",
|
|
87
|
+
id: createHash("sha256").update(`${Date.now()}\u0000${input.tool}\u0000${input.index}`).digest("hex").slice(0, 16),
|
|
88
|
+
...(input.parentId ? { parentId: input.parentId } : {}),
|
|
89
|
+
index: input.index,
|
|
90
|
+
tool: input.tool,
|
|
91
|
+
argsDigest: createHash("sha256").update(`${input.tool}\u0000${JSON.stringify(input.args ?? {})}`).digest("hex"),
|
|
92
|
+
argSummary: summarizeArgsObject(input.args ?? {}),
|
|
93
|
+
stdoutBytes: input.stdoutBytes,
|
|
94
|
+
durationMs: input.durationMs,
|
|
95
|
+
status,
|
|
96
|
+
exitCode: input.exitCode,
|
|
97
|
+
...(typeof error?.code === "string" ? { errorCode: error.code } : {}),
|
|
98
|
+
...(blocker ? { blocker } : {}),
|
|
99
|
+
startedAt: new Date().toISOString(),
|
|
100
|
+
...(target.scope === "user" ? { scope: "user", cwd: resolve(process.cwd()) } : {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
20
103
|
/** The args digest of the in-flight invocation (session-scoped memo key). */
|
|
21
104
|
export function currentUsageArgsDigest() {
|
|
22
105
|
return pending?.argsDigest;
|
|
@@ -126,7 +209,7 @@ function commandOf(argv) {
|
|
|
126
209
|
for (let index = 2; index < argv.length && values.length < 2; index += 1) {
|
|
127
210
|
const token = argv[index];
|
|
128
211
|
if (token.startsWith("--")) {
|
|
129
|
-
if (["--out", "--max-output-bytes", "--args", "--args-file", "--env", "--environment", "--request-id", "--timeout-ms", "--wait", "--path"].includes(token))
|
|
212
|
+
if (["--out", "--max-output-bytes", "--args", "--args-file", "--env", "--environment", "--request-id", "--timeout-ms", "--wait", "--path", "--top"].includes(token))
|
|
130
213
|
index += 1;
|
|
131
214
|
continue;
|
|
132
215
|
}
|
|
@@ -143,25 +226,32 @@ function facesOf(argv) {
|
|
|
143
226
|
return faces.filter((face) => argv.includes(face));
|
|
144
227
|
}
|
|
145
228
|
const safeArgumentKeys = new Set(["schemaCode", "propertyCode", "code", "workflowCode", "ruleCode", "sheetCode", "id", "dataRuleType", "propertyType", "published", "handler"]);
|
|
146
|
-
function
|
|
229
|
+
function summarizeArgsObject(parsed) {
|
|
147
230
|
const summary = { argKeys: [] };
|
|
231
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
232
|
+
const keys = Object.keys(parsed).sort();
|
|
233
|
+
summary.argKeys = keys;
|
|
234
|
+
// Only key names plus a fixed allowlist of business identifiers: the log is
|
|
235
|
+
// durable and shared, so free-form values (scripts, form data) stay out.
|
|
236
|
+
for (const key of keys)
|
|
237
|
+
if (safeArgumentKeys.has(key)) {
|
|
238
|
+
const value = parsed[key];
|
|
239
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
240
|
+
summary[key] = value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return summary;
|
|
244
|
+
}
|
|
245
|
+
function summarizeArgs(argv) {
|
|
246
|
+
let summary = { argKeys: [] };
|
|
148
247
|
const argsIndex = argv.indexOf("--args");
|
|
149
248
|
if (argsIndex >= 0) {
|
|
150
249
|
try {
|
|
151
250
|
const parsed = JSON.parse(argv[argsIndex + 1] ?? "{}");
|
|
152
|
-
|
|
153
|
-
const keys = Object.keys(parsed).sort();
|
|
154
|
-
summary.argKeys = keys;
|
|
155
|
-
for (const key of keys)
|
|
156
|
-
if (safeArgumentKeys.has(key)) {
|
|
157
|
-
const value = parsed[key];
|
|
158
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
159
|
-
summary[key] = value;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
251
|
+
summary = summarizeArgsObject(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
|
|
162
252
|
}
|
|
163
253
|
catch {
|
|
164
|
-
summary
|
|
254
|
+
summary = { argKeys: ["<invalid-json>"] };
|
|
165
255
|
}
|
|
166
256
|
}
|
|
167
257
|
const envIndex = argv.findIndex((token) => token === "--env" || token === "--environment");
|
|
@@ -175,8 +265,8 @@ function summarizeArgs(argv) {
|
|
|
175
265
|
return summary;
|
|
176
266
|
}
|
|
177
267
|
export function beginUsage(argv) {
|
|
178
|
-
const
|
|
179
|
-
if (!
|
|
268
|
+
const target = recordLogPath();
|
|
269
|
+
if (!target)
|
|
180
270
|
return;
|
|
181
271
|
const raw = argv.slice(2).join("\u0000");
|
|
182
272
|
const argsDigest = createHash("sha256").update(raw).digest("hex");
|
|
@@ -191,8 +281,13 @@ export function beginUsage(argv) {
|
|
|
191
281
|
stdoutBytes: 0,
|
|
192
282
|
fileBytes: 0,
|
|
193
283
|
status: "no-envelope",
|
|
284
|
+
scope: target.scope,
|
|
194
285
|
};
|
|
195
286
|
}
|
|
287
|
+
/** The id of the in-flight invocation — `script-call` records point back at it. */
|
|
288
|
+
export function currentUsageId() {
|
|
289
|
+
return pending?.id;
|
|
290
|
+
}
|
|
196
291
|
export function recordUsageOutput(value, stdoutBytes, fileBytes = 0) {
|
|
197
292
|
if (!pending)
|
|
198
293
|
return;
|
|
@@ -218,10 +313,9 @@ export function completeUsage(exitCode) {
|
|
|
218
313
|
return;
|
|
219
314
|
const current = pending;
|
|
220
315
|
pending = undefined;
|
|
221
|
-
const
|
|
222
|
-
if (!
|
|
316
|
+
const target = recordLogPath();
|
|
317
|
+
if (!target)
|
|
223
318
|
return;
|
|
224
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
225
319
|
const record = {
|
|
226
320
|
kind: "invocation",
|
|
227
321
|
id: current.id,
|
|
@@ -238,8 +332,37 @@ export function completeUsage(exitCode) {
|
|
|
238
332
|
...(current.errorCode ? { errorCode: current.errorCode } : {}),
|
|
239
333
|
...(current.blocker ? { blocker: current.blocker } : {}),
|
|
240
334
|
...(current.requestId ? { requestId: current.requestId } : {}),
|
|
335
|
+
...(target.scope === "user" ? { scope: "user", cwd: resolve(process.cwd()) } : {}),
|
|
241
336
|
};
|
|
242
|
-
|
|
337
|
+
appendRecord(target.path, record);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Whether the project's `.omc/` directory is excluded from version control.
|
|
341
|
+
* Reported by `omc usage`; the actual append happens only in `env use`, because
|
|
342
|
+
* a read command must not mutate the working tree (r10 review: `omc usage` in a
|
|
343
|
+
* repo would otherwise edit .gitignore behind the caller's back).
|
|
344
|
+
*/
|
|
345
|
+
export function projectIgnoreState(projectRoot = process.cwd()) {
|
|
346
|
+
if (!existsSync(join(projectRoot, ".git")))
|
|
347
|
+
return "no-git";
|
|
348
|
+
const ignoreFile = join(projectRoot, ".gitignore");
|
|
349
|
+
if (!existsSync(ignoreFile))
|
|
350
|
+
return "no-gitignore";
|
|
351
|
+
const text = readFileSync(ignoreFile, "utf8");
|
|
352
|
+
const present = text.split(/\r?\n/).some((line) => [".omc/", ".omc", "/.omc/", "/.omc"].includes(line.trim()));
|
|
353
|
+
return present ? "ignored" : "missing";
|
|
354
|
+
}
|
|
355
|
+
/** Add `.omc/` to an existing .gitignore (explicit setup path only). */
|
|
356
|
+
export function ensureProjectIgnore(projectRoot = process.cwd()) {
|
|
357
|
+
const state = projectIgnoreState(projectRoot);
|
|
358
|
+
if (state === "no-git" || state === "no-gitignore")
|
|
359
|
+
return state;
|
|
360
|
+
if (state === "ignored")
|
|
361
|
+
return "ignored";
|
|
362
|
+
const ignoreFile = join(projectRoot, ".gitignore");
|
|
363
|
+
const text = readFileSync(ignoreFile, "utf8");
|
|
364
|
+
appendFileSync(ignoreFile, `${text.endsWith("\n") || text.length === 0 ? "" : "\n"}.omc/\n`, "utf8");
|
|
365
|
+
return "appended";
|
|
243
366
|
}
|
|
244
367
|
function readUsageTail(path, maxBytes) {
|
|
245
368
|
if (!existsSync(path))
|
|
@@ -280,67 +403,132 @@ function readUsage(path) {
|
|
|
280
403
|
}
|
|
281
404
|
});
|
|
282
405
|
}
|
|
283
|
-
|
|
284
|
-
|
|
406
|
+
/** `call rule.save` → `rule.save`; anything else keeps its command name. */
|
|
407
|
+
function toolOf(command) {
|
|
408
|
+
return command.startsWith("call ") ? command.slice(5) : command;
|
|
409
|
+
}
|
|
410
|
+
export function usageReport(pathArg, options = {}) {
|
|
411
|
+
const fallback = options.resolveLikeWriter === true ? recordLogPath() : undefined;
|
|
412
|
+
const path = pathArg ? resolve(pathArg) : fallback?.path ?? logPath() ?? resolve(process.cwd(), ".omc", "usage.jsonl");
|
|
413
|
+
const scope = pathArg ? "explicit" : fallback?.scope ?? "project";
|
|
285
414
|
const records = readUsage(path);
|
|
286
415
|
const calls = records.filter((record) => record.kind === "invocation");
|
|
416
|
+
const scriptCalls = records.filter((record) => record.kind === "script-call");
|
|
417
|
+
const top = Math.max(1, Math.min(options.top ?? 10, 100));
|
|
287
418
|
const commandCounts = new Map();
|
|
419
|
+
const toolCounts = new Map();
|
|
288
420
|
let stdoutBytes = 0;
|
|
289
421
|
let fileBytes = 0;
|
|
290
422
|
let fullCalls = 0;
|
|
291
423
|
for (const record of calls) {
|
|
292
424
|
const command = String(record.command ?? "(unknown)");
|
|
425
|
+
const bytes = Number(record.stdoutBytes ?? 0);
|
|
293
426
|
commandCounts.set(command, (commandCounts.get(command) ?? 0) + 1);
|
|
294
|
-
|
|
427
|
+
const tool = toolOf(command);
|
|
428
|
+
const entry = toolCounts.get(tool) ?? { calls: 0, bytes: 0, viaScript: 0 };
|
|
429
|
+
toolCounts.set(tool, { ...entry, calls: entry.calls + 1, bytes: entry.bytes + bytes });
|
|
430
|
+
stdoutBytes += bytes;
|
|
295
431
|
fileBytes += Number(record.fileBytes ?? 0);
|
|
296
432
|
if (Array.isArray(record.faces) && record.faces.includes("--full"))
|
|
297
433
|
fullCalls += 1;
|
|
298
434
|
}
|
|
299
|
-
|
|
435
|
+
// Inner `omc script` calls: the outer record's command is just "script <file>"
|
|
436
|
+
// and its bytes are 0 (script owns stdout), so without this roll-up every
|
|
437
|
+
// batch was invisible to cost accounting (round 10: 23 such calls, 0 B).
|
|
438
|
+
let scriptBytes = 0;
|
|
439
|
+
for (const record of scriptCalls) {
|
|
440
|
+
const bytes = Number(record.stdoutBytes ?? 0);
|
|
441
|
+
scriptBytes += bytes;
|
|
442
|
+
const tool = String(record.tool ?? "(unknown)");
|
|
443
|
+
const entry = toolCounts.get(tool) ?? { calls: 0, bytes: 0, viaScript: 0 };
|
|
444
|
+
toolCounts.set(tool, { ...entry, calls: entry.calls + 1, bytes: entry.bytes + bytes, viaScript: entry.viaScript + 1 });
|
|
445
|
+
}
|
|
446
|
+
const totalBytes = stdoutBytes + scriptBytes;
|
|
447
|
+
// Measurement, not vanity: how much of the session was the SAME call repeated.
|
|
300
448
|
const exactCounts = new Map();
|
|
301
449
|
const createdCodes = new Set();
|
|
302
450
|
const referencedCodes = new Set();
|
|
303
|
-
|
|
451
|
+
// Redundancy spans both faces: the same tool called twice inside ONE batch is
|
|
452
|
+
// exactly as wasteful as twice across two `omc call` invocations (round 10
|
|
453
|
+
// hid 23 batched calls behind `script -`).
|
|
454
|
+
const examined = [...calls, ...scriptCalls];
|
|
455
|
+
for (const record of examined) {
|
|
304
456
|
const summary = record.argSummary && typeof record.argSummary === "object" ? record.argSummary : {};
|
|
305
|
-
const command = String(record.command ?? "");
|
|
457
|
+
const command = record.kind === "script-call" ? String(record.tool ?? "") : String(record.command ?? "");
|
|
306
458
|
const digest = `${command}:${String(record.argsDigest ?? "")}`;
|
|
307
|
-
|
|
459
|
+
const entry = exactCounts.get(digest) ?? { count: 0, bytes: 0, command };
|
|
460
|
+
exactCounts.set(digest, { count: entry.count + 1, bytes: entry.bytes + Number(record.stdoutBytes ?? 0), command });
|
|
308
461
|
if (command === "call model.create" && typeof summary.code === "string")
|
|
309
462
|
createdCodes.add(summary.code);
|
|
310
463
|
for (const key of ["schemaCode", "workflowCode"])
|
|
311
464
|
if (typeof summary[key] === "string")
|
|
312
465
|
referencedCodes.add(summary[key]);
|
|
313
466
|
}
|
|
314
|
-
const
|
|
467
|
+
const repeated = [...commandCounts.entries()].filter(([, count]) => count > 1).map(([command, count]) => ({ command, count })).sort((left, right) => right.count - left.count);
|
|
468
|
+
// Wasted bytes: for a group of N identical calls, N-1 copies bought nothing.
|
|
469
|
+
let redundantCalls = 0;
|
|
470
|
+
let redundantBytes = 0;
|
|
471
|
+
const exactRepeated = [...exactCounts.entries()]
|
|
472
|
+
.filter(([, entry]) => entry.count > 1)
|
|
473
|
+
.map(([digest, entry]) => {
|
|
474
|
+
const wasted = Math.round((entry.bytes * (entry.count - 1)) / entry.count);
|
|
475
|
+
redundantCalls += entry.count - 1;
|
|
476
|
+
redundantBytes += wasted;
|
|
477
|
+
return { digest: digest.slice(-16), command: entry.command, count: entry.count, wastedBytes: wasted };
|
|
478
|
+
})
|
|
479
|
+
.sort((left, right) => right.wastedBytes - left.wastedBytes)
|
|
480
|
+
.slice(0, top);
|
|
315
481
|
const unseenTargetReferences = [...referencedCodes].filter((code) => !createdCodes.has(code));
|
|
316
|
-
// Round 4: attribute large reads. The complex benchmark showed `artifact get`
|
|
317
|
-
// at 56% of stdout; without this the next session cannot see the whale.
|
|
318
482
|
const artifactBytes = calls.filter((record) => String(record.command).startsWith("artifact get")).reduce((sum, record) => sum + Number(record.stdoutBytes ?? 0), 0);
|
|
319
483
|
const largestReads = calls
|
|
320
484
|
.filter((record) => Number(record.stdoutBytes ?? 0) >= 8192)
|
|
321
485
|
.map((record) => ({ command: String(record.command ?? ""), bytes: Number(record.stdoutBytes ?? 0), summary: record.argSummary ?? {} }))
|
|
322
486
|
.sort((left, right) => right.bytes - left.bytes)
|
|
323
|
-
.slice(0, 8);
|
|
487
|
+
.slice(0, Math.min(top, 8));
|
|
488
|
+
const byTool = [...toolCounts.entries()]
|
|
489
|
+
.map(([tool, entry]) => ({
|
|
490
|
+
tool,
|
|
491
|
+
calls: entry.calls,
|
|
492
|
+
bytes: entry.bytes,
|
|
493
|
+
...(entry.viaScript > 0 ? { viaScript: entry.viaScript } : {}),
|
|
494
|
+
share: totalBytes === 0 ? 0 : Number((entry.bytes / totalBytes).toFixed(3)),
|
|
495
|
+
}))
|
|
496
|
+
.sort((left, right) => right.bytes - left.bytes)
|
|
497
|
+
.slice(0, top);
|
|
324
498
|
const violations = [
|
|
325
499
|
...(fullCalls > 3 ? [{ code: "batch-full-prefetch", message: `${fullCalls} calls used --full; prefer next/recipe/default describe` }] : []),
|
|
326
500
|
...(calls.some((record) => String(record.command).startsWith("commands") && !Array.isArray(record.faces)) ? [{ code: "broad-command-catalog", message: "full commands inventory was requested" }] : []),
|
|
327
501
|
...(artifactBytes > 32768 ? [{ code: "large-artifact-read", message: `artifact get pulled ${artifactBytes} B; prefer --summary/--keys/--jq or the projected workflow summary` }] : []),
|
|
502
|
+
...(scope === "project" && projectIgnoreState(process.cwd()) === "missing" ? [{ code: "project-log-not-ignored", message: ".omc/ is not in .gitignore;重跑 `omc setup` 并选择绑定本项目(会自动追加),或手动加一行 .omc/,否则用量账本与产物可能被提交" }] : []),
|
|
328
503
|
];
|
|
329
504
|
return {
|
|
330
505
|
status: "ok",
|
|
331
506
|
path,
|
|
507
|
+
scope,
|
|
508
|
+
// `path` is redacted (host layout stays private), so name the file itself:
|
|
509
|
+
// project logs are `<project>/.omc/usage.jsonl`, user logs live under
|
|
510
|
+
// `<config root>/usage/<hash>.jsonl`.
|
|
511
|
+
logFile: basename(path),
|
|
332
512
|
calls: calls.length,
|
|
513
|
+
scriptCalls: scriptCalls.length,
|
|
333
514
|
stdoutBytes,
|
|
515
|
+
scriptBytes,
|
|
516
|
+
totalBytes,
|
|
334
517
|
fileBytes,
|
|
518
|
+
// Explicit estimate, never presented as a measurement: the CLI cannot see
|
|
519
|
+
// the agent's tokenizer, so bytes/4 is a floor for planning only.
|
|
520
|
+
estimatedTokens: Math.round(totalBytes / 4),
|
|
335
521
|
fullCalls,
|
|
336
|
-
|
|
522
|
+
redundancy: { calls: redundantCalls, bytes: redundantBytes, share: Number((redundantCalls / Math.max(examined.length, 1)).toFixed(3)) },
|
|
523
|
+
repeated: repeated.slice(0, top),
|
|
337
524
|
exactRepeated,
|
|
525
|
+
byTool,
|
|
338
526
|
createdCodes: [...createdCodes],
|
|
339
527
|
unseenTargetReferences,
|
|
340
528
|
artifactBytes,
|
|
341
529
|
largestReads,
|
|
342
530
|
violations,
|
|
343
|
-
records,
|
|
531
|
+
...(options.records === true ? { records } : {}),
|
|
344
532
|
};
|
|
345
533
|
}
|
|
346
534
|
//# sourceMappingURL=usage-log.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.80",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@stalfh233/omc-fake-platform": "0.4.
|
|
53
|
+
"@stalfh233/omc-fake-platform": "0.4.80",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.80",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
package/skills/omc-start-here.md
CHANGED
|
@@ -24,7 +24,7 @@ be treated as a second source. Read this, then follow the topics it names.
|
|
|
24
24
|
- `build`/`modify` → 按阶梯内联的**写工具 + 必填参数**执行。
|
|
25
25
|
- `scout`(能不能/怎么写/是否支持)→ 先探边界(`omc-capability-scouting`),别直接写。若 `next` 返回 **`boundary`**(`match.kind:"boundary"`,如 `ocr-e-signature`/`release-prod`/`inline-coding`),说明需求**不在覆盖内**(`write:false`):如实报告、用 `omc capabilities` 核对,**不要**用相近能力伪造实现。
|
|
26
26
|
阶梯步骤已内联工具必填参数(`route.ladder[].tools`/`readTools`)与命中 recipe 的 `describe <tool> --example <id>`;据此直接执行——**不要再逐能力 `commands --lane`**,也不要先扫 `commands --json`/全部 `contract`/全部 `describe --full`。
|
|
27
|
-
方法论按 `route.readSections` 逐节读:对每条 `{topic, section}` 执行 `omc help <topic> --section "<section>"`(单节 0.4–3.4KB
|
|
27
|
+
方法论按 `route.readSections` 逐节读:对每条 `{topic, section}` 执行 `omc help <topic> --section "<section>"`(单节 0.4–3.4KB)。`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数);**不要 `--all` 读整篇**(5–14KB,读一次就留在后续每一轮窗口)。
|
|
28
28
|
2. 写前只需读 `omc-config-draft-review`(人审门)与 `omc-five-piece-flow`(发现→草稿→权限→执行→回读);`omc-business-configuration` 按命中的能力分节读(见 `next` 的 `readSections`),不要整篇预取。
|
|
29
29
|
3. 环境名以 `omc doctor` 的「环境=」或 `.omc-env` 为准;工具不确定先用默认 `describe`/`--example`,只有单个工具缺形状才用 `--full`;`describe` 带 `exampleHint`/`recipeHint` 时优先 `--example`。
|
|
30
30
|
4. 只有工具不在 `next` 命中能力内、或要看 purpose/枚举时,才补一次 `commands --json --compact --lane <lane>`。非显然形状(规则图、流程模板、表单布局、RESTFUL)才单独 `describe --example`。
|