@bigbrain-work/mcp-connect 1.3.0 → 1.3.1

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/README.md CHANGED
@@ -24,6 +24,12 @@ npx -y @bigbrain-work/mcp-connect status
24
24
  # 显示服务实时返回的工具
25
25
  npx -y @bigbrain-work/mcp-connect tools
26
26
 
27
+ # 每个项目最多每24小时检查一次石榴 Skill;仅有变化时定向更新
28
+ shiliu skill refresh
29
+
30
+ # 人工忽略冷却并立即重试
31
+ shiliu skill refresh --force
32
+
27
33
  # 撤销刷新令牌并清除本机凭据
28
34
  npx -y @bigbrain-work/mcp-connect logout
29
35
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigbrain-work/mcp-connect",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "石榴 AI 官方 CLI:登录、配置 Agent、检查 MCP 状态并桥接远程工具。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/arguments.js CHANGED
@@ -8,6 +8,7 @@ const COMMANDS = new Set([
8
8
  "tools",
9
9
  "mcp",
10
10
  "proxy",
11
+ "skill",
11
12
  "update",
12
13
  ]);
13
14
 
@@ -50,6 +51,10 @@ export function parseCliArguments(args) {
50
51
  type: "boolean",
51
52
  default: false,
52
53
  },
54
+ force: {
55
+ type: "boolean",
56
+ default: false,
57
+ },
53
58
  session: {
54
59
  type: "string",
55
60
  },
@@ -75,9 +80,13 @@ export function parseCliArguments(args) {
75
80
  throw new Error(`不支持的命令:${command}`);
76
81
  }
77
82
  const subcommand = parsed.positionals[1];
83
+ const validSubcommand =
84
+ (command === "login" && subcommand === "poll") ||
85
+ (command === "skill" && subcommand === "refresh");
78
86
  if (
79
87
  parsed.positionals.length > 2 ||
80
- (subcommand && !(command === "login" && subcommand === "poll"))
88
+ (subcommand && !validSubcommand) ||
89
+ (command === "skill" && subcommand !== "refresh")
81
90
  ) {
82
91
  throw new Error(`无法识别的参数:${parsed.positionals.slice(1).join(" ")}`);
83
92
  }
@@ -103,6 +112,7 @@ export function parseCliArguments(args) {
103
112
  legacyApiKey: parsed.values["legacy-api-key"],
104
113
  noWait: parsed.values["no-wait"],
105
114
  wait: parsed.values.wait,
115
+ force: parsed.values.force,
106
116
  session,
107
117
  json: parsed.values.json,
108
118
  help: parsed.values.help,
package/src/cli.js CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  validateMcpUrl,
34
34
  } from "./security.js";
35
35
  import { printStatus, printTools, probeMcp } from "./status.js";
36
+ import { printSkillRefresh } from "./skill-refresh.js";
36
37
  import { PendingLoginStore, TokenStore } from "./token-store.js";
37
38
  import { printUpdateStatus } from "./updater.js";
38
39
 
