tyc-cli 0.3.4 → 0.3.6

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.
@@ -0,0 +1,32 @@
1
+ import { callCoreToolWithOAuthRefresh } from "./coreCall.js";
2
+ import { emitToolResult } from "./output.js";
3
+ const TOOL_NAME = "get_company_capabilities";
4
+ export function registerCompanyCapabilitiesCommand(companyCmd, program) {
5
+ companyCmd
6
+ .command("capabilities")
7
+ .description("[DISCOVER] 输入 search_companies 候选表 company_id,返回该企业可直接执行的 tyc CLI 工具清单")
8
+ .argument("<company_id>", "search_companies 候选表 items[*].id 中的数字企业 ID")
9
+ .option("--company-name <value>", "企业名称(可选,仅用于展示和后续主体参数示例)")
10
+ .action(async (companyID, options) => {
11
+ const args = {
12
+ company_id: companyID,
13
+ };
14
+ if (options.companyName !== undefined) {
15
+ args.company_name = options.companyName;
16
+ }
17
+ const opts = program.opts();
18
+ const verbose = !!opts.verbose;
19
+ try {
20
+ const result = await callCoreToolWithOAuthRefresh(TOOL_NAME, args, {
21
+ verbose,
22
+ format: "markdown",
23
+ });
24
+ emitToolResult(result, opts, TOOL_NAME);
25
+ }
26
+ catch (err) {
27
+ const msg = err instanceof Error ? err.message : String(err);
28
+ console.error(`请求失败: ${msg}`);
29
+ process.exit(1);
30
+ }
31
+ });
32
+ }
@@ -1,8 +1,10 @@
1
1
  import { getCategories, getToolsByGroup } from "../registry.js";
