tyc-cli 0.1.1 → 0.2.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.
@@ -1,70 +1,107 @@
1
1
  import { getCategories, getToolsByGroup } from "../registry.js";
2
- import { getAuthorization } from "../config.js";
3
- import { runTool } from "../aggregator.js";
4
- import { applyTransformer } from "../transformer.js";
2
+ import { resolveConfig } from "../config.js";
3
+ import { callTool } from "../mcpClient.js";
5
4
  import { jsonToMarkdown } from "../utils/jsonToMarkdown.js";
6
- // 动态注册:按 api-registry.yaml 生成 `tyc <group> <method>` 命令。
7
5
  export function registerCategoryCommands(program) {
8
- const categories = getCategories();
9
- for (const cat of categories) {
6
+ for (const cat of getCategories()) {
7
+ const tools = getToolsByGroup(cat.group);
8
+ const layerCounts = countLayers(tools);
10
9
  const catCmd = program
11
10
  .command(cat.group)
12
- .description(`${cat.name_zh}(${cat.tool_count} 个工具)`);
13
- const tools = getToolsByGroup(cat.group);
11
+ .description(`${cat.name_zh}(${cat.tool_count} 个 · L0=${layerCounts.L0} L1=${layerCounts.L1} L2=${layerCounts.L2} L3=${layerCounts.L3})`);
14
12
  for (const tool of tools) {
15
- const params = tool.params || [];
16
- const requiredParams = params.filter((p) => p.required);
17
- const optionalParams = params.filter((p) => !p.required);
18
- // 第一个必填参数作为位置参数
19
- const positional = requiredParams[0] || null;
20
- const remainingRequired = requiredParams.slice(1);
21
- let methodCmd = catCmd
22
- .command(tool.cliMethod)
23
- .description(tool.description);
24
- if (positional) {
25
- methodCmd = methodCmd.argument(`<${positional.name}>`, positional.description);
26
- }
27
- for (const p of remainingRequired) {
28
- methodCmd = methodCmd.requiredOption(`--${p.name} <value>`, p.description);
29
- }
30
- for (const p of optionalParams) {
31
- methodCmd = methodCmd.option(`--${p.name} <value>`, p.description);
32
- }
33
- const bound = tool;
34
- methodCmd.action(async (posVal, options) => {
35
- const auth = getAuthorization();
36
- const args = {};
37
- if (positional && posVal)
38
- args[positional.name] = posVal;
39
- for (const p of [...remainingRequired, ...optionalParams]) {
40
- if (options[p.name] !== undefined)
41
- args[p.name] = options[p.name];
42
- }
43
- try {
44
- const { results, warnings } = await runTool(bound, args, auth, {
45
- verbose: !!program.opts().verbose,
46
- });
47
- const out = applyTransformer(bound, results, warnings);
48
- // 输出格式优先级:--md > --pretty > 紧凑 JSON
49
- const opts = program.opts();
50
- let text;
51
- if (opts.md) {
52
- text = jsonToMarkdown(out, bound.name);
53
- }
54
- else if (opts.pretty) {
55
- text = JSON.stringify(out, null, 2);
56
- }
57
- else {
58
- text = JSON.stringify(out);
59
- }
60
- console.log(text);
61
- }
62
- catch (err) {
63
- const msg = err instanceof Error ? err.message : String(err);
64
- console.error(`请求失败: ${msg}`);
65
- process.exit(1);
66
- }
67
- });
13
+ bindMethod(catCmd, tool, program);
14
+ }
15
+ }
16
+ }
17
+ function countLayers(tools) {
18
+ const c = { L0: 0, L1: 0, L2: 0, L3: 0 };
19
+ for (const t of tools)
20
+ c[t.layer] += 1;
21
+ return c;
22
+ }
23
+ function bindMethod(catCmd, tool, program) {
24
+ const params = tool.params || [];
25
+ const requiredParams = params.filter((p) => p.required);
26
+ const optionalParams = params.filter((p) => !p.required);
27
+ const positional = requiredParams[0] || null;
28
+ const remainingRequired = requiredParams.slice(1);
29
+ // 在描述首部打上 [LX] 徽章,让 `tyc <group> --help` / `tyc <group> <method> --help`
30
+ // 都能直观看到分层信息;徽章不影响 Agent 对 description 的语义理解。
31
+ let methodCmd = catCmd
32
+ .command(tool.cliMethod)
33
+ .description(`[${tool.layer}] ${tool.description}`);
34
+ if (positional) {
35
+ methodCmd = methodCmd.argument(`<${positional.name}>`, positional.description);
36
+ }
37
+ for (const p of remainingRequired) {
38
+ methodCmd = methodCmd.requiredOption(`--${p.name} <value>`, p.description);
39
+ }
40
+ for (const p of optionalParams) {
41
+ methodCmd = methodCmd.option(`--${p.name} <value>`, p.description);
42
+ }
43
+ methodCmd.action(async (posVal, options) => {
44
+ const args = {};
45
+ if (positional && posVal)
46
+ args[positional.name] = posVal;
47
+ for (const p of [...remainingRequired, ...optionalParams]) {
48
+ if (options[p.name] !== undefined)
49
+ args[p.name] = options[p.name];
50
+ }
51
+ const cfg = resolveConfig();
52
+ const opts = program.opts();
53
+ const verbose = !!opts.verbose;
54
+ try {
55
+ const result = await callTool(cfg, tool.name, args, { verbose });
56
+ emit(result, opts, tool.name);
68
57
  }
58
+ catch (err) {
59
+ const msg = err instanceof Error ? err.message : String(err);
60
+ console.error(`请求失败: ${msg}`);
61
+ process.exit(1);
62
+ }
63
+ });
64
+ }
65
+ function emit(result, opts, toolName) {
66
+ // MCP 业务错误:isError + content[0].text = 错误描述
67
+ if (result.isError) {
68
+ const errText = extractText(result) || "未知业务错误";
69
+ console.error(errText);
70
+ process.exit(1);
71
+ }
72
+ const text = extractText(result);
73
+ if (text === null) {
74
+ console.error("tools/call 返回无可解析内容");
75
+ process.exit(1);
76
+ }
77
+ // MCP Server 返回的是 JSON 字符串(已完成多源合并 / 时间戳格式化 /
78
+ // _summary / _empty / _warnings 注入)。CLI 只做呈现格式化。
79
+ let payload;
80
+ try {
81
+ payload = JSON.parse(text);
82
+ }
83
+ catch {
84
+ // 非 JSON 文本:直接原样输出
85
+ process.stdout.write(text.endsWith("\n") ? text : text + "\n");
86
+ return;
87
+ }
88
+ let rendered;
89
+ if (opts.md) {
90
+ rendered = jsonToMarkdown(payload, toolName);
91
+ }
92
+ else if (opts.pretty) {
93
+ rendered = JSON.stringify(payload, null, 2);
94
+ }
95
+ else {
96
+ rendered = JSON.stringify(payload);
97
+ }
98
+ console.log(rendered);
99
+ }
100
+ function extractText(result) {
101
+ const content = result.content || [];
102
+ for (const c of content) {
103
+ if (typeof c.text === "string" && c.text.length > 0)
104
+ return c.text;
69
105
  }
106
+ return null;
70
107
  }
@@ -1,14 +1,64 @@
1
- import { saveConfig, loadConfig } from "../config.js";
1
+ import { DEFAULT_MCP_URL, loadConfig, resolveConfig, saveConfig } from "../config.js";
2
+ import { clearSession } from "../session.js";
3
+ import { ensureSession } from "../mcpClient.js";
2
4
  export function registerInitCommand(program) {
3
5
  program
4
6
  .command("init")
5
- .description("配置 Authorization(保存到 ~/.tyc/config.json)")
6
- .option("--authorization <token>", "tyc OpenAPI Authorization")
7
- .action((opts) => {
8
- const existing = loadConfig() || {};
7
+ .description("配置 MCP endpoint / Authorization(保存到 ~/.tyc/config.json,并校验连通性)")
8
+ .option("--authorization <token>", "tyc OpenAPI Authorization(写入 headers.Authorization)")
9
+ .option("--url <url>", `MCP endpoint(默认 ${DEFAULT_MCP_URL})`)
10
+ .option("--header <kv...>", "自定义 header,格式 K=V,可重复;留空清除该 key")
11
+ .option("--clear-session", "仅清除本地 MCP session 缓存(~/.tyc/session.json)")
12
+ .option("--no-verify", "跳过连通性校验(不发 initialize)")
13
+ .action(async (opts) => {
14
+ const cfg = loadConfig() || {};
15
+ if (!cfg.headers)
16
+ cfg.headers = {};
17
+ if (opts.url)
18
+ cfg.url = opts.url;
19
+ if (!cfg.url)
20
+ cfg.url = DEFAULT_MCP_URL;
9
21
  if (opts.authorization)
10
- existing.authorization = opts.authorization;
11
- saveConfig(existing);
12
- console.log("已保存到 ~/.tyc/config.json");
22
+ cfg.headers.Authorization = opts.authorization;
23
+ if (opts.header && opts.header.length > 0) {
24
+ for (const kv of opts.header) {
25
+ const idx = kv.indexOf("=");
26
+ if (idx < 0) {
27
+ console.error(`忽略无效 header(缺 =):${kv}`);
28
+ continue;
29
+ }
30
+ const k = kv.slice(0, idx).trim();
31
+ const v = kv.slice(idx + 1);
32
+ if (!k)
33
+ continue;
34
+ if (v === "")
35
+ delete cfg.headers[k];
36
+ else
37
+ cfg.headers[k] = v;
38
+ }
39
+ }
40
+ saveConfig(cfg);
41
+ // 任何配置变更都清掉 session 缓存,避免 endpoint/鉴权切换后复用旧 sessionId
42
+ clearSession();
43
+ if (opts.clearSession) {
44
+ console.log("已清除 ~/.tyc/session.json");
45
+ }
46
+ console.log(`已保存 ~/.tyc/config.json (url=${cfg.url})`);
47
+ if (opts.verify === false) {
48
+ return;
49
+ }
50
+ // 立即发 initialize,拿到 Mcp-Session-Id 落盘;失败则非 0 退出
51
+ try {
52
+ const resolved = resolveConfig();
53
+ const verbose = !!program.opts().verbose;
54
+ const sess = await ensureSession(resolved, { verbose });
55
+ console.log(`已建立 MCP session(sessionId=${sess.sessionId.slice(0, 16)}…)`);
56
+ }
57
+ catch (err) {
58
+ const msg = err instanceof Error ? err.message : String(err);
59
+ console.error(`MCP 连通性校验失败:${msg}`);
60
+ console.error("配置已保存;确认 --url 可达后重新运行 tyc init,或加 --no-verify 跳过校验。");
61
+ process.exit(1);
62
+ }
13
63
  });
14
64
  }
@@ -0,0 +1,245 @@
1
+ import { getCategories, getCategoryName, getLayerCount, getToolsByLayer, getTotalCount, } from "../registry.js";
2
+ const LAYER_SPECS = [
3
+ {
4
+ layer: "L0",
5
+ title: "Resolve · 实体锚定层",
6
+ summary: "把用户输入(简称 / 曾用名 / 模糊指代)解析成精确企业列表(USCC + 别名 + 状态)。Agent 第 0 跳:所有下游工具都依赖锚定结果。",
7
+ trigger: '"是哪家公司 / 帮我查 XX / 我想看 X 集团" — 用户给的是名字而非 USCC',
8
+ contract: "返回候选企业列表(含 USCC、企业全名、注册状态、别名);Agent 据此挑定唯一企业再走 L1。",
9
+ },
10
+ {
11
+ layer: "L1",
12
+ title: "Overview · 概要层",
13
+ summary: "跨维度聚合、总览、评分、实体校验。Agent 拿到 USCC 后的第 1 跳,回包带 _summary + drill_down 线索。",
14
+ trigger: '"这家公司是什么 / 整体怎么样 / 有没有风险 / 能不能信任"',
15
+ contract: "不需要你提前知道维度;一次调用就能拿到 _summary + drill_down 线索。",
16
+ },
17
+ {
18
+ layer: "L2",
19
+ title: "Drill-down · 明细层",
20
+ summary: "一级数据维度展开:股权 / 诉讼 / 招投标 / 年报 / 知识产权 / 历史工商 / 人员任职 ……",
21
+ trigger: "L1 `_summary` 指示\"有 X 条 Y\"时,针对该维度精确下钻。",
22
+ contract: "返回 tyc 英文 key 透传的 items 列表 + 顶层聚合字段。",
23
+ },
24
+ {
25
+ layer: "L3",
26
+ title: "Specialized · 专业层",
27
+ summary: "ID 详情、search_*、上市公司专项、私募基金、建筑资质、投资机构、地理位置、人员微查询 ……",
28
+ trigger: "L2 列表中某条记录的 id 需要展开、或触发垂直行业 SKILL(尽调/风控/知产)。",
29
+ contract: "面向专业 Agent 和 SKILL Pack;多数需要 id 或二级关键词。",
30
+ },
31
+ ];
32
+ export function registerLayerCommands(program) {
33
+ registerLayers(program);
34
+ for (const spec of LAYER_SPECS) {
35
+ registerLayerCmd(program, spec);
36
+ }
37
+ }
38
+ function registerLayers(program) {
39
+ program
40
+ .command("layers")
41
+ .description(`一屏总览 tyc-cli 的 4 层工具架构(L0 ${getLayerCount("L0")} / L1 ${getLayerCount("L1")} / L2 ${getLayerCount("L2")} / L3 ${getLayerCount("L3")}),面向 AI Agent 的推荐调用顺序`)
42
+ .option("--md", "Markdown 表格(适合 Agent 上屏 / 粘进 README)")
43
+ .option("--json", "机读 JSON(layers 汇总 + 各层工具元数据)")
44
+ .action((opts) => {
45
+ if (opts.json) {
46
+ const payload = {
47
+ total: getTotalCount(),
48
+ layers: LAYER_SPECS.map((s) => ({
49
+ layer: s.layer,
50
+ title: s.title,
51
+ count: getLayerCount(s.layer),
52
+ trigger: s.trigger,
53
+ summary: s.summary,
54
+ contract: s.contract,
55
+ })),
56
+ categories: getCategories(),
57
+ recommended_call_order: [
58
+ `0. Anchor at L0 — feed the user's raw company name into the ${getLayerCount("L0")} L0 tool (search_companies); lock onto a USCC + official name`,
59
+ `1. Ascend to L1 — pick ONE of the ${getLayerCount("L1")} overview tools with the anchored USCC`,
60
+ "2. Read _summary + drill_down hints from the response",
61
+ `3. Drill to L2 for a specific dimension (items + totals; ${getLayerCount("L2")} tools)`,
62
+ `4. Reach L3 for id-based detail, search_*, or vertical SKILL tools (${getLayerCount("L3")} tools)`,
63
+ ],
64
+ };
65
+ console.log(JSON.stringify(payload, null, 2));
66
+ return;
67
+ }
68
+ if (opts.md) {
69
+ console.log(renderLayersMarkdown());
70
+ return;
71
+ }
72
+ console.log(renderLayersText());
73
+ });
74
+ }
75
+ function registerLayerCmd(program, spec) {
76
+ const layer = spec.layer;
77
+ const cmd = program
78
+ .command(layer)
79
+ .description(`${spec.title}(${getLayerCount(layer)} 个)— ${spec.summary}`);
80
+ cmd
81
+ .command("list")
82
+ .description(`列出 ${layer} 全部 ${getLayerCount(layer)} 个工具(默认按分类分组文本表格)`)
83
+ .option("--md", "Markdown 表格(适合 Agent 上屏)")
84
+ .option("--json", "机读 JSON(工具 ID / cliMethod / 分类 / 参数 / 描述)")
85
+ .action((opts) => {
86
+ const tools = getToolsByLayer(layer);
87
+ if (opts.json) {
88
+ const payload = {
89
+ layer,
90
+ title: spec.title,
91
+ count: tools.length,
92
+ trigger: spec.trigger,
93
+ contract: spec.contract,
94
+ tools: tools.map((t) => ({
95
+ name: t.name,
96
+ group: t.group,
97
+ category_name_zh: t.categoryNameZh,
98
+ cli: `tyc ${t.group} ${t.cliMethod}`,
99
+ description: t.description,
100
+ params: t.params ?? [],
101
+ })),
102
+ };
103
+ console.log(JSON.stringify(payload, null, 2));
104
+ return;
105
+ }
106
+ if (opts.md) {
107
+ console.log(renderLayerListMarkdown(spec, tools));
108
+ return;
109
+ }
110
+ console.log(renderLayerListText(spec, tools));
111
+ });
112
+ }
113
+ // ─────────────────────────── 渲染:tyc layers ───────────────────────────
114
+ function renderLayersText() {
115
+ const total = getTotalCount();
116
+ const defaultSurface = getLayerCount("L0") + getLayerCount("L1");
117
+ const lines = [];
118
+ lines.push("tyc-cli · Layered Tool Architecture (v5 §3.3 ①)");
119
+ lines.push(`Total ${total} tools · L0=${getLayerCount("L0")} L1=${getLayerCount("L1")} L2=${getLayerCount("L2")} L3=${getLayerCount("L3")}`);
120
+ lines.push("Rationale: LLM tool-selection accuracy collapses beyond 30 tools.");
121
+ lines.push(` tyc carves L0 into a single entity-resolution tool, so the default LLM`);
122
+ lines.push(` surface is L0 + L1 = ${defaultSurface} tools (≤ v5's 15-tool limit);`);
123
+ lines.push(" L2/L3 are discovered on demand via _summary / drill_down_tools.");
124
+ lines.push("");
125
+ for (const s of LAYER_SPECS) {
126
+ const n = getLayerCount(s.layer);
127
+ lines.push(`[${s.layer}] ${s.title} (${n} tool${n === 1 ? "" : "s"})`);
128
+ lines.push(` ${s.summary}`);
129
+ lines.push(` Trigger: ${s.trigger}`);
130
+ lines.push(` Contract: ${s.contract}`);
131
+ lines.push(` List: tyc ${s.layer} list [--md|--json]`);
132
+ lines.push("");
133
+ }
134
+ lines.push("Recommended call order for agents:");
135
+ lines.push(` 0. Anchor at L0 — feed the user's raw company name into the ${getLayerCount("L0")} L0 tool (search_companies)`);
136
+ lines.push(` 1. Ascend to L1 — one of the ${getLayerCount("L1")} overview tools, with the anchored USCC`);
137
+ lines.push(" 2. Read _summary + drill_down hints in the response");
138
+ lines.push(` 3. Drill to L2 for a specific dimension (${getLayerCount("L2")} tools)`);
139
+ lines.push(` 4. Reach L3 for id-based detail / search_* / vertical SKILL (${getLayerCount("L3")} tools)`);
140
+ return lines.join("\n");
141
+ }
142
+ function renderLayersMarkdown() {
143
+ const defaultSurface = getLayerCount("L0") + getLayerCount("L1");
144
+ const lines = [];
145
+ lines.push("# tyc-cli · Layered Tool Architecture");
146
+ lines.push("");
147
+ lines.push(`> Total **${getTotalCount()}** tools · **L0=${getLayerCount("L0")}** · **L1=${getLayerCount("L1")}** · **L2=${getLayerCount("L2")}** · **L3=${getLayerCount("L3")}** `);
148
+ lines.push(`> Design rationale (v5 §3.3 ①): LLM tool-selection accuracy collapses above 30 tools; tyc carves L0 into a dedicated entity-resolution tool so the default LLM surface = L0 + L1 = ${defaultSurface} tools (≤ 15-tool limit), discovers L2/L3 on demand.`);
149
+ lines.push("");
150
+ lines.push("| Layer | Tools | Summary | Trigger |");
151
+ lines.push("|---|---:|---|---|");
152
+ for (const s of LAYER_SPECS) {
153
+ lines.push(`| **${s.layer}** ${s.title} | ${getLayerCount(s.layer)} | ${escapeCell(s.summary)} | ${escapeCell(s.trigger)} |`);
154
+ }
155
+ lines.push("");
156
+ lines.push("**Recommended call order for agents**");
157
+ lines.push("");
158
+ lines.push(`0. Anchor at L0 — feed the user's raw company name into the ${getLayerCount("L0")} L0 tool (\`search_companies\`); lock onto a USCC + official name.`);
159
+ lines.push(`1. Ascend to L1 — pick ONE of the ${getLayerCount("L1")} overview tools with the anchored USCC.`);
160
+ lines.push("2. Read `_summary` + `drill_down` hints in the response.");
161
+ lines.push(`3. Drill to L2 for a specific dimension (items + totals; ${getLayerCount("L2")} tools).`);
162
+ lines.push(`4. Reach L3 for id-based detail, \`search_*\`, or vertical SKILL tools (${getLayerCount("L3")} tools).`);
163
+ lines.push("");
164
+ lines.push("**Discover each layer**");
165
+ lines.push("");
166
+ for (const s of LAYER_SPECS) {
167
+ lines.push(`- \`tyc ${s.layer} list\` — ${s.title}`);
168
+ }
169
+ return lines.join("\n");
170
+ }
171
+ // ─────────────────── 渲染:tyc L0/L1/L2 list ───────────────────
172
+ function renderLayerListText(spec, tools) {
173
+ const lines = [];
174
+ const n = tools.length;
175
+ lines.push(`${spec.layer} · ${spec.title} (${n} tool${n === 1 ? "" : "s"})`);
176
+ lines.push(`Trigger: ${spec.trigger}`);
177
+ lines.push("");
178
+ // 按 group 分组打印(按 categories 中声明顺序)
179
+ const groupsOrder = getCategories().map((c) => c.group);
180
+ const grouped = new Map();
181
+ for (const t of tools) {
182
+ const arr = grouped.get(t.group) || [];
183
+ arr.push(t);
184
+ grouped.set(t.group, arr);
185
+ }
186
+ for (const g of groupsOrder) {
187
+ const arr = grouped.get(g);
188
+ if (!arr || arr.length === 0)
189
+ continue;
190
+ const zh = getCategoryName(g);
191
+ lines.push(`── ${g} (${zh}, ${arr.length}) ──`);
192
+ for (const t of arr) {
193
+ const cli = `tyc ${t.group} ${t.cliMethod}`;
194
+ lines.push(` ${pad(t.name, 46)} ${cli}`);
195
+ lines.push(` ${oneLine(t.description)}`);
196
+ }
197
+ lines.push("");
198
+ }
199
+ lines.push(`Next: read ${spec.contract}`);
200
+ return lines.join("\n");
201
+ }
202
+ function renderLayerListMarkdown(spec, tools) {
203
+ const lines = [];
204
+ const n = tools.length;
205
+ lines.push(`# ${spec.layer} · ${spec.title}`);
206
+ lines.push("");
207
+ lines.push(`> **${n} tool${n === 1 ? "" : "s"}** · ${spec.summary} `);
208
+ lines.push(`> Trigger: ${spec.trigger}`);
209
+ lines.push("");
210
+ const groupsOrder = getCategories().map((c) => c.group);
211
+ const grouped = new Map();
212
+ for (const t of tools) {
213
+ const arr = grouped.get(t.group) || [];
214
+ arr.push(t);
215
+ grouped.set(t.group, arr);
216
+ }
217
+ for (const g of groupsOrder) {
218
+ const arr = grouped.get(g);
219
+ if (!arr || arr.length === 0)
220
+ continue;
221
+ const zh = getCategoryName(g);
222
+ lines.push(`## ${g} — ${zh}(${arr.length})`);
223
+ lines.push("");
224
+ lines.push("| Tool | CLI | Description |");
225
+ lines.push("|---|---|---|");
226
+ for (const t of arr) {
227
+ const cli = `\`tyc ${t.group} ${t.cliMethod}\``;
228
+ lines.push(`| \`${t.name}\` | ${cli} | ${escapeCell(oneLine(t.description))} |`);
229
+ }
230
+ lines.push("");
231
+ }
232
+ return lines.join("\n");
233
+ }
234
+ // ─────────────────── utils ───────────────────
235
+ function pad(s, width) {
236
+ if (s.length >= width)
237
+ return s;
238
+ return s + " ".repeat(width - s.length);
239
+ }
240
+ function oneLine(s) {
241
+ return s.replace(/\s+/g, " ").trim();
242
+ }
243
+ function escapeCell(s) {
244
+ return s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
245
+ }
package/dist/config.js CHANGED
@@ -1,25 +1,47 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
1
+ // ~/.tyc/config.json 读写
2
+ //
3
+ // 配置文件格式与通用 MCP 客户端保持一致:
4
+ // { "url": "https://ai-mcp.tianyancha.com/mcp",
5
+ // "headers": { "Authorization": "xxxx" } }
6
+ //
7
+ // 默认端点指向 tyc 官方生产 MCP;本地调试通过
8
+ // tyc init --url http://localhost:8080/mcp 或 TYC_MCP_ENDPOINT 环境变量覆盖。
9
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
2
10
  import { join } from "node:path";
3
11
  import { homedir } from "node:os";
4
- // ~/.tyc/config.json
5
12
  const CONFIG_DIR = join(homedir(), ".tyc");
6
13
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
14
+ export const DEFAULT_MCP_URL = "https://ai-mcp.tianyancha.com/mcp";
7
15
  export function loadConfig() {
8
16
  if (!existsSync(CONFIG_FILE))
9
17
  return null;
10
- const raw = readFileSync(CONFIG_FILE, "utf-8");
11
- return JSON.parse(raw);
18
+ try {
19
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
20
+ return JSON.parse(raw);
21
+ }
22
+ catch {
23
+ return null;
24
+ }
12
25
  }
13
26
  export function saveConfig(config) {
14
27
  if (!existsSync(CONFIG_DIR))
15
- mkdirSync(CONFIG_DIR, { recursive: true });
28
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
16
29
  writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
30
+ try {
31
+ chmodSync(CONFIG_FILE, 0o600);
32
+ }
33
+ catch { /* best effort */ }
17
34
  }
18
- export function getAuthorization() {
19
- const cfg = loadConfig();
20
- if (!cfg?.authorization) {
35
+ export function resolveConfig() {
36
+ const cfg = loadConfig() || {};
37
+ const url = process.env.TYC_MCP_ENDPOINT || cfg.url || DEFAULT_MCP_URL;
38
+ const headers = { ...(cfg.headers || {}) };
39
+ if (!headers.Authorization && process.env.TYC_AUTHORIZATION) {
40
+ headers.Authorization = process.env.TYC_AUTHORIZATION;
41
+ }
42
+ if (!headers.Authorization) {
21
43
  console.error("未配置 Authorization。请先运行:tyc init --authorization YOUR_API_KEY");
22
44
  process.exit(1);
23
45
  }
24
- return cfg.authorization;
46
+ return { url, headers };
25
47
  }
package/dist/index.js CHANGED
@@ -2,15 +2,92 @@
2
2
  import { Command } from "commander";
3
3
  import { registerInitCommand } from "./commands/init.js";
4
4
  import { registerCategoryCommands } from "./commands/category.js";
5
- import { getTotalCount, getCategories } from "./registry.js";
5
+ import { registerLayerCommands } from "./commands/layers.js";
6
+ import { getCategories, getLayerCount, getTotalCount, } from "./registry.js";
7
+ // 顶层 help:把"最懂 AI 的 CLI"落在 4 层架构 + 调用顺序 + 可发现性上。
8
+ // Agent 看 tyc --help 时应立刻读懂"先 L0 锚定企业,再 L1 概览,再 L2 下钻,最后 L3 详情"这条主线。
9
+ const helpText = `
10
+ ARCHITECTURE — Layered Tool Discovery for LLM Agents (v5 §3.3 ①)
11
+
12
+ Cognitive research shows LLM tool-selection accuracy collapses above
13
+ 30 tools (>95% at 10, ~70% at 30, <50% at 100+). tyc counters this with
14
+ a 4-layer progressive-disclosure architecture, led by a dedicated
15
+ entity-resolution layer:
16
+
17
+ ┌─ L0 Resolve ${pad(getLayerCount("L0"), 3)} tool entity resolution · 简称/曾用名/模糊名 → 精确企业
18
+ ├─ L1 Overview ${pad(getLayerCount("L1"), 3)} tools cross-facet aggregation · scoring · entity-check
19
+ ├─ L2 Drill-down ${pad(getLayerCount("L2"), 3)} tools one-hop into a specific data dimension
20
+ └─ L3 Specialized ${pad(getLayerCount("L3"), 3)} tools id-based detail · search_* · vertical SKILLs
21
+
22
+ Recommended call order:
23
+
24
+ 0. Anchor at L0. Feed the user's raw company name (可能是简称/曾用名/模糊
25
+ 指代) into \`search_companies\`; lock onto one entity
26
+ with a USCC + official name before anything else.
27
+ 1. Ascend to L1. Pick ONE of the ${getLayerCount("L1")} overview tools with the
28
+ anchored USCC. Read the response's _summary + drill_down.
29
+ 2. Descend to L2. Follow hints to a specific dimension
30
+ (shareholders / litigation / patents / annual reports …).
31
+ 3. Land on L3. When L2 returns a list item with an \`id\`, a \`search_*\`
32
+ need arises, or a vertical SKILL is activated.
33
+
34
+ Rule of thumb: the smaller the layer number, the higher the information
35
+ density per token. L0 is MANDATORY — skipping it (without a USCC already
36
+ in hand) wastes downstream calls on the wrong entity.
37
+ Default LLM surface = L0 + L1 = ${getLayerCount("L0") + getLayerCount("L1")} tools (≤ v5's 15-tool limit).
38
+
39
+ DISCOVER BY LAYER
40
+
41
+ tyc layers one-screen architecture map
42
+ tyc L0 list list the ${getLayerCount("L0")} L0 tool (entity-resolve layer)
43
+ tyc L1 list list all ${getLayerCount("L1")} L1 tools (overview, grouped)
44
+ tyc L2 list list all ${getLayerCount("L2")} L2 tools (drill-down, grouped)
45
+ tyc L3 list list all ${getLayerCount("L3")} L3 tools (specialized, grouped)
46
+ tyc L0 list --md Markdown tables (agent on-screen)
47
+ tyc L0 list --json machine-readable JSON (id · cli · params · description)
48
+
49
+ DISCOVER BY CATEGORY (orthogonal to layers, 6 groups)
50
+
51
+ ${renderCategoryLines()}
52
+
53
+ OUTPUT FORMATS (mutually exclusive, priority: --md > --pretty > default)
54
+
55
+ (default) compact single-line JSON (pipe-friendly)
56
+ --pretty indented JSON (debug)
57
+ --md Markdown tables (human + agent on-screen)
58
+ --verbose also print MCP request details to stderr
59
+
60
+ SETUP
61
+
62
+ tyc init --authorization <KEY> default https://ai-mcp.tianyancha.com/mcp
63
+ tyc init --url <MCP_URL> --authorization <KEY> local / self-hosted
64
+
65
+ Every tool returns tyc OpenAPI native English keys verbatim. The MCP Server
66
+ handles multi-source merge, Asia/Shanghai timestamp formatting, _summary
67
+ injection, and empty-result normalization — the CLI just tells the Agent
68
+ where the data is and what it looks like.
69
+ `;
70
+ function pad(n, width) {
71
+ const s = String(n);
72
+ return s.length >= width ? s : " ".repeat(width - s.length) + s;
73
+ }
74
+ function renderCategoryLines() {
75
+ const lines = [];
76
+ for (const c of getCategories()) {
77
+ lines.push(` tyc ${c.group.padEnd(22)} ${c.name_zh} (${c.tool_count} tools)`);
78
+ }
79
+ return lines.join("\n");
80
+ }
6
81
  const program = new Command()
7
82
  .name("tyc")
8
- .description(`天眼查 CLI — ${getTotalCount()} 个业务语义聚合工具 · ${getCategories().length} 个分类 · 返回 tyc 英文 key 透传结构`)
9
- .version("0.1.0")
83
+ .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)`)
84
+ .version("0.3.0")
10
85
  .option("--pretty", "格式化 JSON 输出(缩进 2 空格)")
11
86
  .option("--md", "Markdown 表格化输出(适合人类阅读 / Agent 上屏)")
12
- .option("--verbose", "输出请求详情到 stderr");
87
+ .option("--verbose", "打印 MCP 请求详情到 stderr")
88
+ .addHelpText("after", helpText);
13
89
  registerInitCommand(program);
90
+ registerLayerCommands(program);
14
91
  registerCategoryCommands(program);
15
92
  program.parseAsync(process.argv).catch((err) => {
16
93
  const msg = err instanceof Error ? err.message : String(err);