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