tyc-cli 0.2.5 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 规范,版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.4.0] - 2026-05-03
6
+
7
+ ### 新增
8
+
9
+ - **输出格式三选一**(互斥优先级 `--md` > `--compact` > `--pretty` / 默认):
10
+ - `--compact`:紧凑单行 JSON(旧默认行为;管道 / `jq` 场景)
11
+ - 默认改为缩进 2 空格 JSON(pretty),人/Agent 通用
12
+ - `--pretty`:同默认;保留 flag 以保持向后兼容 / 显式声明意图
13
+ - `--md`:Markdown 表格(人类阅读 / Agent 上屏;自动渲染 `_summary` / 元数据 / `items` 表格)
14
+ - **截断与落盘**(与输出格式正交,可叠加任意子命令):
15
+ - `--head [N]`(默认 50):仅打印前 N 行;与 `--tail` 同时给则同时输出两端
16
+ - `--tail [M]`(默认 20):仅打印后 M 行
17
+ - `--full`:强制完整输出(最高优先级,覆盖 `--head/--tail/--threshold`)
18
+ - `--threshold <BYTES>`(默认 5000):**字节截断主开关**——超过该字节数从头按字节截断;**不传则永不截断**;与 `--head/--tail` 同时给则按字节截,行参数被忽略
19
+ - `--output-file <PATH>`:把完整结果写入指定路径;**必须显式指定才落盘**(不传则永不落盘)
20
+ - 截断/落盘提示打到 stderr,stdout 保持纯净数据流,便于管道与 `jq` 处理
21
+
22
+ ### 变更
23
+
24
+ - 默认 stdout 由紧凑单行 JSON 改为缩进 2 空格 JSON。需要旧默认行为的脚本请显式加 `--compact`
25
+
26
+ ## [0.3.0] - 2026-04-29
27
+
28
+ ### 新增
29
+
30
+ - **分层发现入口**:`tyc layers` / `tyc L0 list` / `tyc L1 list` / `tyc L2 list`,支持 `--md` / `--json`
31
+ - 每个 tool 的 `tyc ... --help` 标题带 `[L0]/[L1]/[L2]` 徽章,让 Agent 先看结构、后发一次 `tools/call`
32
+
5
33
  ## [0.2.0] - 2026-04-27
6
34
 
7
35
  ### 架构重构
package/README.md CHANGED
@@ -100,13 +100,58 @@ tyc executive personnel-dishonest "..." --humanName "张三"
100
100
 
101
101
  ### 全局选项
102
102
 
103
+ #### 输出格式(互斥优先级 `--md` > `--compact` > `--pretty` / 默认)
104
+
103
105
  | 选项 | 说明 |
104
106
  |------|------|
105
- | `--pretty` | 缩进 2 空格 JSON 输出(调试友好) |
107
+ | _(默认)_ | 缩进 2 空格 JSON(pretty)—— 人/Agent 都好读,已成默认 |
108
+ | `--pretty` | 同默认;保留 flag 以保持向后兼容 / 显式声明意图 |
109
+ | `--compact` | 紧凑单行 JSON(旧默认行为;管道 / `jq` 场景) |
106
110
  | `--md` | Markdown 表格化输出(人类阅读 / Agent 上屏) |