2
- import { resolveConfig } from "../config.js";
3
- import { callTool } from "../mcpClient.js";
4
- import { jsonToMarkdown } from "../utils/jsonToMarkdown.js";
5
- import { applyTruncation, resolveOptionalInt, TRUNCATE_DEFAULTS, } from "../utils/truncate.js";
2
+ import { registerCompanyCapabilitiesCommand } from "./capabilities.js";
3
+ import { callCoreToolWithOAuthRefresh } from "./coreCall.js";
4
+ import { emitToolResult } from "./output.js";
5
+ const GLOBAL_OUTPUT_HELP = `
6
+ 全局输出选项:所有工具命令均支持 --md / --compact / --pretty / --head / --tail / --full / --threshold / --output-file / --verbose;完整说明见 tyc --help。
7
+ `;
6
8
  export function registerCategoryCommands(program) {
7
9
  for (const cat of getCategories()) {
8
10
  const tools = getToolsByGroup(cat.group);
@@ -10,6 +12,9 @@ export function registerCategoryCommands(program) {
10
12
  const catCmd = program
11
13
  .command(cat.group)
12
14
  .description(`${cat.name_zh}(${cat.tool_count} 个 · L0=${layerCounts.L0} L1=${layerCounts.L1} L2=${layerCounts.L2} L3=${layerCounts.L3})`);
15
+ if (cat.group === "company") {
16
+ registerCompanyCapabilitiesCommand(catCmd, program);
17
+ }
13
18
  for (const tool of tools) {
14
19
  bindMethod(catCmd, tool, program);
15
20
  }
@@ -36,11 +41,12 @@ function bindMethod(catCmd, tool, program) {
36
41
  methodCmd = methodCmd.argument(`<${positional.name}>`, positional.description);
37
42
  }
38
43
  for (const p of remainingRequired) {
39
- methodCmd = methodCmd.requiredOption(`--${p.name} <value>`, p.description);
44
+ methodCmd = methodCmd.requiredOption(`--${p.name} <value>`, `必填;${p.description}`);
40
45
  }
41
46
  for (const p of optionalParams) {
42
47
  methodCmd = methodCmd.option(`--${p.name} <value>`, p.description);
43
48
  }
49
+ methodCmd.addHelpText("after", GLOBAL_OUTPUT_HELP);
44
50
  methodCmd.action(async (posVal, options) => {
45
51
  const args = {};
46
52
  if (positional && posVal)
@@ -49,12 +55,11 @@ function bindMethod(catCmd, tool, program) {
49
55
  if (options[p.name] !== undefined)
50
56
  args[p.name] = options[p.name];
51
57
  }
52
- const cfg = resolveConfig();
53
58
  const opts = program.opts();
54
59
  const verbose = !!opts.verbose;
55
60
  try {
56
- const result = await callTool(cfg, tool.name, args, { verbose });
57
- emit(result, opts, tool.name);
61
+ const result = await callToolWithOAuthRefresh(tool.name, args, verbose);
62
+ emitToolResult(result, opts, tool.name);
58
63
  }
59
64
  catch (err) {
60
65
  const msg = err instanceof Error ? err.message : String(err);
@@ -63,68 +68,6 @@ function bindMethod(catCmd, tool, program) {
63
68
  }
64
69
  });
65
70
  }
66
- function emit(result, opts, toolName) {
67
- // MCP 业务错误:isError + content[0].text = 错误描述
68
- if (result.isError) {
69
- const errText = extractText(result) || "未知业务错误";
70
- console.error(errText);
71
- process.exit(1);
72
- }
73
- const text = extractText(result);
74
- if (text === null) {
75
- console.error("tools/call 返回无可解析内容");
76
- process.exit(1);
77
- }
78
- // MCP Server 返回的是 JSON 字符串(已完成多源合并 / 时间戳格式化 /
79
- // _summary / _empty / _warnings 注入)。CLI 只做呈现格式化。
80
- let payload;
81
- try {
82
- payload = JSON.parse(text);
83
- }
84
- catch {
85
- // 非 JSON 文本:直接原样输出(仍走截断 / 落盘)
86
- emitWithTruncation(text.endsWith("\n") ? text.slice(0, -1) : text, opts);
87
- return;
88
- }
89
- // 输出模式选择(互斥优先级 --md > --compact > --pretty / 默认 = pretty)
90
- let rendered;
91
- if (opts.md) {
92
- rendered = jsonToMarkdown(payload, toolName);
93
- }
94
- else if (opts.compact) {
95
- rendered = JSON.stringify(payload);
96
- }
97
- else {
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");
121
- }
122
- }
123
- function extractText(result) {
124
- const content = result.content || [];
125
- for (const c of content) {
126
- if (typeof c.text === "string" && c.text.length > 0)
127
- return c.text;
128
- }
129
- return null;
71
+ async function callToolWithOAuthRefresh(name, args, verbose) {
72
+ return callCoreToolWithOAuthRefresh(name, args, { verbose });
130
73
  }
@@ -0,0 +1,23 @@
1
+ import { refreshResolvedConfigOAuth, resolveConfigWithFreshOAuth, } from "../config.js";
2
+ import { callCoreTool } from "../coreClient.js";
3
+ function isUnauthorizedError(err) {
4
+ const msg = err instanceof Error ? err.message : String(err);
5
+ return /\bHTTP 401\b/i.test(msg) || /invalid_token|expired_token|token expired/i.test(msg);
6
+ }
7
+ export async function callCoreToolWithOAuthRefresh(name, args, opts) {
8
+ let refreshResult = await resolveConfigWithFreshOAuth({ verbose: !!opts?.verbose });
9
+ try {
10
+ return await callCoreTool(refreshResult.config, name, args, opts);
11
+ }
12
+ catch (err) {
13
+ if (!isUnauthorizedError(err))
14
+ throw err;
15
+ refreshResult = await refreshResolvedConfigOAuth(refreshResult.config, {
16
+ force: true,
17
+ verbose: !!opts?.verbose,
18
+ });
19
+ if (!refreshResult.refreshed)
20
+ throw err;
21
+ return callCoreTool(refreshResult.config, name, args, opts);
22
+ }
23
+ }
@@ -1,25 +1,36 @@
1
- import { DEFAULT_MCP_URL, loadConfig, resolveConfig, saveConfig } from "../config.js";
2
- import { clearSession } from "../session.js";
3
- import { ensureSession } from "../mcpClient.js";
1
+ import { DEFAULT_MCP_URL, defaultCoreURL, loadConfig, resolveConfig, saveConfig, } from "../config.js";
2
+ import { verifyCoreEndpoint } from "../coreClient.js";
4
3
  export function registerInitCommand(program) {
5
4
  program
6
5
  .command("init")
7
- .description("配置 MCP endpoint / Authorization(保存到 ~/.tyc/config.json,并校验连通性)")
6
+ .description("配置 endpoint / Authorization(保存到 ~/.tyc/config.json,并校验 shared core 连通性)")
8
7
  .option("--authorization <token>", "tyc OpenAPI Authorization(写入 headers.Authorization)")
9
8
  .option("--url <url>", `MCP endpoint(默认 ${DEFAULT_MCP_URL})`)
9
+ .option("--core-url <url>", "Shared Core HTTP endpoint(默认从 --url 推导 /v1/core/tools/call)")
10
10
  .option("--header <kv...>", "自定义 header,格式 K=V,可重复;留空清除该 key")
11
- .option("--clear-session", "仅清除本地 MCP session 缓存(~/.tyc/session.json)")
12
- .option("--no-verify", "跳过连通性校验(不发 initialize)")
11
+ .option("--no-verify", "跳过 shared core 连通性校验")
13
12
  .action(async (opts) => {
14
13
  const cfg = loadConfig() || {};
15
14
  if (!cfg.headers)
16
15
  cfg.headers = {};
17
- if (opts.url)
16
+ let resetOAuth = false;
17
+ if (opts.url) {
18
18
  cfg.url = opts.url;
19
+ resetOAuth = true;
20
+ }
19
21
  if (!cfg.url)
20
22
  cfg.url = DEFAULT_MCP_URL;
21
- if (opts.authorization)
23
+ delete cfg.transport;
24
+ if (opts.coreUrl) {
25
+ cfg.coreUrl = opts.coreUrl;
26
+ }
27
+ else if (opts.url || !cfg.coreUrl) {
28
+ cfg.coreUrl = defaultCoreURL(cfg.url);
29
+ }
30
+ if (opts.authorization) {
22
31
  cfg.headers.Authorization = opts.authorization;
32
+ resetOAuth = true;
33
+ }
23
34
  if (opts.header && opts.header.length > 0) {
24
35
  for (const kv of opts.header) {
25
36
  const idx = kv.indexOf("=");
@@ -31,32 +42,30 @@ export function registerInitCommand(program) {
31
42
  const v = kv.slice(idx + 1);
32
43
  if (!k)
33
44
  continue;
45
+ if (k.toLowerCase() === "authorization")
46
+ resetOAuth = true;
34
47
  if (v === "")
35
48
  delete cfg.headers[k];
36
49
  else
37
50
  cfg.headers[k] = v;
38
51
  }
39
52
  }
53
+ if (resetOAuth)
54
+ delete cfg.oauth;
40
55
  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})`);
56
+ console.log(`已保存 ~/.tyc/config.json (url=${cfg.url}, coreUrl=${cfg.coreUrl})`);
47
57
  if (opts.verify === false) {
48
58
  return;
49
59
  }
50
- // 立即发 initialize,拿到 Mcp-Session-Id 落盘;失败则非 0 退出
51
60
  try {
52
61
  const resolved = resolveConfig();
53
62
  const verbose = !!program.opts().verbose;
54
- const sess = await ensureSession(resolved, { verbose });
55
- console.log(`已建立 MCP session(sessionId=${sess.sessionId.slice(0, 16)}…)`);
63
+ await verifyCoreEndpoint(resolved, verbose);
64
+ console.log("Shared Core endpoint 校验通过");
56
65
  }
57
66
  catch (err) {
58
67
  const msg = err instanceof Error ? err.message : String(err);
59
- console.error(`MCP 连通性校验失败:${msg}`);
68
+ console.error(`连通性校验失败:${msg}`);
60
69
  console.error("配置已保存;确认 --url 可达后重新运行 tyc init,或加 --no-verify 跳过校验。");
61
70
  process.exit(1);
62
71
  }
@@ -82,7 +82,7 @@ function registerLayerCmd(program, spec) {
82
82
  const layer = spec.layer;
83
83
  const cmd = program
84
84
  .command(layer)
85
- .description(`${spec.title}(${getLayerCount(layer)} 个)— ${spec.summary}`);
85
+ .description(layerCommandDescription(spec));
86
86
  cmd
87
87
  .command("list")
88
88
  .description(`列出 ${layer} 全部 ${getLayerCount(layer)} 个工具(默认按分类分组文本表格)`)
@@ -120,6 +120,18 @@ function registerLayerCmd(program, spec) {
120
120
  console.log(renderLayerListText(spec, tools));
121
121
  });
122
122
  }
123
+ function layerCommandDescription(spec) {
124
+ switch (spec.layer) {
125
+ case "L0":
126
+ return `实体锚定层(${getLayerCount("L0")} 个)— 把简称、曾用名或模糊指代解析成精确企业。`;
127
+ case "L1":
128
+ return `概要总览层(${getLayerCount("L1")} 个)— 每个业务面 1 个总览工具,用 _summary 指引下一跳。`;
129
+ case "L2":
130
+ return `维度下钻层(${getLayerCount("L2")} 个)— 股权、诉讼、招投标、年报、知识产权等明细维度。`;
131
+ case "L3":
132
+ return `专项工具层(${getLayerCount("L3")} 个)— id 详情、search_*、垂直场景和专业查询。`;
133
+ }
134
+ }
123
135
  // ─────────────────────────── 渲染:tyc layers ───────────────────────────
124
136
  function renderLayersText() {
125
137
  const total = getTotalCount();
@@ -0,0 +1,290 @@
1
+ import { DEFAULT_MCP_URL, applyOAuthTokenToConfig, defaultCoreURL, loadConfig, resolveConfig, saveConfig, } from "../config.js";
2
+ import { verifyCoreAuthEndpoint } from "../coreClient.js";
3
+ import { buildAuthorizationURL, createPKCE, discoverAuthorizationServer, discoverProtectedResourceMetadata, exchangeAuthorizationCode, knownOAuthDefaultsForMcpURL, openBrowser, parseAuthorizationCallbackCode, randomState, registerDynamicOAuthClient, startCallbackServer, } from "../oauth.js";
4
+ import { clearPendingOAuthLogin, loadPendingOAuthLogin, savePendingOAuthLogin, } from "../oauthPending.js";
5
+ import { printLoginSuccessBanner } from "../logo.js";
6
+ function parsePort(raw) {
7
+ if (!raw)
8
+ return 0;
9
+ const n = Number(raw);
10
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
11
+ throw new Error(`invalid --redirect-port: ${raw}`);
12
+ }
13
+ return n;
14
+ }
15
+ function normalizeCallbackPath(raw) {
16
+ const path = raw || "/oauth/callback";
17
+ return path.startsWith("/") ? path : `/${path}`;
18
+ }
19
+ function buildLoopbackRedirectUri(host, port, path) {
20
+ if (port === 0) {
21
+ throw new Error("--no-block 不能与 --redirect-port 0 同时使用,请指定固定回调端口。");
22
+ }
23
+ return `http://${host}:${port}${path}`;
24
+ }
25
+ function pickCallbackToken(opts) {
26
+ if (opts.callbackToken && opts.token) {
27
+ throw new Error("--callback-token 和 --token 只能指定一个。");
28
+ }
29
+ return opts.callbackToken || opts.token;
30
+ }
31
+ function saveOAuthTokenConfig(mcpURL, token, oauth, verbose) {
32
+ const cfg = loadConfig() || {};
33
+ cfg.url = mcpURL;
34
+ delete cfg.transport;
35
+ cfg.coreUrl = defaultCoreURL(mcpURL);
36
+ applyOAuthTokenToConfig(cfg, token, oauth);
37
+ saveConfig(cfg);
38
+ if (verbose) {
39
+ console.error(`> OAuth login state saved to ~/.tyc/config.json (url=${cfg.url})`);
40
+ }
41
+ }
42
+ async function verifySavedOAuthLogin(verify, verbose) {
43
+ if (!verify) {
44
+ printLoginSuccessBanner();
45
+ return;
46
+ }
47
+ const resolved = resolveConfig();
48
+ await verifyCoreAuthEndpoint(resolved, verbose);
49
+ if (verbose) {
50
+ console.error("> Shared Core auth check passed");
51
+ }
52
+ printLoginSuccessBanner();
53
+ }
54
+ async function completePendingOAuthLogin(pending, callbackToken, verify, verbose) {
55
+ const code = parseAuthorizationCallbackCode(callbackToken, pending.state);
56
+ if (verbose) {
57
+ console.error("> authorization code provided; exchanging access token");
58
+ }
59
+ const token = await exchangeAuthorizationCode({
60
+ tokenEndpoint: pending.tokenEndpoint,
61
+ clientId: pending.clientId,
62
+ clientSecret: pending.clientSecret,
63
+ code,
64
+ redirectUri: pending.redirectUri,
65
+ codeVerifier: pending.codeVerifier,
66
+ resource: pending.resource,
67
+ });
68
+ saveOAuthTokenConfig(pending.mcpURL, token, {
69
+ tokenEndpoint: pending.tokenEndpoint,
70
+ clientId: pending.clientId,
71
+ clientSecret: pending.clientSecret,
72
+ resource: pending.resource,
73
+ }, verbose);
74
+ clearPendingOAuthLogin(verbose);
75
+ await verifySavedOAuthLogin(verify, verbose);
76
+ }
77
+ export function registerLoginCommand(program) {
78
+ program
79
+ .command("login")
80
+ .description("通过 OAuth 浏览器登录并自动写入 ~/.tyc/config.json")
81
+ .option("--url <url>", `MCP endpoint(默认 ${DEFAULT_MCP_URL},或沿用当前配置)`)
82
+ .option("--issuer <url>", "OAuth authorization server issuer(默认从 MCP metadata 发现)")
83
+ .option("--client-id <id>", "OAuth client_id(默认通过 DCR 自动注册)")
84
+ .option("--client-secret <secret>", "OAuth client_secret(默认 TYC_OAUTH_CLIENT_SECRET;公共 CLI 通常不需要)")
85
+ .option("--scope <scope>", "OAuth scope(默认 mcp:tools.call)")
86
+ .option("--resource <url>", "OAuth resource/audience(默认从 MCP metadata 发现)")
87
+ .option("--resource-metadata-url <url>", "手动指定 MCP protected resource metadata URL")
88
+ .option("--authorization-endpoint <url>", "手动指定 OAuth authorization endpoint")
89
+ .option("--token-endpoint <url>", "手动指定 OAuth token endpoint")
90
+ .option("--redirect-host <host>", "本地回调 host(默认 localhost)")
91
+ .option("--redirect-port <port>", "本地回调端口(默认 7078)")
92
+ .option("--callback-path <path>", "本地回调路径(默认 /oauth/callback)")
93
+ .option("--callback-token <code-or-url>", "继续非阻塞登录:传入授权回调 code 或包含 code 的完整 callback URL")
94
+ .option("--token <code-or-url>", "同 --callback-token(兼容别名;仍需传授权回调 code)")
95
+ .option("--no-open", "不自动打开浏览器,只打印授权 URL")
96
+ .option("--no-block", "配合 --no-open:打印授权 URL 并保存 pending 状态后立即退出")
97
+ .option("--no-verify", "保存 token 后跳过 shared core 鉴权校验")
98
+ .action(async (opts) => {
99
+ const verbose = !!program.opts().verbose;
100
+ const callbackToken = pickCallbackToken(opts);
101
+ if (callbackToken) {
102
+ const pending = loadPendingOAuthLogin();
103
+ if (!pending) {
104
+ throw new Error("未找到待完成的 OAuth 登录状态。请先运行 tyc login --no-open --no-block。");
105
+ }
106
+ if (verbose) {
107
+ console.error(`> loaded pending OAuth login from ~/.tyc/oauth_pending.json (url=${pending.mcpURL})`);
108
+ }
109
+ await completePendingOAuthLogin(pending, callbackToken, opts.verify !== false && pending.verify !== false, verbose);
110
+ return;
111
+ }
112
+ const noBlock = opts.block === false;
113
+ if (noBlock && opts.open !== false) {
114
+ throw new Error("--no-block 需要与 --no-open 一起使用。");
115
+ }
116
+ const current = loadConfig() || {};
117
+ const mcpURL = opts.url || process.env.TYC_MCP_ENDPOINT || current.url || DEFAULT_MCP_URL;
118
+ const knownDefaults = knownOAuthDefaultsForMcpURL(mcpURL);
119
+ let clientId = opts.clientId || process.env.TYC_OAUTH_CLIENT_ID || "";
120
+ let clientSecret = opts.clientSecret || process.env.TYC_OAUTH_CLIENT_SECRET;
121
+ const scope = opts.scope || "mcp:tools.call";
122
+ const redirectHost = opts.redirectHost || "localhost";
123
+ const redirectPort = parsePort(opts.redirectPort || "7078");
124
+ const callbackPath = normalizeCallbackPath(opts.callbackPath);
125
+ let issuer = opts.issuer || "";
126
+ let resource = opts.resource || "";
127
+ if (!opts.issuer || !opts.resource) {
128
+ if (verbose) {
129
+ console.error(`> discovering MCP OAuth metadata: ${mcpURL}`);
130
+ }
131
+ try {
132
+ const protectedResource = await discoverProtectedResourceMetadata(mcpURL, opts.resourceMetadataUrl);
133
+ if (verbose) {
134
+ console.error(`> protected resource metadata: ${protectedResource.metadataURL}`);
135
+ }
136
+ const metadataIssuer = protectedResource.metadata.authorization_servers?.[0];
137
+ if (!issuer && metadataIssuer) {
138
+ issuer = metadataIssuer;
139
+ }
140
+ resource ||= protectedResource.metadata.resource || mcpURL;
141
+ }
142
+ catch (err) {
143
+ issuer ||= knownDefaults.issuer || "";
144
+ resource ||= knownDefaults.resource || "";
145
+ if (!issuer || !resource) {
146
+ throw err;
147
+ }
148
+ if (verbose) {
149
+ const msg = err instanceof Error ? err.message : String(err);
150
+ console.error(`> metadata discovery failed, using known defaults: ${msg}`);
151
+ }
152
+ }
153
+ }
154
+ else if (verbose) {
155
+ console.error("> using explicit --issuer and --resource; skip protected resource metadata discovery");
156
+ }
157
+ if (!issuer) {
158
+ throw new Error("MCP metadata 未提供 authorization_servers,请用 --issuer 指定。");
159
+ }
160
+ // 双端点都手动指定时跳过自动发现;否则 metadata 发现失败会直接报错
161
+ // (discovery 不再猜测端点,见 discoverAuthorizationServer)。
162
+ let endpoints;
163
+ if (opts.authorizationEndpoint && opts.tokenEndpoint) {
164
+ endpoints = {
165
+ authorizationEndpoint: opts.authorizationEndpoint,
166
+ tokenEndpoint: opts.tokenEndpoint,
167
+ issuer,
168
+ };
169
+ }
170
+ else {
171
+ endpoints = await discoverAuthorizationServer(issuer);
172
+ endpoints.authorizationEndpoint =
173
+ opts.authorizationEndpoint || endpoints.authorizationEndpoint;
174
+ endpoints.tokenEndpoint = opts.tokenEndpoint || endpoints.tokenEndpoint;
175
+ }
176
+ resource ||= endpoints.resourceDocumentation || mcpURL;
177
+ if (verbose) {
178
+ console.error(`> authorization endpoint: ${endpoints.authorizationEndpoint}`);
179
+ console.error(`> token endpoint: ${endpoints.tokenEndpoint}`);
180
+ if (endpoints.registrationEndpoint) {
181
+ console.error(`> registration endpoint: ${endpoints.registrationEndpoint}`);
182
+ }
183
+ }
184
+ const state = randomState();
185
+ const pkce = createPKCE();
186
+ let callback;
187
+ const redirectUri = noBlock
188
+ ? buildLoopbackRedirectUri(redirectHost, redirectPort, callbackPath)
189
+ : (callback = await startCallbackServer({
190
+ host: redirectHost,
191
+ port: redirectPort,
192
+ path: callbackPath,
193
+ state,
194
+ timeoutMs: 5 * 60 * 1000,
195
+ })).redirectUri;
196
+ if (!clientId) {
197
+ if (!endpoints.registrationEndpoint) {
198
+ callback?.close();
199
+ throw new Error("OAuth metadata 未提供 registration_endpoint,请用 --client-id 指定预注册客户端。");
200
+ }
201
+ if (verbose) {
202
+ console.error("> registering OAuth client via Dynamic Client Registration");
203
+ }
204
+ let registeredClient;
205
+ try {
206
+ registeredClient = await registerDynamicOAuthClient({
207
+ registrationEndpoint: endpoints.registrationEndpoint,
208
+ clientName: "tyc-cli",
209
+ redirectUris: [redirectUri],
210
+ tokenEndpointAuthMethod: "none",
211
+ });
212
+ }
213
+ catch (err) {
214
+ callback?.close();
215
+ throw err;
216
+ }
217
+ clientId = registeredClient.clientId;
218
+ clientSecret ||= registeredClient.clientSecret;
219
+ if (verbose) {
220
+ console.error(`> registered client_id: ${clientId}`);
221
+ }
222
+ }
223
+ const authorizationURL = buildAuthorizationURL({
224
+ authorizationEndpoint: endpoints.authorizationEndpoint,
225
+ clientId,
226
+ redirectUri,
227
+ scope,
228
+ state,
229
+ codeChallenge: pkce.codeChallenge,
230
+ resource,
231
+ });
232
+ if (verbose) {
233
+ console.error(`> OAuth callback: ${redirectUri}`);
234
+ }
235
+ if (noBlock) {
236
+ savePendingOAuthLogin({
237
+ version: 1,
238
+ createdAt: Date.now(),
239
+ mcpURL,
240
+ tokenEndpoint: endpoints.tokenEndpoint,
241
+ clientId,
242
+ clientSecret,
243
+ redirectUri,
244
+ codeVerifier: pkce.codeVerifier,
245
+ resource,
246
+ state,
247
+ verify: opts.verify !== false,
248
+ });
249
+ console.error("请在浏览器打开以下 URL 完成登录:");
250
+ console.error(authorizationURL);
251
+ console.error("");
252
+ console.error("完成授权后,复制回调 URL 中的 code 参数,然后运行:");
253
+ console.error("tyc login --callback-token <code_or_callback_url>");
254
+ console.error("也可以传完整 callback URL:tyc login --callback-token \"http://localhost:7078/oauth/callback?code=...&state=...\"");
255
+ return;
256
+ }
257
+ if (opts.open === false) {
258
+ console.error("请在浏览器打开以下 URL 完成登录:");
259
+ console.error(authorizationURL);
260
+ }
261
+ else {
262
+ console.error("正在打开浏览器完成 OAuth 登录...");
263
+ openBrowser(authorizationURL);
264
+ }
265
+ if (!callback) {
266
+ throw new Error("OAuth callback server not initialized");
267
+ }
268
+ const code = await callback.waitForCode();
269
+ if (verbose) {
270
+ console.error("> authorization code received; exchanging access token");
271
+ }
272
+ const token = await exchangeAuthorizationCode({
273
+ tokenEndpoint: endpoints.tokenEndpoint,
274
+ clientId,
275
+ clientSecret,
276
+ code,
277
+ redirectUri,
278
+ codeVerifier: pkce.codeVerifier,
279
+ resource,
280
+ });
281
+ saveOAuthTokenConfig(mcpURL, token, {
282
+ tokenEndpoint: endpoints.tokenEndpoint,
283
+ clientId,
284
+ clientSecret,
285
+ resource,
286
+ }, verbose);
287
+ clearPendingOAuthLogin(verbose);
288
+ await verifySavedOAuthLogin(opts.verify !== false, verbose);
289
+ });
290
+ }
@@ -0,0 +1,60 @@
1
+ import { jsonToMarkdown } from "../utils/jsonToMarkdown.js";
2
+ import { applyTruncation, resolveOptionalInt, TRUNCATE_DEFAULTS, } from "../utils/truncate.js";
3
+ export function emitToolResult(result, opts, toolName) {
4
+ if (result.isError) {
5
+ const errText = extractText(result) || "未知业务错误";
6
+ console.error(errText);
7
+ process.exit(1);
8
+ }
9
+ const text = extractText(result);
10
+ if (text === null) {
11
+ console.error("tools/call 返回无可解析内容");
12
+ process.exit(1);
13
+ }
14
+ let payload;
15
+ try {
16
+ payload = JSON.parse(text);
17
+ }
18
+ catch {
19
+ emitWithTruncation(text.endsWith("\n") ? text.slice(0, -1) : text, opts);
20
+ return;
21
+ }
22
+ let rendered;
23
+ if (opts.md) {
24
+ rendered = jsonToMarkdown(payload, toolName);
25
+ }
26
+ else if (opts.compact) {
27
+ rendered = JSON.stringify(payload);
28
+ }
29
+ else {
30
+ rendered = JSON.stringify(payload, null, 2);
31
+ }
32
+ emitWithTruncation(rendered, opts);
33
+ }
34
+ function emitWithTruncation(rendered, opts) {
35
+ const head = resolveOptionalInt(opts.head, TRUNCATE_DEFAULTS.HEAD);
36
+ const tail = resolveOptionalInt(opts.tail, TRUNCATE_DEFAULTS.TAIL);
37
+ const threshold = resolveOptionalInt(opts.threshold, TRUNCATE_DEFAULTS.THRESHOLD);
38
+ const outputFile = typeof opts.outputFile === "string" && opts.outputFile.length > 0
39
+ ? opts.outputFile
40
+ : undefined;
41
+ const r = applyTruncation(rendered, {
42
+ full: !!opts.full,
43
+ head,
44
+ tail,
45
+ threshold,
46
+ outputFile,
47
+ });
48
+ console.log(r.rendered);
49
+ for (const n of r.notices) {
50
+ process.stderr.write(n + "\n");
51
+ }
52
+ }
53
+ function extractText(result) {
54
+ const content = result.content || [];
55
+ for (const c of content) {
56
+ if (typeof c.text === "string" && c.text.length > 0)
57
+ return c.text;
58
+ }
59
+ return null;
60
+ }