tyc-cli 0.3.5 → 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.
package/dist/index.js CHANGED
@@ -6,98 +6,72 @@ import { registerCategoryCommands } from "./commands/category.js";
6
6
  import { registerLayerCommands } from "./commands/layers.js";
7
7
  import { getCategories, getLayerCount, getPriorityTools, getTotalCount, } from "./registry.js";
8
8
  import { VERSION } from "./version.js";
9
- // 顶层 help:把"最懂 AI 的 CLI"落在 4 层架构 + 调用顺序 + 可发现性上。
10
- // Agent 看 tyc --help 时应立刻读懂"先 L0 锚定企业,再 L1 概览,再 L2 下钻,最后 L3 详情"这条主线。
9
+ // 顶层 help 保持为快速入口:讲清分层、调用顺序、发现命令和输出控制;
10
+ // 更完整的架构说明放在 `tyc layers`。
11
11
  const helpText = `
12
- ARCHITECTURE — Layered Tool Discovery for LLM Agents
13
-
14
- Cognitive research shows LLM tool-selection accuracy collapses above
15
- 30 tools (>95% at 10, ~70% at 30, <50% at 100+). tyc counters this with
16
- a 4-layer progressive-disclosure architecture, led by a dedicated
17
- entity-resolution layer:
18
-
19
- ┌─ L0 Resolve ${pad(getLayerCount("L0"), 3)} tool entity resolution · 简称/曾用名/模糊名 → 精确企业
20
- ├─ L1 Overview ${pad(getLayerCount("L1"), 3)} tools one captain per facet · returns _summary for routing
21
- ├─ L2 Drill-down ${pad(getLayerCount("L2"), 3)} tools one-hop into a specific data dimension (★ = L2 优先, see below)
22
- └─ L3 Specialized ${pad(getLayerCount("L3"), 3)} tools id-based detail · search_* · vertical SKILLs
23
-
24
- Recommended call order:
25
-
26
- 0. Anchor at L0. Feed the user's raw company name (可能是简称/曾用名/模糊
27
- 指代) into \`search_companies\`; lock onto one entity
28
- with a USCC + official name before anything else.
29
- 1. Ascend to L1. Pick ONE of the ${getLayerCount("L1")} overview tools with the
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
33
- (shareholders / litigation / patents / annual reports …).
34
- If unsure which L2, default to that facet's L2★ 优先 (below).
35
- 3. Land on L3. When L2 returns a list item with an \`id\`, a \`search_*\`
36
- need arises, or a vertical SKILL is activated.
37
-
38
- Rule of thumb: the smaller the layer number, the higher the information
39
- density per token. L0 is MANDATORY — skipping it (without a USCC already
40
- in hand) wastes downstream calls on the wrong entity.
41
- Default LLM surface = L0 + L1 = ${getLayerCount("L0") + getLayerCount("L1")} tools (≤ 15-tool exposure limit).
42
-
43
- L2 优先 ★ — \`L2★ 队副\`(${getPriorityTools().length} 个,每 facet 1 个)
12
+ 工具分层
13
+
14
+ tyc 采用 4 层渐进发现,默认只把 L0 + L1 的 ${getLayerCount("L0") + getLayerCount("L1")} 个高密度工具暴露给 Agent,再按需下钻到 L2/L3。
15
+
16
+ ┌─ L0 实体锚定 ${pad(getLayerCount("L0"), 3)} 个工具 简称 / 曾用名 / 模糊名 → 精确企业
17
+ ├─ L1 概要总览 ${pad(getLayerCount("L1"), 3)} 个工具 每个 facet 1 个总览工具,返回 _summary
18
+ ├─ L2 维度下钻 ${pad(getLayerCount("L2"), 3)} 个工具 股东、诉讼、商标、招投标、年报等明细维度
19
+ └─ L3 专项工具 ${pad(getLayerCount("L3"), 3)} 个工具 id 详情、search_*、垂直场景和专业查询
20
+
21
+ 推荐调用顺序
22
+
23
+ 0. 先做 L0:用用户给出的原始企业名调用 \`search_companies\`,确认唯一企业、
24
+ 统一社会信用代码和企业全名。
25
+ 1. 再看 L1:按问题选择 1 个概要工具,读取 _summary 判断下一步。
26
+ 2. 下钻 L2:选择具体数据维度;不确定时优先用对应 facet 的 L2★ 工具。
27
+ 3. 进入 L3:需要记录 id 详情、跨主体 search_* 或垂直 SKILL 时再使用。
28
+
29
+ L2 优先 ★(${getPriorityTools().length} 个,每个 facet 1 个)
44
30
 
45
31
  Agent 拿到 L1 _summary 后若不确定走哪个 L2,对应 facet 的"L2 优先"是默认下钻:
46
32
 
47
33
  ${renderPriorityLines()}
48
34
 
49
- Detail: tyc layers / tyc L2 list (★ marks priority rows)
50
-
51
- DISCOVER BY LAYER
35
+ 按层发现
52
36
 
53
- tyc layers one-screen architecture map (含 L2 优先映射)
54
- tyc L0 list list the ${getLayerCount("L0")} L0 tool (entity-resolve layer)
55
- tyc L1 list list all ${getLayerCount("L1")} L1 tools (overview, grouped)
56
- tyc L2 list list all ${getLayerCount("L2")} L2 tools (drill-down; ★ priority pinned to top)
57
- tyc L3 list list all ${getLayerCount("L3")} L3 tools (specialized, grouped)
58
- tyc L0 list --md Markdown tables (agent on-screen)
59
- tyc L0 list --json machine-readable JSON (id · cli · params · description · priority)
37
+ tyc layers 查看一屏架构图和推荐调用顺序
38
+ tyc L0 list 列出 ${getLayerCount("L0")} 个 L0 工具
39
+ tyc L1 list 列出 ${getLayerCount("L1")} 个 L1 工具
40
+ tyc L2 list 列出 ${getLayerCount("L2")} 个 L2 工具(★ 优先工具置顶)
41
+ tyc L3 list 列出 ${getLayerCount("L3")} 个 L3 工具
42
+ tyc L0 list --md 输出 Markdown 表格
43
+ tyc L0 list --json 输出 JSON(含工具名、CLI、参数、描述、优先级)
44
+ tyc company capabilities <company_id> --company-name <name>
45
+ 根据 L0 返回的 company_id 打印可直接执行的工具清单
60
46
 
61
- DISCOVER BY CATEGORY (orthogonal to layers, 6 groups)
47
+ 按分类发现(与层级正交,共 6 组)
62
48
 
63
49
  ${renderCategoryLines()}
64
50
 
65
- OUTPUT FORMATS (mutually exclusive, priority: --md > --compact > --pretty / default)
66
-
67
- (default) indented JSON (pretty) — readable for both humans and agents
68
- --pretty same as default (kept for backward compatibility / explicit intent)
69
- --compact compact single-line JSON (pipe / jq friendly; old default behavior)
70
- --md Markdown tables (human + agent on-screen)
71
- --verbose also print core/MCP request details to stderr (orthogonal to above)
72
-
73
- OUTPUT TRUNCATION & DUMP (orthogonal — apply to any sub-command)
74
-
75
- --head [N] print only the first N lines (N defaults to 50 if flag given alone)
76
- --tail [M] print only the last M lines (M defaults to 20 if flag given alone)
77
- · --head + --tail together: head + "... omitted ..." + tail
78
- · either flag alone: that side only
79
- --full force-print the FULL rendered content (no truncation; highest priority)
80
- --threshold <BYTES> master switch for byte-truncation. Without --threshold there is
81
- NO automatic truncation, regardless of output size.
82
- With --threshold N: when rendered output > N bytes, byte-truncate
83
- from the head; --head / --tail are IGNORED in this mode.
84
- --output-file <PATH> write the FULL rendered content to PATH. **No file is written
85
- unless --output-file is explicitly passed** (no auto-dump).
86
- Combine with --head/--tail/--threshold to keep stdout terse
87
- while preserving the full result on disk.
88
-
89
- SETUP
90
-
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
96
-
97
- Every tool returns tyc OpenAPI native English keys verbatim. The MCP Server
98
- handles multi-source merge, Asia/Shanghai timestamp formatting, _summary
99
- injection, and empty-result normalization — the CLI just tells the Agent
100
- where the data is and what it looks like.
51
+ 全局输出选项(所有工具命令均可使用)
52
+
53
+ 以下选项可用于所有工具调用;具体工具 help 不重复展开完整列表。
54
+
55
+ 默认 缩进 JSON,兼顾人类阅读和 Agent 解析
56
+ --pretty 同默认输出,保留用于显式声明意图
57
+ --compact 单行紧凑 JSON,适合管道和 jq
58
+ --md Markdown 表格,适合终端阅读或粘贴给 Agent
59
+ --verbose 额外把 shared core 请求详情打印到 stderr
60
+
61
+ --head [N] 只输出前 N 行;只写 flag 时默认 50 行
62
+ --tail [M] 只输出后 M 行;只写 flag 时默认 20 行
63
+ --full 强制输出完整内容,优先级最高
64
+ --threshold <BYTES> 超过指定字节数时从头截断;不传则不自动截断
65
+ --output-file <PATH> 把完整结果写入文件;必须显式指定才会落盘
66
+
67
+ 初始化与登录
68
+
69
+ tyc login OAuth 浏览器登录
70
+ tyc login --no-open --no-block 打印 OAuth URL 后立即退出,手动完成
71
+ tyc login --callback-token <CODE_OR_CALLBACK_URL> 继续非阻塞 OAuth 登录
72
+ tyc login --url <MCP_URL> 登录本地、预发或自托管 MCP
73
+ tyc init --authorization <KEY> 使用 API Key 兼容路径
74
+ tyc init --url <MCP_URL> --authorization <KEY> 配置本地或自托管服务
101
75
  `;