107
- | `--verbose` | 打印 MCP 请求详情到 stderr(URL / Mcp-Session-Id / 掩码 Authorization / 响应原文) |
111
+ | `--verbose` | 打印 MCP 请求详情到 stderr(与上述输出格式正交) |
112
+
113
+ #### 输出截断 / 落盘(与上述输出格式正交,可叠加任意子命令)
114
+
115
+ | 选项 | 默认值 | 说明 |
116
+ |------|-------:|------|
117
+ | `--head [N]` | 50 | 仅打印前 N 行;与 `--tail` 同时给则同时输出两端,否则只输出 head |
118
+ | `--tail [M]` | 20 | 仅打印后 M 行;与 `--head` 同时给则同时输出两端 |
119
+ | `--full` | false | 强制完整输出(最高优先级,覆盖 `--head/--tail/--threshold`) |
120
+ | `--threshold <BYTES>` | 5000 | **字节截断主开关**:超过该字节数则从头按字节截断;**不传则永不截断** |
121
+ | `--output-file <PATH>` | — | 把完整结果写入指定路径;**不传则永不落盘**(无自动落盘) |
122
+
123
+ > ⚠️ **截断决策树(与 stdout / 落盘行为)**
124
+ >
125
+ > 1. `--output-file` 给了 → 落盘永远写**完整**结果(与 stdout 是否截断无关)
126
+ > 2. `--full` 最高优先级 → stdout 也输出完整
127
+ > 3. 否则若 `--threshold N` 给了:超过 N 字节从头按字节截;**此模式下 `--head/--tail` 被忽略**
128
+ > 4. 否则若 `--head/--tail` 任一给了 → 行级截断
129
+ > 5. 否则 → 无截断、无落盘
130
+ >
131
+ > 截断与落盘的提示信息("已写入 …"、"输出被 head=5 截断 …")打到 stderr,stdout 仍为可程序化解析的纯净数据流。
132
+
133
+ #### 用法示例
134
+
135
+ ```bash
136
+ # 调试时只看头部
137
+ tyc company registration-info "百度" --head # 默认 50 行
138
+ tyc company registration-info "百度" --head 10 # 自定义 10 行
139
+
140
+ # 看头看尾,确认数据起止
141
+ tyc company key-personnel "百度" --head 5 --tail 5
108
142
 
109
- 三种输出互斥优先级:`--md > --pretty > 默认`。
143
+ # 字节预算控制(适合塞进 LLM context)
144
+ tyc risk overview "百度" --threshold 4000
145
+
146
+ # stdout 简洁 + 完整结果落盘
147
+ tyc company equity-tree "百度" --head 30 --output-file ./equity-tree.json
148
+
149
+ # 强制全量(覆盖以上一切截断)
150
+ tyc operation bidding-info "百度" --full
151
+
152
+ # 管道场景沿用旧紧凑 JSON
153
+ tyc company registration-info "百度" --compact | jq .name
154
+ ```
110
155
 
111
156
  ### 环境变量覆盖
112
157
 
@@ -201,7 +246,7 @@ tyc executive person-risk-overview "..." --humanName "张三"
201
246
  │ │
202
247
  │ └─ 多源并发聚合 · 时间戳格式化 · _summary 注入 · 空结果归一化
203
248
  │
204
- └─ 仅命令树 · 参数透传 · Session 管理 · --md/--pretty 呈现
249
+ └─ 仅命令树 · 参数透传 · Session 管理 · pretty/--md/--compact 呈现 · --head/--tail/--full/--threshold/--output-file 截断与落盘
205
250
  ```
206
251
 
207
252
  **CLI 的职责**:
@@ -210,7 +255,7 @@ tyc executive person-risk-overview "..." --humanName "张三"
210
255
  2. 组装 `tools/call` JSON-RPC 请求,透传 `Authorization` header
211
256
  3. Session 管理(`initialize` + 24h 缓存 + 失效重建)
212
257
  4. 解析 MCP Streamable HTTP 响应(纯 JSON 或 SSE)
213
- 5. 格式化输出(紧凑 JSON / `--pretty` / `--md`)
258
+ 5. 格式化输出(默认 pretty / `--compact` / `--md`),并可叠加 `--head/--tail/--full/--threshold/--output-file` 做截断与落盘
214
259
 
215
260
  **CLI 不做**:
216
261
 
@@ -243,7 +288,8 @@ tyc-cli/
243
288
  │ ├── init.ts # tyc init
244
289
  │ └── category.ts # 动态注册 6 分类 × N 方法
245
290
  └── utils/
246
- └── jsonToMarkdown.ts # --md 选项的 Markdown 渲染
291
+ ├── jsonToMarkdown.ts # --md 选项的 Markdown 渲染
292
+ └── truncate.ts # --head/--tail/--full/--threshold/--output-file 截断与落盘
247
293
  ```
