@ats-cx/cx-cli 0.1.0

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.
Files changed (57) hide show
  1. package/.env.example +8 -0
  2. package/README.md +119 -0
  3. package/bin/cx-cli.cjs +90 -0
  4. package/config/apm-provider.json +7 -0
  5. package/config/apm-provider.template.jsonc +48 -0
  6. package/config/diagnostic-rules.json +54 -0
  7. package/config/diagnostic-rules.template.jsonc +82 -0
  8. package/config/event-semantics.json +828 -0
  9. package/config/event-semantics.template.jsonc +13 -0
  10. package/config/toolkit.json +3 -0
  11. package/dist/apm-help.d.ts +5 -0
  12. package/dist/apm-help.js +108 -0
  13. package/dist/apm-output.d.ts +49 -0
  14. package/dist/apm-output.js +115 -0
  15. package/dist/budget.d.ts +14 -0
  16. package/dist/budget.js +16 -0
  17. package/dist/cli.d.ts +20 -0
  18. package/dist/cli.js +596 -0
  19. package/dist/context.d.ts +12 -0
  20. package/dist/context.js +20 -0
  21. package/dist/contract.d.ts +11 -0
  22. package/dist/contract.js +4 -0
  23. package/dist/errors.d.ts +4 -0
  24. package/dist/errors.js +8 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +8 -0
  27. package/dist/init.d.ts +17 -0
  28. package/dist/init.js +194 -0
  29. package/dist/output.d.ts +7 -0
  30. package/dist/output.js +7 -0
  31. package/dist/project-pull.d.ts +32 -0
  32. package/dist/project-pull.js +81 -0
  33. package/dist/report.d.ts +3 -0
  34. package/dist/report.js +42 -0
  35. package/dist/skills.d.ts +12 -0
  36. package/dist/skills.js +430 -0
  37. package/dist/tools/apm-tools.d.ts +115 -0
  38. package/dist/tools/apm-tools.js +329 -0
  39. package/dist/tools/conclusion-tools.d.ts +24 -0
  40. package/dist/tools/conclusion-tools.js +61 -0
  41. package/dist/tools/create-run.d.ts +61 -0
  42. package/dist/tools/create-run.js +273 -0
  43. package/dist/tools/log-tools.d.ts +68 -0
  44. package/dist/tools/log-tools.js +116 -0
  45. package/dist/tools/project-tools.d.ts +82 -0
  46. package/dist/tools/project-tools.js +139 -0
  47. package/dist/workspace.d.ts +51 -0
  48. package/dist/workspace.js +104 -0
  49. package/package.json +37 -0
  50. package/skills/apm-query/SKILL.md +304 -0
  51. package/skills/apm-query/agents/openai.yaml +10 -0
  52. package/skills/cx-cli-setup/SKILL.md +120 -0
  53. package/skills/cx-cli-setup/agents/openai.yaml +11 -0
  54. package/skills/editor-diagnostic/SKILL.md +255 -0
  55. package/skills/editor-diagnostic/agents/openai.yaml +13 -0
  56. package/skills/semantics-curation/SKILL.md +151 -0
  57. package/skills/semantics-curation/agents/openai.yaml +12 -0
