tyc-cli 0.3.3 → 0.3.5

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,5 +1,6 @@
1
1
  import { getCategories, getToolsByGroup } from "../registry.js";
2
2
  import { resolveConfig } from "../config.js";
3
+ import { callCoreTool } from "../coreClient.js";
3
4
  import { callTool } from "../mcpClient.js";
4
5
  import { jsonToMarkdown } from "../utils/jsonToMarkdown.js";
5
6
  import { applyTruncation, resolveOptionalInt, TRUNCATE_DEFAULTS, } from "../utils/truncate.js";
@@ -53,7 +54,9 @@ function bindMethod(catCmd, tool, program) {
53
54
  const opts = program.opts();
54
55
  const verbose = !!opts.verbose;
55
56
  try {
56
- const result = await callTool(cfg, tool.name, args, { verbose });
57
+ const result = cfg.transport === "mcp"
58
+ ? await callTool(cfg, tool.name, args, { verbose })
59
+ : await callCoreTool(cfg, tool.name, args, { verbose });
57
60
  emit(result, opts, tool.name);
58
61
  }
59
62
  catch (err) {
@@ -1,12 +1,23 @@
1
- import { DEFAULT_MCP_URL, loadConfig, resolveConfig, saveConfig } from "../config.js";
1
+ import { DEFAULT_MCP_URL, defaultCoreURL, loadConfig, resolveConfig, saveConfig, } from "../config.js";
2
+ import { verifyCoreEndpoint } from "../coreClient.js";
2
3
  import { clearSession } from "../session.js";
3
4
  import { ensureSession } from "../mcpClient.js";
5
+ function parseTransport(raw) {
6
+ if (!raw)
7
+ return undefined;
8
+ const v = raw.trim().toLowerCase();
9
+ if (v === "core" || v === "mcp")
10
+ return v;
11
+ throw new Error(`invalid --transport: ${raw} (expected core or mcp)`);
12
+ }
4
13
  export function registerInitCommand(program) {
5
14
  program
6
15
  .command("init")
7
16
  .description("配置 MCP endpoint / Authorization(保存到 ~/.tyc/config.json,并校验连通性)")
8
17
  .option("--authorization <token>", "tyc OpenAPI Authorization(写入 headers.Authorization)")
9
18
  .option("--url <url>", `MCP endpoint(默认 ${DEFAULT_MCP_URL})`)
19
+ .option("--core-url <url>", "Shared Core HTTP endpoint(默认从 --url 推导 /v1/core/tools/call)")
20
+ .option("--transport <core|mcp>", "默认调用通道(默认 core;mcp 保留旧 session 链路)")
10
21
  .option("--header <kv...>", "自定义 header,格式 K=V,可重复;留空清除该 key")
11
22
  .option("--clear-session", "仅清除本地 MCP session 缓存(~/.tyc/session.json)")
12
23
  .option("--no-verify", "跳过连通性校验(不发 initialize)")
@@ -18,6 +29,15 @@ export function registerInitCommand(program) {
18
29
  cfg.url = opts.url;
19
30
  if (!cfg.url)
20
31
  cfg.url = DEFAULT_MCP_URL;
32
+ const transport = parseTransport(opts.transport);
33
+ if (transport)
34
+ cfg.transport = transport;
35
+ if (!cfg.transport)
36
+ cfg.transport = "core";
37
+ if (opts.coreUrl)
38
+ cfg.coreUrl = opts.coreUrl;
39
+ if (!cfg.coreUrl)
40
+ cfg.coreUrl = defaultCoreURL(cfg.url);
21
41
  if (opts.authorization)
22
42
  cfg.headers.Authorization = opts.authorization;
23
43
  if (opts.header && opts.header.length > 0) {
@@ -43,20 +63,26 @@ export function registerInitCommand(program) {
43
63
  if (opts.clearSession) {
44
64
  console.log("已清除 ~/.tyc/session.json");
45
65
  }
46
- console.log(`已保存 ~/.tyc/config.json (url=${cfg.url})`);
66
+ console.log(`已保存 ~/.tyc/config.json (transport=${cfg.transport}, url=${cfg.url}, coreUrl=${cfg.coreUrl})`);
47
67
  if (opts.verify === false) {
48
68
  return;
49
69
  }
50
- // 立即发 initialize,拿到 Mcp-Session-Id 落盘;失败则非 0 退出
70
+ // core 模式只探活 stateless endpoint;mcp 模式才 initialize 并缓存 session。
51
71
  try {
52
72
  const resolved = resolveConfig();
53
73
  const verbose = !!program.opts().verbose;
54
- const sess = await ensureSession(resolved, { verbose });
55
- console.log(`已建立 MCP session(sessionId=${sess.sessionId.slice(0, 16)}…)`);
74
+ if (resolved.transport === "mcp") {
75
+ const sess = await ensureSession(resolved, { verbose });
76
+ console.log(`已建立 MCP session(sessionId=${sess.sessionId.slice(0, 16)}…)`);
77
+ }
78
+ else {
79
+ await verifyCoreEndpoint(resolved, verbose);
80
+ console.log("Shared Core endpoint 校验通过");
81
+ }
56
82
  }
57
83
  catch (err) {
58
84
  const msg = err instanceof Error ? err.message : String(err);
59
- console.error(`MCP 连通性校验失败:${msg}`);
85
+ console.error(`连通性校验失败:${msg}`);
60
86
  console.error("配置已保存;确认 --url 可达后重新运行 tyc init,或加 --no-verify 跳过校验。");
61
87
  process.exit(1);
62
88
  }
@@ -12,7 +12,7 @@ const LAYER_SPECS = [
12
12
  title: "Overview · 概要层",
13
13
  summary: "**6 个 facet 队长,每 facet 1 个总览**:company-base / 风险 / 知产 / 经营信用 / 历史 / 董监高 各 1 个 L1 工具,单次调用聚合该 facet 多子维度。",
14
14
  trigger: '"这家公司整体怎么样 / 风险如何 / 知产实力 / 经营状况 / 历史变更 / 这个人是谁"',
15
- contract: "Agent 按 facet 选 1 个 L1 调用即得 _summary + drill_down 线索;不需要你预先在 facet 内挑子维度。",
15
+ contract: "Agent 按 facet 选 1 个 L1 调用即得 _summary;下一跳可用 tyc L2 list 或 MCP get_company_capabilities 确认工具。",
16
16
  },
17
17
  {
18
18
  layer: "L2",
@@ -63,7 +63,7 @@ function registerLayers(program) {
63
63
  recommended_call_order: [
64
64
  `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`,
65
65
  `1. Ascend to L1 — pick ONE of the ${getLayerCount("L1")} overview tools with the anchored USCC`,
66
- "2. Read _summary + drill_down hints from the response",
66
+ "2. Read _summary and pick the next dimension via tyc layer lists or MCP get_company_capabilities",
67
67
  `3. Drill to L2 for a specific dimension (items + totals; ${getLayerCount("L2")} tools); when unsure which L2, default to the facet's L2 优先 (★, see l2_priority)`,
68
68
  `4. Reach L3 for id-based detail, search_*, or vertical SKILL tools (${getLayerCount("L3")} tools)`,
69
69
  ],
@@ -130,7 +130,7 @@ function renderLayersText() {
130
130
  lines.push("Rationale: LLM tool-selection accuracy collapses beyond 30 tools.");
131
131
  lines.push(` tyc carves L0 into a single entity-resolution tool, so the default LLM`);
132
132
  lines.push(` surface is L0 + L1 = ${defaultSurface} tools (≤ 15-tool exposure limit);`);
133
- lines.push(" L2/L3 are discovered on demand via _summary / drill_down_tools.");
133
+ lines.push(" L2/L3 are discovered on demand via _summary, layer lists, or get_company_capabilities.");
134
134
  lines.push("");
135
135
  for (const s of LAYER_SPECS) {
136
136
  const n = getLayerCount(s.layer);
@@ -153,7 +153,7 @@ function renderLayersText() {
153
153
  lines.push("Recommended call order for agents:");
154
154
  lines.push(` 0. Anchor at L0 — feed the user's raw company name into the ${getLayerCount("L0")} L0 tool (search_companies)`);
155
155
  lines.push(` 1. Ascend to L1 — one of the ${getLayerCount("L1")} overview tools, with the anchored USCC`);
156
- lines.push(" 2. Read _summary + drill_down hints in the response");
156
+ lines.push(" 2. Read _summary, then choose the next dimension via layer lists or get_company_capabilities");
157
157
  lines.push(` 3. Drill to L2 for a specific dimension (${getLayerCount("L2")} tools); unsure which L2 → default to that facet's L2★ 优先 (see L2 block above)`);
158
158
  lines.push(` 4. Reach L3 for id-based detail / search_* / vertical SKILL (${getLayerCount("L3")} tools)`);
159
159
  return lines.join("\n");
@@ -188,7 +188,7 @@ function renderLayersMarkdown() {
188
188
  lines.push("");
189
189
  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.`);
190
190
  lines.push(`1. Ascend to L1 — pick ONE of the ${getLayerCount("L1")} overview tools with the anchored USCC.`);
191
- lines.push("2. Read `_summary` + `drill_down` hints in the response.");
191
+ lines.push("2. Read `_summary`, then choose the next dimension via layer lists or MCP `get_company_capabilities`.");
192
192
  lines.push(`3. Drill to L2 for a specific dimension (items + totals; ${getLayerCount("L2")} tools). Unsure which L2 → default to that facet's L2★ 优先 (table above).`);
193
193
  lines.push(`4. Reach L3 for id-based detail, \`search_*\`, or vertical SKILL tools (${getLayerCount("L3")} tools).`);
194
194
  lines.push("");
@@ -0,0 +1,210 @@
1
+ import { DEFAULT_MCP_URL, defaultCoreURL, loadConfig, resolveConfig, saveConfig, } from "../config.js";
2
+ import { verifyCoreAuthEndpoint } from "../coreClient.js";
3
+ import { clearSession } from "../session.js";
4
+ import { ensureSession } from "../mcpClient.js";
5
+ import { buildAuthorizationURL, createPKCE, discoverAuthorizationServer, discoverProtectedResourceMetadata, exchangeAuthorizationCode, knownOAuthDefaultsForMcpURL, openBrowser, randomState, registerDynamicOAuthClient, startCallbackServer, } from "../oauth.js";
6
+ import { printLoginSuccessBanner } from "../logo.js";
7
+ function parsePort(raw) {
8
+ if (!raw)
9
+ return 0;
10
+ const n = Number(raw);
11
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
12
+ throw new Error(`invalid --redirect-port: ${raw}`);
13
+ }
14
+ return n;
15
+ }
16
+ function normalizeTokenType(raw) {
17
+ if (!raw)
18
+ return "Bearer";
19
+ return raw.toLowerCase() === "bearer" ? "Bearer" : raw;
20
+ }
21
+ export function registerLoginCommand(program) {
22
+ program
23
+ .command("login")
24
+ .description("通过 OAuth 浏览器登录并自动写入 ~/.tyc/config.json")
25
+ .option("--url <url>", `MCP endpoint(默认 ${DEFAULT_MCP_URL},或沿用当前配置)`)
26
+ .option("--issuer <url>", "OAuth authorization server issuer(默认从 MCP metadata 发现)")
27
+ .option("--client-id <id>", "OAuth client_id(默认通过 DCR 自动注册)")
28
+ .option("--client-secret <secret>", "OAuth client_secret(默认 TYC_OAUTH_CLIENT_SECRET;公共 CLI 通常不需要)")
29
+ .option("--scope <scope>", "OAuth scope(默认 mcp:tools.call)")
30
+ .option("--resource <url>", "OAuth resource/audience(默认从 MCP metadata 发现)")
31
+ .option("--resource-metadata-url <url>", "手动指定 MCP protected resource metadata URL")
32
+ .option("--authorization-endpoint <url>", "手动指定 OAuth authorization endpoint")
33
+ .option("--token-endpoint <url>", "手动指定 OAuth token endpoint")
34
+ .option("--redirect-host <host>", "本地回调 host(默认 localhost)")
35
+ .option("--redirect-port <port>", "本地回调端口(默认 7078)")
36
+ .option("--callback-path <path>", "本地回调路径(默认 /oauth/callback)")
37
+ .option("--no-open", "不自动打开浏览器,只打印授权 URL")
38
+ .option("--no-verify", "保存 token 后跳过 initialize 连通性校验")
39
+ .action(async (opts) => {
40
+ const current = loadConfig() || {};
41
+ const mcpURL = opts.url || process.env.TYC_MCP_ENDPOINT || current.url || DEFAULT_MCP_URL;
42
+ const knownDefaults = knownOAuthDefaultsForMcpURL(mcpURL);
43
+ let clientId = opts.clientId || process.env.TYC_OAUTH_CLIENT_ID || "";
44
+ let clientSecret = opts.clientSecret || process.env.TYC_OAUTH_CLIENT_SECRET;
45
+ const scope = opts.scope || "mcp:tools.call";
46
+ const redirectHost = opts.redirectHost || "localhost";
47
+ const redirectPort = parsePort(opts.redirectPort || "7078");
48
+ const callbackPath = opts.callbackPath || "/oauth/callback";
49
+ const verbose = !!program.opts().verbose;
50
+ let issuer = opts.issuer || "";
51
+ let resource = opts.resource || "";
52
+ if (!opts.issuer || !opts.resource) {
53
+ if (verbose) {
54
+ console.error(`> discovering MCP OAuth metadata: ${mcpURL}`);
55
+ }
56
+ try {
57
+ const protectedResource = await discoverProtectedResourceMetadata(mcpURL, opts.resourceMetadataUrl);
58
+ if (verbose) {
59
+ console.error(`> protected resource metadata: ${protectedResource.metadataURL}`);
60
+ }
61
+ const metadataIssuer = protectedResource.metadata.authorization_servers?.[0];
62
+ if (!issuer && metadataIssuer) {
63
+ issuer = metadataIssuer;
64
+ }
65
+ resource ||= protectedResource.metadata.resource || mcpURL;
66
+ }
67
+ catch (err) {
68
+ issuer ||= knownDefaults.issuer || "";
69
+ resource ||= knownDefaults.resource || "";
70
+ if (!issuer || !resource) {
71
+ throw err;
72
+ }
73
+ if (verbose) {
74
+ const msg = err instanceof Error ? err.message : String(err);
75
+ console.error(`> metadata discovery failed, using known defaults: ${msg}`);
76
+ }
77
+ }
78
+ }
79
+ else if (verbose) {
80
+ console.error("> using explicit --issuer and --resource; skip protected resource metadata discovery");
81
+ }
82
+ if (!issuer) {
83
+ throw new Error("MCP metadata 未提供 authorization_servers,请用 --issuer 指定。");
84
+ }
85
+ // 双端点都手动指定时跳过自动发现;否则 metadata 发现失败会直接报错
86
+ // (discovery 不再猜测端点,见 discoverAuthorizationServer)。
87
+ let endpoints;
88
+ if (opts.authorizationEndpoint && opts.tokenEndpoint) {
89
+ endpoints = {
90
+ authorizationEndpoint: opts.authorizationEndpoint,
91
+ tokenEndpoint: opts.tokenEndpoint,
92
+ issuer,
93
+ };
94
+ }
95
+ else {
96
+ endpoints = await discoverAuthorizationServer(issuer);
97
+ endpoints.authorizationEndpoint =
98
+ opts.authorizationEndpoint || endpoints.authorizationEndpoint;
99
+ endpoints.tokenEndpoint = opts.tokenEndpoint || endpoints.tokenEndpoint;
100
+ }
101
+ resource ||= endpoints.resourceDocumentation || mcpURL;
102
+ if (verbose) {
103
+ console.error(`> authorization endpoint: ${endpoints.authorizationEndpoint}`);
104
+ console.error(`> token endpoint: ${endpoints.tokenEndpoint}`);
105
+ if (endpoints.registrationEndpoint) {
106
+ console.error(`> registration endpoint: ${endpoints.registrationEndpoint}`);
107
+ }
108
+ }
109
+ const state = randomState();
110
+ const pkce = createPKCE();
111
+ const callback = await startCallbackServer({
112
+ host: redirectHost,
113
+ port: redirectPort,
114
+ path: callbackPath.startsWith("/") ? callbackPath : `/${callbackPath}`,
115
+ state,
116
+ timeoutMs: 5 * 60 * 1000,
117
+ });
118
+ if (!clientId) {
119
+ if (!endpoints.registrationEndpoint) {
120
+ callback.close();
121
+ throw new Error("OAuth metadata 未提供 registration_endpoint,请用 --client-id 指定预注册客户端。");
122
+ }
123
+ if (verbose) {
124
+ console.error("> registering OAuth client via Dynamic Client Registration");
125
+ }
126
+ let registeredClient;
127
+ try {
128
+ registeredClient = await registerDynamicOAuthClient({
129
+ registrationEndpoint: endpoints.registrationEndpoint,
130
+ clientName: "tyc-cli",
131
+ redirectUris: [callback.redirectUri],
132
+ tokenEndpointAuthMethod: "none",
133
+ });
134
+ }
135
+ catch (err) {
136
+ callback.close();
137
+ throw err;
138
+ }
139
+ clientId = registeredClient.clientId;
140
+ clientSecret ||= registeredClient.clientSecret;
141
+ if (verbose) {
142
+ console.error(`> registered client_id: ${clientId}`);
143
+ }
144
+ }
145
+ const authorizationURL = buildAuthorizationURL({
146
+ authorizationEndpoint: endpoints.authorizationEndpoint,
147
+ clientId,
148
+ redirectUri: callback.redirectUri,
149
+ scope,
150
+ state,
151
+ codeChallenge: pkce.codeChallenge,
152
+ resource,
153
+ });
154
+ if (verbose) {
155
+ console.error(`> OAuth callback: ${callback.redirectUri}`);
156
+ }
157
+ if (opts.open === false) {
158
+ console.error("请在浏览器打开以下 URL 完成登录:");
159
+ console.error(authorizationURL);
160
+ }
161
+ else {
162
+ console.error("正在打开浏览器完成 OAuth 登录...");
163
+ openBrowser(authorizationURL);
164
+ }
165
+ const code = await callback.waitForCode();
166
+ if (verbose) {
167
+ console.error("> authorization code received; exchanging access token");
168
+ }
169
+ const token = await exchangeAuthorizationCode({
170
+ tokenEndpoint: endpoints.tokenEndpoint,
171
+ clientId,
172
+ clientSecret,
173
+ code,
174
+ redirectUri: callback.redirectUri,
175
+ codeVerifier: pkce.codeVerifier,
176
+ resource,
177
+ });
178
+ const cfg = loadConfig() || {};
179
+ cfg.url = mcpURL;
180
+ if (!cfg.transport)
181
+ cfg.transport = "core";
182
+ if (!cfg.coreUrl)
183
+ cfg.coreUrl = defaultCoreURL(mcpURL);
184
+ cfg.headers = { ...(cfg.headers || {}) };
185
+ cfg.headers.Authorization = `${normalizeTokenType(token.token_type)} ${token.access_token}`;
186
+ saveConfig(cfg);
187
+ clearSession();
188
+ if (verbose) {
189
+ console.error(`> OAuth login state saved to ~/.tyc/config.json (url=${cfg.url})`);
190
+ }
191
+ if (opts.verify === false) {
192
+ printLoginSuccessBanner();
193
+ return;
194
+ }
195
+ const resolved = resolveConfig();
196
+ if (resolved.transport === "mcp") {
197
+ const sess = await ensureSession(resolved, { verbose });
198
+ if (verbose) {
199
+ console.error(`> MCP session initialized (sessionId=${sess.sessionId.slice(0, 16)}...)`);
200
+ }
201
+ }
202
+ else {
203
+ await verifyCoreAuthEndpoint(resolved, verbose);
204
+ if (verbose) {
205
+ console.error("> Shared Core auth check passed");
206
+ }
207
+ }
208
+ printLoginSuccessBanner();
209
+ });
210
+ }
package/dist/config.js CHANGED
@@ -1,17 +1,20 @@
1
1
  // ~/.tyc/config.json 读写
2
2
  //
3
3
  // 配置文件格式与通用 MCP 客户端保持一致:
4
- // { "url": "https://mcp.tianyancha.com/v1",
5
- // "headers": { "Authorization": "xxxx" } }
4
+ // { "url": "https://mcp.tianyancha.com/mcp",
5
+ // "headers": { "Authorization": "xxxx" },
6
+ // "transport": "core",
7
+ // "coreUrl": "https://mcp.tianyancha.com/v1/core/tools/call" }
6
8
  //
7
- // 默认端点指向 tyc 官方生产 MCP;本地调试通过
8
- // tyc init --url http://localhost:8080/v1 或 TYC_MCP_ENDPOINT 环境变量覆盖。
9
+ // 默认端点指向天眼查公网 MCP Server 的 canonical 入口 /mcp(/v1 是兼容别名,
10
+ // coreUrl 的 REST 端点仍只挂在 /v1/core 下);连接本地或私有环境可通过
11
+ // tyc init --url http://localhost:8080/mcp 或 TYC_MCP_ENDPOINT 环境变量覆盖。
9
12
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
10
13
  import { join } from "node:path";
11
14
  import { homedir } from "node:os";
12
15
  const CONFIG_DIR = join(homedir(), ".tyc");
13
16
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
14
- export const DEFAULT_MCP_URL = "https://mcp.tianyancha.com/v1";
17
+ export const DEFAULT_MCP_URL = "https://mcp.tianyancha.com/mcp";
15
18
  export function loadConfig() {
16
19
  if (!existsSync(CONFIG_FILE))
17
20
  return null;
@@ -32,9 +35,30 @@ export function saveConfig(config) {
32
35
  }
33
36
  catch { /* best effort */ }
34
37
  }
38
+ function normalizeTransport(raw) {
39
+ const v = (raw || "").trim().toLowerCase();
40
+ if (v === "mcp")
41
+ return "mcp";
42
+ return "core";
43
+ }
44
+ export function defaultCoreURL(mcpURL) {
45
+ const u = new URL(mcpURL);
46
+ const path = u.pathname.replace(/\/+$/, "");
47
+ if (path === "" || path === "/v1" || path === "/mcp") {
48
+ u.pathname = "/v1/core/tools/call";
49
+ }
50
+ else if (!path.endsWith("/core/tools/call")) {
51
+ u.pathname = `${path}/core/tools/call`;
52
+ }
53
+ u.search = "";
54
+ u.hash = "";
55
+ return u.toString();
56
+ }
35
57
  export function resolveConfig() {
36
58
  const cfg = loadConfig() || {};
37
59
  const url = process.env.TYC_MCP_ENDPOINT || cfg.url || DEFAULT_MCP_URL;
60
+ const transport = normalizeTransport(process.env.TYC_TRANSPORT || cfg.transport);
61
+ const coreUrl = process.env.TYC_CORE_ENDPOINT || cfg.coreUrl || defaultCoreURL(url);
38
62
  const headers = { ...(cfg.headers || {}) };
39
63
  if (!headers.Authorization && process.env.TYC_AUTHORIZATION) {
40
64
  headers.Authorization = process.env.TYC_AUTHORIZATION;
@@ -43,5 +67,5 @@ export function resolveConfig() {
43
67
  console.error("未配置 Authorization。请先运行:tyc init --authorization YOUR_API_KEY");
44
68
  process.exit(1);
45
69
  }
46
- return { url, headers };
70
+ return { url, coreUrl, transport, headers };
47
71
  }
@@ -0,0 +1,121 @@
1
+ function maskToken(v) {
2
+ if (!v)
3
+ return "";
4
+ if (v.length <= 8)
5
+ return "****";
6
+ return v.slice(0, 4) + "****" + v.slice(-4);
7
+ }
8
+ function coreSiblingURL(coreUrl, siblingPath) {
9
+ const u = new URL(coreUrl);
10
+ const path = u.pathname.replace(/\/+$/, "");
11
+ if (path.endsWith("/tools/call")) {
12
+ u.pathname = path.slice(0, -"/tools/call".length) + siblingPath;
13
+ }
14
+ else {
15
+ u.pathname = path + siblingPath;
16
+ }
17
+ u.search = "";
18
+ u.hash = "";
19
+ return u.toString();
20
+ }
21
+ function readyURL(coreUrl) {
22
+ return coreSiblingURL(coreUrl, "/ready");
23
+ }
24
+ function authReadyURL(coreUrl) {
25
+ return coreSiblingURL(coreUrl, "/auth/ready");
26
+ }
27
+ export async function verifyCoreEndpoint(cfg, verbose = false) {
28
+ const url = readyURL(cfg.coreUrl);
29
+ if (verbose)
30
+ console.error(`> GET ${url}`);
31
+ const resp = await fetch(url, {
32
+ method: "GET",
33
+ headers: { Accept: "text/plain" },
34
+ });
35
+ const text = await resp.text();
36
+ if (verbose) {
37
+ console.error(`< ${resp.status} ${resp.statusText}`);
38
+ console.error(`< body: ${text.slice(0, 500)}`);
39
+ }
40
+ if (resp.status < 200 || resp.status >= 300) {
41
+ throw new Error(`core endpoint 校验失败:HTTP ${resp.status}\n${text.slice(0, 500)}`);
42
+ }
43
+ }
44
+ export async function verifyCoreAuthEndpoint(cfg, verbose = false) {
45
+ const url = authReadyURL(cfg.coreUrl);
46
+ if (verbose)
47
+ console.error(`> GET ${url}`);
48
+ const headers = {
49
+ Accept: "application/json, text/plain",
50
+ ...cfg.headers,
51
+ };
52
+ const resp = await fetch(url, {
53
+ method: "GET",
54
+ headers,
55
+ });
56
+ const text = await resp.text();
57
+ if (verbose) {
58
+ console.error(`< ${resp.status} ${resp.statusText}`);
59
+ console.error(`< body: ${text.slice(0, 500)}`);
60
+ }
61
+ if (resp.status < 200 || resp.status >= 300) {
62
+ let desc = text.slice(0, 500);
63
+ try {
64
+ const parsed = JSON.parse(text);
65
+ desc = String(parsed.error_description || parsed.error || desc);
66
+ }
67
+ catch {
68
+ // keep raw text
69
+ }
70
+ throw new Error(`core/auth/ready HTTP ${resp.status}\n${desc}`);
71
+ }
72
+ }
73
+ export async function callCoreTool(cfg, name, args, opts) {
74
+ const verbose = !!opts?.verbose;
75
+ const headers = {
76
+ "Content-Type": "application/json",
77
+ Accept: "application/json",
78
+ ...cfg.headers,
79
+ };
80
+ const body = {
81
+ tool_name: name,
82
+ arguments: args,
83
+ format: "json",
84
+ };
85
+ if (verbose) {
86
+ const masked = {};
87
+ for (const [k, v] of Object.entries(headers)) {
88
+ masked[k] = k.toLowerCase() === "authorization" ? maskToken(v) : v;
89
+ }
90
+ console.error(`> POST ${cfg.coreUrl}`);
91
+ console.error(`> transport: core`);
92
+ console.error(`> headers: ${JSON.stringify(masked)}`);
93
+ console.error(`> body: ${JSON.stringify(body)}`);
94
+ }
95
+ const resp = await fetch(cfg.coreUrl, {
96
+ method: "POST",
97
+ headers,
98
+ body: JSON.stringify(body),
99
+ });
100
+ const text = await resp.text();
101
+ if (verbose) {
102
+ console.error(`< ${resp.status} ${resp.statusText}`);
103
+ console.error(`< body: ${text.slice(0, 2000)}`);
104
+ }
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(text);
108
+ }
109
+ catch {
110
+ throw new Error(`core 响应不是 JSON:HTTP ${resp.status}\n${text.slice(0, 500)}`);
111
+ }
112
+ if (resp.status < 200 || resp.status >= 300) {
113
+ const desc = parsed.error_description || parsed.error || text.slice(0, 500);
114
+ throw new Error(`core/tools/call HTTP ${resp.status}\n${desc}`);
115
+ }
116
+ const content = parsed.content;
117
+ const contentText = typeof content === "string" ? content : JSON.stringify(content ?? {});
118
+ return {
119
+ content: [{ type: "text", text: contentText }],
120
+ };
121
+ }
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { registerInitCommand } from "./commands/init.js";
4
+ import { registerLoginCommand } from "./commands/login.js";
4
5
  import { registerCategoryCommands } from "./commands/category.js";
5
6
  import { registerLayerCommands } from "./commands/layers.js";
6
7
  import { getCategories, getLayerCount, getPriorityTools, getTotalCount, } from "./registry.js";
@@ -16,7 +17,7 @@ ARCHITECTURE — Layered Tool Discovery for LLM Agents
16
17
  entity-resolution layer:
17
18
 
18
19
  ┌─ L0 Resolve ${pad(getLayerCount("L0"), 3)} tool entity resolution · 简称/曾用名/模糊名 → 精确企业
19
- ├─ L1 Overview ${pad(getLayerCount("L1"), 3)} tools one captain per facet · returns _summary + drill_down
20
+ ├─ L1 Overview ${pad(getLayerCount("L1"), 3)} tools one captain per facet · returns _summary for routing
20
21
  ├─ L2 Drill-down ${pad(getLayerCount("L2"), 3)} tools one-hop into a specific data dimension (★ = L2 优先, see below)
21
22
  └─ L3 Specialized ${pad(getLayerCount("L3"), 3)} tools id-based detail · search_* · vertical SKILLs
22
23
 
@@ -26,8 +27,9 @@ ARCHITECTURE — Layered Tool Discovery for LLM Agents
26
27
  指代) into \`search_companies\`; lock onto one entity
27
28
  with a USCC + official name before anything else.
28
29
  1. Ascend to L1. Pick ONE of the ${getLayerCount("L1")} overview tools with the
29
- anchored USCC. Read the response's _summary + drill_down.
30
- 2. Descend to L2. Follow hints to a specific dimension
30
+ anchored USCC. Read _summary, then use layer lists or
31
+ MCP get_company_capabilities for the next tool.
32
+ 2. Descend to L2. Pick a specific dimension
31
33
  (shareholders / litigation / patents / annual reports …).
32
34
  If unsure which L2, default to that facet's L2★ 优先 (below).
33
35
  3. Land on L3. When L2 returns a list item with an \`id\`, a \`search_*\`
@@ -66,7 +68,7 @@ OUTPUT FORMATS (mutually exclusive, priority: --md > --compact > --pretty / defa
66
68
  --pretty same as default (kept for backward compatibility / explicit intent)
67
69
  --compact compact single-line JSON (pipe / jq friendly; old default behavior)
68
70
  --md Markdown tables (human + agent on-screen)
69
- --verbose also print MCP request details to stderr (orthogonal to above)
71
+ --verbose also print core/MCP request details to stderr (orthogonal to above)
70
72
 
71
73
  OUTPUT TRUNCATION & DUMP (orthogonal — apply to any sub-command)
72
74
 
@@ -86,8 +88,11 @@ OUTPUT TRUNCATION & DUMP (orthogonal — apply to any sub-command)
86
88
 
87
89
  SETUP
88
90
 
89
- tyc init --authorization <KEY> default https://mcp.tianyancha.com/v1
90
- tyc init --url <MCP_URL> --authorization <KEY> local / self-hosted
91
+ tyc login OAuth browser login
92
+ tyc login --url <MCP_URL> OAuth for local / pre / self-hosted
93
+ tyc init --authorization <KEY> API-key compatibility path (default transport: core)
94
+ tyc init --transport mcp --authorization <KEY> legacy MCP session transport
95
+ tyc init --url <MCP_URL> --authorization <KEY> local / self-hosted service
91
96
 
92
97
  Every tool returns tyc OpenAPI native English keys verbatim. The MCP Server
93
98
  handles multi-source merge, Asia/Shanghai timestamp formatting, _summary
@@ -119,7 +124,7 @@ const program = new Command()
119
124
  .option("--pretty", "缩进 JSON 输出(默认行为,flag 保留以保持向后兼容)")
120
125
  .option("--md", "Markdown 表格化输出(适合人类阅读 / Agent 上屏)")
121
126
  .option("--compact", "紧凑单行 JSON(管道 / jq 场景;为旧默认行为)")
122
- .option("--verbose", "打印 MCP 请求详情到 stderr")
127
+ .option("--verbose", "打印 core/MCP 请求详情到 stderr")
123
128
  .option("--head [n]", "仅输出前 N 行(不传值默认 50;与 --tail 同时给则同时输出两端)")
124
129
  .option("--tail [m]", "仅输出后 M 行(不传值默认 20;与 --head 同时给则同时输出两端)")
125
130
  .option("--full", "强制输出完整内容(最高优先级,覆盖 --head/--tail/--threshold)")
@@ -127,6 +132,7 @@ const program = new Command()
127
132
  .option("--threshold <bytes>", "字节截断主开关:超过该字节数则从头按字节截断;不传则永不截断")
128
133
  .addHelpText("after", helpText);
129
134
  registerInitCommand(program);
135
+ registerLoginCommand(program);
130
136
  registerLayerCommands(program);
131
137
  registerCategoryCommands(program);
132
138
  program.parseAsync(process.argv).catch((err) => {
package/dist/logo.js ADDED
@@ -0,0 +1,24 @@
1
+ export const TY_LOGO = `\x1b[38;2;22;135;245m ▄▄▄████████▄▄▄
2
+ ▄▄█████████▀▀▀▀▀▀▀▀▀
3
+ ▄████████▀▀▄▄▄▄██████▄▄▄▄
4
+ ▄████████████████████████████▄
5
+ ▄███████████████████▀▀▀▀▀▀▀████▄
6
+ ▄██████████████▀▀ ███
7
+ ████████████▀▀ ▄▄▀ █▀
8
+ ███████████▀ ▄▄██▀
9
+ █████████▀ ▄█████ ▄
10
+ ███████▀ ▄██████▀ ▄█
11
+ ██████▀ ▄███████ █ ▄███
12
+ ████ █████████ █▄ ▄▄▄▄▄▄▄▄▄█████
13
+ ▀██ ██████████ ██▄ ▀▀██████████▀
14
+ ▀ ███████████▄ ████▄ ▀▀▀▀▀▀▀▀
15
+ █████████████ ██████▄▄▄
16
+ ▀███████████▄ ▀██████████▀
17
+ ▀▀█████████▄ ▀██████▀▀
18
+ ▀▀▀██████ ▀▀▀\x1b[0m`;
19
+ export const LOGIN_SUCCESS_BANNER = `登录成功
20
+ ${TY_LOGO}
21
+ 公平看清世界`;
22
+ export function printLoginSuccessBanner() {
23
+ console.log(LOGIN_SUCCESS_BANNER);
24
+ }