248
294
 
249
295
  ---
@@ -2,6 +2,7 @@ import { getCategories, getToolsByGroup } from "../registry.js";
2
2
  import { resolveConfig } from "../config.js";
3
3
  import { callTool } from "../mcpClient.js";
4
4
  import { jsonToMarkdown } from "../utils/jsonToMarkdown.js";
5
+ import { applyTruncation, resolveOptionalInt, TRUNCATE_DEFAULTS, } from "../utils/truncate.js";
5
6
  export function registerCategoryCommands(program) {
6
7
  for (const cat of getCategories()) {
7
8
  const tools = getToolsByGroup(cat.group);
@@ -81,21 +82,43 @@ function emit(result, opts, toolName) {
81
82
  payload = JSON.parse(text);
82
83
  }
83
84
  catch {
84
- // 非 JSON 文本:直接原样输出
85
- process.stdout.write(text.endsWith("\n") ? text : text + "\n");
85
+ // 非 JSON 文本:直接原样输出(仍走截断 / 落盘)
86
+ emitWithTruncation(text.endsWith("\n") ? text.slice(0, -1) : text, opts);
86
87
  return;
87
88
  }
89
+ // 输出模式选择(互斥优先级 --md > --compact > --pretty / 默认 = pretty)
88
90
  let rendered;
89
91
  if (opts.md) {
90
92
  rendered = jsonToMarkdown(payload, toolName);
91
93
  }
92
- else if (opts.pretty) {
93
- rendered = JSON.stringify(payload, null, 2);
94
+ else if (opts.compact) {
95
+ rendered = JSON.stringify(payload);
94
96
  }
95
97
  else {
96
- rendered = JSON.stringify(payload);
98
+ // 默认 = pretty(含 --pretty 显式情况)
99
+ rendered = JSON.stringify(payload, null, 2);
100
+ }
101
+ emitWithTruncation(rendered, opts);
102
+ }
103
+ function emitWithTruncation(rendered, opts) {
104
+ // 解析 head / tail / threshold:commander 对 `[n]` 在未传值时给 true
105
+ const head = resolveOptionalInt(opts.head, TRUNCATE_DEFAULTS.HEAD);
106
+ const tail = resolveOptionalInt(opts.tail, TRUNCATE_DEFAULTS.TAIL);
107
+ const threshold = resolveOptionalInt(opts.threshold, TRUNCATE_DEFAULTS.THRESHOLD);
108
+ const outputFile = typeof opts.outputFile === "string" && opts.outputFile.length > 0
109
+ ? opts.outputFile
110
+ : undefined;
111
+ const r = applyTruncation(rendered, {
112
+ full: !!opts.full,
113
+ head,
114
+ tail,
115
+ threshold,
116
+ outputFile,
117
+ });
118
+ console.log(r.rendered);
119
+ for (const n of r.notices) {
120
+ process.stderr.write(n + "\n");
97
121
  }
98
- console.log(rendered);
99
122
  }
100
123
  function extractText(result) {
101
124
  const content = result.content || [];
package/dist/index.js CHANGED
@@ -59,12 +59,29 @@ DISCOVER BY CATEGORY (orthogonal to layers, 6 groups)
59
59
 
60
60
  ${renderCategoryLines()}
61
61
 
62
- OUTPUT FORMATS (mutually exclusive, priority: --md > --pretty > default)
62
+ OUTPUT FORMATS (mutually exclusive, priority: --md > --compact > --pretty / default)
63
63
 
64
- (default) compact single-line JSON (pipe-friendly)
65
- --pretty indented JSON (debug)
64
+ (default) indented JSON (pretty) — readable for both humans and agents
65
+ --pretty same as default (kept for backward compatibility / explicit intent)
66
+ --compact compact single-line JSON (pipe / jq friendly; old default behavior)
66
67
  --md Markdown tables (human + agent on-screen)
67
- --verbose also print MCP request details to stderr
68
+ --verbose also print MCP request details to stderr (orthogonal to above)
69
+
70
+ OUTPUT TRUNCATION & DUMP (orthogonal — apply to any sub-command)
71
+
72
+ --head [N] print only the first N lines (N defaults to 50 if flag given alone)
73
+ --tail [M] print only the last M lines (M defaults to 20 if flag given alone)
74
+ · --head + --tail together: head + "... omitted ..." + tail
75
+ · either flag alone: that side only
76
+ --full force-print the FULL rendered content (no truncation; highest priority)
77
+ --threshold <BYTES> master switch for byte-truncation. Without --threshold there is
78
+ NO automatic truncation, regardless of output size.
79
+ With --threshold N: when rendered output > N bytes, byte-truncate
80
+ from the head; --head / --tail are IGNORED in this mode.
81
+ --output-file <PATH> write the FULL rendered content to PATH. **No file is written
82
+ unless --output-file is explicitly passed** (no auto-dump).
83
+ Combine with --head/--tail/--threshold to keep stdout terse
84
+ while preserving the full result on disk.
68
85
 
69
86
  SETUP
70
87
 
@@ -97,10 +114,16 @@ function renderPriorityLines() {
97
114
  const program = new Command()
98
115
  .name("tyc")
99
116
  .description(`Tianyan AI CLI — ${getTotalCount()} commercial-data tools · L0=${getLayerCount("L0")} L1=${getLayerCount("L1")} L2=${getLayerCount("L2")} L3=${getLayerCount("L3")} · the AI-native gateway (pair with the TA MCP Server)`)
100
- .version("0.3.0")
101
- .option("--pretty", "格式化 JSON 输出(缩进 2 空格)")
117
+ .version("0.4.0")
118
+ .option("--pretty", "缩进 JSON 输出(默认行为,flag 保留以保持向后兼容)")
102
119
  .option("--md", "Markdown 表格化输出(适合人类阅读 / Agent 上屏)")
120
+ .option("--compact", "紧凑单行 JSON(管道 / jq 场景;为旧默认行为)")
103
121
  .option("--verbose", "打印 MCP 请求详情到 stderr")
122
+ .option("--head [n]", "仅输出前 N 行(不传值默认 50;与 --tail 同时给则同时输出两端)")
123
+ .option("--tail [m]", "仅输出后 M 行(不传值默认 20;与 --head 同时给则同时输出两端)")
124
+ .option("--full", "强制输出完整内容(最高优先级,覆盖 --head/--tail/--threshold)")
125
+ .option("--output-file <path>", "把完整结果写入指定路径(必须显式指定;不传则永不落盘)")
126
+ .option("--threshold <bytes>", "字节截断主开关:超过该字节数则从头按字节截断;不传则永不截断")
104
127
  .addHelpText("after", helpText);
105
128
  registerInitCommand(program);
106
129
  registerLayerCommands(program);
@@ -0,0 +1,152 @@
1
+ // 输出截断 / 落盘工具
2
+ //
3
+ // 设计原则(与用户对齐):
4
+ // 1. --threshold 是截断逻辑的"主开关"。**没有 --threshold 时永不按字节截断**。
5
+ // 2. --head N / --tail M 各自显式生效,互不依赖;同时给则同时输出两端。
6
+ // head/tail 是 line-based、对人类可读输出(pretty / md / compact)都适用。
7
+ // 3. --threshold 与 --head/--tail 同时出现时按"从头按字节截"语义优先(用户原话),
8
+ // 避免用户的字节预算被行截断绕过。
9
+ // 4. --full 最高优先级:永远输出完整内容;仍可与 --output-file 共存(落盘 + 全量打印)。
10
+ // 5. **不指定 --output-file 永不落盘**(用户明确要求)。
11
+ //
12
+ // 渲染顺序:先决定 mode(md / pretty / compact),渲染成字符串后送入 truncate;
13
+ // 落盘永远写"渲染后的字符串",与 stdout 看到的同源,便于 diff / 复盘。
14
+ import { writeFileSync, mkdirSync } from "node:fs";
15
+ import { dirname, resolve } from "node:path";
16
+ const HEAD_DEFAULT = 50;
17
+ const TAIL_DEFAULT = 20;
18
+ const THRESHOLD_DEFAULT = 5000;
19
+ /**
20
+ * 解析 commander 接收到的可选值参数:
21
+ * - undefined:用户未指定该 flag
22
+ * - true:用户指定了 flag 但未给值(commander `[n]` 语法)→ 用 fallback
23
+ * - string:用户指定了具体值 → parseInt
24
+ * - 非法值 → fallback(保持宽容;CLI 不该因为 --head abc 直接崩)
25
+ */
26
+ export function resolveOptionalInt(raw, fallback) {
27
+ if (raw === undefined)
28
+ return undefined;
29
+ if (raw === true || raw === "")
30
+ return fallback;
31
+ const n = parseInt(String(raw), 10);
32
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
33
+ }
34
+ /** Buffer 字节数(与 stdout 实际字节数一致;JS .length 是 UTF-16 code units,会高估纯 ASCII / 低估中文) */
35
+ function byteLen(s) {
36
+ return Buffer.byteLength(s, "utf8");
37
+ }
38
+ /**
39
+ * 按字节截到 ≤ maxBytes,且回退到 UTF-8 字符边界(避免半个汉字)。
40
+ * 我们只截 head 端(用户原话:"直接从头到尾按字节数量来截断")。
41
+ */
42
+ function truncateByBytes(s, maxBytes) {
43
+ const buf = Buffer.from(s, "utf8");
44
+ if (buf.length <= maxBytes)
45
+ return s;
46
+ let end = maxBytes;
47
+ // UTF-8 续字节模式 10xxxxxx:往回退到上一个起始字节
48
+ while (end > 0 && (buf[end] & 0xc0) === 0x80)
49
+ end--;
50
+ return buf.subarray(0, end).toString("utf8");
51
+ }
52
+ function truncateByLines(s, head, tail) {
53
+ const lines = s.split("\n");
54
+ const total = lines.length;
55
+ if (head === undefined && tail === undefined) {
56
+ return { out: s, truncated: false, total };
57
+ }
58
+ const h = head ?? 0;
59
+ const t = tail ?? 0;
60
+ if (h + t >= total) {
61
+ return { out: s, truncated: false, total };
62
+ }
63
+ const omitted = total - h - t;
64
+ const parts = [];
65
+ if (head !== undefined)
66
+ parts.push(...lines.slice(0, h));
67
+ if (head !== undefined && tail !== undefined) {
68
+ parts.push(`... (省略中间 ${omitted} 行) ...`);
69
+ parts.push(...lines.slice(-t));
70
+ }
71
+ else if (head !== undefined) {
72
+ parts.push(`... (省略后续 ${omitted} 行) ...`);
73
+ }
74
+ else if (tail !== undefined) {
75
+ parts.push(`... (省略前 ${omitted} 行) ...`);
76
+ parts.push(...lines.slice(-t));
77
+ }
78
+ return { out: parts.join("\n"), truncated: true, total };
79
+ }
80
+ function writeDump(absPath, content) {
81
+ const dir = dirname(absPath);
82
+ mkdirSync(dir, { recursive: true });
83
+ writeFileSync(absPath, content, { encoding: "utf8" });
84
+ }
85
+ /**
86
+ * 按用户语义对渲染好的字符串做截断 / 落盘。
87
+ *
88
+ * 决策树(每条分支互斥):
89
+ * 1. --output-file 给了 → 落盘(写"全量"原文,与 stdout 截断版本无关)
90
+ * 2. --full → stdout 全量
91
+ * 3. --threshold 给了:
92
+ * size > threshold → 字节截到 threshold(head 端)
93
+ * size ≤ threshold → 全量
94
+ * (此分支下 --head/--tail 被忽略,符合"从头按字节截断"语义)
95
+ * 4. --head 或 --tail 给了(且无 --threshold)→ line-based 头/尾/头+尾截断
96
+ * 5. 都没给 → 全量
97
+ */
98
+ export function applyTruncation(rendered, opts) {
99
+ const notices = [];
100
+ let fileWritten;
101
+ // 1. 落盘永远写全量
102
+ if (opts.outputFile && opts.outputFile.length > 0) {
103
+ const abs = resolve(opts.outputFile);
104
+ try {
105
+ writeDump(abs, rendered);
106
+ fileWritten = abs;
107
+ notices.push(`已写入完整结果到 ${abs}(${byteLen(rendered)} 字节)`);
108
+ }
109
+ catch (err) {
110
+ const msg = err instanceof Error ? err.message : String(err);
111
+ notices.push(`⚠️ --output-file 写入失败:${msg}(继续打印 stdout)`);
112
+ }
113
+ }
114
+ // 2. --full 最高优先级
115
+ if (opts.full) {
116
+ return { rendered, fileWritten, notices };
117
+ }
118
+ // 3. --threshold 主导:字节截
119
+ if (opts.threshold !== undefined) {
120
+ const size = byteLen(rendered);
121
+ if (size > opts.threshold) {
122
+ const cut = truncateByBytes(rendered, opts.threshold);
123
+ const omitted = size - byteLen(cut);
124
+ const tail = `\n... (已按 --threshold=${opts.threshold} 字节截断,省略尾部约 ${omitted} 字节) ...`;
125
+ notices.push(`输出被 --threshold=${opts.threshold} 截断(原始 ${size} 字节 → 输出 ${byteLen(cut)} 字节)` +
126
+ (fileWritten ? `;完整内容见 ${fileWritten}` : ";如需完整内容追加 --output-file <path>"));
127
+ return { rendered: cut + tail, fileWritten, notices };
128
+ }
129
+ return { rendered, fileWritten, notices };
130
+ }
131
+ // 4. line-based head/tail(无 --threshold)
132
+ if (opts.head !== undefined || opts.tail !== undefined) {
133
+ const r = truncateByLines(rendered, opts.head, opts.tail);
134
+ if (r.truncated) {
135
+ const segs = [];
136
+ if (opts.head !== undefined)
137
+ segs.push(`head=${opts.head}`);
138
+ if (opts.tail !== undefined)
139
+ segs.push(`tail=${opts.tail}`);
140
+ notices.push(`输出被 ${segs.join(" / ")} 截断(原始 ${r.total} 行)` +
141
+ (fileWritten ? `;完整内容见 ${fileWritten}` : ";如需完整内容追加 --output-file <path>"));
142
+ }
143
+ return { rendered: r.out, fileWritten, notices };
144
+ }
145
+ // 5. 无任何截断 flag
146
+ return { rendered, fileWritten, notices };
147
+ }
148
+ export const TRUNCATE_DEFAULTS = {
149
+ HEAD: HEAD_DEFAULT,
150
+ TAIL: TAIL_DEFAULT,
151
+ THRESHOLD: THRESHOLD_DEFAULT,
152
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyc-cli",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "天眼查 MCP CLI — 163 个业务语义聚合工具 · 命令行直达企业数据 · 通过 MCP Server 调用",
5
5
  "main": "dist/index.js",
6
6
  "bin": {