ccpanel 0.1.3 → 0.1.5
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 +20 -78
- package/dist/{chunk-SZFZF7CH.js → chunk-LZFMCC5M.js} +241 -74
- package/dist/cli.js +1087 -144
- package/dist/server/app.js +1 -1
- package/package.json +29 -12
- package/web/dist/assets/index-B5vT_l-U.css +1 -0
- package/web/dist/assets/{index-BGhhpb6e.js → index-XKaR9SwA.js} +27 -30
- package/web/dist/index.html +3 -3
- package/web/dist/assets/index-DZh_OBPf.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,93 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
ConfigCorruptError,
|
|
3
4
|
ConfigStore,
|
|
5
|
+
ConflictError,
|
|
4
6
|
InvalidJsonError,
|
|
5
7
|
SkillFrontmatterSchema,
|
|
6
8
|
assertNoConflict,
|
|
7
9
|
createApp,
|
|
8
10
|
currentMtime
|
|
9
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-LZFMCC5M.js";
|
|
10
12
|
|
|
11
13
|
// src/cli.ts
|
|
12
14
|
import { homedir, networkInterfaces } from "os";
|
|
13
|
-
import { join as
|
|
15
|
+
import { join as join12 } from "path";
|
|
14
16
|
import { createServer } from "net";
|
|
15
17
|
import { fileURLToPath } from "url";
|
|
16
18
|
import { realpathSync } from "fs";
|
|
17
19
|
import { serve } from "@hono/node-server";
|
|
18
20
|
import open from "open";
|
|
19
21
|
|
|
22
|
+
// src/core/mcp-service.ts
|
|
23
|
+
import { join as join2 } from "path";
|
|
24
|
+
|
|
25
|
+
// src/core/mcp-store.ts
|
|
26
|
+
import { constants, copyFile, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
27
|
+
import { dirname, join } from "path";
|
|
28
|
+
import { randomUUID } from "crypto";
|
|
29
|
+
import { parse, stringify } from "smol-toml";
|
|
30
|
+
var ClaudeMcpStore = class {
|
|
31
|
+
constructor(store, file) {
|
|
32
|
+
this.store = store;
|
|
33
|
+
this.file = file;
|
|
34
|
+
}
|
|
35
|
+
store;
|
|
36
|
+
file;
|
|
37
|
+
async read() {
|
|
38
|
+
const data = await this.store.readJson(this.file) ?? {};
|
|
39
|
+
return data.mcpServers ?? {};
|
|
40
|
+
}
|
|
41
|
+
async write(map) {
|
|
42
|
+
const data = await this.store.readJson(this.file) ?? {};
|
|
43
|
+
data.mcpServers = map;
|
|
44
|
+
await this.store.writeJson(this.file, data);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var CodexMcpStore = class {
|
|
48
|
+
constructor(file, backupDir) {
|
|
49
|
+
this.file = file;
|
|
50
|
+
this.backupDir = backupDir;
|
|
51
|
+
}
|
|
52
|
+
file;
|
|
53
|
+
backupDir;
|
|
54
|
+
async parseFile() {
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = await readFile(this.file, "utf8");
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err.code === "ENOENT") return {};
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
if (raw.trim() === "") return {};
|
|
63
|
+
try {
|
|
64
|
+
return parse(raw);
|
|
65
|
+
} catch {
|
|
66
|
+
throw new ConfigCorruptError(this.file);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async read() {
|
|
70
|
+
const data = await this.parseFile();
|
|
71
|
+
return data.mcp_servers ?? {};
|
|
72
|
+
}
|
|
73
|
+
async write(map) {
|
|
74
|
+
const data = await this.parseFile();
|
|
75
|
+
if (Object.keys(map).length > 0) data.mcp_servers = map;
|
|
76
|
+
else delete data.mcp_servers;
|
|
77
|
+
await mkdir(this.backupDir, { recursive: true });
|
|
78
|
+
try {
|
|
79
|
+
await copyFile(this.file, join(this.backupDir, "config.toml.bak"), constants.COPYFILE_EXCL);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const code = err.code;
|
|
82
|
+
if (code !== "EEXIST" && code !== "ENOENT") throw err;
|
|
83
|
+
}
|
|
84
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
85
|
+
const tmp = `${this.file}.${process.pid}.${randomUUID()}.tmp`;
|
|
86
|
+
await writeFile(tmp, stringify(data) + "\n", "utf8");
|
|
87
|
+
await rename(tmp, this.file);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
20
91
|
// src/core/mcp-service.ts
|
|
21
92
|
var McpService = class {
|
|
22
93
|
constructor(store) {
|
|
@@ -27,20 +98,23 @@ var McpService = class {
|
|
|
27
98
|
const { name, ...server } = input;
|
|
28
99
|
return { name, server };
|
|
29
100
|
}
|
|
101
|
+
// 按 provider 选择用户级主配置的存储实现(claude=JSON / codex=TOML)。
|
|
102
|
+
userStore(paths) {
|
|
103
|
+
return paths.provider === "codex" ? new CodexMcpStore(paths.userMcpFile, join2(paths.panelDir, "backup")) : new ClaudeMcpStore(this.store, paths.userMcpFile);
|
|
104
|
+
}
|
|
105
|
+
async readDisabled(paths) {
|
|
106
|
+
return await this.store.readJson(paths.userDisabledMcp) ?? {};
|
|
107
|
+
}
|
|
30
108
|
async list(paths, scope) {
|
|
31
109
|
if (scope === "user") {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
enabled: true,
|
|
41
|
-
path: paths.userClaudeJson
|
|
42
|
-
})
|
|
43
|
-
);
|
|
110
|
+
const active = await this.userStore(paths).read();
|
|
111
|
+
const disabled = await this.readDisabled(paths);
|
|
112
|
+
const enabled = Object.entries(active).map(([name, server]) => ({
|
|
113
|
+
name,
|
|
114
|
+
server,
|
|
115
|
+
enabled: true,
|
|
116
|
+
path: paths.userMcpFile
|
|
117
|
+
}));
|
|
44
118
|
const off = Object.entries(disabled).map(([name, server]) => ({
|
|
45
119
|
name,
|
|
46
120
|
server,
|
|
@@ -66,17 +140,16 @@ var McpService = class {
|
|
|
66
140
|
async upsert(paths, scope, input) {
|
|
67
141
|
const { name, server } = this.stripName(input);
|
|
68
142
|
if (scope === "user") {
|
|
69
|
-
const
|
|
70
|
-
paths.userClaudeJson
|
|
71
|
-
) ?? {};
|
|
72
|
-
const disabled = await this.store.readJson(paths.userDisabledMcp) ?? {};
|
|
143
|
+
const disabled = await this.readDisabled(paths);
|
|
73
144
|
if (disabled[name]) {
|
|
74
145
|
disabled[name] = server;
|
|
75
146
|
await this.store.writeJson(paths.userDisabledMcp, disabled);
|
|
76
147
|
return;
|
|
77
148
|
}
|
|
78
|
-
|
|
79
|
-
await
|
|
149
|
+
const store = this.userStore(paths);
|
|
150
|
+
const active = await store.read();
|
|
151
|
+
active[name] = server;
|
|
152
|
+
await store.write(active);
|
|
80
153
|
return;
|
|
81
154
|
}
|
|
82
155
|
const mcp = await this.store.readJson(
|
|
@@ -87,13 +160,12 @@ var McpService = class {
|
|
|
87
160
|
}
|
|
88
161
|
async remove(paths, scope, name) {
|
|
89
162
|
if (scope === "user") {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (claude.mcpServers) delete claude.mcpServers[name];
|
|
163
|
+
const store = this.userStore(paths);
|
|
164
|
+
const active = await store.read();
|
|
165
|
+
const disabled = await this.readDisabled(paths);
|
|
166
|
+
delete active[name];
|
|
95
167
|
delete disabled[name];
|
|
96
|
-
await
|
|
168
|
+
await store.write(active);
|
|
97
169
|
await this.store.writeJson(paths.userDisabledMcp, disabled);
|
|
98
170
|
return;
|
|
99
171
|
}
|
|
@@ -106,11 +178,9 @@ var McpService = class {
|
|
|
106
178
|
}
|
|
107
179
|
async toggle(paths, scope, name, enabled) {
|
|
108
180
|
if (scope === "user") {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const disabled = await this.store.readJson(paths.userDisabledMcp) ?? {};
|
|
113
|
-
const active = claude.mcpServers ?? {};
|
|
181
|
+
const store = this.userStore(paths);
|
|
182
|
+
const active = await store.read();
|
|
183
|
+
const disabled = await this.readDisabled(paths);
|
|
114
184
|
if (enabled && disabled[name]) {
|
|
115
185
|
active[name] = disabled[name];
|
|
116
186
|
delete disabled[name];
|
|
@@ -118,8 +188,7 @@ var McpService = class {
|
|
|
118
188
|
disabled[name] = active[name];
|
|
119
189
|
delete active[name];
|
|
120
190
|
}
|
|
121
|
-
|
|
122
|
-
await this.store.writeJson(paths.userClaudeJson, claude);
|
|
191
|
+
await store.write(active);
|
|
123
192
|
await this.store.writeJson(paths.userDisabledMcp, disabled);
|
|
124
193
|
return;
|
|
125
194
|
}
|
|
@@ -140,17 +209,35 @@ var McpService = class {
|
|
|
140
209
|
// src/core/skill-service.ts
|
|
141
210
|
import {
|
|
142
211
|
cp,
|
|
143
|
-
mkdir,
|
|
212
|
+
mkdir as mkdir2,
|
|
144
213
|
readdir,
|
|
145
|
-
readFile,
|
|
214
|
+
readFile as readFile2,
|
|
146
215
|
readlink,
|
|
147
|
-
rename,
|
|
216
|
+
rename as rename2,
|
|
148
217
|
rm,
|
|
149
218
|
stat,
|
|
150
|
-
writeFile
|
|
219
|
+
writeFile as writeFile2
|
|
151
220
|
} from "fs/promises";
|
|
152
|
-
import { dirname, isAbsolute, join, relative, resolve } from "path";
|
|
221
|
+
import { dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "path";
|
|
153
222
|
import matter from "gray-matter";
|
|
223
|
+
|
|
224
|
+
// src/core/link-utils.ts
|
|
225
|
+
import { resolve, sep } from "path";
|
|
226
|
+
function normalizeLinkTarget(p) {
|
|
227
|
+
const stripped = p.startsWith("\\\\?\\") ? p.slice(4) : p;
|
|
228
|
+
const abs = resolve(stripped);
|
|
229
|
+
return process.platform === "win32" ? abs.toLowerCase() : abs;
|
|
230
|
+
}
|
|
231
|
+
function classifyLinkTarget(target, agentsSkillsDir) {
|
|
232
|
+
const t = normalizeLinkTarget(target);
|
|
233
|
+
const base = normalizeLinkTarget(agentsSkillsDir);
|
|
234
|
+
return t === base || t.startsWith(base + sep) ? "shared" : "external";
|
|
235
|
+
}
|
|
236
|
+
function isSameLink(target, expected) {
|
|
237
|
+
return normalizeLinkTarget(target) === normalizeLinkTarget(expected);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/core/skill-service.ts
|
|
154
241
|
function assertSafeName(name) {
|
|
155
242
|
if (!name || name.includes("/") || name.includes("\\") || name.includes("..") || name.includes("\0")) {
|
|
156
243
|
throw new Error(`\u975E\u6CD5 skill \u540D\u79F0\uFF1A${name}`);
|
|
@@ -177,7 +264,7 @@ var SkillService = class {
|
|
|
177
264
|
if (!dir) throw new Error("\u7F3A\u5C11\u9879\u76EE\u7981\u7528 skills \u76EE\u5F55");
|
|
178
265
|
return dir;
|
|
179
266
|
}
|
|
180
|
-
async scan(dir, enabled) {
|
|
267
|
+
async scan(dir, enabled, agentsSkillsDir) {
|
|
181
268
|
let entries;
|
|
182
269
|
try {
|
|
183
270
|
entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -187,22 +274,37 @@ var SkillService = class {
|
|
|
187
274
|
}
|
|
188
275
|
const out = [];
|
|
189
276
|
for (const e of entries) {
|
|
277
|
+
if (e.name.startsWith(".")) continue;
|
|
190
278
|
if (!e.isDirectory() && !e.isSymbolicLink()) continue;
|
|
191
|
-
const path =
|
|
279
|
+
const path = join3(dir, e.name);
|
|
192
280
|
let linkTarget;
|
|
281
|
+
let linkKind;
|
|
193
282
|
if (e.isSymbolicLink()) {
|
|
283
|
+
linkTarget = await readlink(path).catch(() => void 0);
|
|
284
|
+
if (linkTarget !== void 0) {
|
|
285
|
+
linkKind = classifyLinkTarget(resolve2(dir, linkTarget), agentsSkillsDir);
|
|
286
|
+
}
|
|
194
287
|
try {
|
|
195
288
|
if (!(await stat(path)).isDirectory()) continue;
|
|
196
|
-
linkTarget = await readlink(path).catch(() => void 0);
|
|
197
289
|
} catch {
|
|
290
|
+
out.push({
|
|
291
|
+
name: e.name,
|
|
292
|
+
description: "(\u65AD\u94FE\uFF1A\u94FE\u63A5\u76EE\u6807\u4E0D\u5B58\u5728)",
|
|
293
|
+
enabled,
|
|
294
|
+
path,
|
|
295
|
+
linkTarget,
|
|
296
|
+
linkKind,
|
|
297
|
+
broken: true,
|
|
298
|
+
mtime: 0
|
|
299
|
+
});
|
|
198
300
|
continue;
|
|
199
301
|
}
|
|
200
302
|
}
|
|
201
|
-
const mtime = await currentMtime(
|
|
303
|
+
const mtime = await currentMtime(join3(path, "SKILL.md")) ?? await currentMtime(path) ?? 0;
|
|
202
304
|
try {
|
|
203
|
-
const raw = await
|
|
305
|
+
const raw = await readFile2(join3(path, "SKILL.md"), "utf8");
|
|
204
306
|
const fm = SkillFrontmatterSchema.parse(matter(raw).data);
|
|
205
|
-
out.push({ name: e.name, description: fm.description, enabled, path, linkTarget, mtime });
|
|
307
|
+
out.push({ name: e.name, description: fm.description, enabled, path, linkTarget, linkKind, mtime });
|
|
206
308
|
} catch {
|
|
207
309
|
out.push({
|
|
208
310
|
name: e.name,
|
|
@@ -210,6 +312,7 @@ var SkillService = class {
|
|
|
210
312
|
enabled,
|
|
211
313
|
path,
|
|
212
314
|
linkTarget,
|
|
315
|
+
linkKind,
|
|
213
316
|
mtime
|
|
214
317
|
});
|
|
215
318
|
}
|
|
@@ -217,15 +320,15 @@ var SkillService = class {
|
|
|
217
320
|
return out;
|
|
218
321
|
}
|
|
219
322
|
async list(paths, scope) {
|
|
220
|
-
const on = await this.scan(this.enabledDir(paths, scope), true);
|
|
221
|
-
const off = await this.scan(this.disabledDir(paths, scope), false);
|
|
323
|
+
const on = await this.scan(this.enabledDir(paths, scope), true, paths.agentsSkillsDir);
|
|
324
|
+
const off = await this.scan(this.disabledDir(paths, scope), false, paths.agentsSkillsDir);
|
|
222
325
|
return [...on, ...off].sort((a, b) => a.name.localeCompare(b.name));
|
|
223
326
|
}
|
|
224
327
|
// 返回该技能实际所在目录(启用或禁用),都不存在则抛错。
|
|
225
328
|
async resolveSkillDir(paths, scope, name) {
|
|
226
329
|
assertSafeName(name);
|
|
227
330
|
for (const base of [this.enabledDir(paths, scope), this.disabledDir(paths, scope)]) {
|
|
228
|
-
const dir =
|
|
331
|
+
const dir = join3(base, name);
|
|
229
332
|
try {
|
|
230
333
|
await readdir(dir);
|
|
231
334
|
return dir;
|
|
@@ -236,7 +339,7 @@ var SkillService = class {
|
|
|
236
339
|
}
|
|
237
340
|
// 把相对路径安全地解析到技能目录内,防止 `..` 越权。
|
|
238
341
|
resolveWithin(baseDir, relPath) {
|
|
239
|
-
const full =
|
|
342
|
+
const full = resolve2(baseDir, relPath);
|
|
240
343
|
const rel = relative(baseDir, full);
|
|
241
344
|
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
242
345
|
throw new Error(`\u975E\u6CD5\u6587\u4EF6\u8DEF\u5F84\uFF1A${relPath}`);
|
|
@@ -245,14 +348,14 @@ var SkillService = class {
|
|
|
245
348
|
}
|
|
246
349
|
async read(paths, scope, name) {
|
|
247
350
|
assertSafeName(name);
|
|
248
|
-
const enabledPath =
|
|
249
|
-
const disabledPath =
|
|
351
|
+
const enabledPath = join3(this.enabledDir(paths, scope), name, "SKILL.md");
|
|
352
|
+
const disabledPath = join3(this.disabledDir(paths, scope), name, "SKILL.md");
|
|
250
353
|
let raw;
|
|
251
354
|
try {
|
|
252
|
-
raw = await
|
|
355
|
+
raw = await readFile2(enabledPath, "utf8");
|
|
253
356
|
} catch {
|
|
254
357
|
try {
|
|
255
|
-
raw = await
|
|
358
|
+
raw = await readFile2(disabledPath, "utf8");
|
|
256
359
|
} catch {
|
|
257
360
|
throw new Error(`\u672A\u627E\u5230 skill\u300C${name}\u300D\uFF08scope: ${scope}\uFF09`);
|
|
258
361
|
}
|
|
@@ -270,12 +373,12 @@ var SkillService = class {
|
|
|
270
373
|
const walk = async (dir) => {
|
|
271
374
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
272
375
|
for (const e of entries) {
|
|
273
|
-
const full =
|
|
376
|
+
const full = join3(dir, e.name);
|
|
274
377
|
if (e.isDirectory()) {
|
|
275
378
|
if (IGNORED_DIRS.has(e.name)) continue;
|
|
276
379
|
await walk(full);
|
|
277
380
|
} else if (e.isFile()) {
|
|
278
|
-
if (isBinary(await
|
|
381
|
+
if (isBinary(await readFile2(full))) continue;
|
|
279
382
|
out.push(relative(root, full).split("\\").join("/"));
|
|
280
383
|
}
|
|
281
384
|
}
|
|
@@ -286,7 +389,7 @@ var SkillService = class {
|
|
|
286
389
|
async readFileRaw(paths, scope, name, relPath) {
|
|
287
390
|
const root = await this.resolveSkillDir(paths, scope, name);
|
|
288
391
|
const full = this.resolveWithin(root, relPath);
|
|
289
|
-
const buf = await
|
|
392
|
+
const buf = await readFile2(full);
|
|
290
393
|
if (isBinary(buf)) throw new Error(`\u4E8C\u8FDB\u5236\u6587\u4EF6\u65E0\u6CD5\u7F16\u8F91\uFF1A${relPath}`);
|
|
291
394
|
return { content: buf.toString("utf8"), mtime: await currentMtime(full) };
|
|
292
395
|
}
|
|
@@ -294,32 +397,32 @@ var SkillService = class {
|
|
|
294
397
|
const root = await this.resolveSkillDir(paths, scope, name);
|
|
295
398
|
const full = this.resolveWithin(root, relPath);
|
|
296
399
|
await assertNoConflict(full, baseMtime);
|
|
297
|
-
await
|
|
298
|
-
await
|
|
400
|
+
await mkdir2(dirname2(full), { recursive: true });
|
|
401
|
+
await writeFile2(full, content, "utf8");
|
|
299
402
|
}
|
|
300
403
|
async upsert(paths, scope, name, frontmatter, body) {
|
|
301
404
|
assertSafeName(name);
|
|
302
|
-
const file =
|
|
303
|
-
await
|
|
405
|
+
const file = join3(this.enabledDir(paths, scope), name, "SKILL.md");
|
|
406
|
+
await mkdir2(dirname2(file), { recursive: true });
|
|
304
407
|
const content = matter.stringify(body, frontmatter);
|
|
305
|
-
await
|
|
408
|
+
await writeFile2(file, content, "utf8");
|
|
306
409
|
}
|
|
307
410
|
async remove(paths, scope, name) {
|
|
308
411
|
assertSafeName(name);
|
|
309
|
-
await rm(
|
|
412
|
+
await rm(join3(this.enabledDir(paths, scope), name), {
|
|
310
413
|
recursive: true,
|
|
311
414
|
force: true
|
|
312
415
|
});
|
|
313
|
-
await rm(
|
|
416
|
+
await rm(join3(this.disabledDir(paths, scope), name), {
|
|
314
417
|
recursive: true,
|
|
315
418
|
force: true
|
|
316
419
|
});
|
|
317
420
|
}
|
|
318
421
|
// 跨盘符 rename 会抛 EXDEV(如项目在 D: 而 ~/.ccpanel 在 C:),回退到复制+删除。
|
|
319
422
|
async moveDir(from, to) {
|
|
320
|
-
await
|
|
423
|
+
await mkdir2(dirname2(to), { recursive: true });
|
|
321
424
|
try {
|
|
322
|
-
await
|
|
425
|
+
await rename2(from, to);
|
|
323
426
|
} catch (err) {
|
|
324
427
|
if (err.code === "EXDEV") {
|
|
325
428
|
await cp(from, to, { recursive: true });
|
|
@@ -331,11 +434,11 @@ var SkillService = class {
|
|
|
331
434
|
}
|
|
332
435
|
async toggle(paths, scope, name, enabled) {
|
|
333
436
|
assertSafeName(name);
|
|
334
|
-
const from =
|
|
437
|
+
const from = join3(
|
|
335
438
|
enabled ? this.disabledDir(paths, scope) : this.enabledDir(paths, scope),
|
|
336
439
|
name
|
|
337
440
|
);
|
|
338
|
-
const to =
|
|
441
|
+
const to = join3(
|
|
339
442
|
enabled ? this.enabledDir(paths, scope) : this.disabledDir(paths, scope),
|
|
340
443
|
name
|
|
341
444
|
);
|
|
@@ -344,19 +447,37 @@ var SkillService = class {
|
|
|
344
447
|
};
|
|
345
448
|
|
|
346
449
|
// src/core/project-service.ts
|
|
450
|
+
import { stat as stat2 } from "fs/promises";
|
|
347
451
|
var ProjectService = class {
|
|
348
|
-
constructor(store, registryFile) {
|
|
452
|
+
constructor(store, registryFile, userClaudeJson) {
|
|
349
453
|
this.store = store;
|
|
350
454
|
this.registryFile = registryFile;
|
|
455
|
+
this.userClaudeJson = userClaudeJson;
|
|
351
456
|
}
|
|
352
457
|
store;
|
|
353
458
|
registryFile;
|
|
459
|
+
userClaudeJson;
|
|
354
460
|
async list() {
|
|
355
461
|
const data = await this.store.readJson(
|
|
356
462
|
this.registryFile
|
|
357
463
|
);
|
|
358
464
|
return data?.projects ?? [];
|
|
359
465
|
}
|
|
466
|
+
// Claude Code 在 ~/.claude.json 的 projects 键下记录所有打开过的项目路径,
|
|
467
|
+
// 作为「添加项目」的候选来源;剔除已注册与已删除的目录。
|
|
468
|
+
async known() {
|
|
469
|
+
const data = await this.store.readJson(this.userClaudeJson);
|
|
470
|
+
const registered = new Set(await this.list());
|
|
471
|
+
const out = [];
|
|
472
|
+
for (const p of Object.keys(data?.projects ?? {})) {
|
|
473
|
+
if (registered.has(p)) continue;
|
|
474
|
+
try {
|
|
475
|
+
if ((await stat2(p)).isDirectory()) out.push(p);
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return out.sort();
|
|
480
|
+
}
|
|
360
481
|
async add(path) {
|
|
361
482
|
const current = await this.list();
|
|
362
483
|
if (!current.includes(path)) current.push(path);
|
|
@@ -373,8 +494,8 @@ var ProjectService = class {
|
|
|
373
494
|
};
|
|
374
495
|
|
|
375
496
|
// src/core/memory-service.ts
|
|
376
|
-
import { mkdir as
|
|
377
|
-
import { dirname as
|
|
497
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
498
|
+
import { dirname as dirname3 } from "path";
|
|
378
499
|
var MemoryService = class {
|
|
379
500
|
file(paths, scope) {
|
|
380
501
|
const f = scope === "user" ? paths.userClaudeMd : paths.projectClaudeMd;
|
|
@@ -385,7 +506,7 @@ var MemoryService = class {
|
|
|
385
506
|
async read(paths, scope) {
|
|
386
507
|
const f = this.file(paths, scope);
|
|
387
508
|
try {
|
|
388
|
-
return { content: await
|
|
509
|
+
return { content: await readFile3(f, "utf8"), mtime: await currentMtime(f) };
|
|
389
510
|
} catch (err) {
|
|
390
511
|
if (err.code === "ENOENT")
|
|
391
512
|
return { content: "", mtime: null };
|
|
@@ -395,19 +516,19 @@ var MemoryService = class {
|
|
|
395
516
|
async write(paths, scope, content, baseMtime) {
|
|
396
517
|
const f = this.file(paths, scope);
|
|
397
518
|
await assertNoConflict(f, baseMtime);
|
|
398
|
-
await
|
|
399
|
-
await
|
|
519
|
+
await mkdir3(dirname3(f), { recursive: true });
|
|
520
|
+
await writeFile3(f, content, "utf8");
|
|
400
521
|
}
|
|
401
522
|
};
|
|
402
523
|
|
|
403
524
|
// src/core/memory-files-service.ts
|
|
404
|
-
import { readdir as readdir3, readFile as
|
|
405
|
-
import { join as
|
|
525
|
+
import { readdir as readdir3, readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
526
|
+
import { join as join5 } from "path";
|
|
406
527
|
import matter2 from "gray-matter";
|
|
407
528
|
|
|
408
529
|
// src/core/session-service.ts
|
|
409
|
-
import { readdir as readdir2, readFile as
|
|
410
|
-
import { join as
|
|
530
|
+
import { readdir as readdir2, readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
531
|
+
import { join as join4 } from "path";
|
|
411
532
|
var MAX_BLOCK = 1e4;
|
|
412
533
|
var NOISE_PREFIXES = ["<local-command", "<command-", "<system-reminder", "Caveat:"];
|
|
413
534
|
function encodeProjectDir(projectPath) {
|
|
@@ -491,7 +612,7 @@ function toBlocks(content) {
|
|
|
491
612
|
}
|
|
492
613
|
var SessionService = class {
|
|
493
614
|
dir(paths, projectPath) {
|
|
494
|
-
return
|
|
615
|
+
return join4(paths.userProjectsDir, encodeProjectDir(projectPath));
|
|
495
616
|
}
|
|
496
617
|
async list(paths, projectPath) {
|
|
497
618
|
const dir = this.dir(paths, projectPath);
|
|
@@ -504,8 +625,8 @@ var SessionService = class {
|
|
|
504
625
|
}
|
|
505
626
|
const out = [];
|
|
506
627
|
for (const name of names) {
|
|
507
|
-
const full =
|
|
508
|
-
const st = await
|
|
628
|
+
const full = join4(dir, name);
|
|
629
|
+
const st = await stat3(full);
|
|
509
630
|
const { firstPrompt, messageCount } = await this.summarize(full);
|
|
510
631
|
out.push({
|
|
511
632
|
id: name.replace(/\.jsonl$/, ""),
|
|
@@ -518,7 +639,7 @@ var SessionService = class {
|
|
|
518
639
|
return out.sort((a, b) => b.mtime - a.mtime);
|
|
519
640
|
}
|
|
520
641
|
async summarize(file) {
|
|
521
|
-
const raw = await
|
|
642
|
+
const raw = await readFile4(file, "utf8");
|
|
522
643
|
let firstPrompt = "";
|
|
523
644
|
let messageCount = 0;
|
|
524
645
|
for (const line of raw.split("\n")) {
|
|
@@ -543,9 +664,9 @@ var SessionService = class {
|
|
|
543
664
|
async read(paths, projectPath, id) {
|
|
544
665
|
assertSafeId(id);
|
|
545
666
|
const dir = this.dir(paths, projectPath);
|
|
546
|
-
const raw = await
|
|
667
|
+
const raw = await readFile4(join4(dir, `${id}.jsonl`), "utf8");
|
|
547
668
|
const messages = parseMessages(raw);
|
|
548
|
-
const subagents = await this.readSubagents(
|
|
669
|
+
const subagents = await this.readSubagents(join4(dir, id, "subagents"));
|
|
549
670
|
return { messages, subagents };
|
|
550
671
|
}
|
|
551
672
|
// 读取某会话的 subagents/ 目录:每个 agent-<id>.jsonl 配对同名 .meta.json。
|
|
@@ -562,8 +683,8 @@ var SessionService = class {
|
|
|
562
683
|
for (const name of names) {
|
|
563
684
|
const agentId = parseAgentId(name);
|
|
564
685
|
if (!agentId) continue;
|
|
565
|
-
const messages = parseMessages(await
|
|
566
|
-
const meta = await this.readMeta(
|
|
686
|
+
const messages = parseMessages(await readFile4(join4(subDir, name), "utf8"));
|
|
687
|
+
const meta = await this.readMeta(join4(subDir, `agent-${agentId}.meta.json`));
|
|
567
688
|
out.push({
|
|
568
689
|
toolUseId: meta.toolUseId,
|
|
569
690
|
agentId,
|
|
@@ -576,7 +697,7 @@ var SessionService = class {
|
|
|
576
697
|
}
|
|
577
698
|
async readMeta(file) {
|
|
578
699
|
try {
|
|
579
|
-
const obj = JSON.parse(await
|
|
700
|
+
const obj = JSON.parse(await readFile4(file, "utf8"));
|
|
580
701
|
return {
|
|
581
702
|
toolUseId: String(obj.toolUseId ?? ""),
|
|
582
703
|
agentType: String(obj.agentType ?? "subagent"),
|
|
@@ -597,7 +718,7 @@ function assertSafeMemoryFile(file) {
|
|
|
597
718
|
}
|
|
598
719
|
var MemoryFilesService = class {
|
|
599
720
|
dir(paths, projectPath) {
|
|
600
|
-
return
|
|
721
|
+
return join5(paths.userProjectsDir, encodeProjectDir(projectPath), "memory");
|
|
601
722
|
}
|
|
602
723
|
// 目录不存在视为「尚无记忆」,返回空概览。
|
|
603
724
|
async overview(paths, projectPath) {
|
|
@@ -612,12 +733,12 @@ var MemoryFilesService = class {
|
|
|
612
733
|
let index = null;
|
|
613
734
|
const files = [];
|
|
614
735
|
for (const name of names) {
|
|
615
|
-
const full =
|
|
736
|
+
const full = join5(dir, name);
|
|
616
737
|
if (name === INDEX_FILE) {
|
|
617
|
-
index = await
|
|
738
|
+
index = await readFile5(full, "utf8");
|
|
618
739
|
continue;
|
|
619
740
|
}
|
|
620
|
-
const st = await
|
|
741
|
+
const st = await stat4(full);
|
|
621
742
|
const fm = await this.parseFront(full, name);
|
|
622
743
|
files.push({ file: name, ...fm, size: st.size, mtime: st.mtimeMs });
|
|
623
744
|
}
|
|
@@ -628,7 +749,7 @@ var MemoryFilesService = class {
|
|
|
628
749
|
async parseFront(full, fileName) {
|
|
629
750
|
const fallbackName = fileName.replace(/\.md$/, "");
|
|
630
751
|
try {
|
|
631
|
-
const data = matter2(await
|
|
752
|
+
const data = matter2(await readFile5(full, "utf8")).data;
|
|
632
753
|
const meta = data.metadata ?? {};
|
|
633
754
|
return {
|
|
634
755
|
name: typeof data.name === "string" && data.name ? data.name : fallbackName,
|
|
@@ -642,13 +763,13 @@ var MemoryFilesService = class {
|
|
|
642
763
|
// 读取单个记忆文件原文(含 frontmatter)。
|
|
643
764
|
async read(paths, projectPath, file) {
|
|
644
765
|
assertSafeMemoryFile(file);
|
|
645
|
-
return { content: await
|
|
766
|
+
return { content: await readFile5(join5(this.dir(paths, projectPath), file), "utf8") };
|
|
646
767
|
}
|
|
647
768
|
};
|
|
648
769
|
|
|
649
770
|
// src/core/plugin-service.ts
|
|
650
|
-
import { readFile as
|
|
651
|
-
import { isAbsolute as isAbsolute2, join as
|
|
771
|
+
import { readFile as readFile6, readdir as readdir4, stat as stat5 } from "fs/promises";
|
|
772
|
+
import { isAbsolute as isAbsolute2, join as join6, relative as relative2, resolve as resolve3 } from "path";
|
|
652
773
|
import matter3 from "gray-matter";
|
|
653
774
|
var PluginService = class {
|
|
654
775
|
constructor(store) {
|
|
@@ -656,7 +777,7 @@ var PluginService = class {
|
|
|
656
777
|
}
|
|
657
778
|
store;
|
|
658
779
|
async pathExists(p) {
|
|
659
|
-
return
|
|
780
|
+
return stat5(p).then(() => true).catch(() => false);
|
|
660
781
|
}
|
|
661
782
|
// 插件元数据要尽量容错:单个文件损坏不应让整页报错(启停写入除外)。
|
|
662
783
|
async safeReadJson(file) {
|
|
@@ -667,7 +788,7 @@ var PluginService = class {
|
|
|
667
788
|
}
|
|
668
789
|
}
|
|
669
790
|
manifestFile(installPath) {
|
|
670
|
-
return
|
|
791
|
+
return join6(installPath, ".claude-plugin", "plugin.json");
|
|
671
792
|
}
|
|
672
793
|
// id 形如 "name@marketplace",按最后一个 @ 切分。
|
|
673
794
|
splitId(id) {
|
|
@@ -699,7 +820,8 @@ var PluginService = class {
|
|
|
699
820
|
repo: markets[marketplace]?.source?.repo,
|
|
700
821
|
installPath,
|
|
701
822
|
enabled: enabled[id] === true,
|
|
702
|
-
exists
|
|
823
|
+
exists,
|
|
824
|
+
source: "user"
|
|
703
825
|
};
|
|
704
826
|
return { entry, manifest };
|
|
705
827
|
}
|
|
@@ -744,7 +866,7 @@ var PluginService = class {
|
|
|
744
866
|
// 读取 frontmatter 的 description / name 作为子组件摘要,失败返回空串。
|
|
745
867
|
async readDescription(file) {
|
|
746
868
|
try {
|
|
747
|
-
const fm = matter3(await
|
|
869
|
+
const fm = matter3(await readFile6(file, "utf8")).data;
|
|
748
870
|
return fm.description ?? fm.name ?? "";
|
|
749
871
|
} catch {
|
|
750
872
|
return "";
|
|
@@ -761,7 +883,7 @@ var PluginService = class {
|
|
|
761
883
|
for (const e of entries) {
|
|
762
884
|
if (!e.isDirectory()) continue;
|
|
763
885
|
const description = await this.readDescription(
|
|
764
|
-
|
|
886
|
+
join6(dir, e.name, "SKILL.md")
|
|
765
887
|
);
|
|
766
888
|
out.push({ type: "skill", name: e.name, description });
|
|
767
889
|
}
|
|
@@ -778,7 +900,7 @@ var PluginService = class {
|
|
|
778
900
|
return;
|
|
779
901
|
}
|
|
780
902
|
for (const e of entries) {
|
|
781
|
-
const full =
|
|
903
|
+
const full = join6(cur, e.name);
|
|
782
904
|
if (e.isDirectory()) {
|
|
783
905
|
await walk(full);
|
|
784
906
|
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
@@ -796,7 +918,7 @@ var PluginService = class {
|
|
|
796
918
|
}
|
|
797
919
|
async scanHooks(file) {
|
|
798
920
|
try {
|
|
799
|
-
const json = JSON.parse(await
|
|
921
|
+
const json = JSON.parse(await readFile6(file, "utf8"));
|
|
800
922
|
const events = Object.keys(json.hooks ?? json);
|
|
801
923
|
return [
|
|
802
924
|
{
|
|
@@ -819,7 +941,7 @@ var PluginService = class {
|
|
|
819
941
|
// 插件 MCP 既可能在 .mcp.json(顶层即 server 映射或带 mcpServers 包裹),
|
|
820
942
|
// 也可能在 plugin.json 的 mcpServers 字段;合并两者。
|
|
821
943
|
async loadMcpServers(installPath, manifest) {
|
|
822
|
-
const fromFile = await this.safeReadJson(
|
|
944
|
+
const fromFile = await this.safeReadJson(join6(installPath, ".mcp.json"));
|
|
823
945
|
const fileMap = fromFile?.mcpServers ?? fromFile ?? {};
|
|
824
946
|
const manifestMap = manifest?.mcpServers ?? {};
|
|
825
947
|
return { ...fileMap, ...manifestMap };
|
|
@@ -834,17 +956,17 @@ var PluginService = class {
|
|
|
834
956
|
}
|
|
835
957
|
async scanComponents(installPath, manifest) {
|
|
836
958
|
const [skills, commands, agents, hooks, mcp] = await Promise.all([
|
|
837
|
-
this.scanSkills(
|
|
838
|
-
this.scanMarkdownDir(
|
|
839
|
-
this.scanMarkdownDir(
|
|
840
|
-
this.scanHooks(
|
|
959
|
+
this.scanSkills(join6(installPath, "skills")),
|
|
960
|
+
this.scanMarkdownDir(join6(installPath, "commands"), "command"),
|
|
961
|
+
this.scanMarkdownDir(join6(installPath, "agents"), "agent"),
|
|
962
|
+
this.scanHooks(join6(installPath, "hooks", "hooks.json")),
|
|
841
963
|
this.scanMcp(installPath, manifest)
|
|
842
964
|
]);
|
|
843
965
|
return [...skills, ...commands, ...agents, ...hooks, ...mcp];
|
|
844
966
|
}
|
|
845
967
|
// 把外部传入的相对路径安全解析到 baseDir 内,防止 `..` 越权。
|
|
846
968
|
resolveWithin(baseDir, relPath) {
|
|
847
|
-
const full =
|
|
969
|
+
const full = resolve3(baseDir, relPath);
|
|
848
970
|
const rel = relative2(baseDir, full);
|
|
849
971
|
if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
|
|
850
972
|
throw new Error(`\u975E\u6CD5\u7EC4\u4EF6\u8DEF\u5F84\uFF1A${relPath}`);
|
|
@@ -856,33 +978,33 @@ var PluginService = class {
|
|
|
856
978
|
const base = rec.installPath;
|
|
857
979
|
if (type === "skill") {
|
|
858
980
|
const file = this.resolveWithin(
|
|
859
|
-
|
|
860
|
-
|
|
981
|
+
join6(base, "skills"),
|
|
982
|
+
join6(name, "SKILL.md")
|
|
861
983
|
);
|
|
862
984
|
return {
|
|
863
985
|
type,
|
|
864
986
|
name,
|
|
865
987
|
language: "markdown",
|
|
866
|
-
content: await
|
|
988
|
+
content: await readFile6(file, "utf8")
|
|
867
989
|
};
|
|
868
990
|
}
|
|
869
991
|
if (type === "command" || type === "agent") {
|
|
870
992
|
const sub = type === "command" ? "commands" : "agents";
|
|
871
|
-
const file = this.resolveWithin(
|
|
993
|
+
const file = this.resolveWithin(join6(base, sub), `${name}.md`);
|
|
872
994
|
return {
|
|
873
995
|
type,
|
|
874
996
|
name,
|
|
875
997
|
language: "markdown",
|
|
876
|
-
content: await
|
|
998
|
+
content: await readFile6(file, "utf8")
|
|
877
999
|
};
|
|
878
1000
|
}
|
|
879
1001
|
if (type === "hook") {
|
|
880
|
-
const file =
|
|
1002
|
+
const file = join6(base, "hooks", "hooks.json");
|
|
881
1003
|
return {
|
|
882
1004
|
type,
|
|
883
1005
|
name,
|
|
884
1006
|
language: "json",
|
|
885
|
-
content: await
|
|
1007
|
+
content: await readFile6(file, "utf8")
|
|
886
1008
|
};
|
|
887
1009
|
}
|
|
888
1010
|
const manifest = await this.safeReadJson(
|
|
@@ -908,55 +1030,758 @@ var PluginService = class {
|
|
|
908
1030
|
}
|
|
909
1031
|
};
|
|
910
1032
|
|
|
1033
|
+
// src/core/codex-plugin-service.ts
|
|
1034
|
+
import { readFile as readFile7, readdir as readdir5, stat as stat6 } from "fs/promises";
|
|
1035
|
+
import { isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve4 } from "path";
|
|
1036
|
+
import matter4 from "gray-matter";
|
|
1037
|
+
import { parse as parse2, stringify as stringify2 } from "smol-toml";
|
|
1038
|
+
var BUILTIN_MARKETPLACES = /* @__PURE__ */ new Set([
|
|
1039
|
+
"openai-bundled",
|
|
1040
|
+
"openai-primary-runtime",
|
|
1041
|
+
"openai-curated"
|
|
1042
|
+
]);
|
|
1043
|
+
var CodexPluginService = class {
|
|
1044
|
+
constructor(store) {
|
|
1045
|
+
this.store = store;
|
|
1046
|
+
}
|
|
1047
|
+
store;
|
|
1048
|
+
async pathExists(p) {
|
|
1049
|
+
return stat6(p).then(() => true).catch(() => false);
|
|
1050
|
+
}
|
|
1051
|
+
async readConfig(paths) {
|
|
1052
|
+
let raw;
|
|
1053
|
+
try {
|
|
1054
|
+
raw = await readFile7(paths.codexConfigToml, "utf8");
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
if (err.code === "ENOENT") return {};
|
|
1057
|
+
throw err;
|
|
1058
|
+
}
|
|
1059
|
+
if (!raw.trim()) return {};
|
|
1060
|
+
return parse2(raw);
|
|
1061
|
+
}
|
|
1062
|
+
// id 形如 "name@marketplace",按最后一个 @ 切分,兼容插件名自身含 @。
|
|
1063
|
+
splitId(id) {
|
|
1064
|
+
const at = id.lastIndexOf("@");
|
|
1065
|
+
if (at <= 0 || at === id.length - 1) {
|
|
1066
|
+
throw new Error(`\u975E\u6CD5\u63D2\u4EF6 id\uFF1A${id}`);
|
|
1067
|
+
}
|
|
1068
|
+
return { name: id.slice(0, at), marketplace: id.slice(at + 1) };
|
|
1069
|
+
}
|
|
1070
|
+
authorName(a) {
|
|
1071
|
+
if (!a) return void 0;
|
|
1072
|
+
return typeof a === "string" ? a : a.name;
|
|
1073
|
+
}
|
|
1074
|
+
manifestFile(installPath) {
|
|
1075
|
+
return join7(installPath, ".codex-plugin", "plugin.json");
|
|
1076
|
+
}
|
|
1077
|
+
async readManifest(installPath) {
|
|
1078
|
+
try {
|
|
1079
|
+
return JSON.parse(await readFile7(this.manifestFile(installPath), "utf8"));
|
|
1080
|
+
} catch {
|
|
1081
|
+
return void 0;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
compareVersionDir(a, b) {
|
|
1085
|
+
const pa = a.split(".").map((x) => Number.parseInt(x, 10));
|
|
1086
|
+
const pb = b.split(".").map((x) => Number.parseInt(x, 10));
|
|
1087
|
+
const validA = pa.every(Number.isFinite);
|
|
1088
|
+
const validB = pb.every(Number.isFinite);
|
|
1089
|
+
if (validA && validB) {
|
|
1090
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1091
|
+
const diff = (pb[i] ?? 0) - (pa[i] ?? 0);
|
|
1092
|
+
if (diff !== 0) return diff;
|
|
1093
|
+
}
|
|
1094
|
+
return 0;
|
|
1095
|
+
}
|
|
1096
|
+
if (validA !== validB) return validA ? -1 : 1;
|
|
1097
|
+
return b.localeCompare(a);
|
|
1098
|
+
}
|
|
1099
|
+
async findInstallPath(paths, id) {
|
|
1100
|
+
const { name, marketplace } = this.splitId(id);
|
|
1101
|
+
const root = join7(paths.codexPluginsDir, "cache", marketplace, name);
|
|
1102
|
+
let entries;
|
|
1103
|
+
try {
|
|
1104
|
+
entries = await readdir5(root, { withFileTypes: true });
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
if (err.code === "ENOENT") return void 0;
|
|
1107
|
+
throw err;
|
|
1108
|
+
}
|
|
1109
|
+
const versions = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => this.compareVersionDir(a, b));
|
|
1110
|
+
return versions[0] ? join7(root, versions[0]) : void 0;
|
|
1111
|
+
}
|
|
1112
|
+
async buildEntry(paths, id, enabled, config) {
|
|
1113
|
+
const { name: idName, marketplace } = this.splitId(id);
|
|
1114
|
+
const installPath = await this.findInstallPath(paths, id) ?? "";
|
|
1115
|
+
const exists = installPath ? await this.pathExists(installPath) : false;
|
|
1116
|
+
const manifest = exists ? await this.readManifest(installPath) : void 0;
|
|
1117
|
+
const entry = {
|
|
1118
|
+
id,
|
|
1119
|
+
name: manifest?.name ?? idName,
|
|
1120
|
+
description: manifest?.description ?? "",
|
|
1121
|
+
version: manifest?.version ?? (installPath ? installPath.split(/[\\/]/).at(-1) : "unknown") ?? "unknown",
|
|
1122
|
+
author: this.authorName(manifest?.author),
|
|
1123
|
+
homepage: manifest?.homepage,
|
|
1124
|
+
marketplace,
|
|
1125
|
+
repo: config.marketplaces?.[marketplace]?.source,
|
|
1126
|
+
installPath,
|
|
1127
|
+
enabled,
|
|
1128
|
+
exists,
|
|
1129
|
+
source: BUILTIN_MARKETPLACES.has(marketplace) ? "builtin" : "user"
|
|
1130
|
+
};
|
|
1131
|
+
return { entry, manifest };
|
|
1132
|
+
}
|
|
1133
|
+
async list(paths) {
|
|
1134
|
+
const config = await this.readConfig(paths);
|
|
1135
|
+
const out = [];
|
|
1136
|
+
for (const [id, rec] of Object.entries(config.plugins ?? {})) {
|
|
1137
|
+
const { entry } = await this.buildEntry(paths, id, rec.enabled === true, config);
|
|
1138
|
+
out.push(entry);
|
|
1139
|
+
}
|
|
1140
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1141
|
+
}
|
|
1142
|
+
async detail(paths, id) {
|
|
1143
|
+
const config = await this.readConfig(paths);
|
|
1144
|
+
const rec = config.plugins?.[id];
|
|
1145
|
+
if (!rec) throw new Error(`\u672A\u627E\u5230\u63D2\u4EF6\u300C${id}\u300D`);
|
|
1146
|
+
const { entry, manifest } = await this.buildEntry(
|
|
1147
|
+
paths,
|
|
1148
|
+
id,
|
|
1149
|
+
rec.enabled === true,
|
|
1150
|
+
config
|
|
1151
|
+
);
|
|
1152
|
+
const components = entry.exists ? await this.scanComponents(entry.installPath, manifest) : [];
|
|
1153
|
+
return { ...entry, components };
|
|
1154
|
+
}
|
|
1155
|
+
async readDescription(file) {
|
|
1156
|
+
try {
|
|
1157
|
+
const fm = matter4(await readFile7(file, "utf8")).data;
|
|
1158
|
+
return fm.description ?? fm.name ?? "";
|
|
1159
|
+
} catch {
|
|
1160
|
+
return "";
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
async scanSkills(dir) {
|
|
1164
|
+
let entries;
|
|
1165
|
+
try {
|
|
1166
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
1167
|
+
} catch {
|
|
1168
|
+
return [];
|
|
1169
|
+
}
|
|
1170
|
+
const out = [];
|
|
1171
|
+
for (const e of entries) {
|
|
1172
|
+
if (!e.isDirectory()) continue;
|
|
1173
|
+
out.push({
|
|
1174
|
+
type: "skill",
|
|
1175
|
+
name: e.name,
|
|
1176
|
+
description: await this.readDescription(join7(dir, e.name, "SKILL.md"))
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1180
|
+
}
|
|
1181
|
+
async scanMarkdownDir(dir, type) {
|
|
1182
|
+
const out = [];
|
|
1183
|
+
const walk = async (cur) => {
|
|
1184
|
+
let entries;
|
|
1185
|
+
try {
|
|
1186
|
+
entries = await readdir5(cur, { withFileTypes: true });
|
|
1187
|
+
} catch {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
for (const e of entries) {
|
|
1191
|
+
const full = join7(cur, e.name);
|
|
1192
|
+
if (e.isDirectory()) {
|
|
1193
|
+
await walk(full);
|
|
1194
|
+
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
1195
|
+
out.push({
|
|
1196
|
+
type,
|
|
1197
|
+
name: relative3(dir, full).split("\\").join("/").replace(/\.md$/, ""),
|
|
1198
|
+
description: await this.readDescription(full)
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
await walk(dir);
|
|
1204
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1205
|
+
}
|
|
1206
|
+
async scanHooks(file) {
|
|
1207
|
+
try {
|
|
1208
|
+
const json = JSON.parse(await readFile7(file, "utf8"));
|
|
1209
|
+
const events = Object.keys(json.hooks ?? json);
|
|
1210
|
+
return [
|
|
1211
|
+
{
|
|
1212
|
+
type: "hook",
|
|
1213
|
+
name: "hooks.json",
|
|
1214
|
+
description: events.length ? `\u4E8B\u4EF6\uFF1A${events.join(", ")}` : ""
|
|
1215
|
+
}
|
|
1216
|
+
];
|
|
1217
|
+
} catch {
|
|
1218
|
+
return [];
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
summarizeMcp(config) {
|
|
1222
|
+
if (config.command) {
|
|
1223
|
+
return `${config.command} ${(config.args ?? []).join(" ")}`.trim();
|
|
1224
|
+
}
|
|
1225
|
+
if (config.url) return `${config.type ?? ""} ${config.url}`.trim();
|
|
1226
|
+
return "";
|
|
1227
|
+
}
|
|
1228
|
+
async scanMcp(manifest) {
|
|
1229
|
+
return Object.entries(manifest?.mcpServers ?? {}).map(([name, config]) => ({
|
|
1230
|
+
type: "mcp",
|
|
1231
|
+
name,
|
|
1232
|
+
description: this.summarizeMcp(config)
|
|
1233
|
+
}));
|
|
1234
|
+
}
|
|
1235
|
+
async scanComponents(installPath, manifest) {
|
|
1236
|
+
const [skills, commands, agents, hooks, mcp] = await Promise.all([
|
|
1237
|
+
this.scanSkills(join7(installPath, "skills")),
|
|
1238
|
+
this.scanMarkdownDir(join7(installPath, "commands"), "command"),
|
|
1239
|
+
this.scanMarkdownDir(join7(installPath, "agents"), "agent"),
|
|
1240
|
+
this.scanHooks(join7(installPath, "hooks", "hooks.json")),
|
|
1241
|
+
this.scanMcp(manifest)
|
|
1242
|
+
]);
|
|
1243
|
+
return [...skills, ...commands, ...agents, ...hooks, ...mcp];
|
|
1244
|
+
}
|
|
1245
|
+
resolveWithin(baseDir, relPath) {
|
|
1246
|
+
const full = resolve4(baseDir, relPath);
|
|
1247
|
+
const rel = relative3(baseDir, full);
|
|
1248
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute3(rel)) {
|
|
1249
|
+
throw new Error(`\u975E\u6CD5\u7EC4\u4EF6\u8DEF\u5F84\uFF1A${relPath}`);
|
|
1250
|
+
}
|
|
1251
|
+
return full;
|
|
1252
|
+
}
|
|
1253
|
+
async readComponent(paths, id, type, name) {
|
|
1254
|
+
const detail = await this.detail(paths, id);
|
|
1255
|
+
const base = detail.installPath;
|
|
1256
|
+
if (type === "skill") {
|
|
1257
|
+
const file = this.resolveWithin(join7(base, "skills"), join7(name, "SKILL.md"));
|
|
1258
|
+
return { type, name, language: "markdown", content: await readFile7(file, "utf8") };
|
|
1259
|
+
}
|
|
1260
|
+
if (type === "command" || type === "agent") {
|
|
1261
|
+
const sub = type === "command" ? "commands" : "agents";
|
|
1262
|
+
const file = this.resolveWithin(join7(base, sub), `${name}.md`);
|
|
1263
|
+
return { type, name, language: "markdown", content: await readFile7(file, "utf8") };
|
|
1264
|
+
}
|
|
1265
|
+
if (type === "hook") {
|
|
1266
|
+
const file = this.resolveWithin(join7(base, "hooks"), name);
|
|
1267
|
+
return { type, name, language: "json", content: await readFile7(file, "utf8") };
|
|
1268
|
+
}
|
|
1269
|
+
const manifest = await this.readManifest(base);
|
|
1270
|
+
const config = manifest?.mcpServers?.[name];
|
|
1271
|
+
if (!config) throw new Error(`\u672A\u627E\u5230 MCP server\u300C${name}\u300D`);
|
|
1272
|
+
return {
|
|
1273
|
+
type,
|
|
1274
|
+
name,
|
|
1275
|
+
language: "json",
|
|
1276
|
+
content: JSON.stringify(config, null, 2)
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
async setEnabled(paths, id, enabled, baseMtime) {
|
|
1280
|
+
if (baseMtime !== void 0) {
|
|
1281
|
+
const st = await stat6(paths.codexConfigToml);
|
|
1282
|
+
if (Math.abs(st.mtimeMs - baseMtime) > 1) {
|
|
1283
|
+
throw new Error("\u914D\u7F6E\u6587\u4EF6\u5DF2\u88AB\u5176\u4ED6\u8FDB\u7A0B\u4FEE\u6539\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5");
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const config = await this.readConfig(paths);
|
|
1287
|
+
config.plugins = { ...config.plugins ?? {} };
|
|
1288
|
+
config.plugins[id] = { ...config.plugins[id] ?? {}, enabled };
|
|
1289
|
+
await this.store.writeText(paths.codexConfigToml, stringify2(config));
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
// src/core/codex-memory-service.ts
|
|
1294
|
+
import Database from "better-sqlite3";
|
|
1295
|
+
import { readFile as readFile8, readdir as readdir6, stat as stat7 } from "fs/promises";
|
|
1296
|
+
import { basename, extname, isAbsolute as isAbsolute4, join as join8, relative as relative4, resolve as resolve5 } from "path";
|
|
1297
|
+
var CodexDataCompatibilityError = class extends Error {
|
|
1298
|
+
constructor(message = "Codex \u8BB0\u5FC6\u6570\u636E\u5E93\u683C\u5F0F\u6682\u4E0D\u652F\u6301") {
|
|
1299
|
+
super(message);
|
|
1300
|
+
this.name = "CodexDataCompatibilityError";
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
var SUPPORTED_COLUMNS = /* @__PURE__ */ new Set([
|
|
1304
|
+
"id",
|
|
1305
|
+
"title",
|
|
1306
|
+
"content",
|
|
1307
|
+
"project_path",
|
|
1308
|
+
"created_at",
|
|
1309
|
+
"updated_at"
|
|
1310
|
+
]);
|
|
1311
|
+
var FILE_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".txt", ".json"]);
|
|
1312
|
+
var STAGE1_COLUMNS = /* @__PURE__ */ new Set([
|
|
1313
|
+
"thread_id",
|
|
1314
|
+
"source_updated_at",
|
|
1315
|
+
"raw_memory",
|
|
1316
|
+
"rollout_summary",
|
|
1317
|
+
"rollout_slug",
|
|
1318
|
+
"generated_at"
|
|
1319
|
+
]);
|
|
1320
|
+
var CodexMemoryService = class {
|
|
1321
|
+
async overview(paths) {
|
|
1322
|
+
const [sqliteItems, fileItems] = await Promise.all([
|
|
1323
|
+
this.sqliteOverview(paths),
|
|
1324
|
+
this.fileOverview(paths)
|
|
1325
|
+
]);
|
|
1326
|
+
return {
|
|
1327
|
+
items: [...sqliteItems, ...fileItems].sort((a, b) => b.mtime - a.mtime)
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
async read(paths, id) {
|
|
1331
|
+
if (id.startsWith("sqlite:")) {
|
|
1332
|
+
return this.readSqlite(paths, id.slice("sqlite:".length));
|
|
1333
|
+
}
|
|
1334
|
+
if (id.startsWith("file:")) {
|
|
1335
|
+
const file = this.resolveMemoryFile(paths, id.slice("file:".length));
|
|
1336
|
+
return { content: await readFile8(file, "utf8") };
|
|
1337
|
+
}
|
|
1338
|
+
throw new Error(`\u975E\u6CD5\u8BB0\u5FC6 id\uFF1A${id}`);
|
|
1339
|
+
}
|
|
1340
|
+
async sqliteOverview(paths) {
|
|
1341
|
+
if (!await this.exists(paths.codexMemoriesDb)) return [];
|
|
1342
|
+
const db = new Database(paths.codexMemoriesDb, {
|
|
1343
|
+
readonly: true,
|
|
1344
|
+
fileMustExist: true
|
|
1345
|
+
});
|
|
1346
|
+
try {
|
|
1347
|
+
const schema = this.detectSupportedSchema(db);
|
|
1348
|
+
if (schema === "memories") {
|
|
1349
|
+
const rows2 = db.prepare(
|
|
1350
|
+
`SELECT id, title, content, project_path, created_at, updated_at
|
|
1351
|
+
FROM memories
|
|
1352
|
+
ORDER BY COALESCE(updated_at, created_at, 0) DESC`
|
|
1353
|
+
).all();
|
|
1354
|
+
return rows2.map((row) => ({
|
|
1355
|
+
id: `sqlite:${row.id}`,
|
|
1356
|
+
name: row.title || row.id,
|
|
1357
|
+
description: this.describe(row.content),
|
|
1358
|
+
source: "sqlite",
|
|
1359
|
+
projectPath: row.project_path ?? void 0,
|
|
1360
|
+
mtime: row.updated_at ?? row.created_at ?? 0
|
|
1361
|
+
}));
|
|
1362
|
+
}
|
|
1363
|
+
const rows = db.prepare(
|
|
1364
|
+
`SELECT thread_id, source_updated_at, raw_memory, rollout_summary,
|
|
1365
|
+
rollout_slug, generated_at
|
|
1366
|
+
FROM stage1_outputs
|
|
1367
|
+
ORDER BY source_updated_at DESC`
|
|
1368
|
+
).all();
|
|
1369
|
+
return rows.map((row) => ({
|
|
1370
|
+
id: `sqlite:stage1:${row.thread_id}`,
|
|
1371
|
+
name: row.rollout_slug || row.thread_id,
|
|
1372
|
+
description: row.rollout_summary || this.describe(row.raw_memory),
|
|
1373
|
+
source: "sqlite",
|
|
1374
|
+
mtime: row.source_updated_at || row.generated_at || 0
|
|
1375
|
+
}));
|
|
1376
|
+
} finally {
|
|
1377
|
+
db.close();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
async readSqlite(paths, id) {
|
|
1381
|
+
if (!id || id.includes("/") || id.includes("\\") || id.includes("..")) {
|
|
1382
|
+
throw new Error(`\u975E\u6CD5\u8BB0\u5FC6 id\uFF1Asqlite:${id}`);
|
|
1383
|
+
}
|
|
1384
|
+
const db = new Database(paths.codexMemoriesDb, {
|
|
1385
|
+
readonly: true,
|
|
1386
|
+
fileMustExist: true
|
|
1387
|
+
});
|
|
1388
|
+
try {
|
|
1389
|
+
const schema = this.detectSupportedSchema(db);
|
|
1390
|
+
if (schema === "memories") {
|
|
1391
|
+
const row2 = db.prepare("SELECT content FROM memories WHERE id = ?").get(id);
|
|
1392
|
+
if (!row2) throw new Error(`\u672A\u627E\u5230\u8BB0\u5FC6\uFF1Asqlite:${id}`);
|
|
1393
|
+
return { content: row2.content ?? "" };
|
|
1394
|
+
}
|
|
1395
|
+
const stage1Id = id.startsWith("stage1:") ? id.slice("stage1:".length) : id;
|
|
1396
|
+
const row = db.prepare("SELECT raw_memory FROM stage1_outputs WHERE thread_id = ?").get(stage1Id);
|
|
1397
|
+
if (!row) throw new Error(`\u672A\u627E\u5230\u8BB0\u5FC6\uFF1Asqlite:${id}`);
|
|
1398
|
+
return { content: row.raw_memory ?? "" };
|
|
1399
|
+
} finally {
|
|
1400
|
+
db.close();
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
detectSupportedSchema(db) {
|
|
1404
|
+
if (this.hasColumns(db, "memories", SUPPORTED_COLUMNS)) return "memories";
|
|
1405
|
+
if (this.hasColumns(db, "stage1_outputs", STAGE1_COLUMNS)) return "stage1";
|
|
1406
|
+
throw new CodexDataCompatibilityError();
|
|
1407
|
+
}
|
|
1408
|
+
hasColumns(db, tableName, expectedColumns) {
|
|
1409
|
+
const table = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName);
|
|
1410
|
+
if (!table) return false;
|
|
1411
|
+
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
1412
|
+
const names = new Set(columns.map((c) => c.name));
|
|
1413
|
+
for (const expected of expectedColumns) {
|
|
1414
|
+
if (!names.has(expected)) return false;
|
|
1415
|
+
}
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
async fileOverview(paths) {
|
|
1419
|
+
let entries;
|
|
1420
|
+
try {
|
|
1421
|
+
entries = await readdir6(paths.codexMemoriesDir, { withFileTypes: true });
|
|
1422
|
+
} catch (err) {
|
|
1423
|
+
if (err.code === "ENOENT") return [];
|
|
1424
|
+
throw err;
|
|
1425
|
+
}
|
|
1426
|
+
const out = [];
|
|
1427
|
+
for (const entry of entries) {
|
|
1428
|
+
if (!entry.isFile() || !FILE_EXTENSIONS.has(extname(entry.name))) continue;
|
|
1429
|
+
const file = join8(paths.codexMemoriesDir, entry.name);
|
|
1430
|
+
const st = await stat7(file);
|
|
1431
|
+
out.push({
|
|
1432
|
+
id: `file:${entry.name}`,
|
|
1433
|
+
name: entry.name,
|
|
1434
|
+
description: this.describe(await readFile8(file, "utf8")),
|
|
1435
|
+
source: "file",
|
|
1436
|
+
mtime: st.mtimeMs
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
return out;
|
|
1440
|
+
}
|
|
1441
|
+
resolveMemoryFile(paths, name) {
|
|
1442
|
+
if (!name || name !== basename(name) || !FILE_EXTENSIONS.has(extname(name))) {
|
|
1443
|
+
throw new Error(`\u975E\u6CD5\u8BB0\u5FC6\u6587\u4EF6\uFF1A${name}`);
|
|
1444
|
+
}
|
|
1445
|
+
const full = resolve5(paths.codexMemoriesDir, name);
|
|
1446
|
+
const rel = relative4(paths.codexMemoriesDir, full);
|
|
1447
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute4(rel)) {
|
|
1448
|
+
throw new Error(`\u975E\u6CD5\u8BB0\u5FC6\u6587\u4EF6\uFF1A${name}`);
|
|
1449
|
+
}
|
|
1450
|
+
return full;
|
|
1451
|
+
}
|
|
1452
|
+
describe(content) {
|
|
1453
|
+
return content.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
1454
|
+
}
|
|
1455
|
+
async exists(file) {
|
|
1456
|
+
return stat7(file).then(() => true).catch((err) => {
|
|
1457
|
+
if (err.code === "ENOENT") return false;
|
|
1458
|
+
throw err;
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
// src/core/codex-session-service.ts
|
|
1464
|
+
import { readdir as readdir7, readFile as readFile9, stat as stat8 } from "fs/promises";
|
|
1465
|
+
import { join as join9, resolve as resolve6 } from "path";
|
|
1466
|
+
var MAX_BLOCK2 = 1e4;
|
|
1467
|
+
function truncate2(s) {
|
|
1468
|
+
return s.length > MAX_BLOCK2 ? s.slice(0, MAX_BLOCK2) + "\n\u2026(\u5DF2\u622A\u65AD)" : s;
|
|
1469
|
+
}
|
|
1470
|
+
function assertSafeId2(id) {
|
|
1471
|
+
if (!id || !/^[A-Za-z0-9-]+$/.test(id)) {
|
|
1472
|
+
throw new Error(`\u975E\u6CD5\u4F1A\u8BDD id\uFF1A${id}`);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
function normalizeProjectPath(p) {
|
|
1476
|
+
return resolve6(p).replace(/\\/g, "/").toLowerCase();
|
|
1477
|
+
}
|
|
1478
|
+
function payloadOf(obj) {
|
|
1479
|
+
const payload = obj.payload;
|
|
1480
|
+
return payload && typeof payload === "object" ? payload : obj;
|
|
1481
|
+
}
|
|
1482
|
+
function textFromContent(content) {
|
|
1483
|
+
if (typeof content === "string") return content;
|
|
1484
|
+
if (!Array.isArray(content)) return "";
|
|
1485
|
+
return content.map((raw) => {
|
|
1486
|
+
if (!raw || typeof raw !== "object") return "";
|
|
1487
|
+
const b = raw;
|
|
1488
|
+
if (typeof b.text === "string") return b.text;
|
|
1489
|
+
return "";
|
|
1490
|
+
}).join(" ");
|
|
1491
|
+
}
|
|
1492
|
+
function blocksFromContent(content) {
|
|
1493
|
+
if (typeof content === "string") {
|
|
1494
|
+
return content.trim() ? [{ kind: "text", text: truncate2(content) }] : [];
|
|
1495
|
+
}
|
|
1496
|
+
if (!Array.isArray(content)) return [];
|
|
1497
|
+
const out = [];
|
|
1498
|
+
for (const raw of content) {
|
|
1499
|
+
if (!raw || typeof raw !== "object") continue;
|
|
1500
|
+
const b = raw;
|
|
1501
|
+
const type = String(b.type ?? "");
|
|
1502
|
+
if ((type === "input_text" || type === "output_text" || type === "text") && b.text) {
|
|
1503
|
+
out.push({ kind: "text", text: truncate2(String(b.text)) });
|
|
1504
|
+
}
|
|
1505
|
+
if (type === "thinking") {
|
|
1506
|
+
out.push({ kind: "thinking", text: truncate2(String(b.text ?? "")) });
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return out;
|
|
1510
|
+
}
|
|
1511
|
+
function parseFunctionArguments(args) {
|
|
1512
|
+
if (typeof args !== "string") return JSON.stringify(args ?? {}, null, 2);
|
|
1513
|
+
try {
|
|
1514
|
+
return JSON.stringify(JSON.parse(args), null, 2);
|
|
1515
|
+
} catch {
|
|
1516
|
+
return args;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
var CodexSessionService = class {
|
|
1520
|
+
async list(paths, projectPath) {
|
|
1521
|
+
const [rollouts, index] = await Promise.all([
|
|
1522
|
+
this.scanRollouts(paths),
|
|
1523
|
+
this.readIndex(paths)
|
|
1524
|
+
]);
|
|
1525
|
+
const project = normalizeProjectPath(projectPath);
|
|
1526
|
+
return rollouts.filter((r) => normalizeProjectPath(r.cwd) === project).map((r) => {
|
|
1527
|
+
const idx = index.get(r.id);
|
|
1528
|
+
const mtime = idx?.updatedAt ? Date.parse(idx.updatedAt) : r.mtime;
|
|
1529
|
+
return {
|
|
1530
|
+
id: r.id,
|
|
1531
|
+
mtime: Number.isFinite(mtime) ? mtime : r.mtime,
|
|
1532
|
+
size: r.size,
|
|
1533
|
+
firstPrompt: idx?.threadName || r.firstPrompt,
|
|
1534
|
+
messageCount: r.messageCount
|
|
1535
|
+
};
|
|
1536
|
+
}).sort((a, b) => b.mtime - a.mtime);
|
|
1537
|
+
}
|
|
1538
|
+
async read(paths, projectPath, id) {
|
|
1539
|
+
assertSafeId2(id);
|
|
1540
|
+
const project = normalizeProjectPath(projectPath);
|
|
1541
|
+
const found = (await this.scanRollouts(paths)).find(
|
|
1542
|
+
(r) => r.id === id && normalizeProjectPath(r.cwd) === project
|
|
1543
|
+
);
|
|
1544
|
+
if (!found) throw new Error(`\u672A\u627E\u5230\u4F1A\u8BDD\uFF1A${id}`);
|
|
1545
|
+
const parsed = this.parseDetail(await readFile9(found.file, "utf8"));
|
|
1546
|
+
return { messages: parsed.messages, subagents: [], skippedRecords: parsed.skipped };
|
|
1547
|
+
}
|
|
1548
|
+
async scanRollouts(paths) {
|
|
1549
|
+
const files = await this.rolloutFiles(paths.codexSessionsDir);
|
|
1550
|
+
const out = [];
|
|
1551
|
+
for (const file of files) {
|
|
1552
|
+
const info = await this.summarizeRollout(file);
|
|
1553
|
+
if (info) out.push(info);
|
|
1554
|
+
}
|
|
1555
|
+
return out;
|
|
1556
|
+
}
|
|
1557
|
+
async rolloutFiles(dir) {
|
|
1558
|
+
let entries;
|
|
1559
|
+
try {
|
|
1560
|
+
entries = await readdir7(dir, { withFileTypes: true });
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
if (err.code === "ENOENT") return [];
|
|
1563
|
+
throw err;
|
|
1564
|
+
}
|
|
1565
|
+
const out = [];
|
|
1566
|
+
for (const entry of entries) {
|
|
1567
|
+
const full = join9(dir, entry.name);
|
|
1568
|
+
if (entry.isDirectory()) {
|
|
1569
|
+
out.push(...await this.rolloutFiles(full));
|
|
1570
|
+
} else if (entry.isFile() && /^rollout-[A-Za-z0-9-]+\.jsonl$/.test(entry.name)) {
|
|
1571
|
+
out.push(full);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return out;
|
|
1575
|
+
}
|
|
1576
|
+
async summarizeRollout(file) {
|
|
1577
|
+
const st = await stat8(file);
|
|
1578
|
+
const raw = await readFile9(file, "utf8");
|
|
1579
|
+
let id = "";
|
|
1580
|
+
let cwd = "";
|
|
1581
|
+
let firstPrompt = "";
|
|
1582
|
+
let messageCount = 0;
|
|
1583
|
+
for (const line of raw.split("\n")) {
|
|
1584
|
+
if (!line.trim()) continue;
|
|
1585
|
+
let obj;
|
|
1586
|
+
try {
|
|
1587
|
+
obj = JSON.parse(line);
|
|
1588
|
+
} catch {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const payload = payloadOf(obj);
|
|
1592
|
+
if (obj.type === "session_meta" || payload.type === "session_meta") {
|
|
1593
|
+
id = String(payload.id ?? id);
|
|
1594
|
+
cwd = String(payload.cwd ?? cwd);
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
if (payload.type === "message") {
|
|
1598
|
+
const role = String(payload.role ?? "");
|
|
1599
|
+
if (role === "user" || role === "assistant") messageCount++;
|
|
1600
|
+
if (!firstPrompt && role === "user") {
|
|
1601
|
+
firstPrompt = textFromContent(payload.content).replace(/\s+/g, " ").trim();
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (!id) {
|
|
1606
|
+
const m = /rollout-([A-Za-z0-9-]+)\.jsonl$/.exec(file);
|
|
1607
|
+
id = m?.[1] ?? "";
|
|
1608
|
+
}
|
|
1609
|
+
if (!id || !cwd) return void 0;
|
|
1610
|
+
return { id, file, cwd, mtime: st.mtimeMs, size: st.size, firstPrompt, messageCount };
|
|
1611
|
+
}
|
|
1612
|
+
async readIndex(paths) {
|
|
1613
|
+
const out = /* @__PURE__ */ new Map();
|
|
1614
|
+
let raw;
|
|
1615
|
+
try {
|
|
1616
|
+
raw = await readFile9(paths.codexSessionIndex, "utf8");
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
if (err.code === "ENOENT") return out;
|
|
1619
|
+
throw err;
|
|
1620
|
+
}
|
|
1621
|
+
for (const line of raw.split("\n")) {
|
|
1622
|
+
if (!line.trim()) continue;
|
|
1623
|
+
try {
|
|
1624
|
+
const obj = JSON.parse(line);
|
|
1625
|
+
const id = String(obj.id ?? "");
|
|
1626
|
+
if (id) {
|
|
1627
|
+
out.set(id, {
|
|
1628
|
+
threadName: obj.thread_name ? String(obj.thread_name) : void 0,
|
|
1629
|
+
updatedAt: obj.updated_at ? String(obj.updated_at) : void 0
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
} catch {
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return out;
|
|
1637
|
+
}
|
|
1638
|
+
parseDetail(raw) {
|
|
1639
|
+
const messages = [];
|
|
1640
|
+
let skipped = 0;
|
|
1641
|
+
let currentAssistant;
|
|
1642
|
+
for (const line of raw.split("\n")) {
|
|
1643
|
+
if (!line.trim()) continue;
|
|
1644
|
+
let obj;
|
|
1645
|
+
try {
|
|
1646
|
+
obj = JSON.parse(line);
|
|
1647
|
+
} catch {
|
|
1648
|
+
skipped++;
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
const payload = payloadOf(obj);
|
|
1652
|
+
if (payload.type === "message") {
|
|
1653
|
+
const role = String(payload.role ?? "");
|
|
1654
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
1655
|
+
const msg = {
|
|
1656
|
+
type: role,
|
|
1657
|
+
role,
|
|
1658
|
+
timestamp: obj.timestamp ? String(obj.timestamp) : void 0,
|
|
1659
|
+
blocks: blocksFromContent(payload.content)
|
|
1660
|
+
};
|
|
1661
|
+
if (msg.blocks.length === 0) continue;
|
|
1662
|
+
messages.push(msg);
|
|
1663
|
+
currentAssistant = role === "assistant" ? msg : void 0;
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (payload.type === "function_call") {
|
|
1667
|
+
const target = currentAssistant ?? {
|
|
1668
|
+
type: "assistant",
|
|
1669
|
+
role: "assistant",
|
|
1670
|
+
timestamp: obj.timestamp ? String(obj.timestamp) : void 0,
|
|
1671
|
+
blocks: []
|
|
1672
|
+
};
|
|
1673
|
+
if (!currentAssistant) {
|
|
1674
|
+
messages.push(target);
|
|
1675
|
+
currentAssistant = target;
|
|
1676
|
+
}
|
|
1677
|
+
target.blocks.push({
|
|
1678
|
+
kind: "tool_use",
|
|
1679
|
+
name: String(payload.name ?? "tool"),
|
|
1680
|
+
text: truncate2(parseFunctionArguments(payload.arguments)),
|
|
1681
|
+
toolUseId: payload.call_id ? String(payload.call_id) : void 0
|
|
1682
|
+
});
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
if (payload.type === "function_call_output") {
|
|
1686
|
+
const target = currentAssistant ?? {
|
|
1687
|
+
type: "assistant",
|
|
1688
|
+
role: "assistant",
|
|
1689
|
+
timestamp: obj.timestamp ? String(obj.timestamp) : void 0,
|
|
1690
|
+
blocks: []
|
|
1691
|
+
};
|
|
1692
|
+
if (!currentAssistant) {
|
|
1693
|
+
messages.push(target);
|
|
1694
|
+
currentAssistant = target;
|
|
1695
|
+
}
|
|
1696
|
+
target.blocks.push({
|
|
1697
|
+
kind: "tool_result",
|
|
1698
|
+
text: truncate2(String(payload.output ?? "")),
|
|
1699
|
+
isError: payload.is_error === true,
|
|
1700
|
+
toolUseId: payload.call_id ? String(payload.call_id) : void 0
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return { messages, skipped };
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
|
|
911
1708
|
// src/core/config-file-service.ts
|
|
912
|
-
import { readFile as
|
|
1709
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1710
|
+
import { parse as parseToml } from "smol-toml";
|
|
913
1711
|
var REGISTRY = {
|
|
914
|
-
settings:
|
|
915
|
-
|
|
1712
|
+
settings: {
|
|
1713
|
+
resolve: (paths, scope) => scope === "user" ? paths.userSettings : paths.projectSettings,
|
|
1714
|
+
kind: "json"
|
|
1715
|
+
},
|
|
1716
|
+
"claude-json": {
|
|
1717
|
+
resolve: (paths, scope) => scope === "user" ? paths.userClaudeJson : void 0,
|
|
1718
|
+
kind: "json"
|
|
1719
|
+
},
|
|
1720
|
+
"codex-config": {
|
|
1721
|
+
resolve: (paths, scope) => scope === "user" ? paths.codexConfigToml : void 0,
|
|
1722
|
+
kind: "toml"
|
|
1723
|
+
},
|
|
1724
|
+
"codex-agents": {
|
|
1725
|
+
resolve: (paths, scope) => scope === "user" ? paths.codexAgentsMd : void 0,
|
|
1726
|
+
kind: "text"
|
|
1727
|
+
}
|
|
916
1728
|
};
|
|
917
1729
|
var ConfigFileService = class {
|
|
918
1730
|
constructor(store) {
|
|
919
1731
|
this.store = store;
|
|
920
1732
|
}
|
|
921
1733
|
store;
|
|
922
|
-
|
|
923
|
-
const
|
|
1734
|
+
entry(paths, fileId, scope) {
|
|
1735
|
+
const e = REGISTRY[fileId];
|
|
1736
|
+
const f = e.resolve(paths, scope);
|
|
924
1737
|
if (!f) {
|
|
925
1738
|
throw new Error(
|
|
926
1739
|
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}`
|
|
927
1740
|
);
|
|
928
1741
|
}
|
|
929
|
-
return f;
|
|
1742
|
+
return { file: f, kind: e.kind };
|
|
930
1743
|
}
|
|
931
1744
|
// 文件不存在视为「尚未创建」,返回空串与 null mtime(与 MemoryService 一致)。
|
|
932
1745
|
async read(paths, fileId, scope) {
|
|
933
|
-
const file = this.
|
|
1746
|
+
const { file } = this.entry(paths, fileId, scope);
|
|
934
1747
|
try {
|
|
935
|
-
return { content: await
|
|
1748
|
+
return { content: await readFile10(file, "utf8"), mtime: await currentMtime(file) };
|
|
936
1749
|
} catch (err) {
|
|
937
1750
|
if (err.code === "ENOENT")
|
|
938
1751
|
return { content: "", mtime: null };
|
|
939
1752
|
throw err;
|
|
940
1753
|
}
|
|
941
1754
|
}
|
|
942
|
-
//
|
|
1755
|
+
// 按 kind 校验后写回;非法内容抛 InvalidJsonError,不触碰磁盘原文件。
|
|
943
1756
|
async write(paths, fileId, scope, content, baseMtime) {
|
|
944
|
-
const file = this.
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1757
|
+
const { file, kind } = this.entry(paths, fileId, scope);
|
|
1758
|
+
if (kind === "json") {
|
|
1759
|
+
let parsed;
|
|
1760
|
+
try {
|
|
1761
|
+
parsed = JSON.parse(content);
|
|
1762
|
+
} catch {
|
|
1763
|
+
throw new InvalidJsonError();
|
|
1764
|
+
}
|
|
1765
|
+
await assertNoConflict(file, baseMtime);
|
|
1766
|
+
await this.store.writeJson(file, parsed);
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (kind === "toml") {
|
|
1770
|
+
try {
|
|
1771
|
+
parseToml(content);
|
|
1772
|
+
} catch {
|
|
1773
|
+
throw new InvalidJsonError("\u5185\u5BB9\u4E0D\u662F\u5408\u6CD5\u7684 TOML");
|
|
1774
|
+
}
|
|
950
1775
|
}
|
|
951
1776
|
await assertNoConflict(file, baseMtime);
|
|
952
|
-
await this.store.
|
|
1777
|
+
await this.store.writeText(file, content);
|
|
953
1778
|
}
|
|
954
1779
|
};
|
|
955
1780
|
|
|
956
1781
|
// src/core/agent-command-service.ts
|
|
957
|
-
import { mkdir as
|
|
958
|
-
import { dirname as
|
|
959
|
-
import
|
|
1782
|
+
import { mkdir as mkdir4, readdir as readdir8, readFile as readFile11, rm as rm2, writeFile as writeFile4 } from "fs/promises";
|
|
1783
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
1784
|
+
import matter5 from "gray-matter";
|
|
960
1785
|
function assertSafeName2(name) {
|
|
961
1786
|
if (!name || name.includes("/") || name.includes("\\") || name.includes("..") || name.includes("\0")) {
|
|
962
1787
|
throw new Error(`\u975E\u6CD5\u540D\u79F0\uFF1A${name}`);
|
|
@@ -970,13 +1795,13 @@ var AgentCommandService = class {
|
|
|
970
1795
|
}
|
|
971
1796
|
fileFor(paths, kind, scope, name) {
|
|
972
1797
|
assertSafeName2(name);
|
|
973
|
-
return
|
|
1798
|
+
return join10(this.dir(paths, kind, scope), `${name}.md`);
|
|
974
1799
|
}
|
|
975
1800
|
async list(paths, kind, scope) {
|
|
976
1801
|
const dir = this.dir(paths, kind, scope);
|
|
977
1802
|
let entries;
|
|
978
1803
|
try {
|
|
979
|
-
entries = await
|
|
1804
|
+
entries = await readdir8(dir, { withFileTypes: true });
|
|
980
1805
|
} catch (err) {
|
|
981
1806
|
if (err.code === "ENOENT") return [];
|
|
982
1807
|
throw err;
|
|
@@ -985,10 +1810,10 @@ var AgentCommandService = class {
|
|
|
985
1810
|
for (const e of entries) {
|
|
986
1811
|
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
987
1812
|
const name = e.name.slice(0, -3);
|
|
988
|
-
const path =
|
|
1813
|
+
const path = join10(dir, e.name);
|
|
989
1814
|
let description;
|
|
990
1815
|
try {
|
|
991
|
-
const fm =
|
|
1816
|
+
const fm = matter5(await readFile11(path, "utf8")).data;
|
|
992
1817
|
description = typeof fm.description === "string" ? fm.description : "";
|
|
993
1818
|
} catch {
|
|
994
1819
|
description = "(frontmatter \u89E3\u6790\u5931\u8D25)";
|
|
@@ -1000,7 +1825,7 @@ var AgentCommandService = class {
|
|
|
1000
1825
|
async read(paths, kind, scope, name) {
|
|
1001
1826
|
const file = this.fileFor(paths, kind, scope, name);
|
|
1002
1827
|
try {
|
|
1003
|
-
return { content: await
|
|
1828
|
+
return { content: await readFile11(file, "utf8"), mtime: await currentMtime(file) };
|
|
1004
1829
|
} catch {
|
|
1005
1830
|
throw new Error(`\u672A\u627E\u5230 ${kind}\u300C${name}\u300D\uFF08scope: ${scope}\uFF09`);
|
|
1006
1831
|
}
|
|
@@ -1008,8 +1833,8 @@ var AgentCommandService = class {
|
|
|
1008
1833
|
async write(paths, kind, scope, name, content, baseMtime) {
|
|
1009
1834
|
const file = this.fileFor(paths, kind, scope, name);
|
|
1010
1835
|
await assertNoConflict(file, baseMtime);
|
|
1011
|
-
await
|
|
1012
|
-
await
|
|
1836
|
+
await mkdir4(dirname4(file), { recursive: true });
|
|
1837
|
+
await writeFile4(file, content, "utf8");
|
|
1013
1838
|
}
|
|
1014
1839
|
async remove(paths, kind, scope, name) {
|
|
1015
1840
|
const file = this.fileFor(paths, kind, scope, name);
|
|
@@ -1017,36 +1842,154 @@ var AgentCommandService = class {
|
|
|
1017
1842
|
}
|
|
1018
1843
|
};
|
|
1019
1844
|
|
|
1845
|
+
// src/core/sync-service.ts
|
|
1846
|
+
import { cp as cp2, lstat, mkdir as mkdir5, readlink as readlink2, rm as rm3, rmdir, stat as stat9, symlink, unlink } from "fs/promises";
|
|
1847
|
+
import { dirname as dirname5, join as join11, resolve as resolve7 } from "path";
|
|
1848
|
+
var SyncService = class {
|
|
1849
|
+
agentsSkillsDir;
|
|
1850
|
+
targets;
|
|
1851
|
+
constructor(home) {
|
|
1852
|
+
this.agentsSkillsDir = join11(home, ".agents", "skills");
|
|
1853
|
+
this.targets = {
|
|
1854
|
+
claude: join11(home, ".claude", "skills"),
|
|
1855
|
+
codex: join11(home, ".codex", "skills")
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
// 删除目录链接本身(不触碰目标):POSIX 符号链接用 unlink,Windows junction 需 rmdir。
|
|
1859
|
+
async removeLink(p) {
|
|
1860
|
+
try {
|
|
1861
|
+
await unlink(p);
|
|
1862
|
+
} catch {
|
|
1863
|
+
await rmdir(p);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
// 读取 p 处链接的绝对目标;不是链接或读取失败返回 undefined。
|
|
1867
|
+
async linkTargetOf(p) {
|
|
1868
|
+
try {
|
|
1869
|
+
if (!(await lstat(p)).isSymbolicLink()) return void 0;
|
|
1870
|
+
} catch {
|
|
1871
|
+
return void 0;
|
|
1872
|
+
}
|
|
1873
|
+
const raw = await readlink2(p).catch(() => void 0);
|
|
1874
|
+
return raw === void 0 ? void 0 : resolve7(dirname5(p), raw);
|
|
1875
|
+
}
|
|
1876
|
+
async stateFor(targetDir, name) {
|
|
1877
|
+
const p = join11(targetDir, name);
|
|
1878
|
+
try {
|
|
1879
|
+
await lstat(p);
|
|
1880
|
+
} catch {
|
|
1881
|
+
return "none";
|
|
1882
|
+
}
|
|
1883
|
+
const target = await this.linkTargetOf(p);
|
|
1884
|
+
if (target !== void 0 && isSameLink(target, join11(this.agentsSkillsDir, name))) {
|
|
1885
|
+
return "linked";
|
|
1886
|
+
}
|
|
1887
|
+
return "conflict";
|
|
1888
|
+
}
|
|
1889
|
+
async status(name) {
|
|
1890
|
+
assertSafeName(name);
|
|
1891
|
+
return {
|
|
1892
|
+
claude: await this.stateFor(this.targets.claude, name),
|
|
1893
|
+
codex: await this.stateFor(this.targets.codex, name)
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
async syncTo(name, target) {
|
|
1897
|
+
assertSafeName(name);
|
|
1898
|
+
const src = join11(this.agentsSkillsDir, name);
|
|
1899
|
+
try {
|
|
1900
|
+
if (!(await stat9(src)).isDirectory()) throw new Error();
|
|
1901
|
+
} catch {
|
|
1902
|
+
throw new Error(`\u672A\u627E\u5230\u516C\u5171 skill\u300C${name}\u300D`);
|
|
1903
|
+
}
|
|
1904
|
+
const dest = join11(this.targets[target], name);
|
|
1905
|
+
try {
|
|
1906
|
+
await lstat(dest);
|
|
1907
|
+
throw new ConflictError(`\u76EE\u6807\u76EE\u5F55\u5DF2\u5B58\u5728\u540C\u540D\u6761\u76EE\u300C${name}\u300D\uFF0C\u8BF7\u5148\u5904\u7406\u51B2\u7A81`);
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
if (err instanceof ConflictError) throw err;
|
|
1910
|
+
}
|
|
1911
|
+
await mkdir5(this.targets[target], { recursive: true });
|
|
1912
|
+
await symlink(src, dest, "junction");
|
|
1913
|
+
}
|
|
1914
|
+
async unsync(name, target) {
|
|
1915
|
+
assertSafeName(name);
|
|
1916
|
+
const dest = join11(this.targets[target], name);
|
|
1917
|
+
const linkTarget = await this.linkTargetOf(dest);
|
|
1918
|
+
if (linkTarget === void 0 || !isSameLink(linkTarget, join11(this.agentsSkillsDir, name))) {
|
|
1919
|
+
throw new Error(`\u300C${name}\u300D\u4E0D\u662F\u6307\u5411\u516C\u5171\u76EE\u5F55\u7684\u94FE\u63A5\uFF0C\u62D2\u7EDD\u5220\u9664`);
|
|
1920
|
+
}
|
|
1921
|
+
await this.removeLink(dest);
|
|
1922
|
+
}
|
|
1923
|
+
async makeShared(provider, name) {
|
|
1924
|
+
assertSafeName(name);
|
|
1925
|
+
const src = join11(this.targets[provider], name);
|
|
1926
|
+
let st;
|
|
1927
|
+
try {
|
|
1928
|
+
st = await lstat(src);
|
|
1929
|
+
} catch {
|
|
1930
|
+
throw new Error(`\u672A\u627E\u5230 skill\u300C${name}\u300D\uFF08\u987B\u4E3A\u542F\u7528\u72B6\u6001\uFF09`);
|
|
1931
|
+
}
|
|
1932
|
+
if (st.isSymbolicLink()) {
|
|
1933
|
+
const target = await this.linkTargetOf(src);
|
|
1934
|
+
const kind = target === void 0 ? "external" : classifyLinkTarget(target, this.agentsSkillsDir);
|
|
1935
|
+
throw new Error(
|
|
1936
|
+
kind === "shared" ? `\u300C${name}\u300D\u5DF2\u7ECF\u662F\u516C\u5171 skill \u7684\u94FE\u63A5` : `\u300C${name}\u300D\u662F\u6307\u5411\u5916\u90E8\u4F4D\u7F6E\u7684\u94FE\u63A5\uFF0C\u4E0D\u652F\u6301\u8F6C\u516C\u5171`
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
const dest = join11(this.agentsSkillsDir, name);
|
|
1940
|
+
try {
|
|
1941
|
+
await lstat(dest);
|
|
1942
|
+
throw new ConflictError(`\u516C\u5171\u76EE\u5F55\u5DF2\u5B58\u5728\u540C\u540D\u6280\u80FD\u300C${name}\u300D`);
|
|
1943
|
+
} catch (err) {
|
|
1944
|
+
if (err instanceof ConflictError) throw err;
|
|
1945
|
+
}
|
|
1946
|
+
await mkdir5(this.agentsSkillsDir, { recursive: true });
|
|
1947
|
+
await cp2(src, dest, { recursive: true });
|
|
1948
|
+
await rm3(src, { recursive: true, force: true });
|
|
1949
|
+
try {
|
|
1950
|
+
await symlink(dest, src, "junction");
|
|
1951
|
+
} catch (err) {
|
|
1952
|
+
await cp2(dest, src, { recursive: true });
|
|
1953
|
+
await rm3(dest, { recursive: true, force: true });
|
|
1954
|
+
throw err;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1020
1959
|
// src/cli.ts
|
|
1021
1960
|
function findFreePort(start) {
|
|
1022
|
-
return new Promise((
|
|
1961
|
+
return new Promise((resolve8, reject) => {
|
|
1023
1962
|
const srv = createServer();
|
|
1024
1963
|
srv.once("error", (err) => {
|
|
1025
|
-
if (err.code === "EADDRINUSE")
|
|
1964
|
+
if (err.code === "EADDRINUSE") resolve8(findFreePort(start + 1));
|
|
1026
1965
|
else reject(err);
|
|
1027
1966
|
});
|
|
1028
1967
|
srv.listen(start, () => {
|
|
1029
|
-
srv.close(() =>
|
|
1968
|
+
srv.close(() => resolve8(start));
|
|
1030
1969
|
});
|
|
1031
1970
|
});
|
|
1032
1971
|
}
|
|
1033
1972
|
async function main() {
|
|
1034
1973
|
const home = homedir();
|
|
1035
1974
|
const store = new ConfigStore();
|
|
1036
|
-
const registryFile =
|
|
1975
|
+
const registryFile = join12(home, ".ccpanel", "ccpanel-projects.json");
|
|
1037
1976
|
const webRoot = fileURLToPath(new URL("../web/dist", import.meta.url));
|
|
1038
1977
|
const app = createApp({
|
|
1039
1978
|
home,
|
|
1040
1979
|
store,
|
|
1041
1980
|
mcp: new McpService(store),
|
|
1042
1981
|
skills: new SkillService(),
|
|
1043
|
-
projects: new ProjectService(store, registryFile),
|
|
1982
|
+
projects: new ProjectService(store, registryFile, join12(home, ".claude.json")),
|
|
1044
1983
|
memory: new MemoryService(),
|
|
1045
1984
|
memoryFiles: new MemoryFilesService(),
|
|
1046
1985
|
sessions: new SessionService(),
|
|
1047
1986
|
plugins: new PluginService(store),
|
|
1987
|
+
codexPlugins: new CodexPluginService(store),
|
|
1988
|
+
codexMemory: new CodexMemoryService(),
|
|
1989
|
+
codexSessions: new CodexSessionService(),
|
|
1048
1990
|
configFiles: new ConfigFileService(store),
|
|
1049
1991
|
agentCommands: new AgentCommandService(),
|
|
1992
|
+
sync: new SyncService(home),
|
|
1050
1993
|
registryFile,
|
|
1051
1994
|
webRoot
|
|
1052
1995
|
});
|