ccpanel 0.1.3 → 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/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-SZFZF7CH.js";
11
+ } from "./chunk-HRPQYUJV.js";
10
12
 
11
13
  // src/cli.ts
12
14
  import { homedir, networkInterfaces } from "os";
13
- import { join as join6 } from "path";
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 claude = await this.store.readJson(
33
- paths.userClaudeJson
34
- ) ?? {};
35
- const disabled = await this.store.readJson(paths.userDisabledMcp) ?? {};
36
- const enabled = Object.entries(claude.mcpServers ?? {}).map(
37
- ([name, server]) => ({
38
- name,
39
- server,
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 claude = await this.store.readJson(
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
- claude.mcpServers = { ...claude.mcpServers ?? {}, [name]: server };
79
- await this.store.writeJson(paths.userClaudeJson, claude);
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 claude = await this.store.readJson(
91
- paths.userClaudeJson
92
- ) ?? {};
93
- const disabled = await this.store.readJson(paths.userDisabledMcp) ?? {};
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 this.store.writeJson(paths.userClaudeJson, claude);
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 claude = await this.store.readJson(
110
- paths.userClaudeJson
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
- claude.mcpServers = active;
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 = join(dir, e.name);
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(join(path, "SKILL.md")) ?? await currentMtime(path) ?? 0;
303
+ const mtime = await currentMtime(join3(path, "SKILL.md")) ?? await currentMtime(path) ?? 0;
202
304
  try {
203
- const raw = await readFile(join(path, "SKILL.md"), "utf8");
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 = join(base, name);
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 = resolve(baseDir, relPath);
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 = join(this.enabledDir(paths, scope), name, "SKILL.md");
249
- const disabledPath = join(this.disabledDir(paths, scope), name, "SKILL.md");
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 readFile(enabledPath, "utf8");
355
+ raw = await readFile2(enabledPath, "utf8");
253
356
  } catch {
254
357
  try {
255
- raw = await readFile(disabledPath, "utf8");
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 = join(dir, e.name);
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 readFile(full))) continue;
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 readFile(full);
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 mkdir(dirname(full), { recursive: true });
298
- await writeFile(full, content, "utf8");
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 = join(this.enabledDir(paths, scope), name, "SKILL.md");
303
- await mkdir(dirname(file), { recursive: true });
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 writeFile(file, content, "utf8");
408
+ await writeFile2(file, content, "utf8");
306
409
  }
307
410
  async remove(paths, scope, name) {
308
411
  assertSafeName(name);
309
- await rm(join(this.enabledDir(paths, scope), name), {
412
+ await rm(join3(this.enabledDir(paths, scope), name), {
310
413
  recursive: true,
311
414
  force: true
312
415
  });
313
- await rm(join(this.disabledDir(paths, scope), name), {
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 mkdir(dirname(to), { recursive: true });
423
+ await mkdir2(dirname2(to), { recursive: true });
321
424
  try {
322
- await rename(from, to);
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 = join(
437
+ const from = join3(
335
438
  enabled ? this.disabledDir(paths, scope) : this.enabledDir(paths, scope),
336
439
  name
337
440
  );
338
- const to = join(
441
+ const to = join3(
339
442
  enabled ? this.enabledDir(paths, scope) : this.disabledDir(paths, scope),
340
443
  name
341
444
  );
@@ -373,8 +476,8 @@ var ProjectService = class {
373
476
  };
374
477
 
375
478
  // src/core/memory-service.ts
376
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
377
- import { dirname as dirname2 } from "path";
479
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
480
+ import { dirname as dirname3 } from "path";
378
481
  var MemoryService = class {
379
482
  file(paths, scope) {
380
483
  const f = scope === "user" ? paths.userClaudeMd : paths.projectClaudeMd;
@@ -385,7 +488,7 @@ var MemoryService = class {
385
488
  async read(paths, scope) {
386
489
  const f = this.file(paths, scope);
387
490
  try {
388
- return { content: await readFile2(f, "utf8"), mtime: await currentMtime(f) };
491
+ return { content: await readFile3(f, "utf8"), mtime: await currentMtime(f) };
389
492
  } catch (err) {
390
493
  if (err.code === "ENOENT")
391
494
  return { content: "", mtime: null };
@@ -395,19 +498,19 @@ var MemoryService = class {
395
498
  async write(paths, scope, content, baseMtime) {
396
499
  const f = this.file(paths, scope);
397
500
  await assertNoConflict(f, baseMtime);
398
- await mkdir2(dirname2(f), { recursive: true });
399
- await writeFile2(f, content, "utf8");
501
+ await mkdir3(dirname3(f), { recursive: true });
502
+ await writeFile3(f, content, "utf8");
400
503
  }
401
504
  };
402
505
 
403
506
  // src/core/memory-files-service.ts
404
- import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
405
- import { join as join3 } from "path";
507
+ import { readdir as readdir3, readFile as readFile5, stat as stat3 } from "fs/promises";
508
+ import { join as join5 } from "path";
406
509
  import matter2 from "gray-matter";
407
510
 
408
511
  // src/core/session-service.ts
409
- import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
410
- import { join as join2 } from "path";
512
+ import { readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
513
+ import { join as join4 } from "path";
411
514
  var MAX_BLOCK = 1e4;
412
515
  var NOISE_PREFIXES = ["<local-command", "<command-", "<system-reminder", "Caveat:"];
413
516
  function encodeProjectDir(projectPath) {
@@ -491,7 +594,7 @@ function toBlocks(content) {
491
594
  }
492
595
  var SessionService = class {
493
596
  dir(paths, projectPath) {
494
- return join2(paths.userProjectsDir, encodeProjectDir(projectPath));
597
+ return join4(paths.userProjectsDir, encodeProjectDir(projectPath));
495
598
  }
496
599
  async list(paths, projectPath) {
497
600
  const dir = this.dir(paths, projectPath);
@@ -504,7 +607,7 @@ var SessionService = class {
504
607
  }
505
608
  const out = [];
506
609
  for (const name of names) {
507
- const full = join2(dir, name);
610
+ const full = join4(dir, name);
508
611
  const st = await stat2(full);
509
612
  const { firstPrompt, messageCount } = await this.summarize(full);
510
613
  out.push({
@@ -518,7 +621,7 @@ var SessionService = class {
518
621
  return out.sort((a, b) => b.mtime - a.mtime);
519
622
  }
520
623
  async summarize(file) {
521
- const raw = await readFile3(file, "utf8");
624
+ const raw = await readFile4(file, "utf8");
522
625
  let firstPrompt = "";
523
626
  let messageCount = 0;
524
627
  for (const line of raw.split("\n")) {
@@ -543,9 +646,9 @@ var SessionService = class {
543
646
  async read(paths, projectPath, id) {
544
647
  assertSafeId(id);
545
648
  const dir = this.dir(paths, projectPath);
546
- const raw = await readFile3(join2(dir, `${id}.jsonl`), "utf8");
649
+ const raw = await readFile4(join4(dir, `${id}.jsonl`), "utf8");
547
650
  const messages = parseMessages(raw);
548
- const subagents = await this.readSubagents(join2(dir, id, "subagents"));
651
+ const subagents = await this.readSubagents(join4(dir, id, "subagents"));
549
652
  return { messages, subagents };
550
653
  }
551
654
  // 读取某会话的 subagents/ 目录:每个 agent-<id>.jsonl 配对同名 .meta.json。
@@ -562,8 +665,8 @@ var SessionService = class {
562
665
  for (const name of names) {
563
666
  const agentId = parseAgentId(name);
564
667
  if (!agentId) continue;
565
- const messages = parseMessages(await readFile3(join2(subDir, name), "utf8"));
566
- const meta = await this.readMeta(join2(subDir, `agent-${agentId}.meta.json`));
668
+ const messages = parseMessages(await readFile4(join4(subDir, name), "utf8"));
669
+ const meta = await this.readMeta(join4(subDir, `agent-${agentId}.meta.json`));
567
670
  out.push({
568
671
  toolUseId: meta.toolUseId,
569
672
  agentId,
@@ -576,7 +679,7 @@ var SessionService = class {
576
679
  }
577
680
  async readMeta(file) {
578
681
  try {
579
- const obj = JSON.parse(await readFile3(file, "utf8"));
682
+ const obj = JSON.parse(await readFile4(file, "utf8"));
580
683
  return {
581
684
  toolUseId: String(obj.toolUseId ?? ""),
582
685
  agentType: String(obj.agentType ?? "subagent"),
@@ -597,7 +700,7 @@ function assertSafeMemoryFile(file) {
597
700
  }
598
701
  var MemoryFilesService = class {
599
702
  dir(paths, projectPath) {
600
- return join3(paths.userProjectsDir, encodeProjectDir(projectPath), "memory");
703
+ return join5(paths.userProjectsDir, encodeProjectDir(projectPath), "memory");
601
704
  }
602
705
  // 目录不存在视为「尚无记忆」,返回空概览。
603
706
  async overview(paths, projectPath) {
@@ -612,9 +715,9 @@ var MemoryFilesService = class {
612
715
  let index = null;
613
716
  const files = [];
614
717
  for (const name of names) {
615
- const full = join3(dir, name);
718
+ const full = join5(dir, name);
616
719
  if (name === INDEX_FILE) {
617
- index = await readFile4(full, "utf8");
720
+ index = await readFile5(full, "utf8");
618
721
  continue;
619
722
  }
620
723
  const st = await stat3(full);
@@ -628,7 +731,7 @@ var MemoryFilesService = class {
628
731
  async parseFront(full, fileName) {
629
732
  const fallbackName = fileName.replace(/\.md$/, "");
630
733
  try {
631
- const data = matter2(await readFile4(full, "utf8")).data;
734
+ const data = matter2(await readFile5(full, "utf8")).data;
632
735
  const meta = data.metadata ?? {};
633
736
  return {
634
737
  name: typeof data.name === "string" && data.name ? data.name : fallbackName,
@@ -642,13 +745,13 @@ var MemoryFilesService = class {
642
745
  // 读取单个记忆文件原文(含 frontmatter)。
643
746
  async read(paths, projectPath, file) {
644
747
  assertSafeMemoryFile(file);
645
- return { content: await readFile4(join3(this.dir(paths, projectPath), file), "utf8") };
748
+ return { content: await readFile5(join5(this.dir(paths, projectPath), file), "utf8") };
646
749
  }
647
750
  };
648
751
 
649
752
  // src/core/plugin-service.ts
650
- import { readFile as readFile5, readdir as readdir4, stat as stat4 } from "fs/promises";
651
- import { isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve2 } from "path";
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";
652
755
  import matter3 from "gray-matter";
653
756
  var PluginService = class {
654
757
  constructor(store) {
@@ -667,7 +770,7 @@ var PluginService = class {
667
770
  }
668
771
  }
669
772
  manifestFile(installPath) {
670
- return join4(installPath, ".claude-plugin", "plugin.json");
773
+ return join6(installPath, ".claude-plugin", "plugin.json");
671
774
  }
672
775
  // id 形如 "name@marketplace",按最后一个 @ 切分。
673
776
  splitId(id) {
@@ -699,7 +802,8 @@ var PluginService = class {
699
802
  repo: markets[marketplace]?.source?.repo,
700
803
  installPath,
701
804
  enabled: enabled[id] === true,
702
- exists
805
+ exists,
806
+ source: "user"
703
807
  };
704
808
  return { entry, manifest };
705
809
  }
@@ -744,7 +848,7 @@ var PluginService = class {
744
848
  // 读取 frontmatter 的 description / name 作为子组件摘要,失败返回空串。
745
849
  async readDescription(file) {
746
850
  try {
747
- const fm = matter3(await readFile5(file, "utf8")).data;
851
+ const fm = matter3(await readFile6(file, "utf8")).data;
748
852
  return fm.description ?? fm.name ?? "";
749
853
  } catch {
750
854
  return "";
@@ -761,7 +865,7 @@ var PluginService = class {
761
865
  for (const e of entries) {
762
866
  if (!e.isDirectory()) continue;
763
867
  const description = await this.readDescription(
764
- join4(dir, e.name, "SKILL.md")
868
+ join6(dir, e.name, "SKILL.md")
765
869
  );
766
870
  out.push({ type: "skill", name: e.name, description });
767
871
  }
@@ -778,7 +882,7 @@ var PluginService = class {
778
882
  return;
779
883
  }
780
884
  for (const e of entries) {
781
- const full = join4(cur, e.name);
885
+ const full = join6(cur, e.name);
782
886
  if (e.isDirectory()) {
783
887
  await walk(full);
784
888
  } else if (e.isFile() && e.name.endsWith(".md")) {
@@ -796,7 +900,7 @@ var PluginService = class {
796
900
  }
797
901
  async scanHooks(file) {
798
902
  try {
799
- const json = JSON.parse(await readFile5(file, "utf8"));
903
+ const json = JSON.parse(await readFile6(file, "utf8"));
800
904
  const events = Object.keys(json.hooks ?? json);
801
905
  return [
802
906
  {
@@ -819,7 +923,7 @@ var PluginService = class {
819
923
  // 插件 MCP 既可能在 .mcp.json(顶层即 server 映射或带 mcpServers 包裹),
820
924
  // 也可能在 plugin.json 的 mcpServers 字段;合并两者。
821
925
  async loadMcpServers(installPath, manifest) {
822
- const fromFile = await this.safeReadJson(join4(installPath, ".mcp.json"));
926
+ const fromFile = await this.safeReadJson(join6(installPath, ".mcp.json"));
823
927
  const fileMap = fromFile?.mcpServers ?? fromFile ?? {};
824
928
  const manifestMap = manifest?.mcpServers ?? {};
825
929
  return { ...fileMap, ...manifestMap };
@@ -834,17 +938,17 @@ var PluginService = class {
834
938
  }
835
939
  async scanComponents(installPath, manifest) {
836
940
  const [skills, commands, agents, hooks, mcp] = await Promise.all([
837
- this.scanSkills(join4(installPath, "skills")),
838
- this.scanMarkdownDir(join4(installPath, "commands"), "command"),
839
- this.scanMarkdownDir(join4(installPath, "agents"), "agent"),
840
- this.scanHooks(join4(installPath, "hooks", "hooks.json")),
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")),
841
945
  this.scanMcp(installPath, manifest)
842
946
  ]);
843
947
  return [...skills, ...commands, ...agents, ...hooks, ...mcp];
844
948
  }
845
949
  // 把外部传入的相对路径安全解析到 baseDir 内,防止 `..` 越权。
846
950
  resolveWithin(baseDir, relPath) {
847
- const full = resolve2(baseDir, relPath);
951
+ const full = resolve3(baseDir, relPath);
848
952
  const rel = relative2(baseDir, full);
849
953
  if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
850
954
  throw new Error(`\u975E\u6CD5\u7EC4\u4EF6\u8DEF\u5F84\uFF1A${relPath}`);
@@ -856,33 +960,33 @@ var PluginService = class {
856
960
  const base = rec.installPath;
857
961
  if (type === "skill") {
858
962
  const file = this.resolveWithin(
859
- join4(base, "skills"),
860
- join4(name, "SKILL.md")
963
+ join6(base, "skills"),
964
+ join6(name, "SKILL.md")
861
965
  );
862
966
  return {
863
967
  type,
864
968
  name,
865
969
  language: "markdown",
866
- content: await readFile5(file, "utf8")
970
+ content: await readFile6(file, "utf8")
867
971
  };
868
972
  }
869
973
  if (type === "command" || type === "agent") {
870
974
  const sub = type === "command" ? "commands" : "agents";
871
- const file = this.resolveWithin(join4(base, sub), `${name}.md`);
975
+ const file = this.resolveWithin(join6(base, sub), `${name}.md`);
872
976
  return {
873
977
  type,
874
978
  name,
875
979
  language: "markdown",
876
- content: await readFile5(file, "utf8")
980
+ content: await readFile6(file, "utf8")
877
981
  };
878
982
  }
879
983
  if (type === "hook") {
880
- const file = join4(base, "hooks", "hooks.json");
984
+ const file = join6(base, "hooks", "hooks.json");
881
985
  return {
882
986
  type,
883
987
  name,
884
988
  language: "json",
885
- content: await readFile5(file, "utf8")
989
+ content: await readFile6(file, "utf8")
886
990
  };
887
991
  }
888
992
  const manifest = await this.safeReadJson(
@@ -908,55 +1012,758 @@ var PluginService = class {
908
1012
  }
909
1013
  };
910
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
+
911
1690
  // src/core/config-file-service.ts
912
- import { readFile as readFile6 } from "fs/promises";
1691
+ import { readFile as readFile10 } from "fs/promises";
1692
+ import { parse as parseToml } from "smol-toml";
913
1693
  var REGISTRY = {
914
- settings: (paths, scope) => scope === "user" ? paths.userSettings : paths.projectSettings,
915
- "claude-json": (paths, scope) => scope === "user" ? paths.userClaudeJson : void 0
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
+ }
916
1710
  };
917
1711
  var ConfigFileService = class {
918
1712
  constructor(store) {
919
1713
  this.store = store;
920
1714
  }
921
1715
  store;
922
- file(paths, fileId, scope) {
923
- const f = REGISTRY[fileId](paths, scope);
1716
+ entry(paths, fileId, scope) {
1717
+ const e = REGISTRY[fileId];
1718
+ const f = e.resolve(paths, scope);
924
1719
  if (!f) {
925
1720
  throw new Error(
926
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}`
927
1722
  );
928
1723
  }
929
- return f;
1724
+ return { file: f, kind: e.kind };
930
1725
  }
931
1726
  // 文件不存在视为「尚未创建」,返回空串与 null mtime(与 MemoryService 一致)。
932
1727
  async read(paths, fileId, scope) {
933
- const file = this.file(paths, fileId, scope);
1728
+ const { file } = this.entry(paths, fileId, scope);
934
1729
  try {
935
- return { content: await readFile6(file, "utf8"), mtime: await currentMtime(file) };
1730
+ return { content: await readFile10(file, "utf8"), mtime: await currentMtime(file) };
936
1731
  } catch (err) {
937
1732
  if (err.code === "ENOENT")
938
1733
  return { content: "", mtime: null };
939
1734
  throw err;
940
1735
  }
941
1736
  }
942
- // 校验合法 JSON 后整文件写回;非法内容抛 InvalidJsonError,不触碰磁盘原文件。
1737
+ // kind 校验后写回;非法内容抛 InvalidJsonError,不触碰磁盘原文件。
943
1738
  async write(paths, fileId, scope, content, baseMtime) {
944
- const file = this.file(paths, fileId, scope);
945
- let parsed;
946
- try {
947
- parsed = JSON.parse(content);
948
- } catch {
949
- throw new InvalidJsonError();
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
+ }
950
1757
  }
951
1758
  await assertNoConflict(file, baseMtime);
952
- await this.store.writeJson(file, parsed);
1759
+ await this.store.writeText(file, content);
953
1760
  }
954
1761
  };
955
1762
 
956
1763
  // src/core/agent-command-service.ts
957
- import { mkdir as mkdir3, readdir as readdir5, readFile as readFile7, rm as rm2, writeFile as writeFile3 } from "fs/promises";
958
- import { dirname as dirname3, join as join5 } from "path";
959
- import matter4 from "gray-matter";
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";
960
1767
  function assertSafeName2(name) {
961
1768
  if (!name || name.includes("/") || name.includes("\\") || name.includes("..") || name.includes("\0")) {
962
1769
  throw new Error(`\u975E\u6CD5\u540D\u79F0\uFF1A${name}`);
@@ -970,13 +1777,13 @@ var AgentCommandService = class {
970
1777
  }
971
1778
  fileFor(paths, kind, scope, name) {
972
1779
  assertSafeName2(name);
973
- return join5(this.dir(paths, kind, scope), `${name}.md`);
1780
+ return join10(this.dir(paths, kind, scope), `${name}.md`);
974
1781
  }
975
1782
  async list(paths, kind, scope) {
976
1783
  const dir = this.dir(paths, kind, scope);
977
1784
  let entries;
978
1785
  try {
979
- entries = await readdir5(dir, { withFileTypes: true });
1786
+ entries = await readdir8(dir, { withFileTypes: true });
980
1787
  } catch (err) {
981
1788
  if (err.code === "ENOENT") return [];
982
1789
  throw err;
@@ -985,10 +1792,10 @@ var AgentCommandService = class {
985
1792
  for (const e of entries) {
986
1793
  if (!e.isFile() || !e.name.endsWith(".md")) continue;
987
1794
  const name = e.name.slice(0, -3);
988
- const path = join5(dir, e.name);
1795
+ const path = join10(dir, e.name);
989
1796
  let description;
990
1797
  try {
991
- const fm = matter4(await readFile7(path, "utf8")).data;
1798
+ const fm = matter5(await readFile11(path, "utf8")).data;
992
1799
  description = typeof fm.description === "string" ? fm.description : "";
993
1800
  } catch {
994
1801
  description = "(frontmatter \u89E3\u6790\u5931\u8D25)";
@@ -1000,7 +1807,7 @@ var AgentCommandService = class {
1000
1807
  async read(paths, kind, scope, name) {
1001
1808
  const file = this.fileFor(paths, kind, scope, name);
1002
1809
  try {
1003
- return { content: await readFile7(file, "utf8"), mtime: await currentMtime(file) };
1810
+ return { content: await readFile11(file, "utf8"), mtime: await currentMtime(file) };
1004
1811
  } catch {
1005
1812
  throw new Error(`\u672A\u627E\u5230 ${kind}\u300C${name}\u300D\uFF08scope: ${scope}\uFF09`);
1006
1813
  }
@@ -1008,8 +1815,8 @@ var AgentCommandService = class {
1008
1815
  async write(paths, kind, scope, name, content, baseMtime) {
1009
1816
  const file = this.fileFor(paths, kind, scope, name);
1010
1817
  await assertNoConflict(file, baseMtime);
1011
- await mkdir3(dirname3(file), { recursive: true });
1012
- await writeFile3(file, content, "utf8");
1818
+ await mkdir4(dirname4(file), { recursive: true });
1819
+ await writeFile4(file, content, "utf8");
1013
1820
  }
1014
1821
  async remove(paths, kind, scope, name) {
1015
1822
  const file = this.fileFor(paths, kind, scope, name);
@@ -1017,23 +1824,137 @@ var AgentCommandService = class {
1017
1824
  }
1018
1825
  };
1019
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
+
1020
1941
  // src/cli.ts
1021
1942
  function findFreePort(start) {
1022
- return new Promise((resolve3, reject) => {
1943
+ return new Promise((resolve8, reject) => {
1023
1944
  const srv = createServer();
1024
1945
  srv.once("error", (err) => {
1025
- if (err.code === "EADDRINUSE") resolve3(findFreePort(start + 1));
1946
+ if (err.code === "EADDRINUSE") resolve8(findFreePort(start + 1));
1026
1947
  else reject(err);
1027
1948
  });
1028
1949
  srv.listen(start, () => {
1029
- srv.close(() => resolve3(start));
1950
+ srv.close(() => resolve8(start));
1030
1951
  });
1031
1952
  });
1032
1953
  }
1033
1954
  async function main() {
1034
1955
  const home = homedir();
1035
1956
  const store = new ConfigStore();
1036
- const registryFile = join6(home, ".ccpanel", "ccpanel-projects.json");
1957
+ const registryFile = join12(home, ".ccpanel", "ccpanel-projects.json");
1037
1958
  const webRoot = fileURLToPath(new URL("../web/dist", import.meta.url));
1038
1959
  const app = createApp({
1039
1960
  home,
@@ -1045,8 +1966,12 @@ async function main() {
1045
1966
  memoryFiles: new MemoryFilesService(),
1046
1967
  sessions: new SessionService(),
1047
1968
  plugins: new PluginService(store),
1969
+ codexPlugins: new CodexPluginService(store),
1970
+ codexMemory: new CodexMemoryService(),
1971
+ codexSessions: new CodexSessionService(),
1048
1972
  configFiles: new ConfigFileService(store),
1049
1973
  agentCommands: new AgentCommandService(),
1974
+ sync: new SyncService(home),
1050
1975
  registryFile,
1051
1976
  webRoot
1052
1977
  });