@shaohui_jin/git-cockpit-mcp-server 0.1.22 → 0.1.24
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 +26 -2
- package/dist/chunk-HNARFTSW.js +405 -0
- package/dist/chunk-HNARFTSW.js.map +1 -0
- package/dist/{chunk-NUNR2ELF.js → chunk-UTGCX3CG.js} +208 -22
- package/dist/chunk-UTGCX3CG.js.map +1 -0
- package/dist/{chunk-GPUMKDZD.js → chunk-ZVMQTPCD.js} +74 -12
- package/dist/chunk-ZVMQTPCD.js.map +1 -0
- package/dist/cli-entry.js +3 -3
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +18 -7
- package/dist/index.js +9 -3
- package/dist/mcpServer-5C8WQYy6.d.ts +44 -0
- package/dist/mcpServer.d.ts +4 -22
- package/dist/mcpServer.js +1 -1
- package/dist/{runtime-CtZr-J1V.d.ts → runtime-BHX-b-Hx.d.ts} +3 -1
- package/dist/web/assets/{index-o8Cz9AM1.js → index-BLrYXhIt.js} +86 -86
- package/dist/web/assets/{index-D7WYxiNm.css → index-DKgoWid0.css} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/webServer.d.ts +1 -1
- package/dist/webServer.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-GPUMKDZD.js.map +0 -1
- package/dist/chunk-NUNR2ELF.js.map +0 -1
- package/dist/chunk-SVQLF7LC.js +0 -72
- package/dist/chunk-SVQLF7LC.js.map +0 -1
package/README.md
CHANGED
|
@@ -22,12 +22,13 @@ npm install -g @shaohui_jin/git-cockpit-mcp-server
|
|
|
22
22
|
```bash
|
|
23
23
|
git-cockpit start # 启动常驻服务:Web UI + MCP Server(Streamable HTTP),默认 http://localhost:3000
|
|
24
24
|
git-cockpit mcp # 以 stdio 模式运行 MCP Server(供 Claude Desktop / Cursor 等客户端连接)
|
|
25
|
+
# 已有常驻服务时自动桥接过去,多客户端共用一个后端
|
|
25
26
|
git-cockpit version # 输出版本号
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
启动后浏览器访问 `http://localhost:3000` 即可使用 Web 界面;`/mcp` 端点提供 MCP Streamable HTTP 接入。
|
|
29
30
|
|
|
30
|
-
### 接入 MCP 客户端(stdio
|
|
31
|
+
### 接入 MCP 客户端(stdio,推荐)
|
|
31
32
|
|
|
32
33
|
以 Claude Desktop / Cursor 为例,在 MCP 配置中加入:
|
|
33
34
|
|
|
@@ -42,7 +43,28 @@ git-cockpit version # 输出版本号
|
|
|
42
43
|
}
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
**唯一 daemon 机制**:`git-cockpit mcp` 探活到已有 daemon 就转发 stdio 请求;没有 daemon 则由当前命令启动一个 daemon,再桥接到它。
|
|
47
|
+
客户端 stdin 关闭时,如果 daemon 是本命令启动的,会一并关闭;桥接失败不会静默创建第二套 Runtime。多个客户端因此可以稳定共用同一个 Git Cockpit、
|
|
48
|
+
任务队列与仓库锁,客户端配置里不需要写密钥。多仓库时先读取 `git-cockpit://repos`,再调用
|
|
49
|
+
`git_repo_select({ "repoId": 1 })` 绑定当前 MCP session;后续省略仓库参数时不会串到其他客户端的仓库。
|
|
50
|
+
|
|
51
|
+
### 接入 MCP 客户端(Streamable HTTP)
|
|
52
|
+
|
|
53
|
+
`/mcp` 与 `/api/*` 一样受本机密钥保护,URL 方式**必须带请求头**:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"git-cockpit-http": {
|
|
59
|
+
"url": "http://localhost:3000/mcp",
|
|
60
|
+
"headers": { "X-Git-Cockpit-Secret": "<你的 localSecret>" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
密钥在 `~/.git-cockpit/config.json` 的 `auth.localSecret`,也可在 Web 界面的「设置」里点 **复制本机访问密钥**。
|
|
67
|
+
密钥不对会得到 **401**;手写请求时 `Accept` 必须同时含 `application/json` 与 `text/event-stream`,否则 **406**。
|
|
46
68
|
|
|
47
69
|
**不要**和 Git Insight(或其它会直接改同一仓的 Git MCP)同时用于同一个仓库。
|
|
48
70
|
|
|
@@ -63,6 +85,8 @@ git-cockpit version # 输出版本号
|
|
|
63
85
|
|
|
64
86
|
宿主可插入这四条 Prompt(`safe_commit` / `merge_preview_apply` / `workspace_continue` / `open_mr`),正文即上面四段。只读状态也可读 Resource:`git-cockpit://repos`、`git-cockpit://repo/current`、`git-cockpit://jobs`、`git-cockpit://jobs/{id}`。
|
|
65
87
|
|
|
88
|
+
多仓库时先读 `git-cockpit://repos` 拿 `repoId`,再 `git_repo_select({ "repoId": 1 })` 绑定当前 MCP session;之后省略 `repoId/repoPath` 的工具都作用在这个仓。要临时操作别的仓,单次调用传 `repoId` 即可,不会改变绑定。
|
|
89
|
+
|
|
66
90
|
默认只给摘要;要某文件正文加 `path` 或 `detail=true`。
|
|
67
91
|
|
|
68
92
|
### 环境变量
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ConfigStore,
|
|
4
|
+
createRuntime,
|
|
5
|
+
createWebServer,
|
|
6
|
+
disposeRuntime
|
|
7
|
+
} from "./chunk-ZVMQTPCD.js";
|
|
8
|
+
import {
|
|
9
|
+
formatDaemonUrl,
|
|
10
|
+
resolveDaemonEndpoint,
|
|
11
|
+
startMcpStdio,
|
|
12
|
+
version
|
|
13
|
+
} from "./chunk-UTGCX3CG.js";
|
|
14
|
+
|
|
15
|
+
// src/mcpBridge.ts
|
|
16
|
+
var SECRET_HEADER = "X-Git-Cockpit-Secret";
|
|
17
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
18
|
+
var DEFAULT_MAX_QUEUE = 100;
|
|
19
|
+
var DEFAULT_RECONNECT_ATTEMPTS = 2;
|
|
20
|
+
var DEFAULT_RECONNECT_DELAY_MS = 250;
|
|
21
|
+
function parseMessage(line) {
|
|
22
|
+
try {
|
|
23
|
+
const value = JSON.parse(line);
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
25
|
+
return value;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function errorResponse(id, message) {
|
|
31
|
+
return JSON.stringify({ jsonrpc: "2.0", error: { code: -32e3, message }, id: id ?? null });
|
|
32
|
+
}
|
|
33
|
+
function extractSsePayload(text) {
|
|
34
|
+
let last = null;
|
|
35
|
+
for (const raw of text.split("\n")) {
|
|
36
|
+
const line = raw.trim();
|
|
37
|
+
if (!line.startsWith("data:")) continue;
|
|
38
|
+
const payload = line.slice(5).trim();
|
|
39
|
+
if (!payload || payload === "[DONE]") continue;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(payload);
|
|
42
|
+
if (parsed && typeof parsed === "object") last = payload;
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return last;
|
|
47
|
+
}
|
|
48
|
+
function wait(ms) {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
function timeoutSignal(ms) {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timer = setTimeout(() => controller.abort(new Error(`\u8BF7\u6C42\u8D85\u65F6\uFF08${ms}ms\uFF09`)), ms);
|
|
54
|
+
return { signal: controller.signal, cancel: () => clearTimeout(timer) };
|
|
55
|
+
}
|
|
56
|
+
function createMcpForwarder(options) {
|
|
57
|
+
const url = formatDaemonUrl(options.host, options.port, "/mcp");
|
|
58
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
59
|
+
const reconnectAttempts = options.reconnectAttempts ?? DEFAULT_RECONNECT_ATTEMPTS;
|
|
60
|
+
const reconnectDelayMs = options.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;
|
|
61
|
+
let sessionId = "";
|
|
62
|
+
let sseStarted = false;
|
|
63
|
+
let closed = false;
|
|
64
|
+
let sseAbort = null;
|
|
65
|
+
const baseHeaders = () => {
|
|
66
|
+
const headers = {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
// SDK 要求 POST 的 Accept 同时列出两者,否则 406
|
|
69
|
+
Accept: "application/json, text/event-stream",
|
|
70
|
+
[SECRET_HEADER]: options.secret
|
|
71
|
+
};
|
|
72
|
+
if (sessionId) headers["Mcp-Session-Id"] = sessionId;
|
|
73
|
+
return headers;
|
|
74
|
+
};
|
|
75
|
+
const fetchWithRetry = async (init) => {
|
|
76
|
+
let lastError;
|
|
77
|
+
for (let attempt = 0; attempt <= reconnectAttempts; attempt += 1) {
|
|
78
|
+
const timed = timeoutSignal(timeoutMs);
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(url, { ...init, signal: timed.signal });
|
|
81
|
+
if (response.status >= 500 && response.status <= 504 && attempt < reconnectAttempts) {
|
|
82
|
+
await response.arrayBuffer().catch(() => void 0);
|
|
83
|
+
await wait(reconnectDelayMs * (attempt + 1));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
return response;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
lastError = err;
|
|
89
|
+
if (attempt >= reconnectAttempts) break;
|
|
90
|
+
await wait(reconnectDelayMs * (attempt + 1));
|
|
91
|
+
} finally {
|
|
92
|
+
timed.cancel();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
96
|
+
};
|
|
97
|
+
const writeSseEvent = (data) => {
|
|
98
|
+
const payload = extractSsePayload(`data: ${data}`);
|
|
99
|
+
if (payload && !closed) process.stdout.write(`${payload}
|
|
100
|
+
`);
|
|
101
|
+
};
|
|
102
|
+
const pumpSse = async () => {
|
|
103
|
+
const maxFailures = 5;
|
|
104
|
+
let failures = 0;
|
|
105
|
+
while (!closed && sessionId) {
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
sseAbort = controller;
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(url, {
|
|
110
|
+
method: "GET",
|
|
111
|
+
headers: {
|
|
112
|
+
Accept: "text/event-stream",
|
|
113
|
+
[SECRET_HEADER]: options.secret,
|
|
114
|
+
"Mcp-Session-Id": sessionId
|
|
115
|
+
},
|
|
116
|
+
signal: controller.signal
|
|
117
|
+
});
|
|
118
|
+
if (!res.ok || !res.body) {
|
|
119
|
+
if (res.status === 401 || res.status === 403 || res.status === 404) return;
|
|
120
|
+
failures += 1;
|
|
121
|
+
if (failures >= maxFailures) return;
|
|
122
|
+
await wait(reconnectDelayMs);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
failures = 0;
|
|
126
|
+
const reader = res.body.getReader();
|
|
127
|
+
const decoder = new TextDecoder();
|
|
128
|
+
let buffer = "";
|
|
129
|
+
while (!closed) {
|
|
130
|
+
const chunk = await reader.read();
|
|
131
|
+
if (chunk.done) break;
|
|
132
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
133
|
+
let split = buffer.indexOf("\n\n");
|
|
134
|
+
while (split >= 0) {
|
|
135
|
+
const event = buffer.slice(0, split);
|
|
136
|
+
buffer = buffer.slice(split + 2);
|
|
137
|
+
const data = event.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
|
|
138
|
+
if (data) writeSseEvent(data);
|
|
139
|
+
split = buffer.indexOf("\n\n");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!closed) await wait(reconnectDelayMs);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (closed) return;
|
|
145
|
+
failures += 1;
|
|
146
|
+
if (failures >= maxFailures) {
|
|
147
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
148
|
+
process.stderr.write(
|
|
149
|
+
`[git-cockpit] MCP \u4E3B\u52A8\u901A\u77E5\u901A\u9053\u5DF2\u65AD\u5F00\uFF08\u8FDE\u7EED ${failures} \u6B21\u5931\u8D25\uFF1A${detail}\uFF09\uFF1B\u8BF7\u6C42-\u54CD\u5E94\u4ECD\u53EF\u7528\u3002
|
|
150
|
+
`
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await wait(reconnectDelayMs);
|
|
155
|
+
} finally {
|
|
156
|
+
if (sseAbort === controller) sseAbort = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const ensureSse = () => {
|
|
161
|
+
if (sseStarted || !sessionId) return;
|
|
162
|
+
sseStarted = true;
|
|
163
|
+
void pumpSse();
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
sessionId: () => sessionId,
|
|
167
|
+
async preflight() {
|
|
168
|
+
let res;
|
|
169
|
+
try {
|
|
170
|
+
res = await fetchWithRetry({
|
|
171
|
+
method: "GET",
|
|
172
|
+
headers: { Accept: "text/event-stream", [SECRET_HEADER]: options.secret }
|
|
173
|
+
});
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
176
|
+
throw new Error(`\u65E0\u6CD5\u8FDE\u63A5 daemon\uFF08${detail}\uFF09`);
|
|
177
|
+
}
|
|
178
|
+
await res.text().catch(() => "");
|
|
179
|
+
if (res.status === 401) throw new Error("daemon \u62D2\u7EDD\u5BC6\u94A5\uFF08HTTP 401\uFF09\uFF0C\u8BF7\u786E\u8BA4\u4E24\u7AEF\u4F7F\u7528\u540C\u4E00\u6570\u636E\u76EE\u5F55");
|
|
180
|
+
if (res.status === 403) throw new Error("daemon \u62D2\u7EDD\u8BF7\u6C42\u6765\u6E90\uFF08HTTP 403\uFF09\uFF0CHost \u6821\u9A8C\u672A\u901A\u8FC7");
|
|
181
|
+
if (res.status !== 400) throw new Error(`daemon MCP \u9884\u68C0\u5931\u8D25\uFF08HTTP ${res.status}\uFF09`);
|
|
182
|
+
},
|
|
183
|
+
async forward(line) {
|
|
184
|
+
const message = parseMessage(line);
|
|
185
|
+
if (!message) return errorResponse(null, "Invalid JSON-RPC message");
|
|
186
|
+
let res;
|
|
187
|
+
try {
|
|
188
|
+
res = await fetchWithRetry({ method: "POST", headers: baseHeaders(), body: line });
|
|
189
|
+
} catch (err) {
|
|
190
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
191
|
+
process.stderr.write(`[git-cockpit] \u8F6C\u53D1\u5230 daemon \u5931\u8D25\uFF1A${detail}
|
|
192
|
+
`);
|
|
193
|
+
return message.id === void 0 || message.id === null ? null : errorResponse(message.id, `\u65E0\u6CD5\u8FDE\u63A5 daemon\uFF1A${detail}`);
|
|
194
|
+
}
|
|
195
|
+
const sid = res.headers.get("mcp-session-id");
|
|
196
|
+
if (sid) {
|
|
197
|
+
sessionId = sid;
|
|
198
|
+
ensureSse();
|
|
199
|
+
}
|
|
200
|
+
const raw = await res.text();
|
|
201
|
+
const text = raw.trim();
|
|
202
|
+
if (text) {
|
|
203
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
204
|
+
const payload = contentType.includes("text/event-stream") ? extractSsePayload(text) : text;
|
|
205
|
+
if (payload) return payload;
|
|
206
|
+
}
|
|
207
|
+
if (!res.ok && message.id !== void 0 && message.id !== null) {
|
|
208
|
+
return errorResponse(message.id, `daemon \u8FD4\u56DE HTTP ${res.status}`);
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
},
|
|
212
|
+
async close() {
|
|
213
|
+
closed = true;
|
|
214
|
+
sseAbort?.abort();
|
|
215
|
+
if (sessionId) {
|
|
216
|
+
const timed = timeoutSignal(timeoutMs);
|
|
217
|
+
try {
|
|
218
|
+
await fetch(url, {
|
|
219
|
+
method: "DELETE",
|
|
220
|
+
headers: { Accept: "application/json", [SECRET_HEADER]: options.secret, "Mcp-Session-Id": sessionId },
|
|
221
|
+
signal: timed.signal
|
|
222
|
+
});
|
|
223
|
+
} catch {
|
|
224
|
+
} finally {
|
|
225
|
+
timed.cancel();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function startMcpBridge(options) {
|
|
232
|
+
const forwarder = createMcpForwarder(options);
|
|
233
|
+
await forwarder.preflight();
|
|
234
|
+
let buffer = "";
|
|
235
|
+
let pending = 0;
|
|
236
|
+
let tail = Promise.resolve();
|
|
237
|
+
const maxQueue = options.maxQueue ?? DEFAULT_MAX_QUEUE;
|
|
238
|
+
const enqueue = (line) => {
|
|
239
|
+
if (pending >= maxQueue) {
|
|
240
|
+
const message = parseMessage(line);
|
|
241
|
+
if (message?.id !== void 0 && message.id !== null) {
|
|
242
|
+
process.stdout.write(`${errorResponse(message.id, `MCP bridge \u961F\u5217\u5DF2\u6EE1\uFF08\u4E0A\u9650 ${maxQueue}\uFF09`)}
|
|
243
|
+
`);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
pending += 1;
|
|
248
|
+
tail = tail.then(async () => {
|
|
249
|
+
try {
|
|
250
|
+
const out = await forwarder.forward(line);
|
|
251
|
+
if (out) process.stdout.write(`${out}
|
|
252
|
+
`);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
const message = parseMessage(line);
|
|
255
|
+
if (message?.id !== void 0 && message.id !== null) {
|
|
256
|
+
process.stdout.write(`${errorResponse(message.id, err instanceof Error ? err.message : String(err))}
|
|
257
|
+
`);
|
|
258
|
+
}
|
|
259
|
+
} finally {
|
|
260
|
+
pending -= 1;
|
|
261
|
+
}
|
|
262
|
+
}, async () => {
|
|
263
|
+
pending -= 1;
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
process.stdin.setEncoding("utf8");
|
|
267
|
+
process.stdin.on("data", (chunk) => {
|
|
268
|
+
buffer += chunk;
|
|
269
|
+
let idx = buffer.indexOf("\n");
|
|
270
|
+
while (idx >= 0) {
|
|
271
|
+
const line = buffer.slice(0, idx).trim();
|
|
272
|
+
buffer = buffer.slice(idx + 1);
|
|
273
|
+
if (line) enqueue(line);
|
|
274
|
+
idx = buffer.indexOf("\n");
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
process.stdin.resume();
|
|
278
|
+
try {
|
|
279
|
+
await new Promise((resolve) => {
|
|
280
|
+
const done = () => resolve();
|
|
281
|
+
process.stdin.on("end", done);
|
|
282
|
+
process.stdin.on("close", done);
|
|
283
|
+
process.once("SIGINT", done);
|
|
284
|
+
process.once("SIGTERM", done);
|
|
285
|
+
});
|
|
286
|
+
await tail;
|
|
287
|
+
} finally {
|
|
288
|
+
await forwarder.close();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/cli.ts
|
|
293
|
+
async function probeDaemon(url, timeoutMs = 1e3) {
|
|
294
|
+
try {
|
|
295
|
+
return (await fetch(url, { signal: AbortSignal.timeout(timeoutMs) })).ok;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function waitForDaemon(url, attempts = 10) {
|
|
301
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
302
|
+
if (await probeDaemon(url)) return true;
|
|
303
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
function printHelp() {
|
|
308
|
+
console.log(`Git Cockpit - \u57FA\u4E8E MCP \u7684 Git \u53EF\u89C6\u5316\u64CD\u4F5C\u5DE5\u5177
|
|
309
|
+
|
|
310
|
+
\u7528\u6CD5:
|
|
311
|
+
git-cockpit start \u542F\u52A8 daemon\uFF1AWeb \u754C\u9762 + MCP Server\uFF08Streamable HTTP\uFF09
|
|
312
|
+
git-cockpit mcp \u8FDE\u63A5\u6216\u542F\u52A8\u552F\u4E00 daemon\uFF0C\u5E76\u6865\u63A5 stdio MCP
|
|
313
|
+
\u5DF2\u6709 daemon \u65F6\u590D\u7528\uFF1B\u6CA1\u6709 daemon \u65F6\u7531\u672C\u547D\u4EE4\u542F\u52A8\uFF0C\u9000\u51FA\u65F6\u4E00\u5E76\u5173\u95ED
|
|
314
|
+
git-cockpit mcp --standalone
|
|
315
|
+
\u663E\u5F0F\u81EA\u5EFA\u540E\u7AEF\uFF08\u6BCF\u4E2A\u5BA2\u6237\u7AEF\u4E00\u4EFD\u961F\u5217\u4E0E\u9501\uFF0C\u4E00\u822C\u4E0D\u9700\u8981\uFF09
|
|
316
|
+
git-cockpit version \u8F93\u51FA\u7248\u672C\u53F7
|
|
317
|
+
git-cockpit help \u663E\u793A\u5E2E\u52A9
|
|
318
|
+
|
|
319
|
+
\u73AF\u5883\u53D8\u91CF:
|
|
320
|
+
GIT_COCKPIT_DATA_DIR \u6570\u636E\u76EE\u5F55\uFF08\u9ED8\u8BA4 ~/.git-cockpit\uFF09
|
|
321
|
+
GIT_COCKPIT_PORT \u7AEF\u53E3\uFF08\u9ED8\u8BA4 3000\uFF09
|
|
322
|
+
GIT_COCKPIT_HOST \u76D1\u542C\u5730\u5740\uFF08\u9ED8\u8BA4 localhost\uFF09
|
|
323
|
+
GIT_COCKPIT_LLM_API_KEY \u5F00\u53D1\u53EF\u9009\uFF1A\u8986\u76D6\u8BBE\u7F6E\u91CC\u7684\u6A21\u578B Key\uFF0C\u4E0D\u5199\u56DE config.json
|
|
324
|
+
GIT_COCKPIT_LLM_MODEL \u5F00\u53D1\u53EF\u9009\uFF1A\u8986\u76D6\u6A21\u578B\u540D
|
|
325
|
+
GIT_COCKPIT_LLM_BASE_URL \u5F00\u53D1\u53EF\u9009\uFF1A\u8986\u76D6 OpenAI \u517C\u5BB9 Base URL
|
|
326
|
+
`);
|
|
327
|
+
}
|
|
328
|
+
async function main(argv) {
|
|
329
|
+
const args = argv.slice(2);
|
|
330
|
+
const command = args[0] ?? "start";
|
|
331
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
332
|
+
printHelp();
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
336
|
+
console.log(version);
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
const dataDir = process.env.GIT_COCKPIT_DATA_DIR ?? "~/.git-cockpit";
|
|
340
|
+
if (command === "mcp") {
|
|
341
|
+
const config = new ConfigStore(dataDir).get();
|
|
342
|
+
const endpoint = resolveDaemonEndpoint(config);
|
|
343
|
+
const secret = config.auth.localSecret;
|
|
344
|
+
if (args.includes("--standalone")) {
|
|
345
|
+
process.stderr.write(
|
|
346
|
+
"[git-cockpit] --standalone\uFF1A\u672C\u8FDB\u7A0B\u81EA\u5EFA\u540E\u7AEF\uFF0C\u591A\u4E2A\u5BA2\u6237\u7AEF\u5C06\u5404\u81EA\u6301\u6709\u4E00\u4EFD\u961F\u5217\u4E0E\u4ED3\u5E93\u9501\u3002\n"
|
|
347
|
+
);
|
|
348
|
+
await startMcpStdio(createRuntime({ dataDir }));
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
if (await probeDaemon(endpoint.healthUrl)) {
|
|
352
|
+
await startMcpBridge({ host: endpoint.host, port: endpoint.port, secret });
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
const ownedRuntime = createRuntime({ dataDir });
|
|
356
|
+
let ownedServer;
|
|
357
|
+
try {
|
|
358
|
+
ownedServer = await createWebServer(ownedRuntime, { host: endpoint.host, port: endpoint.port });
|
|
359
|
+
} catch (err) {
|
|
360
|
+
disposeRuntime(ownedRuntime);
|
|
361
|
+
if (await waitForDaemon(endpoint.healthUrl)) {
|
|
362
|
+
await startMcpBridge({ host: endpoint.host, port: endpoint.port, secret });
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
if (!await waitForDaemon(endpoint.healthUrl)) {
|
|
369
|
+
throw new Error(`daemon \u542F\u52A8\u540E\u5065\u5EB7\u68C0\u67E5\u672A\u901A\u8FC7\uFF1A${endpoint.healthUrl}`);
|
|
370
|
+
}
|
|
371
|
+
process.stderr.write(`[git-cockpit] \u672A\u53D1\u73B0 daemon\uFF0C\u5DF2\u7531\u5F53\u524D mcp \u547D\u4EE4\u542F\u52A8 ${endpoint.baseUrl}
|
|
372
|
+
`);
|
|
373
|
+
await startMcpBridge({ host: endpoint.host, port: endpoint.port, secret });
|
|
374
|
+
return 0;
|
|
375
|
+
} finally {
|
|
376
|
+
await ownedServer.close();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const runtime = createRuntime({ dataDir });
|
|
380
|
+
if (command === "start") {
|
|
381
|
+
const endpoint = resolveDaemonEndpoint(runtime.config);
|
|
382
|
+
const { host, port } = endpoint;
|
|
383
|
+
const server = await createWebServer(runtime, { host, port });
|
|
384
|
+
console.log(`[git-cockpit] Web UI: http://${host}:${port}`);
|
|
385
|
+
console.log(`[git-cockpit] API \u6587\u6863: http://${host}:${port}/docs`);
|
|
386
|
+
console.log(`[git-cockpit] MCP (HTTP): http://${host}:${port}/mcp`);
|
|
387
|
+
console.log(`[git-cockpit] \u6570\u636E\u76EE\u5F55: ${dataDir}`);
|
|
388
|
+
console.log(`[git-cockpit] Ctrl+C \u9000\u51FA`);
|
|
389
|
+
return await new Promise((resolve) => {
|
|
390
|
+
const shutdown = () => {
|
|
391
|
+
void server.close().then(() => resolve(0));
|
|
392
|
+
};
|
|
393
|
+
process.on("SIGINT", shutdown);
|
|
394
|
+
process.on("SIGTERM", shutdown);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
console.error(`\u672A\u77E5\u547D\u4EE4: ${command}`);
|
|
398
|
+
printHelp();
|
|
399
|
+
return 1;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export {
|
|
403
|
+
main
|
|
404
|
+
};
|
|
405
|
+
//# sourceMappingURL=chunk-HNARFTSW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mcpBridge.ts","../src/cli.ts"],"sourcesContent":["/**\n * stdio -> daemon 的 MCP 适配器。\n *\n * 正常情况下它不创建 Runtime、不打开 SQLite,只把一个客户端的 MCP session\n * 转发到唯一 daemon。HTTP bridge 具备:超时、有限重试、有限队列、异常隔离,\n * 并在 initialize 后订阅 GET /mcp SSE,把服务端主动通知写回 stdout。\n */\nimport { formatDaemonUrl } from './daemonEndpoint.ts';\n\nconst SECRET_HEADER = 'X-Git-Cockpit-Secret';\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst DEFAULT_MAX_QUEUE = 100;\nconst DEFAULT_RECONNECT_ATTEMPTS = 2;\nconst DEFAULT_RECONNECT_DELAY_MS = 250;\n\nexport interface McpForwarderOptions {\n host: string;\n port: number;\n secret: string;\n timeoutMs?: number;\n maxQueue?: number;\n reconnectAttempts?: number;\n reconnectDelayMs?: number;\n}\n\ninterface JsonRpcMessage {\n jsonrpc?: string;\n id?: string | number | null;\n method?: string;\n}\n\nexport interface McpForwarder {\n preflight(): Promise<void>;\n forward(line: string): Promise<string | null>;\n sessionId(): string;\n close(): Promise<void>;\n}\n\nfunction parseMessage(line: string): JsonRpcMessage | null {\n try {\n const value = JSON.parse(line) as unknown;\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null;\n return value as JsonRpcMessage;\n } catch {\n return null;\n }\n}\n\nfunction errorResponse(id: string | number | null, message: string): string {\n return JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message }, id: id ?? null });\n}\n\nfunction extractSsePayload(text: string): string | null {\n let last: string | null = null;\n for (const raw of text.split('\\n')) {\n const line = raw.trim();\n if (!line.startsWith('data:')) continue;\n const payload = line.slice(5).trim();\n if (!payload || payload === '[DONE]') continue;\n try {\n const parsed = JSON.parse(payload) as JsonRpcMessage;\n if (parsed && typeof parsed === 'object') last = payload;\n } catch {\n /* 忽略非 JSON data 行 */\n }\n }\n return last;\n}\n\nfunction wait(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction timeoutSignal(ms: number): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new Error(`请求超时(${ms}ms)`)), ms);\n return { signal: controller.signal, cancel: () => clearTimeout(timer) };\n}\n\nexport function createMcpForwarder(options: McpForwarderOptions): McpForwarder {\n const url = formatDaemonUrl(options.host, options.port, '/mcp');\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const reconnectAttempts = options.reconnectAttempts ?? DEFAULT_RECONNECT_ATTEMPTS;\n const reconnectDelayMs = options.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;\n let sessionId = '';\n let sseStarted = false;\n let closed = false;\n let sseAbort: AbortController | null = null;\n\n const baseHeaders = (): Record<string, string> => {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n // SDK 要求 POST 的 Accept 同时列出两者,否则 406\n Accept: 'application/json, text/event-stream',\n [SECRET_HEADER]: options.secret\n };\n if (sessionId) headers['Mcp-Session-Id'] = sessionId;\n return headers;\n };\n\n const fetchWithRetry = async (init: RequestInit): Promise<Response> => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= reconnectAttempts; attempt += 1) {\n const timed = timeoutSignal(timeoutMs);\n try {\n const response = await fetch(url, { ...init, signal: timed.signal });\n if (response.status >= 500 && response.status <= 504 && attempt < reconnectAttempts) {\n await response.arrayBuffer().catch(() => undefined);\n await wait(reconnectDelayMs * (attempt + 1));\n continue;\n }\n return response;\n } catch (err) {\n lastError = err;\n if (attempt >= reconnectAttempts) break;\n await wait(reconnectDelayMs * (attempt + 1));\n } finally {\n timed.cancel();\n }\n }\n throw lastError instanceof Error ? lastError : new Error(String(lastError));\n };\n\n const writeSseEvent = (data: string) => {\n const payload = extractSsePayload(`data: ${data}`);\n if (payload && !closed) process.stdout.write(`${payload}\\n`);\n };\n\n const pumpSse = async (): Promise<void> => {\n // daemon 彻底不可达时不要无限重连:连续失败到上限就停,避免空转刷日志。\n const maxFailures = 5;\n let failures = 0;\n while (!closed && sessionId) {\n const controller = new AbortController();\n sseAbort = controller;\n try {\n const res = await fetch(url, {\n method: 'GET',\n headers: {\n Accept: 'text/event-stream',\n [SECRET_HEADER]: options.secret,\n 'Mcp-Session-Id': sessionId\n },\n signal: controller.signal\n });\n if (!res.ok || !res.body) {\n // 401/403/404 说明鉴权或会话已失效,重连没有意义\n if (res.status === 401 || res.status === 403 || res.status === 404) return;\n failures += 1;\n if (failures >= maxFailures) return;\n await wait(reconnectDelayMs);\n continue;\n }\n failures = 0;\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n while (!closed) {\n const chunk = await reader.read();\n if (chunk.done) break;\n buffer += decoder.decode(chunk.value, { stream: true });\n let split = buffer.indexOf('\\n\\n');\n while (split >= 0) {\n const event = buffer.slice(0, split);\n buffer = buffer.slice(split + 2);\n const data = event\n .split(/\\r?\\n/)\n .filter((line) => line.startsWith('data:'))\n .map((line) => line.slice(5).trim())\n .join('\\n');\n if (data) writeSseEvent(data);\n split = buffer.indexOf('\\n\\n');\n }\n }\n if (!closed) await wait(reconnectDelayMs);\n } catch (err) {\n if (closed) return;\n failures += 1;\n if (failures >= maxFailures) {\n const detail = err instanceof Error ? err.message : String(err);\n process.stderr.write(\n `[git-cockpit] MCP 主动通知通道已断开(连续 ${failures} 次失败:${detail});请求-响应仍可用。\\n`\n );\n return;\n }\n await wait(reconnectDelayMs);\n } finally {\n if (sseAbort === controller) sseAbort = null;\n }\n }\n };\n\n const ensureSse = () => {\n if (sseStarted || !sessionId) return;\n sseStarted = true;\n void pumpSse();\n };\n\n return {\n sessionId: () => sessionId,\n\n async preflight(): Promise<void> {\n let res: Response;\n try {\n // GET 无 session 会由 MCP handler 返回 400,但不会创建 transport;鉴权仍会执行。\n res = await fetchWithRetry({\n method: 'GET',\n headers: { Accept: 'text/event-stream', [SECRET_HEADER]: options.secret }\n });\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`无法连接 daemon(${detail})`);\n }\n await res.text().catch(() => '');\n if (res.status === 401) throw new Error('daemon 拒绝密钥(HTTP 401),请确认两端使用同一数据目录');\n if (res.status === 403) throw new Error('daemon 拒绝请求来源(HTTP 403),Host 校验未通过');\n if (res.status !== 400) throw new Error(`daemon MCP 预检失败(HTTP ${res.status})`);\n },\n\n async forward(line: string): Promise<string | null> {\n const message = parseMessage(line);\n if (!message) return errorResponse(null, 'Invalid JSON-RPC message');\n\n let res: Response;\n try {\n res = await fetchWithRetry({ method: 'POST', headers: baseHeaders(), body: line });\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n process.stderr.write(`[git-cockpit] 转发到 daemon 失败:${detail}\\n`);\n return message.id === undefined || message.id === null ? null : errorResponse(message.id, `无法连接 daemon:${detail}`);\n }\n\n const sid = res.headers.get('mcp-session-id');\n if (sid) {\n sessionId = sid;\n ensureSse();\n }\n\n const raw = await res.text();\n const text = raw.trim();\n if (text) {\n const contentType = res.headers.get('content-type') ?? '';\n const payload = contentType.includes('text/event-stream') ? extractSsePayload(text) : text;\n if (payload) return payload;\n }\n if (!res.ok && message.id !== undefined && message.id !== null) {\n return errorResponse(message.id, `daemon 返回 HTTP ${res.status}`);\n }\n return null;\n },\n\n async close(): Promise<void> {\n closed = true;\n sseAbort?.abort();\n if (sessionId) {\n const timed = timeoutSignal(timeoutMs);\n try {\n await fetch(url, {\n method: 'DELETE',\n headers: { Accept: 'application/json', [SECRET_HEADER]: options.secret, 'Mcp-Session-Id': sessionId },\n signal: timed.signal\n });\n } catch {\n /* daemon 已断开时无需阻塞客户端退出 */\n } finally {\n timed.cancel();\n }\n }\n }\n };\n}\n\nexport async function startMcpBridge(options: McpForwarderOptions): Promise<void> {\n const forwarder = createMcpForwarder(options);\n await forwarder.preflight();\n let buffer = '';\n let pending = 0;\n let tail: Promise<void> = Promise.resolve();\n const maxQueue = options.maxQueue ?? DEFAULT_MAX_QUEUE;\n\n const enqueue = (line: string) => {\n if (pending >= maxQueue) {\n const message = parseMessage(line);\n if (message?.id !== undefined && message.id !== null) {\n process.stdout.write(`${errorResponse(message.id, `MCP bridge 队列已满(上限 ${maxQueue})`)}\\n`);\n }\n return;\n }\n pending += 1;\n tail = tail\n .then(async () => {\n try {\n const out = await forwarder.forward(line);\n if (out) process.stdout.write(`${out}\\n`);\n } catch (err) {\n const message = parseMessage(line);\n if (message?.id !== undefined && message.id !== null) {\n process.stdout.write(`${errorResponse(message.id, err instanceof Error ? err.message : String(err))}\\n`);\n }\n } finally {\n pending -= 1;\n }\n }, async () => {\n pending -= 1;\n });\n };\n\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => {\n buffer += chunk;\n let idx = buffer.indexOf('\\n');\n while (idx >= 0) {\n const line = buffer.slice(0, idx).trim();\n buffer = buffer.slice(idx + 1);\n if (line) enqueue(line);\n idx = buffer.indexOf('\\n');\n }\n });\n process.stdin.resume();\n\n try {\n await new Promise<void>((resolve) => {\n const done = () => resolve();\n process.stdin.on('end', done);\n process.stdin.on('close', done);\n process.once('SIGINT', done);\n process.once('SIGTERM', done);\n });\n await tail;\n } finally {\n await forwarder.close();\n }\n}\n","/**\n * CLI:`git-cockpit` 入口。\n * - git-cockpit start 启动常驻 daemon(Web 服务 + MCP Streamable HTTP),默认端口 3000;\n * - git-cockpit mcp 连接或启动唯一 daemon,并把 stdio 桥接到它;\n * 多客户端共用一个后端,当前命令启动的 daemon 随 stdin 关闭;\n * - git-cockpit version / --help。\n */\nimport { createRuntime, disposeRuntime } from './runtime.ts';\nimport { createWebServer } from './webServer.ts';\nimport { startMcpStdio } from './mcpServer.ts';\nimport { startMcpBridge } from './mcpBridge.ts';\nimport { ConfigStore } from './config.ts';\nimport { resolveDaemonEndpoint } from './daemonEndpoint.ts';\nimport { version } from '../package.json'\n\nasync function probeDaemon(url: string, timeoutMs = 1_000): Promise<boolean> {\n try {\n return (await fetch(url, { signal: AbortSignal.timeout(timeoutMs) })).ok;\n } catch {\n return false;\n }\n}\n\nasync function waitForDaemon(url: string, attempts = 10): Promise<boolean> {\n for (let i = 0; i < attempts; i += 1) {\n if (await probeDaemon(url)) return true;\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n return false;\n}\n\nfunction printHelp(): void {\n console.log(`Git Cockpit - 基于 MCP 的 Git 可视化操作工具\n\n用法:\n git-cockpit start 启动 daemon:Web 界面 + MCP Server(Streamable HTTP)\n git-cockpit mcp 连接或启动唯一 daemon,并桥接 stdio MCP\n 已有 daemon 时复用;没有 daemon 时由本命令启动,退出时一并关闭\n git-cockpit mcp --standalone\n 显式自建后端(每个客户端一份队列与锁,一般不需要)\n git-cockpit version 输出版本号\n git-cockpit help 显示帮助\n\n环境变量:\n GIT_COCKPIT_DATA_DIR 数据目录(默认 ~/.git-cockpit)\n GIT_COCKPIT_PORT 端口(默认 3000)\n GIT_COCKPIT_HOST 监听地址(默认 localhost)\n GIT_COCKPIT_LLM_API_KEY 开发可选:覆盖设置里的模型 Key,不写回 config.json\n GIT_COCKPIT_LLM_MODEL 开发可选:覆盖模型名\n GIT_COCKPIT_LLM_BASE_URL 开发可选:覆盖 OpenAI 兼容 Base URL\n`);\n}\n\nexport async function main(argv: string[]): Promise<number> {\n const args = argv.slice(2);\n const command = args[0] ?? 'start';\n\n if (command === 'help' || command === '--help' || command === '-h') {\n printHelp();\n return 0;\n }\n if (command === 'version' || command === '--version' || command === '-v') {\n console.log(version);\n return 0;\n }\n\n const dataDir = process.env.GIT_COCKPIT_DATA_DIR ?? '~/.git-cockpit';\n\n if (command === 'mcp') {\n // 只用 ConfigStore 读取 endpoint 与密钥;已有 daemon 时本进程不打开 SQLite。\n const config = new ConfigStore(dataDir).get();\n const endpoint = resolveDaemonEndpoint(config);\n const secret = config.auth.localSecret;\n\n // 显式逃生舱:确实需要每个客户端一份后端时才用(会失去全局队列与仓库锁)。\n if (args.includes('--standalone')) {\n process.stderr.write(\n '[git-cockpit] --standalone:本进程自建后端,多个客户端将各自持有一份队列与仓库锁。\\n'\n );\n await startMcpStdio(createRuntime({ dataDir }));\n return 0;\n }\n\n if (await probeDaemon(endpoint.healthUrl)) {\n // 已有 daemon:只桥接,绝不创建第二套 Runtime。\n await startMcpBridge({ host: endpoint.host, port: endpoint.port, secret });\n return 0;\n }\n\n // 没有 daemon:由当前命令启动唯一 daemon,随后仍走同一个 HTTP bridge。\n const ownedRuntime = createRuntime({ dataDir });\n let ownedServer: Awaited<ReturnType<typeof createWebServer>> | undefined;\n try {\n ownedServer = await createWebServer(ownedRuntime, { host: endpoint.host, port: endpoint.port });\n } catch (err) {\n disposeRuntime(ownedRuntime);\n // 并发启动:另一个 mcp 进程刚抢到端口,退回去桥接它,而不是再起一套后端。\n if (await waitForDaemon(endpoint.healthUrl)) {\n await startMcpBridge({ host: endpoint.host, port: endpoint.port, secret });\n return 0;\n }\n throw err;\n }\n\n try {\n if (!(await waitForDaemon(endpoint.healthUrl))) {\n throw new Error(`daemon 启动后健康检查未通过:${endpoint.healthUrl}`);\n }\n process.stderr.write(`[git-cockpit] 未发现 daemon,已由当前 mcp 命令启动 ${endpoint.baseUrl}\\n`);\n await startMcpBridge({ host: endpoint.host, port: endpoint.port, secret });\n return 0;\n } finally {\n // 只关闭自己启动的 daemon;外部 daemon 不受影响。\n await ownedServer.close();\n }\n }\n\n const runtime = createRuntime({ dataDir });\n\n if (command === 'start') {\n const endpoint = resolveDaemonEndpoint(runtime.config);\n const { host, port } = endpoint;\n const server = await createWebServer(runtime, { host, port });\n console.log(`[git-cockpit] Web UI: http://${host}:${port}`);\n console.log(`[git-cockpit] API 文档: http://${host}:${port}/docs`);\n console.log(`[git-cockpit] MCP (HTTP): http://${host}:${port}/mcp`);\n console.log(`[git-cockpit] 数据目录: ${dataDir}`);\n console.log(`[git-cockpit] Ctrl+C 退出`);\n return await new Promise<number>((resolve) => {\n const shutdown = () => {\n void server.close().then(() => resolve(0));\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n });\n }\n\n console.error(`未知命令: ${command}`);\n printHelp();\n return 1;\n}\n\nexport { createRuntime, disposeRuntime };"],"mappings":"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","names":[]}
|