@@ -46,6 +47,7 @@ function printHelp() {
46
47
  shiliu install [--agent <name>]
47
48
  shiliu status [--json]
48
49
  shiliu tools [--json]
50
+ shiliu skill refresh [--force] [--json]
49
51
  shiliu update
50
52
  shiliu logout
51
53
  shiliu mcp
@@ -64,6 +66,7 @@ function printHelp() {
64
66
  --legacy-api-key 使用旧 API Key 兼容登录
65
67
  --no-wait 创建登录会话后立即返回
66
68
  --wait 等待指定登录会话完成
69
+ --force 忽略24小时冷却并立即检查石榴 Skill
67
70
  --session <id> 指定待继续的登录会话
68
71
  --json 以 JSON 输出 status/tools
69
72
  -h, --help 显示帮助
@@ -402,6 +405,14 @@ export async function runCli(argv = process.argv.slice(2)) {
402
405
  }
403
406
 
404
407
  const home = path.resolve(options.home || os.homedir());
408
+ if (options.command === "skill") {
409
+ await printSkillRefresh({
410
+ home,
411
+ force: options.force,
412
+ json: options.json,
413
+ });
414
+ return;
415
+ }
405
416
  const url = validateMcpUrl(options.url || MCP_URL, {
406
417
  allowLocalhost: options.allowLocalhost,
407
418
  });
package/src/constants.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PACKAGE_NAME = "@bigbrain-work/mcp-connect";
2
- export const PACKAGE_VERSION = "1.3.0";
2
+ export const PACKAGE_VERSION = "1.3.1";
3
3
  export const SERVER_NAME = "shiliu_mcp";
4
4
  export const MCP_URL = "https://api.bigbrain.work/shiliu/mcp";
5
5
  export const AUTH_URL = "https://api.bigbrain.work/shiliu/auth/device";
package/src/proxy.js CHANGED
@@ -20,12 +20,14 @@ export async function runProxy({
20
20
  authUrl = AUTH_URL,
21
21
  platform = process.platform,
22
22
  env = process.env,
23
+ tokenStore,
23
24
  } = {}) {
24
25
  const authorization = await resolveAuthorization({
25
26
  home,
26
27
  platform,
27
28
  env,
28
29
  authUrl,
30
+ tokenStore,
29
31
  });
30
32
  if (!authorization.token) {
31
33
  throw new Error(
@@ -0,0 +1,202 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import {
3
+ mkdir,
4
+ open,
5
+ readFile,
6
+ stat,
7
+ unlink,
8
+ writeFile,
9
+ } from "node:fs/promises";
10
+ import path from "node:path";
11
+
12
+ export const SKILL_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
13
+ export const SHILIU_SKILL_NAME = "shiliu-ai-mcp";
14
+ const STALE_LOCK_MS = 15 * 60 * 1000;
15
+
16
+ function stateFile(home) {
17
+ return path.join(home, ".shiliu-ai", "skill-refresh.json");
18
+ }
19
+
20
+ function lockFile(home) {
21
+ return path.join(home, ".shiliu-ai", "skill-refresh.lock");
22
+ }
23
+
24
+ async function acquireLock(home, retried = false) {
25
+ await mkdir(path.dirname(lockFile(home)), { recursive: true });
26
+ let handle;
27
+ try {
28
+ handle = await open(lockFile(home), "wx", 0o600);
29
+ await handle.writeFile(`${process.pid}\n`, "utf8");
30
+ } catch (error) {
31
+ await handle?.close();
32
+ if (error.code === "EEXIST") {
33
+ if (!retried) {
34
+ try {
35
+ const lockStat = await stat(lockFile(home));
36
+ if (Date.now() - lockStat.mtimeMs >= STALE_LOCK_MS) {
37
+ await unlink(lockFile(home));
38
+ return acquireLock(home, true);
39
+ }
40
+ } catch (lockError) {
41
+ if (lockError.code === "ENOENT") return acquireLock(home, true);
42
+ throw lockError;
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+ throw error;
48
+ }
49
+ await handle.close();
50
+ return async () => {
51
+ try {
52
+ await unlink(lockFile(home));
53
+ } catch (error) {
54
+ if (error.code !== "ENOENT") throw error;
55
+ }
56
+ };
57
+ }
58
+
59
+ async function readState(home) {
60
+ try {
61
+ const value = JSON.parse(await readFile(stateFile(home), "utf8"));
62
+ if (value?.version === 1 && value.scopes && typeof value.scopes === "object") {
63
+ return value;
64
+ }
65
+ } catch (error) {
66
+ if (error.code !== "ENOENT" && error.name !== "SyntaxError") throw error;
67
+ }
68
+ return { version: 1, scopes: {} };
69
+ }
70
+
71
+ async function writeState(home, state) {
72
+ const file = stateFile(home);
73
+ await mkdir(path.dirname(file), { recursive: true });
74
+ await writeFile(file, `${JSON.stringify(state, null, 2)}\n`, {
75
+ encoding: "utf8",
76
+ mode: 0o600,
77
+ });
78
+ }
79
+
80
+ function defaultRunUpdate({ cwd, platform = process.platform }) {
81
+ const command = platform === "win32" ? "npx.cmd" : "npx";
82
+ const result = spawnSync(
83
+ command,
84
+ ["-y", "skills", "update", SHILIU_SKILL_NAME, "-y"],
85
+ {
86
+ cwd,
87
+ encoding: "utf8",
88
+ windowsHide: true,
89
+ shell: false,
90
+ maxBuffer: 1024 * 1024,
91
+ },
92
+ );
93
+ if (result.error) throw result.error;
94
+ if (result.status !== 0) {
95
+ throw new Error(
96
+ result.stderr?.trim() ||
97
+ result.stdout?.trim() ||
98
+ `skills update 返回退出码 ${result.status}`,
99
+ );
100
+ }
101
+ }
102
+
103
+ function validTimestamp(value) {
104
+ const timestamp = Date.parse(value || "");
105
+ return Number.isFinite(timestamp) ? timestamp : null;
106
+ }
107
+
108
+ export async function refreshShiliuSkill({
109
+ home,
110
+ cwd = process.cwd(),
111
+ now = Date.now(),
112
+ force = false,
113
+ intervalMs = SKILL_REFRESH_INTERVAL_MS,
114
+ runUpdate = defaultRunUpdate,
115
+ } = {}) {
116
+ if (!home) throw new Error("缺少用户目录,无法记录 Skill 检查时间");
117
+ const scope = path.resolve(cwd);
118
+ const state = await readState(home);
119
+ const previous = state.scopes[scope] || {};
120
+ const lastAttemptAt = validTimestamp(previous.lastAttemptAt);
121
+ if (!force && lastAttemptAt !== null && now - lastAttemptAt < intervalMs) {
122
+ return {
123
+ status: "skipped",
124
+ reason: "within_interval",
125
+ nextCheckAt: new Date(lastAttemptAt + intervalMs).toISOString(),
126
+ };
127
+ }
128
+
129
+ const releaseLock = await acquireLock(home);
130
+ if (!releaseLock) {
131
+ return {
132
+ status: "skipped",
133
+ reason: "refresh_in_progress",
134
+ };
135
+ }
136
+
137
+ try {
138
+ const latestState = await readState(home);
139
+ const latestPrevious = latestState.scopes[scope] || {};
140
+ const latestAttemptAt = validTimestamp(latestPrevious.lastAttemptAt);
141
+ if (
142
+ !force &&
143
+ latestAttemptAt !== null &&
144
+ now - latestAttemptAt < intervalMs
145
+ ) {
146
+ return {
147
+ status: "skipped",
148
+ reason: "within_interval",
149
+ nextCheckAt: new Date(latestAttemptAt + intervalMs).toISOString(),
150
+ };
151
+ }
152
+
153
+ const attemptedAt = new Date(now).toISOString();
154
+ latestState.scopes[scope] = {
155
+ ...latestPrevious,
156
+ lastAttemptAt: attemptedAt,
157
+ };
158
+ await writeState(home, latestState);
159
+
160
+ try {
161
+ await runUpdate({ cwd: scope });
162
+ latestState.scopes[scope].lastSuccessAt = attemptedAt;
163
+ await writeState(home, latestState);
164
+ return {
165
+ status: "checked",
166
+ checkedAt: attemptedAt,
167
+ nextCheckAt: new Date(now + intervalMs).toISOString(),
168
+ };
169
+ } catch (error) {
170
+ return {
171
+ status: "failed",
172
+ checkedAt: attemptedAt,
173
+ nextCheckAt: new Date(now + intervalMs).toISOString(),
174
+ message: error.message,
175
+ };
176
+ }
177
+ } finally {
178
+ await releaseLock();
179
+ }
180
+ }
181
+
182
+ export async function printSkillRefresh(options = {}) {
183
+ const result = await refreshShiliuSkill(options);
184
+ if (options.json) {
185
+ console.log(JSON.stringify(result, null, 2));
186
+ return result;
187
+ }
188
+ if (result.status === "skipped") {
189
+ console.log(
190
+ result.reason === "refresh_in_progress"
191
+ ? "另一个石榴 Skill 检查正在进行,本次继续使用已安装 Skill。"
192
+ : `石榴 Skill 在24小时内已检查,下次检查时间:${result.nextCheckAt}`,
193
+ );
194
+ } else if (result.status === "checked") {
195
+ console.log(`石榴 Skill 检查完成,下次检查时间:${result.nextCheckAt}`);
196
+ } else {
197
+ console.warn(
198
+ `石榴 Skill 检查失败:${result.message}。已进入24小时冷却,本次继续使用已安装 Skill。`,
199
+ );
200
+ }
201
+ return result;
202
+ }