package/dist/cli.js ADDED
@@ -0,0 +1,596 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { ApmRequestError, appendTrace, loadRun, machineConfigDir } from "@ats-cx/cx-core";
6
+ import { Command, CommanderError } from "commander";
7
+ import { ZodError } from "zod";
8
+ import { createToolContext } from "./context.js";
9
+ import { APM_FLUSH_HELP, APM_PROJECT_HELP, APM_QUERY_HELP, APM_RESOLVE_HELP } from "./apm-help.js";
10
+ import { UserFacingError } from "./errors.js";
11
+ import { runInit } from "./init.js";
12
+ import { formatOut, jsonOut } from "./output.js";
13
+ import { runProjectPull } from "./project-pull.js";
14
+ import { runSkillsInstall, runSkillsStatus, runSkillsUninstall, SKILLS_INSTALL_HELP, SKILLS_STATUS_HELP, SKILLS_UNINSTALL_HELP, } from "./skills.js";
15
+ import { finalizeDiagnosisTool, recordHypothesisTool } from "./tools/conclusion-tools.js";
16
+ import { createRunTool } from "./tools/create-run.js";
17
+ import { getCaseBriefTool, getTimelineTool, listSessionsTool, searchLogsTool } from "./tools/log-tools.js";
18
+ import { diffProjectVersionsTool, getProjectSnapshotTool } from "./tools/project-tools.js";
19
+ import { apmFlushTool, apmProjectTool, apmQueryTool, apmResolveTool } from "./tools/apm-tools.js";
20
+ import { discoverWorkspace, workspaceLogSourceWarning } from "./workspace.js";
21
+ export const TOOLKIT_ROOT = resolve(fileURLToPath(new URL("..", import.meta.url)));
22
+ const VERSION = createRequire(import.meta.url)("../package.json").version;
23
+ /** 命令面契约:`.scratch/cx-cli-convergence/assets/02-cx-cli-help-mock.md`。改命令时同步改这里。 */
24
+ const TOP_LEVEL_HELP = `Usage: cx-cli [选项] <命令>
25
+
26
+ 编辑器诊断与项目数据工具箱
27
+
28
+ 全局选项:
29
+ --config <目录> 配置目录(默认: cwd 向上发现 .cx-cli 工作区 > $CX_CLI_CONFIG_DIR > 工具箱 config/)
30
+ --json 输出 JSON(默认与人类可读格式相同,均为缩进 JSON)
31
+ -V, --version 显示版本号
32
+ -h, --help 显示帮助
33
+
34
+ 机器级命令(一台机器一次):
35
+ skills install 把包内 skills 软链进本机 Agent(Claude Code / Codex),并建机器级覆盖目录(幂等)
36
+ skills status 检查 skills 链接与机器级覆盖目录(未就绪 exit 1)
37
+ skills uninstall 解除本包的 skills 链接(不动机器级覆盖目录)
38
+
39
+ 工作区命令:
40
+ init 在当前仓库根创建 .cx-cli 诊断工作区(幂等)
41
+
42
+ 项目数据命令(直连 DB,需 .env):
43
+ project pull <projectId...> 拉取每个项目最新一条快照到本地快照库
44
+ project pull --history 改为拉取全量历史快照
45
+
46
+ APM 查询命令(网页端接口,只读):
47
+ apm query 查询埋点日志(9 张表)
48
+ apm resolve 邮箱/手机 → userId(经网页端 portal 路由)
49
+ apm project <projectId> 现网 project JSON(portal 当前线上版本,非失败快照)
50
+ apm flush 让网页端把攒批未写入的事件落库(= 网页「同步」)
51
+
52
+ 诊断命令(只读本地快照库 + 日志源):
53
+ run new 创建诊断 run
54
+ brief 读取 case 摘要
55
+ sessions 列出 session
56
+ timeline 读取 session 时间线
57
+ search-logs 跨 session 检索日志
58
+ snapshot 读取 project 快照
59
+ diff-project 对比两个 project 版本
60
+ hypothesis 记录诊断假设
61
+ finalize 提交诊断结论
62
+ report 生成诊断报告
63
+ `;
64
+ /** 同上契约(`project pull --help` 一节)。commander 默认排版对不上,这里固定文案。 */
65
+ const PROJECT_PULL_HELP = `Usage: cx-cli project pull [选项] <projectId...>
66
+
67
+ 拉取项目快照到本地快照库(工作区 data/,无工作区时为 toolkit.json 的 projectHistoryDir)。
68
+ 落盘: <projectHistoryDir>/<projectId>/<projectId>_<时间>.json(时间取记录 CREATE_TIME)。
69
+ 同一秒有多条记录时,第 2 条起追加 _02、_03 序号后缀,各自独立成文件。
70
+ XML 数据自动转换为 JSON。
71
+
72
+ 参数:
73
+ projectId... 一个或多个项目 ID
74
+
75
+ 选项:
76
+ --history 拉取全量历史(默认只拉每个项目最新一条)
77
+ --env <文件> 指定 .env 文件路径(默认: 工作区 .env,无工作区时 ~/.config/cx-cli/.env)
78
+ --json 输出 JSON(默认与人类可读格式相同,均为缩进 JSON)
79
+ -h, --help
80
+
81
+ 示例:
82
+ cx-cli project pull 6345434 # 场景 1: 下载 project(最新一条)
83
+ cx-cli project pull 6345434 --history # 场景 2: 拉取全量历史
84
+ cx-cli project pull 6345434 6351428 # 多项目,各拉最新一条
85
+ `;
86
+ /** commander 以异常返回控制流;这三种是「正常输出后退出」,其余一律参数错误(exit 2)。 */
87
+ const NORMAL_EXIT_CODES = new Set(["commander.help", "commander.helpDisplayed", "commander.version"]);
88
+ export function assertConfigDirExists(path, label) {
89
+ if (!existsSync(path) || !statSync(path).isDirectory()) {
90
+ throw new UserFacingError(`${label}: ${path}`);
91
+ }
92
+ }
93
+ /** 配置解析优先级(map 决策 8):显式 --config > cwd 向上发现工作区 > $CX_CLI_CONFIG_DIR > 工具箱 config/。 */
94
+ export function resolveExecution(explicit, cwd = process.cwd()) {
95
+ if (typeof explicit === "string" && explicit.length > 0) {
96
+ const configDir = resolve(explicit);
97
+ assertConfigDirExists(configDir, "错误: --config 指定的配置目录不存在");
98
+ return { configDir, configSource: "explicit", workspace: null };
99
+ }
100
+ const workspace = discoverWorkspace(cwd);
101
+ if (workspace) {
102
+ if (!workspace.toolkitConfigDir) {
103
+ return { configDir: join(TOOLKIT_ROOT, "config"), configSource: "toolkit", workspace };
104
+ }
105
+ try {
106
+ assertConfigDirExists(workspace.toolkitConfigDir, `错误: ${join(workspace.workspaceDir, "config.json")} 的 toolkitConfigDir 指向不存在的目录`);
107
+ }
108
+ catch (error) {
109
+ if (!(error instanceof UserFacingError)) {
110
+ throw error;
111
+ }
112
+ throw new UserFacingError(`${error.message}\n` +
113
+ `删掉这一键即改用当前 cx-cli 自带的共享配置(${join(TOOLKIT_ROOT, "config")});在 ${workspace.hostRoot} 重新运行 cx-cli init 会自动处理。\n` +
114
+ "要钉住别的配置目录,请改成存在的路径。");
115
+ }
116
+ return { configDir: workspace.toolkitConfigDir, configSource: "workspace", workspace };
117
+ }
118
+ const fromEnv = process.env.CX_CLI_CONFIG_DIR;
119
+ if (fromEnv) {
120
+ const configDir = resolve(fromEnv);
121
+ assertConfigDirExists(configDir, "错误: $CX_CLI_CONFIG_DIR 指定的配置目录不存在");
122
+ return { configDir, configSource: "env", workspace: null };
123
+ }
124
+ return { configDir: join(TOOLKIT_ROOT, "config"), configSource: "toolkit", workspace: null };
125
+ }
126
+ /** 合并命令自身与各级父命令的选项(`--config` 可写在子命令前后,就近者优先)。 */
127
+ function collectOptions(command) {
128
+ const chain = [];
129
+ for (let current = command; current; current = current.parent) {
130
+ chain.unshift(current);
131
+ }
132
+ return Object.assign({}, ...chain.map(item => item.opts()));
133
+ }
134
+ function optString(value) {
135
+ return typeof value === "string" ? value : undefined;
136
+ }
137
+ function optNumber(value) {
138
+ return typeof value === "string" ? Number(value) : undefined;
139
+ }
140
+ function optStrings(value) {
141
+ return Array.isArray(value) && value.every(item => typeof item === "string") ? value : undefined;
142
+ }
143
+ function collectOption(value, previous) {
144
+ return [...previous, value];
145
+ }
146
+ function errorMessage(error) {
147
+ return error instanceof Error ? error.message : String(error);
148
+ }
149
+ function sanitizeArgs(args) {
150
+ const sanitized = {};
151
+ for (const [key, value] of Object.entries(args)) {
152
+ if (typeof value === "string" && value.length > 500) {
153
+ continue;
154
+ }
155
+ sanitized[key] = value;
156
+ }
157
+ return sanitized;
158
+ }
159
+ function recordTrace(runDir, command, opts, configDir, data) {
160
+ appendTrace(runDir, {
161
+ actor: "cli",
162
+ command,
163
+ args: sanitizeArgs({ ...opts, config: configDir }),
164
+ outputSummary: JSON.stringify(data).slice(0, 200),
165
+ });
166
+ }
167
+ function readJsonFile(filePath) {
168
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"));
169
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
170
+ }
171
+ /**
172
+ * 诊断命令统一执行路径:解析配置目录 → 建 context → zod 校验 → 调 handler → 记 trace。
173
+ * 退出码:0 成功 / 1 业务失败(Error)/ 2 参数错误(ZodError)。
174
+ */
175
+ async function executeTool(command, name, tool, buildInput, extras = {}) {
176
+ const opts = collectOptions(command);
177
+ let context;
178
+ let data;
179
+ let configDir;
180
+ try {
181
+ const execution = resolveExecution(opts.config);
182
+ configDir = execution.configDir;
183
+ context = createToolContext(execution.configDir, execution.workspace, execution.configSource);
184
+ // 走 stderr:不污染 --json 的 stdout,Agent 与人都看得见。
185
+ if (!extras.skipLogSourceWarning?.(opts)) {
186
+ const logSourceWarning = workspaceLogSourceWarning(context.config, execution.workspace, execution.configSource);
187
+ if (logSourceWarning) {
188
+ console.error(logSourceWarning);
189
+ }
190
+ }
191
+ const input = tool.inputSchema.parse(buildInput(opts));
192
+ data = await tool.handler(input, context);
193
+ }
194
+ catch (error) {
195
+ if (error instanceof ZodError) {
196
+ return jsonOut({ error: "参数校验失败", issues: error.issues }, 2);
197
+ }
198
+ if (error instanceof ApmRequestError) {
199
+ const file = "file" in error && typeof error.file === "string" ? error.file : undefined;
200
+ const errorData = {
201
+ error: error.message,
202
+ kind: error.kind,
203
+ ...(error.hint !== undefined ? { hint: error.hint } : {}),
204
+ ...(error.status !== undefined ? { status: error.status } : {}),
205
+ ...(file !== undefined ? { file } : {}),
206
+ ...(error.fetched !== undefined ? { fetched: error.fetched } : {}),
207
+ ...(error.total !== undefined ? { total: error.total } : {}),
208
+ };
209
+ const runId = optString(opts.run);
210
+ if (file !== undefined && runId !== undefined && context !== undefined && configDir !== undefined) {
211
+ const run = loadRun(context.config.runsDir, runId);
212
+ recordTrace(run.runDir, name, opts, configDir, errorData);
213
+ }
214
+ return jsonOut(errorData, 1);
215
+ }
216
+ if (error instanceof UserFacingError) {
217
+ return { exitCode: 1, stdout: error.message };
218
+ }
219
+ if (error instanceof Error) {
220
+ return jsonOut({ error: error.message }, 1);
221
+ }
222
+ throw error;
223
+ }
224
+ const failed = extras.check?.(data);
225
+ if (failed) {
226
+ return failed;
227
+ }
228
+ // run new 的 runId 只在返回值里(其余命令由 --run 给出)。
229
+ const producedRunId = typeof data === "object" && data !== null ? data.runId : undefined;
230
+ const runId = optString(opts.run) ?? optString(producedRunId);
231
+ if (runId) {
232
+ if (context === undefined || configDir === undefined) {
233
+ throw new Error("工具执行上下文未初始化");
234
+ }
235
+ const run = loadRun(context.config.runsDir, runId);
236
+ recordTrace(run.runDir, name, opts, configDir, data);
237
+ }
238
+ return formatOut(data);
239
+ }
240
+ /** `--config` / `--json` 挂到每个子命令,使其既能写在命令前也能写在命令后。 */
241
+ function withGlobalOptions(command) {
242
+ return command
243
+ .option("--config <目录>", "配置目录(默认: cwd 向上发现 .cx-cli 工作区 > $CX_CLI_CONFIG_DIR > 工具箱 config/)")
244
+ .option("--json", "输出 JSON(默认与人类可读格式相同,均为缩进 JSON)");
245
+ }
246
+ export function createProgram(sink) {
247
+ const program = new Command();
248
+ // exitOverride / configureOutput / enablePositionalOptions 必须早于子命令创建:commander 在建子命令时复制这三项。
249
+ program.exitOverride();
250
+ // 按位置解析:命令前的选项归 program,命令后的选项归该命令。
251
+ // 否则 program 会抢走任意位置的同名选项(例如 `snapshot --version v1` 被顶层 -V 截胡)。
252
+ program.enablePositionalOptions();
253
+ program.configureOutput({
254
+ writeOut: chunk => sink.write(chunk),
255
+ writeErr: chunk => sink.write(chunk),
256
+ });
257
+ program
258
+ .name("cx-cli")
259
+ .description("编辑器诊断与项目数据工具箱")
260
+ .usage("[选项] <命令>")
261
+ .version(VERSION, "-V, --version", "显示版本号")
262
+ .helpOption("-h, --help", "显示帮助");
263
+ withGlobalOptions(program);
264
+ const emit = (result) => sink.emit(result);
265
+ // ---- 机器级命令 ----
266
+ // `--config` 只为兼容全局选项的任意位置写法,skills 组不读取配置目录,也不碰工作区。
267
+ const withSkillsOptions = (command) => command.option("--config <目录>", "忽略;skills 命令固定检查当前包与机器级目录").option("--json", "输出 JSON");
268
+ const skills = program
269
+ .command("skills")
270
+ .description("管理包内 skills 的机器级软链")
271
+ .option("--config <目录>", "忽略;skills 命令固定检查当前包与机器级目录")
272
+ .option("--json", "输出 JSON");
273
+ withSkillsOptions(skills.command("install"))
274
+ .description("把包内 skills 软链进本机 Agent,并建机器级覆盖目录")
275
+ .configureHelp({ formatHelp: () => SKILLS_INSTALL_HELP })
276
+ .action(() => {
277
+ emit(runSkillsInstall({ packageRoot: TOOLKIT_ROOT, packageVersion: VERSION }));
278
+ });
279
+ withSkillsOptions(skills.command("status"))
280
+ .description("检查 skills 链接与机器级覆盖目录")
281
+ .configureHelp({ formatHelp: () => SKILLS_STATUS_HELP })
282
+ .action(() => {
283
+ emit(runSkillsStatus({ packageRoot: TOOLKIT_ROOT, packageVersion: VERSION }));
284
+ });
285
+ withSkillsOptions(skills.command("uninstall"))
286
+ .description("解除本包的 skills 链接")
287
+ .configureHelp({ formatHelp: () => SKILLS_UNINSTALL_HELP })
288
+ .action(() => {
289
+ emit(runSkillsUninstall({ packageRoot: TOOLKIT_ROOT, packageVersion: VERSION }));
290
+ });
291
+ // ---- 工作区命令 ----
292
+ program
293
+ .command("init")
294
+ .description("在当前仓库根创建 .cx-cli 诊断工作区(幂等)")
295
+ .option("--json", "输出 JSON(默认与人类可读格式相同,均为缩进 JSON)")
296
+ .action(() => {
297
+ emit(runInit({ cwd: process.cwd(), toolkitRoot: TOOLKIT_ROOT }));
298
+ });
299
+ // ---- 项目数据命令 ----
300
+ // 按契约不挂全局选项:`--config` 只能写在命令名之前(或用 $CX_CLI_CONFIG_DIR)。
301
+ const project = program.command("project").description("项目数据命令(直连 DB,需 .env)");
302
+ project
303
+ .command("pull")
304
+ .description("拉取项目快照到本地快照库")
305
+ .argument("<projectId...>", "一个或多个项目 ID")
306
+ .option("--history", "拉取全量历史(默认只拉每个项目最新一条)")
307
+ .option("--env <文件>", "指定 .env 文件路径(默认: 工作区 .env,无工作区时 ~/.config/cx-cli/.env)")
308
+ // 输出本就是缩进 JSON;挂上 --json 只为与其余命令一致,Agent 照 skill 里「所有命令支持 --json」写不会被 exit 2 打断。
309
+ .option("--json", "输出 JSON(默认与人类可读格式相同,均为缩进 JSON)")
310
+ .configureHelp({ formatHelp: () => PROJECT_PULL_HELP })
311
+ .action(async (projectIds, _options, command) => {
312
+ const opts = collectOptions(command);
313
+ try {
314
+ const { configDir, configSource, workspace } = resolveExecution(opts.config);
315
+ const envPath = optString(opts.env) ??
316
+ (workspace && existsSync(workspace.envPath) ? workspace.envPath : join(machineConfigDir(), ".env"));
317
+ emit(await runProjectPull(projectIds, {
318
+ history: opts.history === true,
319
+ envPath,
320
+ envExamplePath: join(TOOLKIT_ROOT, ".env.example"),
321
+ configDir,
322
+ configSource,
323
+ workspace,
324
+ }));
325
+ }
326
+ catch (error) {
327
+ // runProjectPull 内部自带错误处理,这里只兜工作区解析(resolveExecution)的错。
328
+ emit(error instanceof UserFacingError
329
+ ? { exitCode: 1, stdout: error.message }
330
+ : jsonOut({ error: errorMessage(error) }, 1));
331
+ }
332
+ });
333
+ // ---- APM 查询命令 ----
334
+ const apm = program.command("apm").description("APM 查询命令(网页端接口,只读)");
335
+ withGlobalOptions(apm.command("query"))
336
+ .description("查询埋点日志(9 张表)")
337
+ .option("--type <表>", "日志类型")
338
+ .option("--project <id>", "project_id")
339
+ .option("--user <id>", "user_id")
340
+ .option("--collection <id>", "collection_id")
341
+ .option("--session <id>", "session_id")
342
+ .option("--event <名>", "事件名 data_name")
343
+ .option("--sub-type <值>", "sub_type")
344
+ .option("--where <列=值>", "按列等值,可重复(type / page / pageSize / order 除外)", collectOption, [])
345
+ .option("--app <id>", "app_id")
346
+ .option("--bizline <id>", "bizline_id")
347
+ .option("--from <时间>", "log_time 下界,必填")
348
+ .option("--to <时间>", "log_time 上界")
349
+ .option("--item <值>", "网页同款 ID 过滤")
350
+ .option("--search <文本>", "网页同款搜索过滤")
351
+ .option("--order <ASC|DESC>", "按 log_time 排序")
352
+ .option("--limit <n>", "最多从服务端取 n 行")
353
+ .option("--count", "只回总数,不落盘")
354
+ .option("--preview <n>", "stdout 内联的预览行数")
355
+ .option("--full", "预览不瘦身")
356
+ .option("--out <文件>", "查询落盘路径")
357
+ .option("--run <runId>", "把查询落盘写进 run")
358
+ .option("--no-flush", "不在查询前同步")
359
+ .configureHelp({ formatHelp: () => APM_QUERY_HELP })
360
+ .action(async (_options, command) => {
361
+ emit(await executeTool(command, "apm query", apmQueryTool, opts => ({
362
+ type: optString(opts.type),
363
+ project: optString(opts.project),
364
+ user: optString(opts.user),
365
+ collection: optString(opts.collection),
366
+ session: optString(opts.session),
367
+ event: optString(opts.event),
368
+ subType: optString(opts.subType),
369
+ where: optStrings(opts.where),
370
+ app: optString(opts.app),
371
+ bizline: optString(opts.bizline),
372
+ from: optString(opts.from),
373
+ to: optString(opts.to),
374
+ item: optString(opts.item),
375
+ search: optString(opts.search),
376
+ order: optString(opts.order),
377
+ limit: optNumber(opts.limit),
378
+ count: opts.count === true ? true : undefined,
379
+ preview: optNumber(opts.preview),
380
+ full: opts.full === true ? true : undefined,
381
+ out: optString(opts.out),
382
+ run: optString(opts.run),
383
+ noFlush: opts.flush === false ? true : undefined,
384
+ }), { skipLogSourceWarning: () => true }));
385
+ });
386
+ withGlobalOptions(apm.command("resolve"))
387
+ .description("邮箱/手机 → userId(经网页端 portal 路由)")
388
+ .option("--email <邮箱>", "邮箱 → userId")
389
+ .option("--phone <手机号>", "手机号 → userId")
390
+ .option("--host <站点 host>", "日志 host 列的完整域名")
391
+ .configureHelp({ formatHelp: () => APM_RESOLVE_HELP })
392
+ .action(async (_options, command) => {
393
+ emit(await executeTool(command, "apm resolve", apmResolveTool, opts => ({
394
+ email: optString(opts.email),
395
+ phone: optString(opts.phone),
396
+ host: optString(opts.host),
397
+ }), { skipLogSourceWarning: () => true }));
398
+ });
399
+ withGlobalOptions(apm.command("project"))
400
+ .description("现网 project JSON(portal 当前线上版本,非失败快照)")
401
+ .argument("<projectId>", "项目 ID")
402
+ .option("--host <站点 host>", "日志 host 列的完整域名")
403
+ .option("--out <文件>", "落盘路径")
404
+ .option("--run <runId>", "落进 run 的 artifacts/apm/")
405
+ .configureHelp({ formatHelp: () => APM_PROJECT_HELP })
406
+ .action(async (projectId, _options, command) => {
407
+ emit(await executeTool(command, "apm project", apmProjectTool, opts => ({
408
+ projectId,
409
+ host: optString(opts.host),
410
+ out: optString(opts.out),
411
+ run: optString(opts.run),
412
+ }), { skipLogSourceWarning: () => true }));
413
+ });
414
+ withGlobalOptions(apm.command("flush"))
415
+ .description("让网页端把攒批未写入的事件落库")
416
+ .configureHelp({ formatHelp: () => APM_FLUSH_HELP })
417
+ .action(async (_options, command) => {
418
+ emit(await executeTool(command, "apm flush", apmFlushTool, () => ({}), {
419
+ skipLogSourceWarning: () => true,
420
+ }));
421
+ });
422
+ // ---- 诊断命令 ----
423
+ const run = program.command("run").description("诊断 run 相关命令");
424
+ withGlobalOptions(run.command("new"))
425
+ .description("创建诊断 run")
426
+ .requiredOption("--projectId <id>", "项目 ID")
427
+ .option("--complaint <文本>", "用户投诉描述", "")
428
+ .option("--logs <文件>", "用查询落盘或任一 {result:[…]} 形状的文件作日志源(不读配置 logSource)")
429
+ .action(async (_options, command) => {
430
+ emit(await executeTool(command, "run new", createRunTool, opts => {
431
+ const logsFile = optString(opts.logs);
432
+ return {
433
+ projectId: optString(opts.projectId),
434
+ complaint: optString(opts.complaint) ?? "",
435
+ logsFile: logsFile === undefined || logsFile.length === 0 ? logsFile : resolve(logsFile),
436
+ };
437
+ }, { skipLogSourceWarning: opts => optString(opts.logs) !== undefined }));
438
+ });
439
+ withGlobalOptions(program.command("brief"))
440
+ .description("读取 case 摘要")
441
+ .requiredOption("--run <runId>", "run ID")
442
+ .action(async (_options, command) => {
443
+ emit(await executeTool(command, "brief", getCaseBriefTool, opts => ({ runId: optString(opts.run) })));
444
+ });
445
+ withGlobalOptions(program.command("sessions"))
446
+ .description("列出 session")
447
+ .requiredOption("--run <runId>", "run ID")
448
+ .action(async (_options, command) => {
449
+ emit(await executeTool(command, "sessions", listSessionsTool, opts => ({ runId: optString(opts.run) })));
450
+ });
451
+ withGlobalOptions(program.command("timeline"))
452
+ .description("读取 session 时间线")
453
+ .requiredOption("--run <runId>", "run ID")
454
+ .requiredOption("--session <sessionId>", "session ID")
455
+ .option("--category <类别>", "按语义类别过滤")
456
+ .option("--payload", "附带每条事件的 payload 原文(默认只列 payloadKeys)")
457
+ .option("--cursor <n>", "分页游标")
458
+ .option("--page-size <n>", "分页大小")
459
+ .action(async (_options, command) => {
460
+ emit(await executeTool(command, "timeline", getTimelineTool, opts => ({
461
+ runId: optString(opts.run),
462
+ sessionId: optString(opts.session),
463
+ category: optString(opts.category),
464
+ payload: opts.payload === true ? true : undefined,
465
+ cursor: optNumber(opts.cursor),
466
+ pageSize: optNumber(opts.pageSize),
467
+ })));
468
+ });
469
+ withGlobalOptions(program.command("search-logs"))
470
+ .description("跨 session 检索日志")
471
+ .requiredOption("--run <runId>", "run ID")
472
+ .option("--event <事件名>", "按事件名过滤")
473
+ .option("--payload-key <key>", "按 payload 字段过滤")
474
+ .option("--from <ISO>", "起始时间")
475
+ .option("--to <ISO>", "结束时间")
476
+ .option("--payload", "附带每条事件的 payload 原文(默认只列 payloadKeys)")
477
+ .option("--cursor <n>", "分页游标")
478
+ .option("--page-size <n>", "分页大小")
479
+ .action(async (_options, command) => {
480
+ emit(await executeTool(command, "search-logs", searchLogsTool, opts => ({
481
+ runId: optString(opts.run),
482
+ eventName: optString(opts.event),
483
+ payloadKey: optString(opts.payloadKey),
484
+ from: optString(opts.from),
485
+ to: optString(opts.to),
486
+ payload: opts.payload === true ? true : undefined,
487
+ cursor: optNumber(opts.cursor),
488
+ pageSize: optNumber(opts.pageSize),
489
+ })));
490
+ });
491
+ withGlobalOptions(program.command("snapshot"))
492
+ .description("读取 project 快照")
493
+ .requiredOption("--run <runId>", "run ID")
494
+ .requiredOption("--version <版本>", "快照版本")
495
+ .option("--detail <粒度>", "summary | pages | elements")
496
+ .option("--cursor <n>", "分页游标")
497
+ .option("--page-size <n>", "分页大小")
498
+ .action(async (_options, command) => {
499
+ emit(await executeTool(command, "snapshot", getProjectSnapshotTool, opts => ({
500
+ runId: optString(opts.run),
501
+ version: optString(opts.version),
502
+ detail: optString(opts.detail),
503
+ cursor: optNumber(opts.cursor),
504
+ pageSize: optNumber(opts.pageSize),
505
+ })));
506
+ });
507
+ withGlobalOptions(program.command("diff-project"))
508
+ .description("对比两个 project 版本")
509
+ .requiredOption("--run <runId>", "run ID")
510
+ .requiredOption("--before <版本>", "较早版本")
511
+ .requiredOption("--after <版本>", "较晚版本")
512
+ .option("--cursor <n>", "分页游标")
513
+ .option("--page-size <n>", "分页大小")
514
+ .action(async (_options, command) => {
515
+ emit(await executeTool(command, "diff-project", diffProjectVersionsTool, opts => ({
516
+ runId: optString(opts.run),
517
+ before: optString(opts.before),
518
+ after: optString(opts.after),
519
+ cursor: optNumber(opts.cursor),
520
+ pageSize: optNumber(opts.pageSize),
521
+ })));
522
+ });
523
+ withGlobalOptions(program.command("hypothesis"))
524
+ .description("记录诊断假设")
525
+ .requiredOption("--run <runId>", "run ID")
526
+ .requiredOption("--file <json 文件>", "假设 JSON 文件")
527
+ .action(async (_options, command) => {
528
+ emit(await executeTool(command, "hypothesis", recordHypothesisTool, opts => ({
529
+ runId: optString(opts.run),
530
+ ...readJsonFile(optString(opts.file) ?? ""),
531
+ })));
532
+ });
533
+ withGlobalOptions(program.command("finalize"))
534
+ .description("提交诊断结论")
535
+ .requiredOption("--run <runId>", "run ID")
536
+ .requiredOption("--file <json 文件>", "结论 JSON 文件")
537
+ .action(async (_options, command) => {
538
+ emit(await executeTool(command, "finalize", finalizeDiagnosisTool, opts => ({ runId: optString(opts.run), result: readJsonFile(optString(opts.file) ?? "") }), {
539
+ // 校验未通过:输出 errors 并按业务失败退出,不记 trace。
540
+ check: data => (data.ok ? undefined : jsonOut(data, 1)),
541
+ }));
542
+ });
543
+ // report 无对应 tool 定义(spec §3.12),直读 run 目录下的 report.md。
544
+ withGlobalOptions(program.command("report"))
545
+ .description("生成诊断报告")
546
+ .requiredOption("--run <runId>", "run ID")
547
+ .action((_options, command) => {
548
+ const opts = collectOptions(command);
549
+ const runId = optString(opts.run) ?? "";
550
+ try {
551
+ const { configDir, configSource, workspace } = resolveExecution(opts.config);
552
+ const context = createToolContext(configDir, workspace, configSource);
553
+ const run = loadRun(context.config.runsDir, runId);
554
+ const reportPath = join(run.runDir, "report.md");
555
+ if (!existsSync(reportPath)) {
556
+ emit(jsonOut({ error: `report.md 不存在于 run ${runId}` }, 1));
557
+ return;
558
+ }
559
+ const data = { report: readFileSync(reportPath, "utf8") };
560
+ recordTrace(run.runDir, "report", opts, configDir, data);
561
+ emit(formatOut(data));
562
+ }
563
+ catch (error) {
564
+ emit(error instanceof UserFacingError
565
+ ? { exitCode: 1, stdout: error.message }
566
+ : jsonOut({ error: errorMessage(error) }, 1));
567
+ }
568
+ });
569
+ // 顶层 help 按契约固定排版;放在子命令注册之后,避免子命令继承该配置。
570
+ program.configureHelp({ formatHelp: () => TOP_LEVEL_HELP });
571
+ return program;
572
+ }
573
+ export async function runCli(argv) {
574
+ const chunks = [];
575
+ const captured = {};
576
+ const program = createProgram({
577
+ write: chunk => {
578
+ chunks.push(chunk);
579
+ },
580
+ emit: result => {
581
+ captured.result = result;
582
+ },
583
+ });
584
+ try {
585
+ await program.parseAsync(argv, { from: "user" });
586
+ }
587
+ catch (error) {
588
+ if (error instanceof CommanderError) {
589
+ const exitCode = error.exitCode === 0 && NORMAL_EXIT_CODES.has(error.code) ? 0 : 2;
590
+ return { exitCode, stdout: chunks.join("").trimEnd() };
591
+ }
592
+ throw error;
593
+ }
594
+ return captured.result ?? { exitCode: 0, stdout: chunks.join("").trimEnd() };
595
+ }
596
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,12 @@
1
+ import type { ConfigSource, LogProvider, ProjectProvider, ResolvedConfig, SourceProvider, SourceRepoBinding } from "@ats-cx/cx-core";
2
+ import type { Workspace } from "./workspace.js";
3
+ export interface ToolContext {
4
+ config: ResolvedConfig;
5
+ configSource: ConfigSource;
6
+ logProvider: LogProvider;
7
+ projectProvider: ProjectProvider;
8
+ sourceProvider: SourceProvider;
9
+ /** 三仓绑定;无工作区时为空数组,此时诊断不带源码版本信息。 */
10
+ sourceRepos: SourceRepoBinding[];
11
+ }
12
+ export declare function createToolContext(configDir: string, workspace?: Workspace | null, configSource?: ConfigSource): ToolContext;
@@ -0,0 +1,20 @@
1
+ import { createLogProvider, createProjectProvider, createSourceProvider, loadConfig, } from "@ats-cx/cx-core";
2
+ import { applyWorkspace } from "./workspace.js";
3
+ export function createToolContext(configDir, workspace = null, configSource = "explicit") {
4
+ // 工作区模式下语义表只认工作区那份(spec 组件 D);其余共享配置读自身 config/,或钉死的配置组。
5
+ const shared = loadConfig(configDir, {
6
+ source: configSource,
7
+ ...(workspace ? { eventSemanticsPath: workspace.semanticsPath } : {}),
8
+ });
9
+ const config = workspace ? applyWorkspace(shared, workspace) : shared;
10
+ const sourceRepos = workspace?.sourceRepos ?? [];
11
+ return {
12
+ config,
13
+ configSource,
14
+ logProvider: createLogProvider(config.logSource, config.apmProvider, {}, config.apmProviderState),
15
+ projectProvider: createProjectProvider(config.projectHistoryDir),
16
+ sourceProvider: createSourceProvider(sourceRepos),
17
+ sourceRepos,
18
+ };
19
+ }
20
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1,11 @@
1
+ import type { z } from "zod";
2
+ import type { ToolContext } from "./context.js";
3
+ /** 工具契约:一份定义,两个传输层(进程内 runtime / CLI)共享。 */
4
+ export interface ToolDefinition<Input, Output> {
5
+ name: string;
6
+ description: string;
7
+ /** 允许 schema 先从 CLI 原始形状 transform 成 handler 输入。 */
8
+ inputSchema: z.ZodType<Input, z.ZodTypeDef, unknown>;
9
+ handler: (input: Input, context: ToolContext) => Promise<Output>;
10
+ }
11
+ export declare function defineTool<Input, Output>(tool: ToolDefinition<Input, Output>): ToolDefinition<Input, Output>;
@@ -0,0 +1,4 @@
1
+ export function defineTool(tool) {
2
+ return tool;
3
+ }
4
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1,4 @@
1
+ /** 面向用户的错误:message 已是可直接打印的中文文案,CLI 原样输出(exit 1),不再包成 JSON。 */
2
+ export declare class UserFacingError extends Error {
3
+ constructor(message: string);
4
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,8 @@
1
+ /** 面向用户的错误:message 已是可直接打印的中文文案,CLI 原样输出(exit 1),不再包成 JSON。 */
2
+ export class UserFacingError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "UserFacingError";
6
+ }
7
+ }
8
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};