102
76
  function pad(n, width) {
103
77
  const s = String(n);
@@ -106,7 +80,7 @@ function pad(n, width) {
106
80
  function renderCategoryLines() {
107
81
  const lines = [];
108
82
  for (const c of getCategories()) {
109
- lines.push(` tyc ${c.group.padEnd(22)} ${c.name_zh} (${c.tool_count} tools)`);
83
+ lines.push(` tyc ${c.group.padEnd(22)} ${c.name_zh} (${c.tool_count} 个工具)`);
110
84
  }
111
85
  return lines.join("\n");
112
86
  }
@@ -119,12 +93,26 @@ function renderPriorityLines() {
119
93
  }
120
94
  const program = new Command()
121
95
  .name("tyc")
122
- .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)`)
123
- .version(VERSION)
96
+ .description(`天眼查 AI CLI:${getTotalCount()} 个商业数据工具 · L0=${getLayerCount("L0")} L1=${getLayerCount("L1")} L2=${getLayerCount("L2")} L3=${getLayerCount("L3")} · 面向 Agent 的 MCP 工具网关`)
97
+ .version(VERSION, "-V, --version", "输出版本号")
98
+ .helpOption("-h, --help", "显示命令帮助")
99
+ .addHelpCommand("help [command]", "显示命令帮助")
100
+ .configureHelp({
101
+ styleTitle(title) {
102
+ const titleMap = {
103
+ "Usage:": "用法:",
104
+ "Arguments:": "参数:",
105
+ "Options:": "选项:",
106
+ "Global Options:": "全局选项:",
107
+ "Commands:": "命令:",
108
+ };
109
+ return titleMap[title] || title;
110
+ },
111
+ })
124
112
  .option("--pretty", "缩进 JSON 输出(默认行为,flag 保留以保持向后兼容)")
125
113
  .option("--md", "Markdown 表格化输出(适合人类阅读 / Agent 上屏)")
126
114
  .option("--compact", "紧凑单行 JSON(管道 / jq 场景;为旧默认行为)")
127
- .option("--verbose", "打印 core/MCP 请求详情到 stderr")
115
+ .option("--verbose", "打印 shared core 请求详情到 stderr")
128
116
  .option("--head [n]", "仅输出前 N 行(不传值默认 50;与 --tail 同时给则同时输出两端)")
129
117
  .option("--tail [m]", "仅输出后 M 行(不传值默认 20;与 --head 同时给则同时输出两端)")
130
118
  .option("--full", "强制输出完整内容(最高优先级,覆盖 --head/--tail/--threshold)")
package/dist/oauth.js CHANGED
@@ -340,6 +340,72 @@ export async function exchangeAuthorizationCode(opts) {
340
340
  }
341
341
  return token;
342
342
  }
343
+ export async function exchangeRefreshToken(opts) {
344
+ const body = new URLSearchParams();
345
+ body.set("grant_type", "refresh_token");
346
+ body.set("client_id", opts.clientId);
347
+ body.set("refresh_token", opts.refreshToken);
348
+ if (opts.clientSecret)
349
+ body.set("client_secret", opts.clientSecret);
350
+ if (opts.resource)
351
+ body.set("resource", opts.resource);
352
+ const resp = await fetch(opts.tokenEndpoint, {
353
+ method: "POST",
354
+ headers: {
355
+ Accept: "application/json",
356
+ "Content-Type": "application/x-www-form-urlencoded",
357
+ },
358
+ body,
359
+ });
360
+ const text = await resp.text();
361
+ if (resp.status < 200 || resp.status >= 300) {
362
+ throw new Error(`token refresh HTTP ${resp.status}: ${text.slice(0, 500)}`);
363
+ }
364
+ const token = JSON.parse(text);
365
+ if (!token.access_token) {
366
+ throw new Error("token refresh response missing access_token");
367
+ }
368
+ return token;
369
+ }
370
+ function callbackSearchParams(raw) {
371
+ try {
372
+ const url = new URL(raw);
373
+ if (url.search)
374
+ return url.searchParams;
375
+ if (url.hash.includes("="))
376
+ return new URLSearchParams(url.hash.replace(/^#/, ""));
377
+ return null;
378
+ }
379
+ catch {
380
+ if (raw.startsWith("?") || raw.includes("code=") || raw.includes("error=")) {
381
+ return new URLSearchParams(raw.replace(/^[?#]/, ""));
382
+ }
383
+ return null;
384
+ }
385
+ }
386
+ export function parseAuthorizationCallbackCode(raw, expectedState) {
387
+ const trimmed = raw.trim();
388
+ if (!trimmed) {
389
+ throw new Error("empty callback token");
390
+ }
391
+ const params = callbackSearchParams(trimmed);
392
+ if (!params) {
393
+ return trimmed;
394
+ }
395
+ const error = params.get("error");
396
+ if (error) {
397
+ throw new Error(`OAuth error: ${params.get("error_description") || error}`);
398
+ }
399
+ const state = params.get("state");
400
+ if (expectedState && state && state !== expectedState) {
401
+ throw new Error("OAuth state mismatch");
402
+ }
403
+ const code = params.get("code");
404
+ if (!code) {
405
+ throw new Error("callback URL missing code");
406
+ }
407
+ return code;
408
+ }
343
409
  export function openBrowser(url) {
344
410
  const platform = process.platform;
345
411
  let command;
@@ -0,0 +1,41 @@
1
+ // ~/.tyc/oauth_pending.json 读写
2
+ //
3
+ // 非阻塞 OAuth 登录会先保存 PKCE code_verifier、state、client_id 等一次性上下文;
4
+ // 用户完成浏览器授权后,再用 tyc login --callback-token <code_or_callback_url> 继续换取 access token。
5
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ const PENDING_DIR = join(homedir(), ".tyc");
9
+ export const PENDING_FILE = join(PENDING_DIR, "oauth_pending.json");
10
+ export function loadPendingOAuthLogin() {
11
+ if (!existsSync(PENDING_FILE))
12
+ return null;
13
+ try {
14
+ const raw = readFileSync(PENDING_FILE, "utf-8");
15
+ return JSON.parse(raw);
16
+ }
17
+ catch (err) {
18
+ const msg = err instanceof Error ? err.message : String(err);
19
+ throw new Error(`读取 OAuth pending 状态失败:${PENDING_FILE}:${msg}`);
20
+ }
21
+ }
22
+ export function savePendingOAuthLogin(pending) {
23
+ if (!existsSync(PENDING_DIR))
24
+ mkdirSync(PENDING_DIR, { recursive: true, mode: 0o700 });
25
+ writeFileSync(PENDING_FILE, JSON.stringify(pending, null, 2) + "\n", "utf-8");
26
+ try {
27
+ chmodSync(PENDING_FILE, 0o600);
28
+ }
29
+ catch { /* best effort */ }
30
+ }
31
+ export function clearPendingOAuthLogin(verbose = false) {
32
+ try {
33
+ rmSync(PENDING_FILE, { force: true });
34
+ }
35
+ catch (err) {
36
+ if (verbose) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ console.error(`> 清理 OAuth pending 状态失败(忽略):${PENDING_FILE}:${msg}`);
39
+ }
40
+ }
41
+ }
package/dist/types.js CHANGED
@@ -2,8 +2,7 @@
2
2
  //
3
3
  // CLI 运行时模型:
4
4
  // - Catalog:构建时由 apimcp 主仓生成,打包在 src/catalog.json,用于构建命令树
5
- // - MCP 层类型:JSON-RPC 请求/响应、tools/call 结果
6
- // - Session:本地缓存的 MCP 会话状态(~/.tyc/session.json)
7
- // - Config:CLI 配置(~/.tyc/config.json),与主流 MCP 客户端配置风格一致
8
- // { url, headers }
5
+ // - Tool call 结果:沿用 MCP content shape,便于共享输出逻辑
6
+ // - Config:CLI 配置(~/.tyc/config.json),{ url, coreUrl, headers };
7
+ // OAuth 登录会额外保存 oauth 刷新上下文
9
8
  export {};
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // CLI 版本号单一来源:从 package.json 读取
2
2
  //
3
- // 历史上 index.ts 与 mcpClient.ts 各自硬编码版本字符串,多处漂移。
3
+ // 历史上 index.ts 与 client 实现各自硬编码版本字符串,多处漂移。
4
4
  // 收敛到 package.json 一处后,bump 版本只需 npm version <x>。
5
5
  // 运行时 fs 读:dev (`tsx src/version.ts`) 与 dist (`dist/version.js`) 与
6
6
  // package.json 都同级 (cli/t1_1/{src,dist} ↔ cli/t1_1/package.json),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tyc-cli",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "天眼查 AI CLI — 162 个业务语义聚合工具 · 默认 Shared Core 调用 · 兼容 MCP transport",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/dist/mcpClient.js DELETED
@@ -1,146 +0,0 @@
1
- // MCP JSON-RPC over Streamable HTTP 客户端
2
- //
3
- // 职责:
4
- // 1. initialize:拿 Mcp-Session-Id(服务端在响应 header 下发)
5
- // 2. tools/call:透传 Authorization + Mcp-Session-Id,解析 SSE/JSON 响应
6
- // 3. Session 失效(404/410 或服务端 "session not found")→ 清 session → 重试 1 次
7
- //
8
- // 不做:
9
- // - 不维护 tools/list 缓存(CLI 命令树从本地 catalog.json 构建)
10
- // - 不做业务聚合/时间戳格式化(由 MCP Server 完成)
11
- // - 不做 Authorization 解析/验签
12
- import { clearSession, isSessionFresh, loadSession, saveSession } from "./session.js";
13
- import { VERSION } from "./version.js";
14
- const PROTOCOL_VERSION = "2024-11-05";
15
- function maskToken(v) {
16
- if (!v)
17
- return "";
18
- if (v.length <= 8)
19
- return "****";
20
- return v.slice(0, 4) + "****" + v.slice(-4);
21
- }
22
- function logVerbose(cfg, title, extra) {
23
- const masked = {};
24
- for (const [k, v] of Object.entries(cfg.headers)) {
25
- masked[k] = k.toLowerCase() === "authorization" ? maskToken(v) : v;
26
- }
27
- console.error(`> ${title}`);
28
- console.error(`> headers: ${JSON.stringify(masked)}`);
29
- if (extra)
30
- console.error(extra);
31
- }
32
- /** 解析 MCP Streamable HTTP 响应——server 端实测固定回 application/json 单包,
33
- * 无论 client Accept 头声明什么。SSE 路径不存在,所以只解析 JSON。 */
34
- function parseJsonRpc(raw) {
35
- const trimmed = raw.trim();
36
- if (!trimmed.startsWith("{")) {
37
- throw new Error(`Unparseable MCP response: ${trimmed.slice(0, 200)}`);
38
- }
39
- return JSON.parse(trimmed);
40
- }
41
- async function httpPost(url, headers, body, verbose) {
42
- const h = {
43
- "Content-Type": "application/json",
44
- "Accept": "application/json",
45
- ...headers,
46
- };
47
- if (verbose) {
48
- console.error(`> POST ${url}`);
49
- const masked = {};
50
- for (const [k, v] of Object.entries(h)) {
51
- masked[k] = k.toLowerCase() === "authorization" ? maskToken(v) : v;
52
- }
53
- console.error(`> headers: ${JSON.stringify(masked)}`);
54
- console.error(`> body: ${JSON.stringify(body)}`);
55
- }
56
- const resp = await fetch(url, {
57
- method: "POST",
58
- headers: h,
59
- body: JSON.stringify(body),
60
- });
61
- const text = await resp.text();
62
- if (verbose) {
63
- console.error(`< ${resp.status} ${resp.statusText}`);
64
- console.error(`< body: ${text.slice(0, 2000)}`);
65
- }
66
- return { status: resp.status, text, headers: resp.headers };
67
- }
68
- async function doInitialize(cfg, verbose) {
69
- if (verbose)
70
- logVerbose(cfg, `MCP initialize → ${cfg.url}`);
71
- const { status, text, headers } = await httpPost(cfg.url, cfg.headers, {
72
- jsonrpc: "2.0",
73
- id: 1,
74
- method: "initialize",
75
- params: {
76
- protocolVersion: PROTOCOL_VERSION,
77
- capabilities: {},
78
- clientInfo: { name: "tyc-cli", version: VERSION },
79
- },
80
- }, verbose);
81
- if (status < 200 || status >= 300) {
82
- throw new Error(`initialize 失败:HTTP ${status}\n${text.slice(0, 500)}`);
83
- }
84
- const sessionId = headers.get("Mcp-Session-Id") || headers.get("mcp-session-id");
85
- if (!sessionId) {
86
- throw new Error(`initialize 响应未包含 Mcp-Session-Id header。请确认 ${cfg.url} 是 Streamable HTTP MCP Server。`);
87
- }
88
- const rpc = parseJsonRpc(text);
89
- if (rpc.error) {
90
- throw new Error(`initialize JSON-RPC error: ${rpc.error.message}`);
91
- }
92
- const s = {
93
- url: cfg.url,
94
- sessionId,
95
- initializedAt: Date.now(),
96
- protocolVersion: rpc.result?.protocolVersion,
97
- };
98
- saveSession(s);
99
- return s;
100
- }
101
- /** 获取可用 session:若缓存新鲜则复用,否则 initialize + 落盘。 */
102
- export async function ensureSession(cfg, opts) {
103
- const cached = loadSession();
104
- if (isSessionFresh(cached, cfg.url))
105
- return cached;
106
- return doInitialize(cfg, !!opts?.verbose);
107
- }
108
- function isSessionInvalidResponse(status, text) {
109
- if (status === 404 || status === 410)
110
- return true;
111
- const lower = text.toLowerCase();
112
- return lower.includes("session not found") || lower.includes("invalid session");
113
- }
114
- /** 调用 MCP tool;透明处理 session 失效重建。 */
115
- export async function callTool(cfg, name, args, opts) {
116
- const verbose = !!opts?.verbose;
117
- let session = await ensureSession(cfg, opts);
118
- const doCall = async (s) => {
119
- const h = { ...cfg.headers, "Mcp-Session-Id": s.sessionId };
120
- return httpPost(cfg.url, h, {
121
- jsonrpc: "2.0",
122
- id: 2,
123
- method: "tools/call",
124
- params: { name, arguments: args },
125
- }, verbose);
126
- };
127
- let resp = await doCall(session);
128
- if (isSessionInvalidResponse(resp.status, resp.text)) {
129
- if (verbose)
130
- console.error("> session invalid, re-initializing …");
131
- clearSession();
132
- session = await doInitialize(cfg, verbose);
133
- resp = await doCall(session);
134
- }
135
- if (resp.status < 200 || resp.status >= 300) {
136
- throw new Error(`tools/call HTTP ${resp.status}\n${resp.text.slice(0, 500)}`);
137
- }
138
- const rpc = parseJsonRpc(resp.text);
139
- if (rpc.error) {
140
- throw new Error(`MCP error ${rpc.error.code}: ${rpc.error.message}`);
141
- }
142
- if (!rpc.result) {
143
- throw new Error("tools/call 响应缺少 result 字段");
144
- }
145
- return rpc.result;
146
- }
package/dist/session.js DELETED
@@ -1,47 +0,0 @@
1
- // ~/.tyc/session.json 读写 + 24h TTL
2
- //
3
- // TTL 规则(按字面 24h,从 initializedAt 起):
4
- // - 命中:复用 sessionId,跳过 initialize
5
- // - 过期 / url 变化:强制重新 initialize
6
- // - 服务端主动失效(4xx + session 提示):mcpClient 调 clearSession 后重建
7
- import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, chmodSync } from "node:fs";
8
- import { join } from "node:path";
9
- import { homedir } from "node:os";
10
- const SESSION_DIR = join(homedir(), ".tyc");
11
- const SESSION_FILE = join(SESSION_DIR, "session.json");
12
- export const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24h 字面
13
- export function loadSession() {
14
- if (!existsSync(SESSION_FILE))
15
- return null;
16
- try {
17
- const raw = readFileSync(SESSION_FILE, "utf-8");
18
- return JSON.parse(raw);
19
- }
20
- catch {
21
- return null;
22
- }
23
- }
24
- export function saveSession(s) {
25
- if (!existsSync(SESSION_DIR))
26
- mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
27
- writeFileSync(SESSION_FILE, JSON.stringify(s, null, 2) + "\n", "utf-8");
28
- try {
29
- chmodSync(SESSION_FILE, 0o600);
30
- }
31
- catch { /* best effort */ }
32
- }
33
- export function clearSession() {
34
- try {
35
- rmSync(SESSION_FILE, { force: true });
36
- }
37
- catch { /* ignore */ }
38
- }
39
- export function isSessionFresh(s, url, now = Date.now()) {
40
- if (!s)
41
- return false;
42
- if (s.url !== url)
43
- return false;
44
- if (!s.sessionId)
45
- return false;
46
- return now - s.initializedAt < SESSION_TTL_MS;
47
- }