ccpanel 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 +78 -0
- package/dist/chunk-7ROXP6AO.js +466 -0
- package/dist/cli.js +1056 -0
- package/dist/server/app.js +6 -0
- package/package.json +35 -0
- package/web/dist/assets/index-CscUfGaI.css +1 -0
- package/web/dist/assets/index-DoKBz_TC.js +88 -0
- package/web/dist/index.html +13 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ConfigStore,
|
|
4
|
+
InvalidJsonError,
|
|
5
|
+
SkillFrontmatterSchema,
|
|
6
|
+
createApp
|
|
7
|
+
} from "./chunk-7ROXP6AO.js";
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join as join6 } from "path";
|
|
12
|
+
import { createServer } from "net";
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
14
|
+
import { serve } from "@hono/node-server";
|
|
15
|
+
import open from "open";
|
|
16
|
+
|
|
17
|
+
// src/core/mcp-service.ts
|
|
18
|
+
var McpService = class {
|
|
19
|
+
constructor(store) {
|
|
20
|
+
this.store = store;
|
|
21
|
+
}
|
|
22
|
+
store;
|
|
23
|
+
stripName(input) {
|
|
24
|
+
const { name, ...server } = input;
|
|
25
|
+
return { name, server };
|
|
26
|
+
}
|
|
27
|
+
async list(paths, scope) {
|
|
28
|
+
if (scope === "user") {
|
|
29
|
+
const claude = await this.store.readJson(
|
|
30
|
+
paths.userClaudeJson
|
|
31
|
+
) ?? {};
|
|
32
|
+
const disabled = await this.store.readJson(paths.userDisabledMcp) ?? {};
|
|
33
|
+
const enabled = Object.entries(claude.mcpServers ?? {}).map(
|
|
34
|
+
([name, server]) => ({
|
|
35
|
+
name,
|
|
36
|
+
server,
|
|
37
|
+
enabled: true,
|
|
38
|
+
path: paths.userClaudeJson
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
const off = Object.entries(disabled).map(([name, server]) => ({
|
|
42
|
+
name,
|
|
43
|
+
server,
|
|
44
|
+
enabled: false,
|
|
45
|
+
path: paths.userDisabledMcp
|
|
46
|
+
}));
|
|
47
|
+
return [...enabled, ...off].sort((a, b) => a.name.localeCompare(b.name));
|
|
48
|
+
}
|
|
49
|
+
const mcp = await this.store.readJson(
|
|
50
|
+
paths.projectMcpJson
|
|
51
|
+
) ?? {};
|
|
52
|
+
const settings = await this.store.readJson(
|
|
53
|
+
paths.projectSettings
|
|
54
|
+
) ?? {};
|
|
55
|
+
const on = new Set(settings.enabledMcpjsonServers ?? []);
|
|
56
|
+
return Object.entries(mcp.mcpServers ?? {}).map(([name, server]) => ({
|
|
57
|
+
name,
|
|
58
|
+
server,
|
|
59
|
+
enabled: on.has(name),
|
|
60
|
+
path: paths.projectMcpJson
|
|
61
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
62
|
+
}
|
|
63
|
+
async upsert(paths, scope, input) {
|
|
64
|
+
const { name, server } = this.stripName(input);
|
|
65
|
+
if (scope === "user") {
|
|
66
|
+
const claude = await this.store.readJson(
|
|
67
|
+
paths.userClaudeJson
|
|
68
|
+
) ?? {};
|
|
69
|
+
const disabled = await this.store.readJson(paths.userDisabledMcp) ?? {};
|
|
70
|
+
if (disabled[name]) {
|
|
71
|
+
disabled[name] = server;
|
|
72
|
+
await this.store.writeJson(paths.userDisabledMcp, disabled);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
claude.mcpServers = { ...claude.mcpServers ?? {}, [name]: server };
|
|
76
|
+
await this.store.writeJson(paths.userClaudeJson, claude);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const mcp = await this.store.readJson(
|
|
80
|
+
paths.projectMcpJson
|
|
81
|
+
) ?? {};
|
|
82
|
+
mcp.mcpServers = { ...mcp.mcpServers ?? {}, [name]: server };
|
|
83
|
+
await this.store.writeJson(paths.projectMcpJson, mcp);
|
|
84
|
+
}
|
|
85
|
+
async remove(paths, scope, name) {
|
|
86
|
+
if (scope === "user") {
|
|
87
|
+
const claude = await this.store.readJson(
|
|
88
|
+
paths.userClaudeJson
|
|
89
|
+
) ?? {};
|
|
90
|
+
const disabled = await this.store.readJson(paths.userDisabledMcp) ?? {};
|
|
91
|
+
if (claude.mcpServers) delete claude.mcpServers[name];
|
|
92
|
+
delete disabled[name];
|
|
93
|
+
await this.store.writeJson(paths.userClaudeJson, claude);
|
|
94
|
+
await this.store.writeJson(paths.userDisabledMcp, disabled);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const mcp = await this.store.readJson(
|
|
98
|
+
paths.projectMcpJson
|
|
99
|
+
) ?? {};
|
|
100
|
+
if (mcp.mcpServers) delete mcp.mcpServers[name];
|
|
101
|
+
await this.store.writeJson(paths.projectMcpJson, mcp);
|
|
102
|
+
await this.setProjectEnabled(paths, name, false);
|
|
103
|
+
}
|
|
104
|
+
async toggle(paths, scope, name, enabled) {
|
|
105
|
+
if (scope === "user") {
|
|
106
|
+
const claude = await this.store.readJson(
|
|
107
|
+
paths.userClaudeJson
|
|
108
|
+
) ?? {};
|
|
109
|
+
const disabled = await this.store.readJson(paths.userDisabledMcp) ?? {};
|
|
110
|
+
const active = claude.mcpServers ?? {};
|
|
111
|
+
if (enabled && disabled[name]) {
|
|
112
|
+
active[name] = disabled[name];
|
|
113
|
+
delete disabled[name];
|
|
114
|
+
} else if (!enabled && active[name]) {
|
|
115
|
+
disabled[name] = active[name];
|
|
116
|
+
delete active[name];
|
|
117
|
+
}
|
|
118
|
+
claude.mcpServers = active;
|
|
119
|
+
await this.store.writeJson(paths.userClaudeJson, claude);
|
|
120
|
+
await this.store.writeJson(paths.userDisabledMcp, disabled);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await this.setProjectEnabled(paths, name, enabled);
|
|
124
|
+
}
|
|
125
|
+
async setProjectEnabled(paths, name, enabled) {
|
|
126
|
+
const settings = await this.store.readJson(
|
|
127
|
+
paths.projectSettings
|
|
128
|
+
) ?? {};
|
|
129
|
+
const set = new Set(settings.enabledMcpjsonServers ?? []);
|
|
130
|
+
if (enabled) set.add(name);
|
|
131
|
+
else set.delete(name);
|
|
132
|
+
settings.enabledMcpjsonServers = [...set];
|
|
133
|
+
await this.store.writeJson(paths.projectSettings, settings);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/core/skill-service.ts
|
|
138
|
+
import {
|
|
139
|
+
cp,
|
|
140
|
+
mkdir,
|
|
141
|
+
readdir,
|
|
142
|
+
readFile,
|
|
143
|
+
readlink,
|
|
144
|
+
rename,
|
|
145
|
+
rm,
|
|
146
|
+
stat,
|
|
147
|
+
writeFile
|
|
148
|
+
} from "fs/promises";
|
|
149
|
+
import { dirname, isAbsolute, join, relative, resolve } from "path";
|
|
150
|
+
import matter from "gray-matter";
|
|
151
|
+
function assertSafeName(name) {
|
|
152
|
+
if (!name || name.includes("/") || name.includes("\\") || name.includes("..") || name.includes("\0")) {
|
|
153
|
+
throw new Error(`\u975E\u6CD5 skill \u540D\u79F0\uFF1A${name}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isBinary(buf) {
|
|
157
|
+
return buf.subarray(0, 8e3).includes(0);
|
|
158
|
+
}
|
|
159
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
160
|
+
"node_modules",
|
|
161
|
+
".git",
|
|
162
|
+
".venv",
|
|
163
|
+
"__pycache__",
|
|
164
|
+
".DS_Store"
|
|
165
|
+
]);
|
|
166
|
+
var SkillService = class {
|
|
167
|
+
enabledDir(paths, scope) {
|
|
168
|
+
const dir = scope === "user" ? paths.userSkillsDir : paths.projectSkillsDir;
|
|
169
|
+
if (!dir) throw new Error("\u7F3A\u5C11\u9879\u76EE skills \u76EE\u5F55");
|
|
170
|
+
return dir;
|
|
171
|
+
}
|
|
172
|
+
disabledDir(paths, scope) {
|
|
173
|
+
const dir = scope === "user" ? paths.userDisabledSkillsDir : paths.projectDisabledSkillsDir;
|
|
174
|
+
if (!dir) throw new Error("\u7F3A\u5C11\u9879\u76EE\u7981\u7528 skills \u76EE\u5F55");
|
|
175
|
+
return dir;
|
|
176
|
+
}
|
|
177
|
+
async scan(dir, enabled) {
|
|
178
|
+
let entries;
|
|
179
|
+
try {
|
|
180
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (err.code === "ENOENT") return [];
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const e of entries) {
|
|
187
|
+
if (!e.isDirectory() && !e.isSymbolicLink()) continue;
|
|
188
|
+
const path = join(dir, e.name);
|
|
189
|
+
let linkTarget;
|
|
190
|
+
if (e.isSymbolicLink()) {
|
|
191
|
+
try {
|
|
192
|
+
if (!(await stat(path)).isDirectory()) continue;
|
|
193
|
+
linkTarget = await readlink(path).catch(() => void 0);
|
|
194
|
+
} catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const raw = await readFile(join(path, "SKILL.md"), "utf8");
|
|
200
|
+
const fm = SkillFrontmatterSchema.parse(matter(raw).data);
|
|
201
|
+
out.push({ name: e.name, description: fm.description, enabled, path, linkTarget });
|
|
202
|
+
} catch {
|
|
203
|
+
out.push({
|
|
204
|
+
name: e.name,
|
|
205
|
+
description: "(SKILL.md \u7F3A\u5931\u6216\u65E0\u6548)",
|
|
206
|
+
enabled,
|
|
207
|
+
path,
|
|
208
|
+
linkTarget
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
async list(paths, scope) {
|
|
215
|
+
const on = await this.scan(this.enabledDir(paths, scope), true);
|
|
216
|
+
const off = await this.scan(this.disabledDir(paths, scope), false);
|
|
217
|
+
return [...on, ...off].sort((a, b) => a.name.localeCompare(b.name));
|
|
218
|
+
}
|
|
219
|
+
// 返回该技能实际所在目录(启用或禁用),都不存在则抛错。
|
|
220
|
+
async resolveSkillDir(paths, scope, name) {
|
|
221
|
+
assertSafeName(name);
|
|
222
|
+
for (const base of [this.enabledDir(paths, scope), this.disabledDir(paths, scope)]) {
|
|
223
|
+
const dir = join(base, name);
|
|
224
|
+
try {
|
|
225
|
+
await readdir(dir);
|
|
226
|
+
return dir;
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
throw new Error(`\u672A\u627E\u5230 skill\u300C${name}\u300D\uFF08scope: ${scope}\uFF09`);
|
|
231
|
+
}
|
|
232
|
+
// 把相对路径安全地解析到技能目录内,防止 `..` 越权。
|
|
233
|
+
resolveWithin(baseDir, relPath) {
|
|
234
|
+
const full = resolve(baseDir, relPath);
|
|
235
|
+
const rel = relative(baseDir, full);
|
|
236
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
237
|
+
throw new Error(`\u975E\u6CD5\u6587\u4EF6\u8DEF\u5F84\uFF1A${relPath}`);
|
|
238
|
+
}
|
|
239
|
+
return full;
|
|
240
|
+
}
|
|
241
|
+
async read(paths, scope, name) {
|
|
242
|
+
assertSafeName(name);
|
|
243
|
+
const enabledPath = join(this.enabledDir(paths, scope), name, "SKILL.md");
|
|
244
|
+
const disabledPath = join(this.disabledDir(paths, scope), name, "SKILL.md");
|
|
245
|
+
let raw;
|
|
246
|
+
try {
|
|
247
|
+
raw = await readFile(enabledPath, "utf8");
|
|
248
|
+
} catch {
|
|
249
|
+
try {
|
|
250
|
+
raw = await readFile(disabledPath, "utf8");
|
|
251
|
+
} catch {
|
|
252
|
+
throw new Error(`\u672A\u627E\u5230 skill\u300C${name}\u300D\uFF08scope: ${scope}\uFF09`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const parsed = matter(raw);
|
|
256
|
+
return {
|
|
257
|
+
frontmatter: SkillFrontmatterSchema.parse(parsed.data),
|
|
258
|
+
body: parsed.content.trimStart()
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// 列出技能目录下全部文本文件的相对路径(POSIX 分隔符),忽略二进制文件。
|
|
262
|
+
async listFiles(paths, scope, name) {
|
|
263
|
+
const root = await this.resolveSkillDir(paths, scope, name);
|
|
264
|
+
const out = [];
|
|
265
|
+
const walk = async (dir) => {
|
|
266
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
267
|
+
for (const e of entries) {
|
|
268
|
+
const full = join(dir, e.name);
|
|
269
|
+
if (e.isDirectory()) {
|
|
270
|
+
if (IGNORED_DIRS.has(e.name)) continue;
|
|
271
|
+
await walk(full);
|
|
272
|
+
} else if (e.isFile()) {
|
|
273
|
+
if (isBinary(await readFile(full))) continue;
|
|
274
|
+
out.push(relative(root, full).split("\\").join("/"));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
await walk(root);
|
|
279
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
280
|
+
}
|
|
281
|
+
async readFileRaw(paths, scope, name, relPath) {
|
|
282
|
+
const root = await this.resolveSkillDir(paths, scope, name);
|
|
283
|
+
const full = this.resolveWithin(root, relPath);
|
|
284
|
+
const buf = await readFile(full);
|
|
285
|
+
if (isBinary(buf)) throw new Error(`\u4E8C\u8FDB\u5236\u6587\u4EF6\u65E0\u6CD5\u7F16\u8F91\uFF1A${relPath}`);
|
|
286
|
+
return { content: buf.toString("utf8") };
|
|
287
|
+
}
|
|
288
|
+
async writeFileRaw(paths, scope, name, relPath, content) {
|
|
289
|
+
const root = await this.resolveSkillDir(paths, scope, name);
|
|
290
|
+
const full = this.resolveWithin(root, relPath);
|
|
291
|
+
await mkdir(dirname(full), { recursive: true });
|
|
292
|
+
await writeFile(full, content, "utf8");
|
|
293
|
+
}
|
|
294
|
+
async upsert(paths, scope, name, frontmatter, body) {
|
|
295
|
+
assertSafeName(name);
|
|
296
|
+
const file = join(this.enabledDir(paths, scope), name, "SKILL.md");
|
|
297
|
+
await mkdir(dirname(file), { recursive: true });
|
|
298
|
+
const content = matter.stringify(body, frontmatter);
|
|
299
|
+
await writeFile(file, content, "utf8");
|
|
300
|
+
}
|
|
301
|
+
async remove(paths, scope, name) {
|
|
302
|
+
assertSafeName(name);
|
|
303
|
+
await rm(join(this.enabledDir(paths, scope), name), {
|
|
304
|
+
recursive: true,
|
|
305
|
+
force: true
|
|
306
|
+
});
|
|
307
|
+
await rm(join(this.disabledDir(paths, scope), name), {
|
|
308
|
+
recursive: true,
|
|
309
|
+
force: true
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// 跨盘符 rename 会抛 EXDEV(如项目在 D: 而 ~/.ccpanel 在 C:),回退到复制+删除。
|
|
313
|
+
async moveDir(from, to) {
|
|
314
|
+
await mkdir(dirname(to), { recursive: true });
|
|
315
|
+
try {
|
|
316
|
+
await rename(from, to);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
if (err.code === "EXDEV") {
|
|
319
|
+
await cp(from, to, { recursive: true });
|
|
320
|
+
await rm(from, { recursive: true, force: true });
|
|
321
|
+
} else {
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async toggle(paths, scope, name, enabled) {
|
|
327
|
+
assertSafeName(name);
|
|
328
|
+
const from = join(
|
|
329
|
+
enabled ? this.disabledDir(paths, scope) : this.enabledDir(paths, scope),
|
|
330
|
+
name
|
|
331
|
+
);
|
|
332
|
+
const to = join(
|
|
333
|
+
enabled ? this.enabledDir(paths, scope) : this.disabledDir(paths, scope),
|
|
334
|
+
name
|
|
335
|
+
);
|
|
336
|
+
await this.moveDir(from, to);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/core/project-service.ts
|
|
341
|
+
var ProjectService = class {
|
|
342
|
+
constructor(store, registryFile) {
|
|
343
|
+
this.store = store;
|
|
344
|
+
this.registryFile = registryFile;
|
|
345
|
+
}
|
|
346
|
+
store;
|
|
347
|
+
registryFile;
|
|
348
|
+
async list() {
|
|
349
|
+
const data = await this.store.readJson(
|
|
350
|
+
this.registryFile
|
|
351
|
+
);
|
|
352
|
+
return data?.projects ?? [];
|
|
353
|
+
}
|
|
354
|
+
async add(path) {
|
|
355
|
+
const current = await this.list();
|
|
356
|
+
if (!current.includes(path)) current.push(path);
|
|
357
|
+
await this.store.writeJson(this.registryFile, { projects: current });
|
|
358
|
+
return current;
|
|
359
|
+
}
|
|
360
|
+
// 仅从注册表移除登记,不删除项目本身的任何文件。
|
|
361
|
+
async remove(path) {
|
|
362
|
+
const current = await this.list();
|
|
363
|
+
const next = current.filter((p) => p !== path);
|
|
364
|
+
await this.store.writeJson(this.registryFile, { projects: next });
|
|
365
|
+
return next;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/core/memory-service.ts
|
|
370
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
371
|
+
import { dirname as dirname2 } from "path";
|
|
372
|
+
var MemoryService = class {
|
|
373
|
+
file(paths, scope) {
|
|
374
|
+
const f = scope === "user" ? paths.userClaudeMd : paths.projectClaudeMd;
|
|
375
|
+
if (!f) throw new Error("\u7F3A\u5C11\u9879\u76EE CLAUDE.md \u8DEF\u5F84");
|
|
376
|
+
return f;
|
|
377
|
+
}
|
|
378
|
+
// 文件不存在视为「尚未创建」,返回空串。
|
|
379
|
+
async read(paths, scope) {
|
|
380
|
+
try {
|
|
381
|
+
return { content: await readFile2(this.file(paths, scope), "utf8") };
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (err.code === "ENOENT") return { content: "" };
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async write(paths, scope, content) {
|
|
388
|
+
const f = this.file(paths, scope);
|
|
389
|
+
await mkdir2(dirname2(f), { recursive: true });
|
|
390
|
+
await writeFile2(f, content, "utf8");
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/core/memory-files-service.ts
|
|
395
|
+
import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
396
|
+
import { join as join3 } from "path";
|
|
397
|
+
import matter2 from "gray-matter";
|
|
398
|
+
|
|
399
|
+
// src/core/session-service.ts
|
|
400
|
+
import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
401
|
+
import { join as join2 } from "path";
|
|
402
|
+
var MAX_BLOCK = 1e4;
|
|
403
|
+
var NOISE_PREFIXES = ["<local-command", "<command-", "<system-reminder", "Caveat:"];
|
|
404
|
+
function encodeProjectDir(projectPath) {
|
|
405
|
+
return projectPath.replace(/[:\\/]/g, "-");
|
|
406
|
+
}
|
|
407
|
+
function assertSafeId(id) {
|
|
408
|
+
if (!id || !/^[A-Za-z0-9-]+$/.test(id)) {
|
|
409
|
+
throw new Error(`\u975E\u6CD5\u4F1A\u8BDD id\uFF1A${id}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function parseAgentId(fileName) {
|
|
413
|
+
const m = /^agent-([A-Za-z0-9]+)\.jsonl$/.exec(fileName);
|
|
414
|
+
return m ? m[1] : null;
|
|
415
|
+
}
|
|
416
|
+
function parseMessages(raw) {
|
|
417
|
+
const out = [];
|
|
418
|
+
for (const line of raw.split("\n")) {
|
|
419
|
+
if (!line.trim()) continue;
|
|
420
|
+
let obj;
|
|
421
|
+
try {
|
|
422
|
+
obj = JSON.parse(line);
|
|
423
|
+
} catch {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (obj.type !== "user" && obj.type !== "assistant") continue;
|
|
427
|
+
const blocks = toBlocks(obj.message?.content);
|
|
428
|
+
if (blocks.length === 0) continue;
|
|
429
|
+
out.push({
|
|
430
|
+
type: obj.type,
|
|
431
|
+
role: obj.message?.role,
|
|
432
|
+
timestamp: obj.timestamp,
|
|
433
|
+
blocks
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
function truncate(s) {
|
|
439
|
+
return s.length > MAX_BLOCK ? s.slice(0, MAX_BLOCK) + "\n\u2026(\u5DF2\u622A\u65AD)" : s;
|
|
440
|
+
}
|
|
441
|
+
function plainText(content) {
|
|
442
|
+
if (typeof content === "string") return content;
|
|
443
|
+
if (Array.isArray(content)) {
|
|
444
|
+
return content.map((b) => b && typeof b === "object" && "text" in b ? String(b.text ?? "") : "").join(" ");
|
|
445
|
+
}
|
|
446
|
+
return "";
|
|
447
|
+
}
|
|
448
|
+
function toBlocks(content) {
|
|
449
|
+
if (typeof content === "string") {
|
|
450
|
+
return content.trim() ? [{ kind: "text", text: truncate(content) }] : [];
|
|
451
|
+
}
|
|
452
|
+
if (!Array.isArray(content)) return [];
|
|
453
|
+
const out = [];
|
|
454
|
+
for (const raw of content) {
|
|
455
|
+
if (!raw || typeof raw !== "object") continue;
|
|
456
|
+
const b = raw;
|
|
457
|
+
switch (b.type) {
|
|
458
|
+
case "text":
|
|
459
|
+
if (b.text) out.push({ kind: "text", text: truncate(String(b.text)) });
|
|
460
|
+
break;
|
|
461
|
+
case "thinking":
|
|
462
|
+
out.push({ kind: "thinking", text: truncate(String(b.thinking ?? "")) });
|
|
463
|
+
break;
|
|
464
|
+
case "tool_use":
|
|
465
|
+
out.push({
|
|
466
|
+
kind: "tool_use",
|
|
467
|
+
name: String(b.name ?? "tool"),
|
|
468
|
+
text: truncate(JSON.stringify(b.input ?? {}, null, 2)),
|
|
469
|
+
toolUseId: b.id ? String(b.id) : void 0
|
|
470
|
+
});
|
|
471
|
+
break;
|
|
472
|
+
case "tool_result":
|
|
473
|
+
out.push({
|
|
474
|
+
kind: "tool_result",
|
|
475
|
+
text: truncate(plainText(b.content)),
|
|
476
|
+
isError: b.is_error === true
|
|
477
|
+
});
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
var SessionService = class {
|
|
484
|
+
dir(paths, projectPath) {
|
|
485
|
+
return join2(paths.userProjectsDir, encodeProjectDir(projectPath));
|
|
486
|
+
}
|
|
487
|
+
async list(paths, projectPath) {
|
|
488
|
+
const dir = this.dir(paths, projectPath);
|
|
489
|
+
let names;
|
|
490
|
+
try {
|
|
491
|
+
names = (await readdir2(dir)).filter((n) => n.endsWith(".jsonl"));
|
|
492
|
+
} catch (err) {
|
|
493
|
+
if (err.code === "ENOENT") return [];
|
|
494
|
+
throw err;
|
|
495
|
+
}
|
|
496
|
+
const out = [];
|
|
497
|
+
for (const name of names) {
|
|
498
|
+
const full = join2(dir, name);
|
|
499
|
+
const st = await stat2(full);
|
|
500
|
+
const { firstPrompt, messageCount } = await this.summarize(full);
|
|
501
|
+
out.push({
|
|
502
|
+
id: name.replace(/\.jsonl$/, ""),
|
|
503
|
+
mtime: st.mtimeMs,
|
|
504
|
+
size: st.size,
|
|
505
|
+
firstPrompt,
|
|
506
|
+
messageCount
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return out.sort((a, b) => b.mtime - a.mtime);
|
|
510
|
+
}
|
|
511
|
+
async summarize(file) {
|
|
512
|
+
const raw = await readFile3(file, "utf8");
|
|
513
|
+
let firstPrompt = "";
|
|
514
|
+
let messageCount = 0;
|
|
515
|
+
for (const line of raw.split("\n")) {
|
|
516
|
+
if (!line.trim()) continue;
|
|
517
|
+
let obj;
|
|
518
|
+
try {
|
|
519
|
+
obj = JSON.parse(line);
|
|
520
|
+
} catch {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (obj.type !== "user" && obj.type !== "assistant") continue;
|
|
524
|
+
messageCount++;
|
|
525
|
+
if (!firstPrompt && obj.type === "user") {
|
|
526
|
+
const text = plainText(obj.message?.content).replace(/\s+/g, " ").trim();
|
|
527
|
+
if (text && !NOISE_PREFIXES.some((p) => text.startsWith(p))) {
|
|
528
|
+
firstPrompt = text.length > 100 ? text.slice(0, 100) + "\u2026" : text;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { firstPrompt, messageCount };
|
|
533
|
+
}
|
|
534
|
+
async read(paths, projectPath, id) {
|
|
535
|
+
assertSafeId(id);
|
|
536
|
+
const dir = this.dir(paths, projectPath);
|
|
537
|
+
const raw = await readFile3(join2(dir, `${id}.jsonl`), "utf8");
|
|
538
|
+
const messages = parseMessages(raw);
|
|
539
|
+
const subagents = await this.readSubagents(join2(dir, id, "subagents"));
|
|
540
|
+
return { messages, subagents };
|
|
541
|
+
}
|
|
542
|
+
// 读取某会话的 subagents/ 目录:每个 agent-<id>.jsonl 配对同名 .meta.json。
|
|
543
|
+
// 目录不存在、meta 缺失或损坏均降级处理,不影响主会话展示。
|
|
544
|
+
async readSubagents(subDir) {
|
|
545
|
+
let names;
|
|
546
|
+
try {
|
|
547
|
+
names = (await readdir2(subDir)).filter((n) => n.endsWith(".jsonl"));
|
|
548
|
+
} catch (err) {
|
|
549
|
+
if (err.code === "ENOENT") return [];
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
552
|
+
const out = [];
|
|
553
|
+
for (const name of names) {
|
|
554
|
+
const agentId = parseAgentId(name);
|
|
555
|
+
if (!agentId) continue;
|
|
556
|
+
const messages = parseMessages(await readFile3(join2(subDir, name), "utf8"));
|
|
557
|
+
const meta = await this.readMeta(join2(subDir, `agent-${agentId}.meta.json`));
|
|
558
|
+
out.push({
|
|
559
|
+
toolUseId: meta.toolUseId,
|
|
560
|
+
agentId,
|
|
561
|
+
agentType: meta.agentType,
|
|
562
|
+
description: meta.description,
|
|
563
|
+
messages
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return out;
|
|
567
|
+
}
|
|
568
|
+
async readMeta(file) {
|
|
569
|
+
try {
|
|
570
|
+
const obj = JSON.parse(await readFile3(file, "utf8"));
|
|
571
|
+
return {
|
|
572
|
+
toolUseId: String(obj.toolUseId ?? ""),
|
|
573
|
+
agentType: String(obj.agentType ?? "subagent"),
|
|
574
|
+
description: String(obj.description ?? "")
|
|
575
|
+
};
|
|
576
|
+
} catch {
|
|
577
|
+
return { toolUseId: "", agentType: "subagent", description: "" };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/core/memory-files-service.ts
|
|
583
|
+
var INDEX_FILE = "MEMORY.md";
|
|
584
|
+
function assertSafeMemoryFile(file) {
|
|
585
|
+
if (!file || file.includes("..") || !/^[A-Za-z0-9._-]+\.md$/.test(file)) {
|
|
586
|
+
throw new Error(`\u975E\u6CD5\u8BB0\u5FC6\u6587\u4EF6\u540D\uFF1A${file}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
var MemoryFilesService = class {
|
|
590
|
+
dir(paths, projectPath) {
|
|
591
|
+
return join3(paths.userProjectsDir, encodeProjectDir(projectPath), "memory");
|
|
592
|
+
}
|
|
593
|
+
// 目录不存在视为「尚无记忆」,返回空概览。
|
|
594
|
+
async overview(paths, projectPath) {
|
|
595
|
+
const dir = this.dir(paths, projectPath);
|
|
596
|
+
let names;
|
|
597
|
+
try {
|
|
598
|
+
names = (await readdir3(dir)).filter((n) => n.endsWith(".md"));
|
|
599
|
+
} catch (err) {
|
|
600
|
+
if (err.code === "ENOENT") return { index: null, files: [] };
|
|
601
|
+
throw err;
|
|
602
|
+
}
|
|
603
|
+
let index = null;
|
|
604
|
+
const files = [];
|
|
605
|
+
for (const name of names) {
|
|
606
|
+
const full = join3(dir, name);
|
|
607
|
+
if (name === INDEX_FILE) {
|
|
608
|
+
index = await readFile4(full, "utf8");
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const st = await stat3(full);
|
|
612
|
+
const fm = await this.parseFront(full, name);
|
|
613
|
+
files.push({ file: name, ...fm, size: st.size, mtime: st.mtimeMs });
|
|
614
|
+
}
|
|
615
|
+
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
616
|
+
return { index, files };
|
|
617
|
+
}
|
|
618
|
+
// 解析 frontmatter,任何缺失/损坏均回退到文件名,保证列表不因坏文件中断。
|
|
619
|
+
async parseFront(full, fileName) {
|
|
620
|
+
const fallbackName = fileName.replace(/\.md$/, "");
|
|
621
|
+
try {
|
|
622
|
+
const data = matter2(await readFile4(full, "utf8")).data;
|
|
623
|
+
const meta = data.metadata ?? {};
|
|
624
|
+
return {
|
|
625
|
+
name: typeof data.name === "string" && data.name ? data.name : fallbackName,
|
|
626
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
627
|
+
type: typeof meta.type === "string" ? meta.type : ""
|
|
628
|
+
};
|
|
629
|
+
} catch {
|
|
630
|
+
return { name: fallbackName, description: "", type: "" };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// 读取单个记忆文件原文(含 frontmatter)。
|
|
634
|
+
async read(paths, projectPath, file) {
|
|
635
|
+
assertSafeMemoryFile(file);
|
|
636
|
+
return { content: await readFile4(join3(this.dir(paths, projectPath), file), "utf8") };
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// src/core/plugin-service.ts
|
|
641
|
+
import { readFile as readFile5, readdir as readdir4, stat as stat4 } from "fs/promises";
|
|
642
|
+
import { isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve2 } from "path";
|
|
643
|
+
import matter3 from "gray-matter";
|
|
644
|
+
var PluginService = class {
|
|
645
|
+
constructor(store) {
|
|
646
|
+
this.store = store;
|
|
647
|
+
}
|
|
648
|
+
store;
|
|
649
|
+
async pathExists(p) {
|
|
650
|
+
return stat4(p).then(() => true).catch(() => false);
|
|
651
|
+
}
|
|
652
|
+
// 插件元数据要尽量容错:单个文件损坏不应让整页报错(启停写入除外)。
|
|
653
|
+
async safeReadJson(file) {
|
|
654
|
+
try {
|
|
655
|
+
return await this.store.readJson(file);
|
|
656
|
+
} catch {
|
|
657
|
+
return void 0;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
manifestFile(installPath) {
|
|
661
|
+
return join4(installPath, ".claude-plugin", "plugin.json");
|
|
662
|
+
}
|
|
663
|
+
// id 形如 "name@marketplace",按最后一个 @ 切分。
|
|
664
|
+
splitId(id) {
|
|
665
|
+
const at = id.lastIndexOf("@");
|
|
666
|
+
if (at <= 0) return { name: id, marketplace: "" };
|
|
667
|
+
return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
|
|
668
|
+
}
|
|
669
|
+
authorName(a) {
|
|
670
|
+
if (!a) return void 0;
|
|
671
|
+
return typeof a === "string" ? a : a.name;
|
|
672
|
+
}
|
|
673
|
+
// 同一 id 可能有多条安装记录,优先 user scope。
|
|
674
|
+
pickRecord(records) {
|
|
675
|
+
return records.find((r) => r.scope === "user") ?? records[0];
|
|
676
|
+
}
|
|
677
|
+
async buildEntry(id, rec, enabled, markets) {
|
|
678
|
+
const { name: idName, marketplace } = this.splitId(id);
|
|
679
|
+
const installPath = rec.installPath;
|
|
680
|
+
const exists = await this.pathExists(installPath);
|
|
681
|
+
const manifest = exists ? await this.safeReadJson(this.manifestFile(installPath)) : void 0;
|
|
682
|
+
const entry = {
|
|
683
|
+
id,
|
|
684
|
+
name: manifest?.name ?? idName,
|
|
685
|
+
description: manifest?.description ?? "",
|
|
686
|
+
version: manifest?.version ?? rec.version ?? "unknown",
|
|
687
|
+
author: this.authorName(manifest?.author),
|
|
688
|
+
homepage: manifest?.homepage,
|
|
689
|
+
marketplace,
|
|
690
|
+
repo: markets[marketplace]?.source?.repo,
|
|
691
|
+
installPath,
|
|
692
|
+
enabled: enabled[id] === true,
|
|
693
|
+
exists
|
|
694
|
+
};
|
|
695
|
+
return { entry, manifest };
|
|
696
|
+
}
|
|
697
|
+
async list(paths) {
|
|
698
|
+
const installed = await this.safeReadJson(paths.installedPluginsJson) ?? {};
|
|
699
|
+
const settings = await this.safeReadJson(paths.userSettings) ?? {};
|
|
700
|
+
const markets = await this.safeReadJson(
|
|
701
|
+
paths.knownMarketplacesJson
|
|
702
|
+
) ?? {};
|
|
703
|
+
const enabled = settings.enabledPlugins ?? {};
|
|
704
|
+
const out = [];
|
|
705
|
+
for (const [id, records] of Object.entries(installed.plugins ?? {})) {
|
|
706
|
+
const rec = this.pickRecord(records ?? []);
|
|
707
|
+
if (!rec) continue;
|
|
708
|
+
const { entry } = await this.buildEntry(id, rec, enabled, markets);
|
|
709
|
+
out.push(entry);
|
|
710
|
+
}
|
|
711
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
712
|
+
}
|
|
713
|
+
async findRecord(paths, id) {
|
|
714
|
+
const installed = await this.safeReadJson(paths.installedPluginsJson) ?? {};
|
|
715
|
+
const records = installed.plugins?.[id];
|
|
716
|
+
const rec = records ? this.pickRecord(records) : void 0;
|
|
717
|
+
if (!rec) throw new Error(`\u672A\u627E\u5230\u63D2\u4EF6\u300C${id}\u300D`);
|
|
718
|
+
return rec;
|
|
719
|
+
}
|
|
720
|
+
async detail(paths, id) {
|
|
721
|
+
const rec = await this.findRecord(paths, id);
|
|
722
|
+
const settings = await this.safeReadJson(paths.userSettings) ?? {};
|
|
723
|
+
const markets = await this.safeReadJson(
|
|
724
|
+
paths.knownMarketplacesJson
|
|
725
|
+
) ?? {};
|
|
726
|
+
const { entry, manifest } = await this.buildEntry(
|
|
727
|
+
id,
|
|
728
|
+
rec,
|
|
729
|
+
settings.enabledPlugins ?? {},
|
|
730
|
+
markets
|
|
731
|
+
);
|
|
732
|
+
const components = entry.exists ? await this.scanComponents(entry.installPath, manifest) : [];
|
|
733
|
+
return { ...entry, components };
|
|
734
|
+
}
|
|
735
|
+
// 读取 frontmatter 的 description / name 作为子组件摘要,失败返回空串。
|
|
736
|
+
async readDescription(file) {
|
|
737
|
+
try {
|
|
738
|
+
const fm = matter3(await readFile5(file, "utf8")).data;
|
|
739
|
+
return fm.description ?? fm.name ?? "";
|
|
740
|
+
} catch {
|
|
741
|
+
return "";
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async scanSkills(dir) {
|
|
745
|
+
let entries;
|
|
746
|
+
try {
|
|
747
|
+
entries = await readdir4(dir, { withFileTypes: true });
|
|
748
|
+
} catch {
|
|
749
|
+
return [];
|
|
750
|
+
}
|
|
751
|
+
const out = [];
|
|
752
|
+
for (const e of entries) {
|
|
753
|
+
if (!e.isDirectory()) continue;
|
|
754
|
+
const description = await this.readDescription(
|
|
755
|
+
join4(dir, e.name, "SKILL.md")
|
|
756
|
+
);
|
|
757
|
+
out.push({ type: "skill", name: e.name, description });
|
|
758
|
+
}
|
|
759
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
760
|
+
}
|
|
761
|
+
// commands / agents:递归收集 .md,name 为相对 dir 的 POSIX 路径(去 .md)。
|
|
762
|
+
async scanMarkdownDir(dir, type) {
|
|
763
|
+
const out = [];
|
|
764
|
+
const walk = async (cur) => {
|
|
765
|
+
let entries;
|
|
766
|
+
try {
|
|
767
|
+
entries = await readdir4(cur, { withFileTypes: true });
|
|
768
|
+
} catch {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
for (const e of entries) {
|
|
772
|
+
const full = join4(cur, e.name);
|
|
773
|
+
if (e.isDirectory()) {
|
|
774
|
+
await walk(full);
|
|
775
|
+
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
776
|
+
const name = relative2(dir, full).split("\\").join("/").replace(/\.md$/, "");
|
|
777
|
+
out.push({
|
|
778
|
+
type,
|
|
779
|
+
name,
|
|
780
|
+
description: await this.readDescription(full)
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
await walk(dir);
|
|
786
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
787
|
+
}
|
|
788
|
+
async scanHooks(file) {
|
|
789
|
+
try {
|
|
790
|
+
const json = JSON.parse(await readFile5(file, "utf8"));
|
|
791
|
+
const events = Object.keys(json.hooks ?? json);
|
|
792
|
+
return [
|
|
793
|
+
{
|
|
794
|
+
type: "hook",
|
|
795
|
+
name: "hooks.json",
|
|
796
|
+
description: events.length ? `\u4E8B\u4EF6\uFF1A${events.join(", ")}` : ""
|
|
797
|
+
}
|
|
798
|
+
];
|
|
799
|
+
} catch {
|
|
800
|
+
return [];
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
summarizeMcp(config) {
|
|
804
|
+
if (config.command) {
|
|
805
|
+
return `${config.command} ${(config.args ?? []).join(" ")}`.trim();
|
|
806
|
+
}
|
|
807
|
+
if (config.url) return `${config.type ?? ""} ${config.url}`.trim();
|
|
808
|
+
return "";
|
|
809
|
+
}
|
|
810
|
+
// 插件 MCP 既可能在 .mcp.json(顶层即 server 映射或带 mcpServers 包裹),
|
|
811
|
+
// 也可能在 plugin.json 的 mcpServers 字段;合并两者。
|
|
812
|
+
async loadMcpServers(installPath, manifest) {
|
|
813
|
+
const fromFile = await this.safeReadJson(join4(installPath, ".mcp.json"));
|
|
814
|
+
const fileMap = fromFile?.mcpServers ?? fromFile ?? {};
|
|
815
|
+
const manifestMap = manifest?.mcpServers ?? {};
|
|
816
|
+
return { ...fileMap, ...manifestMap };
|
|
817
|
+
}
|
|
818
|
+
async scanMcp(installPath, manifest) {
|
|
819
|
+
const servers = await this.loadMcpServers(installPath, manifest);
|
|
820
|
+
return Object.entries(servers).map(([name, config]) => ({
|
|
821
|
+
type: "mcp",
|
|
822
|
+
name,
|
|
823
|
+
description: this.summarizeMcp(config)
|
|
824
|
+
}));
|
|
825
|
+
}
|
|
826
|
+
async scanComponents(installPath, manifest) {
|
|
827
|
+
const [skills, commands, agents, hooks, mcp] = await Promise.all([
|
|
828
|
+
this.scanSkills(join4(installPath, "skills")),
|
|
829
|
+
this.scanMarkdownDir(join4(installPath, "commands"), "command"),
|
|
830
|
+
this.scanMarkdownDir(join4(installPath, "agents"), "agent"),
|
|
831
|
+
this.scanHooks(join4(installPath, "hooks", "hooks.json")),
|
|
832
|
+
this.scanMcp(installPath, manifest)
|
|
833
|
+
]);
|
|
834
|
+
return [...skills, ...commands, ...agents, ...hooks, ...mcp];
|
|
835
|
+
}
|
|
836
|
+
// 把外部传入的相对路径安全解析到 baseDir 内,防止 `..` 越权。
|
|
837
|
+
resolveWithin(baseDir, relPath) {
|
|
838
|
+
const full = resolve2(baseDir, relPath);
|
|
839
|
+
const rel = relative2(baseDir, full);
|
|
840
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
|
|
841
|
+
throw new Error(`\u975E\u6CD5\u7EC4\u4EF6\u8DEF\u5F84\uFF1A${relPath}`);
|
|
842
|
+
}
|
|
843
|
+
return full;
|
|
844
|
+
}
|
|
845
|
+
async readComponent(paths, id, type, name) {
|
|
846
|
+
const rec = await this.findRecord(paths, id);
|
|
847
|
+
const base = rec.installPath;
|
|
848
|
+
if (type === "skill") {
|
|
849
|
+
const file = this.resolveWithin(
|
|
850
|
+
join4(base, "skills"),
|
|
851
|
+
join4(name, "SKILL.md")
|
|
852
|
+
);
|
|
853
|
+
return {
|
|
854
|
+
type,
|
|
855
|
+
name,
|
|
856
|
+
language: "markdown",
|
|
857
|
+
content: await readFile5(file, "utf8")
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
if (type === "command" || type === "agent") {
|
|
861
|
+
const sub = type === "command" ? "commands" : "agents";
|
|
862
|
+
const file = this.resolveWithin(join4(base, sub), `${name}.md`);
|
|
863
|
+
return {
|
|
864
|
+
type,
|
|
865
|
+
name,
|
|
866
|
+
language: "markdown",
|
|
867
|
+
content: await readFile5(file, "utf8")
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
if (type === "hook") {
|
|
871
|
+
const file = join4(base, "hooks", "hooks.json");
|
|
872
|
+
return {
|
|
873
|
+
type,
|
|
874
|
+
name,
|
|
875
|
+
language: "json",
|
|
876
|
+
content: await readFile5(file, "utf8")
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
const manifest = await this.safeReadJson(
|
|
880
|
+
this.manifestFile(base)
|
|
881
|
+
);
|
|
882
|
+
const servers = await this.loadMcpServers(base, manifest);
|
|
883
|
+
const config = servers[name];
|
|
884
|
+
if (!config) throw new Error(`\u672A\u627E\u5230 MCP server\u300C${name}\u300D`);
|
|
885
|
+
return {
|
|
886
|
+
type,
|
|
887
|
+
name,
|
|
888
|
+
language: "json",
|
|
889
|
+
content: JSON.stringify(config, null, 2)
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
async setEnabled(paths, id, enabled) {
|
|
893
|
+
const settings = await this.store.readJson(paths.userSettings) ?? {};
|
|
894
|
+
settings.enabledPlugins = {
|
|
895
|
+
...settings.enabledPlugins ?? {},
|
|
896
|
+
[id]: enabled
|
|
897
|
+
};
|
|
898
|
+
await this.store.writeJson(paths.userSettings, settings);
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// src/core/config-file-service.ts
|
|
903
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
904
|
+
var REGISTRY = {
|
|
905
|
+
settings: (paths, scope) => scope === "user" ? paths.userSettings : paths.projectSettings,
|
|
906
|
+
"claude-json": (paths, scope) => scope === "user" ? paths.userClaudeJson : void 0
|
|
907
|
+
};
|
|
908
|
+
var ConfigFileService = class {
|
|
909
|
+
constructor(store) {
|
|
910
|
+
this.store = store;
|
|
911
|
+
}
|
|
912
|
+
store;
|
|
913
|
+
file(paths, fileId, scope) {
|
|
914
|
+
const f = REGISTRY[fileId](paths, scope);
|
|
915
|
+
if (!f) {
|
|
916
|
+
throw new Error(
|
|
917
|
+
scope === "project" ? `\u914D\u7F6E\u6587\u4EF6 ${fileId} \u4E0D\u652F\u6301\u9879\u76EE\u7EA7\u4F5C\u7528\u57DF` : `\u7F3A\u5C11\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF1A${fileId}`
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
return f;
|
|
921
|
+
}
|
|
922
|
+
// 文件不存在视为「尚未创建」,返回空串(与 MemoryService 一致)。
|
|
923
|
+
async read(paths, fileId, scope) {
|
|
924
|
+
try {
|
|
925
|
+
return { content: await readFile6(this.file(paths, fileId, scope), "utf8") };
|
|
926
|
+
} catch (err) {
|
|
927
|
+
if (err.code === "ENOENT") return { content: "" };
|
|
928
|
+
throw err;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
// 校验合法 JSON 后整文件写回;非法内容抛 InvalidJsonError,不触碰磁盘原文件。
|
|
932
|
+
async write(paths, fileId, scope, content) {
|
|
933
|
+
const file = this.file(paths, fileId, scope);
|
|
934
|
+
let parsed;
|
|
935
|
+
try {
|
|
936
|
+
parsed = JSON.parse(content);
|
|
937
|
+
} catch {
|
|
938
|
+
throw new InvalidJsonError();
|
|
939
|
+
}
|
|
940
|
+
await this.store.writeJson(file, parsed);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// src/core/agent-command-service.ts
|
|
945
|
+
import { mkdir as mkdir3, readdir as readdir5, readFile as readFile7, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
946
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
947
|
+
import matter4 from "gray-matter";
|
|
948
|
+
function assertSafeName2(name) {
|
|
949
|
+
if (!name || name.includes("/") || name.includes("\\") || name.includes("..") || name.includes("\0")) {
|
|
950
|
+
throw new Error(`\u975E\u6CD5\u540D\u79F0\uFF1A${name}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
var AgentCommandService = class {
|
|
954
|
+
dir(paths, kind, scope) {
|
|
955
|
+
const dir = scope === "user" ? kind === "agents" ? paths.userAgentsDir : paths.userCommandsDir : kind === "agents" ? paths.projectAgentsDir : paths.projectCommandsDir;
|
|
956
|
+
if (!dir) throw new Error(`\u7F3A\u5C11\u9879\u76EE ${kind} \u76EE\u5F55`);
|
|
957
|
+
return dir;
|
|
958
|
+
}
|
|
959
|
+
fileFor(paths, kind, scope, name) {
|
|
960
|
+
assertSafeName2(name);
|
|
961
|
+
return join5(this.dir(paths, kind, scope), `${name}.md`);
|
|
962
|
+
}
|
|
963
|
+
async list(paths, kind, scope) {
|
|
964
|
+
const dir = this.dir(paths, kind, scope);
|
|
965
|
+
let entries;
|
|
966
|
+
try {
|
|
967
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
968
|
+
} catch (err) {
|
|
969
|
+
if (err.code === "ENOENT") return [];
|
|
970
|
+
throw err;
|
|
971
|
+
}
|
|
972
|
+
const out = [];
|
|
973
|
+
for (const e of entries) {
|
|
974
|
+
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
975
|
+
const name = e.name.slice(0, -3);
|
|
976
|
+
const path = join5(dir, e.name);
|
|
977
|
+
let description;
|
|
978
|
+
try {
|
|
979
|
+
const fm = matter4(await readFile7(path, "utf8")).data;
|
|
980
|
+
description = typeof fm.description === "string" ? fm.description : "";
|
|
981
|
+
} catch {
|
|
982
|
+
description = "(frontmatter \u89E3\u6790\u5931\u8D25)";
|
|
983
|
+
}
|
|
984
|
+
out.push({ name, description, path });
|
|
985
|
+
}
|
|
986
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
987
|
+
}
|
|
988
|
+
async read(paths, kind, scope, name) {
|
|
989
|
+
const file = this.fileFor(paths, kind, scope, name);
|
|
990
|
+
try {
|
|
991
|
+
return { content: await readFile7(file, "utf8") };
|
|
992
|
+
} catch {
|
|
993
|
+
throw new Error(`\u672A\u627E\u5230 ${kind}\u300C${name}\u300D\uFF08scope: ${scope}\uFF09`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async write(paths, kind, scope, name, content) {
|
|
997
|
+
const file = this.fileFor(paths, kind, scope, name);
|
|
998
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
999
|
+
await writeFile3(file, content, "utf8");
|
|
1000
|
+
}
|
|
1001
|
+
async remove(paths, kind, scope, name) {
|
|
1002
|
+
const file = this.fileFor(paths, kind, scope, name);
|
|
1003
|
+
await rm2(file, { force: true });
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// src/cli.ts
|
|
1008
|
+
function findFreePort(start) {
|
|
1009
|
+
return new Promise((resolve3, reject) => {
|
|
1010
|
+
const srv = createServer();
|
|
1011
|
+
srv.once("error", (err) => {
|
|
1012
|
+
if (err.code === "EADDRINUSE") resolve3(findFreePort(start + 1));
|
|
1013
|
+
else reject(err);
|
|
1014
|
+
});
|
|
1015
|
+
srv.listen(start, () => {
|
|
1016
|
+
srv.close(() => resolve3(start));
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
async function main() {
|
|
1021
|
+
const home = homedir();
|
|
1022
|
+
const store = new ConfigStore();
|
|
1023
|
+
const registryFile = join6(home, ".ccpanel", "ccpanel-projects.json");
|
|
1024
|
+
const webRoot = fileURLToPath(new URL("../web/dist", import.meta.url));
|
|
1025
|
+
const app = createApp({
|
|
1026
|
+
home,
|
|
1027
|
+
store,
|
|
1028
|
+
mcp: new McpService(store),
|
|
1029
|
+
skills: new SkillService(),
|
|
1030
|
+
projects: new ProjectService(store, registryFile),
|
|
1031
|
+
memory: new MemoryService(),
|
|
1032
|
+
memoryFiles: new MemoryFilesService(),
|
|
1033
|
+
sessions: new SessionService(),
|
|
1034
|
+
plugins: new PluginService(store),
|
|
1035
|
+
configFiles: new ConfigFileService(store),
|
|
1036
|
+
agentCommands: new AgentCommandService(),
|
|
1037
|
+
registryFile,
|
|
1038
|
+
webRoot
|
|
1039
|
+
});
|
|
1040
|
+
const port = await findFreePort(7788);
|
|
1041
|
+
serve({ fetch: app.fetch, port });
|
|
1042
|
+
const url = `http://localhost:${port}`;
|
|
1043
|
+
console.log(`ccpanel \u5DF2\u542F\u52A8\uFF1A${url}`);
|
|
1044
|
+
await open(url);
|
|
1045
|
+
}
|
|
1046
|
+
function isMainModule(argvPath, moduleUrl) {
|
|
1047
|
+
return !!argvPath && moduleUrl === pathToFileURL(argvPath).href;
|
|
1048
|
+
}
|
|
1049
|
+
if (isMainModule(process.argv[1], import.meta.url)) {
|
|
1050
|
+
void main();
|
|
1051
|
+
}
|
|
1052
|
+
export {
|
|
1053
|
+
findFreePort,
|
|
1054
|
+
isMainModule,
|
|
1055
|
+
main
|
|
1056
|
+
};
|