@bigbrain-work/mcp-connect 1.0.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.
- package/README.md +53 -0
- package/bin/mcp-connect.js +8 -0
- package/package.json +32 -0
- package/src/arguments.js +56 -0
- package/src/cli.js +68 -0
- package/src/configurators.js +152 -0
- package/src/constants.js +6 -0
- package/src/credentials.js +198 -0
- package/src/status.js +94 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @bigbrain-work/mcp-connect
|
|
2
|
+
|
|
3
|
+
石榴 AI MCP 一键配置工具。用于将石榴 AI 的远程 MCP 服务接入 Codex、Claude Code 或 Cursor。
|
|
4
|
+
|
|
5
|
+
## 使用
|
|
6
|
+
|
|
7
|
+
先在石榴 AI 页面复制当前账户的 API Key,然后执行对应命令:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y @bigbrain-work/mcp-connect --agent codex
|
|
11
|
+
npx -y @bigbrain-work/mcp-connect --agent claude
|
|
12
|
+
npx -y @bigbrain-work/mcp-connect --agent cursor
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
安装器会隐藏输入 API Key。配置完成后,重启对应客户端并检查连接:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx -y @bigbrain-work/mcp-connect status
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
如已设置 `SHILIU_AI_API_KEY` 环境变量,安装器会直接使用该变量,不再提示输入。
|
|
22
|
+
|
|
23
|
+
## 安全说明
|
|
24
|
+
|
|
25
|
+
- npm 包源码中不包含任何用户 API Key。
|
|
26
|
+
- Codex、Claude Code 和 Cursor 的 MCP 配置只保存环境变量引用,不保存真实 API Key。
|
|
27
|
+
- Windows 上,安装器通过标准输入将 API Key 写入当前用户环境变量,避免进入终端历史和子进程命令行。
|
|
28
|
+
- macOS/Linux 上,API Key 保存在权限为 `0600` 的 `~/.config/shiliu-ai/env`,并由用户登录配置加载。
|
|
29
|
+
- `status` 命令只显示“已设置/未设置”,不会输出 API Key。
|
|
30
|
+
|
|
31
|
+
## 服务信息
|
|
32
|
+
|
|
33
|
+
- MCP 地址:`https://api.bigbrain.work/shiliu/mcp`
|
|
34
|
+
- 认证方式:`Authorization: Bearer <API_KEY>`
|
|
35
|
+
- 配置名称:`shiliu_mcp`
|
|
36
|
+
|
|
37
|
+
## 发布
|
|
38
|
+
|
|
39
|
+
首次发布前,确认 npm 账号属于 `bigbrain-work` organization,并启用双重验证:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm login
|
|
43
|
+
npm whoami
|
|
44
|
+
npm pack --dry-run
|
|
45
|
+
npm publish --access public
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
后续版本先更新 `package.json` 版本号,再发布:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm version patch
|
|
52
|
+
npm publish --access public
|
|
53
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bigbrain-work/mcp-connect",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "石榴 AI MCP 一键配置工具,支持 Codex、Claude Code 和 Cursor。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-connect": "bin/mcp-connect.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"pack:check": "npm pack --dry-run"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"codex",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"cursor",
|
|
26
|
+
"shiliu-ai"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"license": "UNLICENSED"
|
|
32
|
+
}
|
package/src/arguments.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util'
|
|
2
|
+
|
|
3
|
+
import { SUPPORTED_AGENTS } from './constants.js'
|
|
4
|
+
|
|
5
|
+
export function parseCliArguments(args) {
|
|
6
|
+
const parsed = parseArgs({
|
|
7
|
+
args,
|
|
8
|
+
allowPositionals: true,
|
|
9
|
+
strict: true,
|
|
10
|
+
options: {
|
|
11
|
+
agent: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
short: 'a',
|
|
14
|
+
},
|
|
15
|
+
'dry-run': {
|
|
16
|
+
type: 'boolean',
|
|
17
|
+
default: false,
|
|
18
|
+
},
|
|
19
|
+
home: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
},
|
|
22
|
+
help: {
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
short: 'h',
|
|
25
|
+
default: false,
|
|
26
|
+
},
|
|
27
|
+
version: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
short: 'v',
|
|
30
|
+
default: false,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const command = parsed.positionals[0] || 'install'
|
|
36
|
+
if (!['install', 'status'].includes(command)) {
|
|
37
|
+
throw new Error(`不支持的命令:${command}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const agent = parsed.values.agent?.toLowerCase()
|
|
41
|
+
if (agent && !SUPPORTED_AGENTS.includes(agent)) {
|
|
42
|
+
throw new Error(`不支持的客户端:${agent}。可选值:${SUPPORTED_AGENTS.join('、')}`)
|
|
43
|
+
}
|
|
44
|
+
if (command === 'install' && !agent && !parsed.values.help && !parsed.values.version) {
|
|
45
|
+
throw new Error('请通过 --agent 指定客户端:codex、claude 或 cursor')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
command,
|
|
50
|
+
agent,
|
|
51
|
+
dryRun: parsed.values['dry-run'],
|
|
52
|
+
home: parsed.values.home,
|
|
53
|
+
help: parsed.values.help,
|
|
54
|
+
version: parsed.values.version,
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { parseCliArguments } from './arguments.js'
|
|
5
|
+
import { configureAgent } from './configurators.js'
|
|
6
|
+
import {
|
|
7
|
+
API_KEY_ENV,
|
|
8
|
+
MCP_URL,
|
|
9
|
+
PACKAGE_NAME,
|
|
10
|
+
PACKAGE_VERSION,
|
|
11
|
+
SUPPORTED_AGENTS,
|
|
12
|
+
} from './constants.js'
|
|
13
|
+
import { persistApiKey, resolveApiKey } from './credentials.js'
|
|
14
|
+
import { printStatus } from './status.js'
|
|
15
|
+
|
|
16
|
+
function printHelp() {
|
|
17
|
+
console.log(`${PACKAGE_NAME} ${PACKAGE_VERSION}
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
mcp-connect --agent <${SUPPORTED_AGENTS.join('|')}>
|
|
21
|
+
mcp-connect status
|
|
22
|
+
|
|
23
|
+
选项:
|
|
24
|
+
-a, --agent <name> 配置指定客户端
|
|
25
|
+
--dry-run 只显示将执行的操作
|
|
26
|
+
--home <path> 指定用户目录(主要用于测试)
|
|
27
|
+
-h, --help 显示帮助
|
|
28
|
+
-v, --version 显示版本
|
|
29
|
+
|
|
30
|
+
认证:
|
|
31
|
+
安装器优先读取 ${API_KEY_ENV};未设置时会隐藏提示输入。
|
|
32
|
+
真实 API Key 不会写入生成的 MCP 配置。`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
36
|
+
const options = parseCliArguments(argv)
|
|
37
|
+
if (options.help) {
|
|
38
|
+
printHelp()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (options.version) {
|
|
42
|
+
console.log(PACKAGE_VERSION)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const home = path.resolve(options.home || os.homedir())
|
|
47
|
+
if (options.command === 'status') {
|
|
48
|
+
const result = await printStatus({ home })
|
|
49
|
+
if (!result.remote.ok) {
|
|
50
|
+
process.exitCode = 1
|
|
51
|
+
}
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const apiKey = await resolveApiKey()
|
|
56
|
+
await persistApiKey(apiKey, {
|
|
57
|
+
dryRun: options.dryRun,
|
|
58
|
+
home,
|
|
59
|
+
})
|
|
60
|
+
await configureAgent(options.agent, {
|
|
61
|
+
dryRun: options.dryRun,
|
|
62
|
+
home,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
console.log(options.dryRun
|
|
66
|
+
? `检查完成:将为 ${options.agent} 配置 ${MCP_URL},未写入任何文件。`
|
|
67
|
+
: `配置完成:${options.agent} 已接入石榴 AI MCP。请重启客户端后运行 ${PACKAGE_NAME} status 检查连接。`)
|
|
68
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
import { API_KEY_ENV, MCP_URL, SERVER_NAME } from './constants.js'
|
|
6
|
+
|
|
7
|
+
export function codexArguments() {
|
|
8
|
+
return [
|
|
9
|
+
'mcp',
|
|
10
|
+
'add',
|
|
11
|
+
SERVER_NAME,
|
|
12
|
+
'--url',
|
|
13
|
+
MCP_URL,
|
|
14
|
+
'--bearer-token-env-var',
|
|
15
|
+
API_KEY_ENV,
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function claudeServerDefinition() {
|
|
20
|
+
return {
|
|
21
|
+
type: 'http',
|
|
22
|
+
url: MCP_URL,
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer \${${API_KEY_ENV}}`,
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function cursorServerDefinition() {
|
|
30
|
+
return {
|
|
31
|
+
type: 'http',
|
|
32
|
+
url: MCP_URL,
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `Bearer \${env:${API_KEY_ENV}}`,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function commandText(command, args) {
|
|
40
|
+
return [command, ...args].map((value) => (
|
|
41
|
+
/[\s"]/u.test(value) ? JSON.stringify(value) : value
|
|
42
|
+
)).join(' ')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function execute(command, args, { dryRun = false } = {}) {
|
|
46
|
+
if (dryRun) {
|
|
47
|
+
console.log(`[dry-run] ${commandText(command, args)}`)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = spawnSync(command, args, {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
windowsHide: true,
|
|
54
|
+
shell: false,
|
|
55
|
+
})
|
|
56
|
+
if (result.error) {
|
|
57
|
+
if (result.error.code === 'ENOENT') {
|
|
58
|
+
throw new Error(`未找到 ${command},请先安装对应客户端`)
|
|
59
|
+
}
|
|
60
|
+
throw result.error
|
|
61
|
+
}
|
|
62
|
+
if (result.status !== 0) {
|
|
63
|
+
const detail = `${result.stderr || ''}\n${result.stdout || ''}`.trim()
|
|
64
|
+
const error = new Error(detail || `${command} 返回退出码 ${result.status}`)
|
|
65
|
+
error.commandOutput = detail
|
|
66
|
+
throw error
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isDuplicateError(error) {
|
|
71
|
+
return /already|exist|duplicate|已存在/iu.test(error.commandOutput || error.message)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function configureCodex(options = {}) {
|
|
75
|
+
try {
|
|
76
|
+
execute('codex', codexArguments(), options)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (!isDuplicateError(error)) {
|
|
79
|
+
throw error
|
|
80
|
+
}
|
|
81
|
+
execute('codex', ['mcp', 'remove', SERVER_NAME], options)
|
|
82
|
+
execute('codex', codexArguments(), options)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function configureClaude(options = {}) {
|
|
87
|
+
const addArgs = [
|
|
88
|
+
'mcp',
|
|
89
|
+
'add-json',
|
|
90
|
+
'--scope',
|
|
91
|
+
'user',
|
|
92
|
+
SERVER_NAME,
|
|
93
|
+
JSON.stringify(claudeServerDefinition()),
|
|
94
|
+
]
|
|
95
|
+
try {
|
|
96
|
+
execute('claude', addArgs, options)
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!isDuplicateError(error)) {
|
|
99
|
+
throw error
|
|
100
|
+
}
|
|
101
|
+
execute('claude', ['mcp', 'remove', '--scope', 'user', SERVER_NAME], options)
|
|
102
|
+
execute('claude', addArgs, options)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function configureCursor({ home, dryRun = false } = {}) {
|
|
107
|
+
const configPath = path.join(home, '.cursor', 'mcp.json')
|
|
108
|
+
if (dryRun) {
|
|
109
|
+
console.log(`[dry-run] 更新 ${configPath} 中的 ${SERVER_NAME},认证头使用 ${API_KEY_ENV} 环境变量`)
|
|
110
|
+
return configPath
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let config = {}
|
|
114
|
+
try {
|
|
115
|
+
const source = await readFile(configPath, 'utf8')
|
|
116
|
+
config = JSON.parse(source)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code !== 'ENOENT') {
|
|
119
|
+
if (error instanceof SyntaxError) {
|
|
120
|
+
throw new Error(`${configPath} 不是有效 JSON,未做任何修改`)
|
|
121
|
+
}
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
config.mcpServers = {
|
|
127
|
+
...(config.mcpServers || {}),
|
|
128
|
+
[SERVER_NAME]: cursorServerDefinition(),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await mkdir(path.dirname(configPath), { recursive: true })
|
|
132
|
+
const tempPath = `${configPath}.${process.pid}.tmp`
|
|
133
|
+
await writeFile(tempPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
134
|
+
await rename(tempPath, configPath)
|
|
135
|
+
return configPath
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function configureAgent(agent, options) {
|
|
139
|
+
if (agent === 'codex') {
|
|
140
|
+
configureCodex(options)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
if (agent === 'claude') {
|
|
144
|
+
configureClaude(options)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
if (agent === 'cursor') {
|
|
148
|
+
await configureCursor(options)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`不支持的客户端:${agent}`)
|
|
152
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const PACKAGE_NAME = '@bigbrain-work/mcp-connect'
|
|
2
|
+
export const PACKAGE_VERSION = '1.0.0'
|
|
3
|
+
export const SERVER_NAME = 'shiliu_mcp'
|
|
4
|
+
export const MCP_URL = 'https://api.bigbrain.work/shiliu/mcp'
|
|
5
|
+
export const API_KEY_ENV = 'SHILIU_AI_API_KEY'
|
|
6
|
+
export const SUPPORTED_AGENTS = ['codex', 'claude', 'cursor']
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { API_KEY_ENV } from './constants.js'
|
|
8
|
+
|
|
9
|
+
function validateApiKey(value) {
|
|
10
|
+
const apiKey = value?.trim()
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
throw new Error('API Key 不能为空')
|
|
13
|
+
}
|
|
14
|
+
if (/\s/.test(apiKey)) {
|
|
15
|
+
throw new Error('API Key 不能包含空白字符')
|
|
16
|
+
}
|
|
17
|
+
if (apiKey.length < 12) {
|
|
18
|
+
throw new Error('API Key 长度异常,请确认复制完整')
|
|
19
|
+
}
|
|
20
|
+
return apiKey
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function readHiddenInput(promptText, input = process.stdin, output = process.stdout) {
|
|
24
|
+
if (!input.isTTY || typeof input.setRawMode !== 'function') {
|
|
25
|
+
throw new Error(`当前终端不支持隐藏输入,请先设置 ${API_KEY_ENV} 环境变量后重试`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
output.write(promptText)
|
|
29
|
+
input.setRawMode(true)
|
|
30
|
+
input.resume()
|
|
31
|
+
input.setEncoding('utf8')
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
let value = ''
|
|
35
|
+
|
|
36
|
+
const cleanup = () => {
|
|
37
|
+
input.off('data', onData)
|
|
38
|
+
input.setRawMode(false)
|
|
39
|
+
input.pause()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const onData = (chunk) => {
|
|
43
|
+
for (const character of chunk) {
|
|
44
|
+
if (character === '\u0003') {
|
|
45
|
+
cleanup()
|
|
46
|
+
output.write('\n')
|
|
47
|
+
reject(new Error('操作已取消'))
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
if (character === '\r' || character === '\n') {
|
|
51
|
+
cleanup()
|
|
52
|
+
output.write('\n')
|
|
53
|
+
resolve(value)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (character === '\u007f' || character === '\b') {
|
|
57
|
+
value = value.slice(0, -1)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
value += character
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
input.on('data', onData)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function resolveApiKey({ env = process.env, prompt = readHiddenInput } = {}) {
|
|
69
|
+
if (env[API_KEY_ENV]) {
|
|
70
|
+
return validateApiKey(env[API_KEY_ENV])
|
|
71
|
+
}
|
|
72
|
+
return validateApiKey(await prompt('请输入石榴 AI API Key(输入内容不会显示):'))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function runPowerShellWithInput(script, input) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const child = spawn('powershell.exe', [
|
|
78
|
+
'-NoLogo',
|
|
79
|
+
'-NoProfile',
|
|
80
|
+
'-NonInteractive',
|
|
81
|
+
'-Command',
|
|
82
|
+
script,
|
|
83
|
+
], {
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
let stderr = ''
|
|
89
|
+
child.stderr.setEncoding('utf8')
|
|
90
|
+
child.stderr.on('data', (chunk) => {
|
|
91
|
+
stderr += chunk
|
|
92
|
+
})
|
|
93
|
+
child.on('error', reject)
|
|
94
|
+
child.on('close', (code) => {
|
|
95
|
+
if (code === 0) {
|
|
96
|
+
resolve()
|
|
97
|
+
} else {
|
|
98
|
+
reject(new Error(stderr.trim() || `PowerShell 返回退出码 ${code}`))
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
child.stdin.end(input)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function shellQuote(value) {
|
|
106
|
+
return `'${value.replaceAll("'", "'\\''")}'`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function persistPosixApiKey(apiKey, home, env) {
|
|
110
|
+
const configDir = path.join(home, '.config', 'shiliu-ai')
|
|
111
|
+
const envFile = path.join(configDir, 'env')
|
|
112
|
+
await mkdir(configDir, { recursive: true })
|
|
113
|
+
await writeFile(envFile, `export ${API_KEY_ENV}=${shellQuote(apiKey)}\n`, {
|
|
114
|
+
encoding: 'utf8',
|
|
115
|
+
mode: 0o600,
|
|
116
|
+
})
|
|
117
|
+
await chmod(envFile, 0o600)
|
|
118
|
+
|
|
119
|
+
const shellName = path.basename(env.SHELL || '')
|
|
120
|
+
const profileName = shellName === 'zsh'
|
|
121
|
+
? '.zprofile'
|
|
122
|
+
: shellName === 'bash'
|
|
123
|
+
? '.bash_profile'
|
|
124
|
+
: '.profile'
|
|
125
|
+
const profilePath = path.join(home, profileName)
|
|
126
|
+
const sourceLine = `. ${shellQuote(envFile)} # shiliu-ai\n`
|
|
127
|
+
let profile = ''
|
|
128
|
+
try {
|
|
129
|
+
profile = await readFile(profilePath, 'utf8')
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error.code !== 'ENOENT') {
|
|
132
|
+
throw error
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!profile.includes('# shiliu-ai')) {
|
|
136
|
+
const separator = profile && !profile.endsWith('\n') ? '\n' : ''
|
|
137
|
+
await writeFile(profilePath, `${profile}${separator}${sourceLine}`, 'utf8')
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function persistApiKey(apiKey, {
|
|
142
|
+
dryRun = false,
|
|
143
|
+
home,
|
|
144
|
+
platform = process.platform,
|
|
145
|
+
env = process.env,
|
|
146
|
+
} = {}) {
|
|
147
|
+
if (dryRun) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (platform === 'win32') {
|
|
152
|
+
const script = `$value = [Console]::In.ReadToEnd(); [Environment]::SetEnvironmentVariable('${API_KEY_ENV}', $value, 'User')`
|
|
153
|
+
await runPowerShellWithInput(script, apiKey)
|
|
154
|
+
} else {
|
|
155
|
+
await persistPosixApiKey(apiKey, home, env)
|
|
156
|
+
}
|
|
157
|
+
process.env[API_KEY_ENV] = apiKey
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function readPersistedApiKey({
|
|
161
|
+
platform = process.platform,
|
|
162
|
+
env = process.env,
|
|
163
|
+
home,
|
|
164
|
+
} = {}) {
|
|
165
|
+
if (env[API_KEY_ENV]) {
|
|
166
|
+
return env[API_KEY_ENV]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (platform === 'win32') {
|
|
170
|
+
const result = spawnSync('powershell.exe', [
|
|
171
|
+
'-NoLogo',
|
|
172
|
+
'-NoProfile',
|
|
173
|
+
'-NonInteractive',
|
|
174
|
+
'-Command',
|
|
175
|
+
`[Console]::Out.Write([Environment]::GetEnvironmentVariable('${API_KEY_ENV}', 'User'))`,
|
|
176
|
+
], {
|
|
177
|
+
windowsHide: true,
|
|
178
|
+
encoding: 'utf8',
|
|
179
|
+
})
|
|
180
|
+
return result.status === 0 ? result.stdout.trim() : ''
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const content = readFileSync(path.join(home, '.config', 'shiliu-ai', 'env'), 'utf8')
|
|
185
|
+
const prefix = `export ${API_KEY_ENV}=`
|
|
186
|
+
const line = content.split(/\r?\n/u).find((item) => item.startsWith(prefix))
|
|
187
|
+
if (!line) {
|
|
188
|
+
return ''
|
|
189
|
+
}
|
|
190
|
+
const encodedValue = line.slice(prefix.length)
|
|
191
|
+
if (encodedValue.startsWith("'") && encodedValue.endsWith("'")) {
|
|
192
|
+
return encodedValue.slice(1, -1).replaceAll("'\\''", "'")
|
|
193
|
+
}
|
|
194
|
+
return encodedValue
|
|
195
|
+
} catch {
|
|
196
|
+
return ''
|
|
197
|
+
}
|
|
198
|
+
}
|
package/src/status.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { API_KEY_ENV, MCP_URL, SERVER_NAME } from './constants.js'
|
|
5
|
+
import { readPersistedApiKey } from './credentials.js'
|
|
6
|
+
|
|
7
|
+
async function readText(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
return await readFile(filePath, 'utf8')
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error.code === 'ENOENT') {
|
|
12
|
+
return ''
|
|
13
|
+
}
|
|
14
|
+
throw error
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function hasServerConfig(filePath) {
|
|
19
|
+
const content = await readText(filePath)
|
|
20
|
+
return content.includes(SERVER_NAME) && content.includes(MCP_URL)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function inspectLocalConfiguration(home) {
|
|
24
|
+
return {
|
|
25
|
+
codex: await hasServerConfig(path.join(home, '.codex', 'config.toml')),
|
|
26
|
+
claude: await hasServerConfig(path.join(home, '.claude.json')),
|
|
27
|
+
cursor: await hasServerConfig(path.join(home, '.cursor', 'mcp.json')),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function probeMcp(apiKey, fetchImpl = fetch) {
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
detail: `${API_KEY_ENV} 尚未设置`,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetchImpl(MCP_URL, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
Accept: 'application/json, text/event-stream',
|
|
44
|
+
Authorization: `Bearer ${apiKey}`,
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
jsonrpc: '2.0',
|
|
49
|
+
id: 1,
|
|
50
|
+
method: 'initialize',
|
|
51
|
+
params: {
|
|
52
|
+
protocolVersion: '2025-03-26',
|
|
53
|
+
capabilities: {},
|
|
54
|
+
clientInfo: {
|
|
55
|
+
name: 'bigbrain-work-mcp-connect',
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
signal: AbortSignal.timeout(10000),
|
|
61
|
+
})
|
|
62
|
+
return {
|
|
63
|
+
ok: response.ok,
|
|
64
|
+
detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status},请检查 API Key`,
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
detail: `连接失败:${error.message}`,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function mark(ok) {
|
|
75
|
+
return ok ? '✓' : '○'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function printStatus({ home, platform = process.platform, env = process.env } = {}) {
|
|
79
|
+
const apiKey = readPersistedApiKey({ platform, env, home })
|
|
80
|
+
const configs = await inspectLocalConfiguration(home)
|
|
81
|
+
const remote = await probeMcp(apiKey)
|
|
82
|
+
|
|
83
|
+
console.log(`API Key 环境变量 ${mark(Boolean(apiKey))} ${apiKey ? '已设置' : '未设置'}`)
|
|
84
|
+
console.log(`Codex 配置 ${mark(configs.codex)} ${configs.codex ? '已写入' : '未发现'}`)
|
|
85
|
+
console.log(`Claude Code 配置 ${mark(configs.claude)} ${configs.claude ? '已写入' : '未发现'}`)
|
|
86
|
+
console.log(`Cursor 配置 ${mark(configs.cursor)} ${configs.cursor ? '已写入' : '未发现'}`)
|
|
87
|
+
console.log(`石榴 AI MCP ${mark(remote.ok)} ${remote.detail}`)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
apiKey: Boolean(apiKey),
|
|
91
|
+
configs,
|
|
92
|
+
remote,
|
|
93
|
+
}
|
|
94
|
+
}
|