@shaohui_jin/git-cockpit-mcp-server 0.1.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 ADDED
@@ -0,0 +1,75 @@
1
+ # @shaohui_jin/git-cockpit-mcp-server
2
+
3
+ Git Cockpit —— 基于 **MCP(Model Context Protocol)** 的 Git 可视化操作工具。一个包同时提供三种形态:**MCP Server**(供 AI 客户端调用 Git 操作)、**Web 管理界面**(浏览器可视化操作)、**CLI**(命令行启动)。支持多仓库管理、权限审批与 dry-run 预览等安全机制。
4
+
5
+ ## 环境要求
6
+
7
+ - Node.js **>= 22.5.0**(底层 core 包使用 `node:sqlite`)
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ # 全局安装(提供 git-cockpit 命令)
13
+ pnpm add -g @shaohui_jin/git-cockpit-mcp-server
14
+ # 或
15
+ npm install -g @shaohui_jin/git-cockpit-mcp-server
16
+ ```
17
+
18
+ 包为纯 ESM,内置 Web 前端(`dist/web`),无需额外安装静态资源。
19
+
20
+ ## 快速开始
21
+
22
+ ```bash
23
+ git-cockpit start # 启动常驻服务:Web UI + MCP Server(Streamable HTTP),默认 http://localhost:3000
24
+ git-cockpit mcp # 以 stdio 模式运行 MCP Server(供 Claude Desktop / Cursor 等客户端连接)
25
+ git-cockpit version # 输出版本号
26
+ ```
27
+
28
+ 启动后浏览器访问 `http://localhost:3000` 即可使用 Web 界面;`/mcp` 端点提供 MCP Streamable HTTP 接入。
29
+
30
+ ### 接入 MCP 客户端(stdio)
31
+
32
+ 以 Claude Desktop / Cursor 为例,在 MCP 配置中加入:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "git-cockpit": {
38
+ "command": "git-cockpit",
39
+ "args": ["mcp"]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ### 环境变量
46
+
47
+ | 变量 | 说明 | 默认 |
48
+ | --- | --- | --- |
49
+ | `GIT_COCKPIT_DATA_DIR` | 数据目录(仓库列表、审计日志、备份) | `~/.git-cockpit` |
50
+ | `GIT_COCKPIT_PORT` | Web 服务端口 | `3000` |
51
+ | `GIT_COCKPIT_HOST` | 监听地址 | `localhost` |
52
+
53
+ ## 主要特性
54
+
55
+ - **多仓库管理**:统一打开/移除仓库,跨仓库操作
56
+ - **安全机制**:工具按风险分级(readonly / write / dangerous),写操作 dry-run 预览 → 确认 → 执行;高危操作可配置人工审批
57
+ - **内置 Web UI**:状态 / 历史 / 日志 / 设置视图,支持提交、分支、stash、pull/push、硬重置等操作
58
+ - **审计日志**:所有工具调用记录落库,可回溯
59
+ - **备份**:高危操作前自动备份分支引用与 stash 快照
60
+
61
+ ## 发布说明(维护者)
62
+
63
+ 版本号维护在 `packages/mcp-server/package.json`,发布由仓库根目录 `.github/workflows/release-mcp-server.yml` 负责:
64
+
65
+ - **判据**:远程不存在 `mcp-server-v{version}` tag 才发布
66
+ - **流程**:构建 web → 构建 mcp-server(`prepublishOnly` 自动把 web/dist 内嵌进 `dist/web`)→ `pnpm publish` → 成功后才推送 tag `mcp-server-v{version}`
67
+ - **依赖**:mcp-server 依赖 `@shaohui_jin/git-cockpit-core`(`workspace:^`,发布时转为 `^0.1.0`),workflow 会先确保 core 已发布(缺失则顺带发布)
68
+ - **触发**:push 到 `master`/`main`,或 GitHub Actions 手动 `workflow_dispatch`
69
+ - **需要仓库 Secret**:`NPM_TOKEN`(npm Automation token)
70
+
71
+ 完整发布手册(含 tag 协议、版本管理、本地发布注意事项)见仓库内 [docs/release.md](../../docs/release.md)。
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,544 @@
1
+ import {
2
+ McpHttpHandler,
3
+ TOOL_DEF_MAP,
4
+ executeTool,
5
+ toolSummaries
6
+ } from "./chunk-HAXZI36Y.js";
7
+
8
+ // src/webServer.ts
9
+ import * as path3 from "path";
10
+ import * as fs2 from "fs";
11
+ import { fileURLToPath } from "url";
12
+ import Fastify from "fastify";
13
+ import cors from "@fastify/cors";
14
+ import fastifyStatic from "@fastify/static";
15
+ import {
16
+ GitOperationError,
17
+ PermissionManager as PermissionManager2,
18
+ RepoNotFoundError as RepoNotFoundError2
19
+ } from "@shaohui_jin/git-cockpit-core";
20
+
21
+ // src/runtime.ts
22
+ import { EventEmitter as EventEmitter2 } from "events";
23
+ import {
24
+ AuditLogger,
25
+ PermissionManager,
26
+ RepoStore as RepoStore2,
27
+ openDatabase
28
+ } from "@shaohui_jin/git-cockpit-core";
29
+
30
+ // src/config.ts
31
+ import * as fs from "fs";
32
+ import * as path from "path";
33
+ import { DEFAULT_CONFIG, expandHome } from "@shaohui_jin/git-cockpit-core";
34
+ function deepMerge(target, source) {
35
+ if (source === null || source === void 0) return target;
36
+ if (Array.isArray(source)) {
37
+ return source;
38
+ }
39
+ if (typeof source === "object" && typeof target === "object" && target !== null) {
40
+ const out = { ...target };
41
+ for (const [k, v] of Object.entries(source)) {
42
+ if (v !== void 0) out[k] = deepMerge(out[k], v);
43
+ }
44
+ return out;
45
+ }
46
+ return source;
47
+ }
48
+ function isPlainObject(v) {
49
+ return typeof v === "object" && v !== null && !Array.isArray(v);
50
+ }
51
+ var ConfigStore = class {
52
+ constructor(dataDir, overrides) {
53
+ this.dataDir = dataDir;
54
+ const dir = expandHome(dataDir);
55
+ const file = path.join(dir, "config.json");
56
+ this.config = {
57
+ ...this.load(file, overrides),
58
+ storage: { dataDir: dir }
59
+ };
60
+ }
61
+ dataDir;
62
+ config;
63
+ load(file, overrides) {
64
+ let fileConfig = {};
65
+ try {
66
+ if (fs.existsSync(file)) {
67
+ fileConfig = JSON.parse(fs.readFileSync(file, "utf8"));
68
+ }
69
+ } catch (err) {
70
+ console.warn(`[git-cockpit] \u914D\u7F6E\u6587\u4EF6 ${file} \u65E0\u6CD5\u89E3\u6790\uFF0C\u5DF2\u4F7F\u7528\u9ED8\u8BA4\u914D\u7F6E:`, err.message);
71
+ }
72
+ let merged = deepMerge(structuredClone(DEFAULT_CONFIG), fileConfig);
73
+ if (overrides) merged = deepMerge(merged, overrides);
74
+ return merged;
75
+ }
76
+ get() {
77
+ return this.config;
78
+ }
79
+ /** 合并并持久化;返回新配置 */
80
+ update(patch) {
81
+ this.config = deepMerge(this.config, patch);
82
+ this.save();
83
+ return this.config;
84
+ }
85
+ save() {
86
+ const dir = expandHome(this.config.storage.dataDir);
87
+ fs.mkdirSync(dir, { recursive: true });
88
+ const file = path.join(dir, "config.json");
89
+ fs.writeFileSync(file, JSON.stringify(this.config, null, 2), "utf8");
90
+ }
91
+ /** 供日志/调试:仅返回脱敏后的配置快照(隐藏敏感键名) */
92
+ snapshot() {
93
+ const copy = structuredClone(this.config);
94
+ const redact = (v) => {
95
+ if (isPlainObject(v)) {
96
+ const out = {};
97
+ for (const [k, val] of Object.entries(v)) {
98
+ const lower = k.toLowerCase();
99
+ if (/password|token|secret|authorization|url/i.test(lower)) out[k] = "[REDACTED]";
100
+ else out[k] = redact(val);
101
+ }
102
+ return out;
103
+ }
104
+ if (Array.isArray(v)) return v.map(redact);
105
+ return v;
106
+ };
107
+ return redact(copy);
108
+ }
109
+ };
110
+
111
+ // src/repoManager.ts
112
+ import "events";
113
+ import * as path2 from "path";
114
+ import {
115
+ GitService,
116
+ RepoNotFoundError
117
+ } from "@shaohui_jin/git-cockpit-core";
118
+ var RepoManager = class {
119
+ constructor(options) {
120
+ this.options = options;
121
+ this.eventBus = options.eventBus;
122
+ for (const repo of options.repoStore.list()) {
123
+ this.pathToId.set(normalizePath(repo.path), repo.id);
124
+ }
125
+ }
126
+ options;
127
+ /** repo 根路径(规范化绝对路径) -> repoId */
128
+ pathToId = /* @__PURE__ */ new Map();
129
+ idToService = /* @__PURE__ */ new Map();
130
+ eventBus;
131
+ list() {
132
+ return this.options.repoStore.list().map((r) => ({
133
+ ...r,
134
+ available: r.path === "" || true
135
+ }));
136
+ }
137
+ /** 取最近打开的仓库(懒创建服务) */
138
+ async getCurrent() {
139
+ const record = this.options.repoStore.list()[0];
140
+ return record ? this.getByRecord(record) : null;
141
+ }
142
+ /** 按 id 取仓库(懒创建服务) */
143
+ async getById(id) {
144
+ const record = this.options.repoStore.getById(id);
145
+ if (!record) return null;
146
+ try {
147
+ return await this.getByRecord(record);
148
+ } catch (err) {
149
+ if (err instanceof Error && /NOT_A_GIT_REPO|REPO_NOT_FOUND/.test(err.message)) {
150
+ return null;
151
+ }
152
+ throw err;
153
+ }
154
+ }
155
+ /** 打开仓库:校验并记录;重复打开返回既有实例 */
156
+ async open(repoPath) {
157
+ const service = await GitService.open(repoPath);
158
+ const root = service.repoPath;
159
+ const key = normalizePath(root);
160
+ const existingId = this.pathToId.get(key);
161
+ if (existingId !== void 0 && existingId !== 0) {
162
+ const record2 = this.options.repoStore.getById(existingId);
163
+ if (record2) {
164
+ this.options.repoStore.open(root);
165
+ this.idToService.set(record2.id, service);
166
+ this.attach(service);
167
+ return { service, record: this.options.repoStore.getById(record2.id) ?? record2 };
168
+ }
169
+ }
170
+ const record = this.options.repoStore.open(root);
171
+ this.pathToId.set(key, record.id);
172
+ this.idToService.set(record.id, service);
173
+ this.attach(service);
174
+ return { service, record };
175
+ }
176
+ /** 按路径取服务,未打开则报错(避免隐式副作用) */
177
+ getByPath(repoPath) {
178
+ const key = normalizePath(repoPath);
179
+ const id = this.pathToId.get(key);
180
+ if (id !== void 0 && id !== 0) {
181
+ const record = this.options.repoStore.getById(id);
182
+ if (record) {
183
+ const service = this.idToService.get(id);
184
+ if (service) return { service, record };
185
+ }
186
+ }
187
+ throw new RepoNotFoundError(`\u4ED3\u5E93\u672A\u6253\u5F00: ${repoPath}\u3002\u8BF7\u5148\u6253\u5F00\u4ED3\u5E93\uFF08POST /api/repos/open\uFF09\u3002`);
188
+ }
189
+ /** 关闭:仅从内存服务池移除;保留历史记录 */
190
+ close(id) {
191
+ const record = this.options.repoStore.getById(id);
192
+ if (record) this.pathToId.delete(normalizePath(record.path));
193
+ const svc = this.idToService.get(id);
194
+ if (svc) svc.removeAllListeners("changed");
195
+ this.idToService.delete(id);
196
+ }
197
+ /** 从历史记录中删除并关闭 */
198
+ remove(id) {
199
+ this.close(id);
200
+ this.options.repoStore.remove(id);
201
+ }
202
+ dispose() {
203
+ for (const id of [...this.idToService.keys()]) this.close(id);
204
+ }
205
+ /** 按记录取句柄;服务缺失时(合法)创建并缓存 */
206
+ async getByRecord(record) {
207
+ let service = this.idToService.get(record.id);
208
+ if (!service) {
209
+ service = await GitService.open(record.path);
210
+ this.pathToId.set(normalizePath(service.repoPath), record.id);
211
+ this.idToService.set(record.id, service);
212
+ this.attach(service);
213
+ }
214
+ return { service, record };
215
+ }
216
+ attach(service) {
217
+ service.on("changed", (payload) => {
218
+ this.eventBus.emit("repo-changed", {
219
+ repoPath: payload.repoPath,
220
+ command: payload.command,
221
+ at: (/* @__PURE__ */ new Date()).toISOString()
222
+ });
223
+ });
224
+ }
225
+ };
226
+ function normalizePath(p) {
227
+ let normalized = path2.resolve(p);
228
+ if (process.platform === "win32") normalized = normalized.toLowerCase();
229
+ return normalized;
230
+ }
231
+
232
+ // src/runtime.ts
233
+ function createRuntime(options = {}) {
234
+ const configStore = new ConfigStore(options.dataDir ?? "~/.git-cockpit", options.configOverrides);
235
+ const config = configStore.get();
236
+ const db = options.db ?? openDatabase(config.storage.dataDir);
237
+ const eventBus = new EventEmitter2();
238
+ eventBus.setMaxListeners(200);
239
+ const auditLogger = new AuditLogger(db, config.logging);
240
+ const permissions = new PermissionManager(config);
241
+ const repoStore = new RepoStore2(db);
242
+ const repoManager = new RepoManager({ repoStore, eventBus, config });
243
+ return {
244
+ configStore,
245
+ config,
246
+ db,
247
+ auditLogger,
248
+ permissions,
249
+ repoStore,
250
+ repoManager,
251
+ eventBus
252
+ };
253
+ }
254
+ function disposeRuntime(runtime) {
255
+ try {
256
+ runtime.eventBus.removeAllListeners();
257
+ runtime.repoManager.dispose();
258
+ } catch {
259
+ }
260
+ try {
261
+ runtime.db.close();
262
+ } catch {
263
+ }
264
+ }
265
+
266
+ // src/webServer.ts
267
+ function resolveWebDist() {
268
+ const here = path3.resolve(path3.dirname(fileURLToPath(import.meta.url)));
269
+ const candidates = [
270
+ path3.resolve(here, "..", "..", "web", "dist"),
271
+ // packages/mcp-server/dist → ../../web/dist
272
+ path3.resolve(here, "web", "dist"),
273
+ // 发布布局:dist/web(若产物直接打入包内)
274
+ path3.resolve(here, "..", "web", "dist"),
275
+ // 已安装于 node_modules/@shaohui_jin/git-cockpit-mcp-server/dist 时
276
+ path3.resolve(process.cwd(), "dist", "web"),
277
+ path3.resolve(process.cwd(), "..", "web", "dist"),
278
+ path3.resolve(process.cwd(), "node_modules", "@shaohui_jin", "git-cockpit-web", "dist")
279
+ ];
280
+ for (const c of candidates) {
281
+ if (fs2.existsSync(path3.join(c, "index.html"))) return c;
282
+ }
283
+ return null;
284
+ }
285
+ async function createWebServer(runtime, options = {}) {
286
+ const app = Fastify({ logger: false, bodyLimit: 2 * 1024 * 1024 });
287
+ await app.register(cors, {
288
+ origin: true,
289
+ methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
290
+ allowedHeaders: ["Content-Type", "Mcp-Session-Id", "Authorization"]
291
+ });
292
+ const staticDir = options.staticDir !== void 0 ? options.staticDir : resolveWebDist();
293
+ if (staticDir && fs2.existsSync(staticDir)) {
294
+ await app.register(fastifyStatic, { root: staticDir, prefix: "/" });
295
+ }
296
+ const mcpHttp = new McpHttpHandler(runtime);
297
+ app.get("/api/health", async () => ({
298
+ ok: true,
299
+ service: "git-cockpit",
300
+ version: "0.1.0",
301
+ uptimeMs: process.uptime() * 1e3
302
+ }));
303
+ app.get("/api/repos", async () => ({
304
+ repos: runtime.repoManager.list()
305
+ }));
306
+ app.post("/api/repos/open", async (req, reply) => {
307
+ const target = req.body?.path?.trim();
308
+ if (!target) {
309
+ return reply.code(400).send({ error: "\u7F3A\u5C11 path \u53C2\u6570" });
310
+ }
311
+ try {
312
+ const handle = await runtime.repoManager.open(target);
313
+ return { repo: handle.record };
314
+ } catch (err) {
315
+ return reply.code(400).send({ error: toError(err) });
316
+ }
317
+ });
318
+ app.delete("/api/repos/:id", async (req, reply) => {
319
+ const id = Number(req.params.id);
320
+ if (!Number.isInteger(id)) {
321
+ return reply.code(400).send({ error: "\u975E\u6CD5\u4ED3\u5E93 id" });
322
+ }
323
+ runtime.repoManager.remove(id);
324
+ return { ok: true };
325
+ });
326
+ async function withRepo(req, reply, fn) {
327
+ const id = Number(req.params.id);
328
+ const handle = await runtime.repoManager.getById(id);
329
+ if (!handle) {
330
+ await reply.code(404).send({ error: "\u4ED3\u5E93\u4E0D\u5B58\u5728\u6216\u5DF2\u5931\u6548" });
331
+ return void 0;
332
+ }
333
+ try {
334
+ return await fn(handle);
335
+ } catch (err) {
336
+ await reply.code(400).send({ error: toError(err) });
337
+ return void 0;
338
+ }
339
+ }
340
+ app.get(
341
+ "/api/repos/:id/status",
342
+ async (req, reply) => withRepo(req, reply, async ({ service }) => service.getStatus())
343
+ );
344
+ app.get(
345
+ "/api/repos/:id/log",
346
+ async (req, reply) => withRepo(req, reply, async ({ service }) => {
347
+ const q = req.query;
348
+ return service.getLog({
349
+ maxCount: q.maxCount ? Math.min(Number(q.maxCount) || 100, 1e4) : void 0,
350
+ from: q.from,
351
+ to: q.to,
352
+ author: q.author,
353
+ path: q.path,
354
+ all: q.all === "true"
355
+ });
356
+ })
357
+ );
358
+ app.get(
359
+ "/api/repos/:id/diff",
360
+ async (req, reply) => withRepo(req, reply, async ({ service }) => {
361
+ const q = req.query;
362
+ return service.getDiff({
363
+ from: q.from,
364
+ to: q.to,
365
+ path: q.path,
366
+ staged: q.staged === "true",
367
+ maxPatchBytes: q.maxPatchBytes ? Number(q.maxPatchBytes) : void 0
368
+ });
369
+ })
370
+ );
371
+ app.get(
372
+ "/api/repos/:id/show/:commit",
373
+ async (req, reply) => withRepo(
374
+ req,
375
+ reply,
376
+ async ({ service }) => service.getShow(req.params.commit, {
377
+ path: req.query.path,
378
+ maxPatchBytes: req.query.maxPatchBytes ? Number(req.query.maxPatchBytes) : void 0
379
+ })
380
+ )
381
+ );
382
+ app.get(
383
+ "/api/repos/:id/branches",
384
+ async (req, reply) => withRepo(req, reply, async ({ service }) => service.listBranches())
385
+ );
386
+ app.get(
387
+ "/api/repos/:id/tags",
388
+ async (req, reply) => withRepo(req, reply, async ({ service }) => service.listTags())
389
+ );
390
+ app.get(
391
+ "/api/repos/:id/remotes",
392
+ async (req, reply) => withRepo(req, reply, async ({ service }) => service.listRemotes())
393
+ );
394
+ app.get(
395
+ "/api/repos/:id/graph",
396
+ async (req, reply) => withRepo(req, reply, async ({ service }) => service.getGraph(req.query.maxCount ? Number(req.query.maxCount) : 500))
397
+ );
398
+ app.get(
399
+ "/api/repos/:id/file",
400
+ async (req, reply) => withRepo(req, reply, async ({ service }) => {
401
+ if (!req.query.commit || !req.query.path) {
402
+ return { error: "\u9700\u8981 commit \u4E0E path \u53C2\u6570" };
403
+ }
404
+ return service.getFileContent(req.query.commit, req.query.path);
405
+ })
406
+ );
407
+ app.post(
408
+ "/api/repos/:id/tools/:tool",
409
+ async (req, reply) => {
410
+ const def = TOOL_DEF_MAP.get(req.params.tool);
411
+ if (!def) return reply.code(404).send({ error: `\u672A\u77E5\u5DE5\u5177: ${req.params.tool}` });
412
+ const params = req.body?.params ?? {};
413
+ const exec = await executeTool(def, params, {
414
+ runtime,
415
+ source: "web",
416
+ repoId: Number(req.params.id)
417
+ });
418
+ if (!exec.success) {
419
+ const status = exec.error?.requiredApproval ? 403 : 400;
420
+ return reply.code(status).send({ error: exec.error?.message, requiredApproval: exec.error?.requiredApproval, code: exec.error?.code });
421
+ }
422
+ return { ...exec };
423
+ }
424
+ );
425
+ app.get("/api/tools", async () => ({
426
+ tools: toolSummaries().map((t) => ({
427
+ ...t,
428
+ riskLevel: runtime.permissions.getRiskLevel(t.name),
429
+ enabled: runtime.permissions.isEnabled(t.name)
430
+ }))
431
+ }));
432
+ app.get("/api/logs", async (req) => {
433
+ const q = req.query;
434
+ return {
435
+ logs: runtime.auditLogger.list({
436
+ limit: q.limit ? Number(q.limit) : void 0,
437
+ offset: q.offset ? Number(q.offset) : void 0,
438
+ tool: q.tool,
439
+ source: q.source ?? void 0
440
+ })
441
+ };
442
+ });
443
+ app.get("/api/settings", async () => ({
444
+ config: runtime.configStore.snapshot(),
445
+ permissions: {
446
+ disabledTools: runtime.config.permissions.disabledTools,
447
+ requireApprovalFor: runtime.config.permissions.requireApprovalFor,
448
+ dryRunDefault: runtime.config.permissions.dryRunDefault
449
+ },
450
+ tools: toolSummaries().map((t) => ({
451
+ name: t.name,
452
+ description: t.description,
453
+ riskLevel: runtime.permissions.getRiskLevel(t.name),
454
+ enabled: runtime.permissions.isEnabled(t.name)
455
+ }))
456
+ }));
457
+ app.put("/api/settings", async (req, reply) => {
458
+ const patch = req.body ?? {};
459
+ if (patch.permissions === void 0) {
460
+ return reply.code(400).send({ error: "\u4EC5\u652F\u6301\u66F4\u65B0 permissions \u6BB5" });
461
+ }
462
+ runtime.configStore.update({ permissions: patch.permissions });
463
+ runtime.config = runtime.configStore.get();
464
+ runtime.permissions = new PermissionManager2(runtime.config);
465
+ return { ok: true, config: runtime.configStore.snapshot() };
466
+ });
467
+ app.get("/api/events", async (req, reply) => {
468
+ reply.hijack();
469
+ const raw = reply.raw;
470
+ raw.writeHead(200, {
471
+ "Content-Type": "text/event-stream; charset=utf-8",
472
+ "Cache-Control": "no-cache, no-transform",
473
+ Connection: "keep-alive",
474
+ "X-Accel-Buffering": "no"
475
+ });
476
+ raw.write(": connected\n\n");
477
+ const listener = (payload) => {
478
+ raw.write(`event: repo-changed
479
+ data: ${JSON.stringify(payload)}
480
+
481
+ `);
482
+ };
483
+ const logListener = () => {
484
+ raw.write("event: log\ndata: {}\n\n");
485
+ };
486
+ runtime.eventBus.on("repo-changed", listener);
487
+ runtime.eventBus.on("log", logListener);
488
+ const keepAlive = setInterval(() => raw.write(": ping\n\n"), 15e3);
489
+ req.raw.on("close", () => {
490
+ clearInterval(keepAlive);
491
+ runtime.eventBus.off("repo-changed", listener);
492
+ runtime.eventBus.off("log", logListener);
493
+ });
494
+ });
495
+ app.get("/mcp", async (req, reply) => {
496
+ reply.hijack();
497
+ await mcpHttp.handle(req.raw, reply.raw);
498
+ });
499
+ app.post("/mcp", async (req, reply) => {
500
+ reply.hijack();
501
+ await mcpHttp.handle(req.raw, reply.raw);
502
+ });
503
+ if (staticDir) {
504
+ app.setNotFoundHandler(async (req, reply) => {
505
+ if (req.url.startsWith("/api") || req.url.startsWith("/mcp")) {
506
+ return reply.code(404).send({ error: "Not Found" });
507
+ }
508
+ const index = path3.join(staticDir, "index.html");
509
+ if (fs2.existsSync(index)) {
510
+ reply.type("text/html").send(fs2.readFileSync(index, "utf8"));
511
+ return;
512
+ }
513
+ return reply.code(404).send({ error: "Not Found" });
514
+ });
515
+ }
516
+ const port = options.port ?? runtime.config.server.port;
517
+ const host = options.host ?? runtime.config.server.host;
518
+ if (!options.noListen) {
519
+ await app.listen({ port, host });
520
+ }
521
+ return {
522
+ app,
523
+ close: async () => {
524
+ mcpHttp.dispose();
525
+ await app.close();
526
+ disposeRuntime(runtime);
527
+ }
528
+ };
529
+ }
530
+ function toError(err) {
531
+ if (err instanceof GitOperationError) return { code: err.code, message: err.message };
532
+ if (err instanceof RepoNotFoundError2) return { code: "REPO_NOT_FOUND", message: err.message };
533
+ if (err instanceof Error) return { code: "INTERNAL_ERROR", message: err.message.slice(0, 500) };
534
+ return { code: "INTERNAL_ERROR", message: String(err) };
535
+ }
536
+
537
+ export {
538
+ ConfigStore,
539
+ RepoManager,
540
+ createRuntime,
541
+ disposeRuntime,
542
+ createWebServer
543
+ };
544
+ //# sourceMappingURL=chunk-4XYICSW4.js.map