@adversity/coding-tool-x 3.1.0 → 3.1.1

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.
Files changed (137) hide show
  1. package/CHANGELOG.md +15 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
  5. package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
  6. package/dist/web/assets/Home-Di2qsylF.css +1 -0
  7. package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
  8. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
  13. package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
  14. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  15. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  16. package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
  17. package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
  18. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-Ufv5rCa5.css +1 -0
  21. package/dist/web/assets/index-lAkrRC3h.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +39 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/paths.js +105 -33
  49. package/src/index.js +64 -3
  50. package/src/plugins/constants.js +3 -2
  51. package/src/plugins/plugin-api.js +1 -1
  52. package/src/reset-config.js +4 -2
  53. package/src/server/api/agents.js +57 -14
  54. package/src/server/api/channels.js +112 -33
  55. package/src/server/api/codex-channels.js +111 -18
  56. package/src/server/api/codex-proxy.js +14 -8
  57. package/src/server/api/commands.js +71 -18
  58. package/src/server/api/config-export.js +0 -6
  59. package/src/server/api/config-registry.js +11 -3
  60. package/src/server/api/config.js +376 -5
  61. package/src/server/api/convert.js +133 -0
  62. package/src/server/api/dashboard.js +22 -6
  63. package/src/server/api/gemini-channels.js +107 -18
  64. package/src/server/api/gemini-proxy.js +14 -8
  65. package/src/server/api/gemini-sessions.js +1 -1
  66. package/src/server/api/health-check.js +4 -3
  67. package/src/server/api/mcp.js +3 -3
  68. package/src/server/api/opencode-channels.js +419 -0
  69. package/src/server/api/opencode-projects.js +99 -0
  70. package/src/server/api/opencode-proxy.js +198 -0
  71. package/src/server/api/opencode-sessions.js +403 -0
  72. package/src/server/api/opencode-statistics.js +57 -0
  73. package/src/server/api/plugins.js +66 -19
  74. package/src/server/api/prompts.js +2 -2
  75. package/src/server/api/proxy.js +7 -4
  76. package/src/server/api/sessions.js +3 -0
  77. package/src/server/api/skills.js +69 -18
  78. package/src/server/api/workspaces.js +78 -6
  79. package/src/server/codex-proxy-server.js +30 -18
  80. package/src/server/dev-server.js +1 -1
  81. package/src/server/gemini-proxy-server.js +15 -3
  82. package/src/server/index.js +165 -58
  83. package/src/server/opencode-proxy-server.js +4375 -0
  84. package/src/server/proxy-server.js +27 -18
  85. package/src/server/services/agents-service.js +61 -24
  86. package/src/server/services/channel-scheduler.js +9 -5
  87. package/src/server/services/channels.js +64 -37
  88. package/src/server/services/codex-channels.js +56 -43
  89. package/src/server/services/codex-settings-manager.js +271 -49
  90. package/src/server/services/codex-statistics-service.js +2 -2
  91. package/src/server/services/commands-service.js +84 -25
  92. package/src/server/services/config-export-service.js +7 -45
  93. package/src/server/services/config-registry-service.js +63 -17
  94. package/src/server/services/config-sync-manager.js +160 -7
  95. package/src/server/services/config-templates-service.js +204 -51
  96. package/src/server/services/env-checker.js +26 -12
  97. package/src/server/services/env-manager.js +126 -18
  98. package/src/server/services/favorites.js +5 -3
  99. package/src/server/services/gemini-channels.js +33 -44
  100. package/src/server/services/gemini-statistics-service.js +2 -2
  101. package/src/server/services/mcp-service.js +350 -9
  102. package/src/server/services/model-detector.js +707 -221
  103. package/src/server/services/network-access.js +80 -0
  104. package/src/server/services/opencode-channels.js +206 -0
  105. package/src/server/services/opencode-gateway-converter.js +639 -0
  106. package/src/server/services/opencode-sessions.js +663 -0
  107. package/src/server/services/opencode-settings-manager.js +342 -0
  108. package/src/server/services/opencode-statistics-service.js +255 -0
  109. package/src/server/services/plugins-service.js +479 -22
  110. package/src/server/services/prompts-service.js +53 -11
  111. package/src/server/services/proxy-runtime.js +1 -1
  112. package/src/server/services/repo-scanner-base.js +1 -1
  113. package/src/server/services/security-config.js +1 -1
  114. package/src/server/services/session-cache.js +1 -1
  115. package/src/server/services/skill-service.js +300 -46
  116. package/src/server/services/speed-test.js +464 -186
  117. package/src/server/services/statistics-service.js +2 -2
  118. package/src/server/services/terminal-commands.js +10 -3
  119. package/src/server/services/terminal-config.js +1 -1
  120. package/src/server/services/ui-config.js +1 -1
  121. package/src/server/services/workspace-service.js +57 -100
  122. package/src/server/websocket-server.js +132 -3
  123. package/src/ui/menu.js +49 -40
  124. package/src/utils/port-helper.js +22 -8
  125. package/src/utils/session.js +5 -4
  126. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  127. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  128. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  129. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  130. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  131. package/src/server/api/oauth.js +0 -294
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/config/oauth-providers.js +0 -68
  134. package/src/server/services/oauth-callback-server.js +0 -284
  135. package/src/server/services/oauth-service.js +0 -378
  136. package/src/server/services/oauth-token-storage.js +0 -135
  137. package/src/server/services/permission-templates-service.js +0 -308
@@ -0,0 +1,663 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { NATIVE_PATHS, PATHS } = require('../../config/paths');
5
+
6
+ /**
7
+ * OpenCode 会话服务
8
+ * 读取 OpenCode CLI 的原生会话数据
9
+ */
10
+
11
+ const PROJECT_ORDER_FILE = path.join(PATHS.base, 'opencode-project-order.json');
12
+ const SESSION_ORDER_FILE = path.join(PATHS.base, 'opencode-session-order.json');
13
+
14
+ function ensureParentDir(filePath) {
15
+ const dir = path.dirname(filePath);
16
+ if (!fs.existsSync(dir)) {
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ }
19
+ }
20
+
21
+ function readJsonSafe(filePath, fallback) {
22
+ if (!filePath || !fs.existsSync(filePath)) {
23
+ return fallback;
24
+ }
25
+ try {
26
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
27
+ } catch (err) {
28
+ return fallback;
29
+ }
30
+ }
31
+
32
+ function writeJsonSafe(filePath, data) {
33
+ ensureParentDir(filePath);
34
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
35
+ }
36
+
37
+ function copyDirectoryRecursive(sourceDir, targetDir) {
38
+ if (!fs.existsSync(sourceDir)) {
39
+ return;
40
+ }
41
+
42
+ if (!fs.existsSync(targetDir)) {
43
+ fs.mkdirSync(targetDir, { recursive: true });
44
+ }
45
+
46
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
47
+ for (const entry of entries) {
48
+ const sourcePath = path.join(sourceDir, entry.name);
49
+ const targetPath = path.join(targetDir, entry.name);
50
+ if (entry.isDirectory()) {
51
+ copyDirectoryRecursive(sourcePath, targetPath);
52
+ } else {
53
+ fs.copyFileSync(sourcePath, targetPath);
54
+ }
55
+ }
56
+ }
57
+
58
+ function sortByOrder(items, order, fallbackCompare) {
59
+ const fallbackSorted = [...items].sort(fallbackCompare);
60
+ if (!Array.isArray(order) || order.length === 0) {
61
+ return fallbackSorted;
62
+ }
63
+
64
+ const orderMap = new Map(order.map((name, idx) => [name, idx]));
65
+ return fallbackSorted.sort((a, b) => {
66
+ const aIndex = orderMap.has(a.name) ? orderMap.get(a.name) : Number.MAX_SAFE_INTEGER;
67
+ const bIndex = orderMap.has(b.name) ? orderMap.get(b.name) : Number.MAX_SAFE_INTEGER;
68
+ if (aIndex === bIndex) {
69
+ return fallbackCompare(a, b);
70
+ }
71
+ return aIndex - bIndex;
72
+ });
73
+ }
74
+
75
+ function extractTextContent(content) {
76
+ if (typeof content === 'string') {
77
+ return content;
78
+ }
79
+ if (Array.isArray(content)) {
80
+ return content
81
+ .filter(item => item && item.type === 'text' && typeof item.text === 'string')
82
+ .map(item => item.text)
83
+ .join('\n')
84
+ .trim();
85
+ }
86
+ return '';
87
+ }
88
+
89
+ function buildContext(text, keyword, contextLength = 35) {
90
+ if (!text || !keyword) {
91
+ return null;
92
+ }
93
+
94
+ const parsedContextLength = Number(contextLength);
95
+ const safeContextLength = Number.isFinite(parsedContextLength) && parsedContextLength >= 0
96
+ ? parsedContextLength
97
+ : 35;
98
+
99
+ const lowerText = text.toLowerCase();
100
+ const lowerKeyword = keyword.toLowerCase();
101
+ const index = lowerText.indexOf(lowerKeyword);
102
+ if (index === -1) {
103
+ return null;
104
+ }
105
+
106
+ const start = Math.max(0, index - safeContextLength);
107
+ const end = Math.min(text.length, index + keyword.length + safeContextLength);
108
+ let context = text.slice(start, end);
109
+ if (start > 0) context = `...${context}`;
110
+ if (end < text.length) context = `${context}...`;
111
+ return context;
112
+ }
113
+
114
+ // 检查 OpenCode 是否安装
115
+ function isOpenCodeInstalled() {
116
+ return fs.existsSync(NATIVE_PATHS.opencode.data);
117
+ }
118
+
119
+ // 获取 OpenCode 数据目录
120
+ function getOpenCodeDataDir() {
121
+ return NATIVE_PATHS.opencode.data;
122
+ }
123
+
124
+ // 获取会话存储目录
125
+ function getSessionsDir() {
126
+ return path.join(getOpenCodeDataDir(), 'storage', 'session');
127
+ }
128
+
129
+ // 获取项目存储目录
130
+ function getProjectsDir() {
131
+ return path.join(getOpenCodeDataDir(), 'storage', 'project');
132
+ }
133
+
134
+ // 获取消息存储目录
135
+ function getMessagesRootDir() {
136
+ return NATIVE_PATHS.opencode.messages;
137
+ }
138
+
139
+ function getMessageDir(sessionId) {
140
+ return path.join(getMessagesRootDir(), sessionId);
141
+ }
142
+
143
+ function getProjectOrder() {
144
+ const order = readJsonSafe(PROJECT_ORDER_FILE, []);
145
+ return Array.isArray(order) ? order : [];
146
+ }
147
+
148
+ function saveProjectOrder(order) {
149
+ if (!Array.isArray(order)) {
150
+ throw new Error('order must be an array');
151
+ }
152
+ writeJsonSafe(PROJECT_ORDER_FILE, order);
153
+ return { success: true };
154
+ }
155
+
156
+ function getSessionOrderMap() {
157
+ const map = readJsonSafe(SESSION_ORDER_FILE, {});
158
+ return map && typeof map === 'object' ? map : {};
159
+ }
160
+
161
+ function saveSessionOrderMap(map) {
162
+ writeJsonSafe(SESSION_ORDER_FILE, map);
163
+ }
164
+
165
+ function getSessionOrder(projectId) {
166
+ const map = getSessionOrderMap();
167
+ const order = map[projectId];
168
+ return Array.isArray(order) ? order : [];
169
+ }
170
+
171
+ function saveSessionOrder(projectId, order) {
172
+ if (!projectId) {
173
+ throw new Error('projectId is required');
174
+ }
175
+ if (!Array.isArray(order)) {
176
+ throw new Error('order must be an array');
177
+ }
178
+
179
+ const map = getSessionOrderMap();
180
+ map[projectId] = order;
181
+ saveSessionOrderMap(map);
182
+ return { success: true };
183
+ }
184
+
185
+ function removeSessionFromOrder(projectId, sessionId) {
186
+ const map = getSessionOrderMap();
187
+ if (!Array.isArray(map[projectId])) {
188
+ return;
189
+ }
190
+ map[projectId] = map[projectId].filter(id => id !== sessionId);
191
+ saveSessionOrderMap(map);
192
+ }
193
+
194
+ function removeProjectFromOrder(projectId) {
195
+ const currentOrder = getProjectOrder().filter(name => name !== projectId);
196
+ saveProjectOrder(currentOrder);
197
+
198
+ const map = getSessionOrderMap();
199
+ if (map[projectId] !== undefined) {
200
+ delete map[projectId];
201
+ saveSessionOrderMap(map);
202
+ }
203
+ }
204
+
205
+ function getProjectEntries() {
206
+ const projectsDir = getProjectsDir();
207
+ if (!fs.existsSync(projectsDir)) {
208
+ return [];
209
+ }
210
+
211
+ const files = fs.readdirSync(projectsDir).filter(file => file.endsWith('.json'));
212
+ const entries = [];
213
+ for (const file of files) {
214
+ const filePath = path.join(projectsDir, file);
215
+ const data = readJsonSafe(filePath, null);
216
+ if (!data || !data.id) {
217
+ continue;
218
+ }
219
+ entries.push({ filePath, data });
220
+ }
221
+ return entries;
222
+ }
223
+
224
+ // 获取所有项目
225
+ function getProjects() {
226
+ const projects = [];
227
+ const entries = getProjectEntries();
228
+
229
+ for (const entry of entries) {
230
+ const project = entry.data;
231
+ const projectSessions = getSessionsByProjectId(project.id);
232
+ projects.push({
233
+ name: project.id,
234
+ displayName: project.id,
235
+ fullPath: project.worktree || '/',
236
+ path: project.worktree || '/',
237
+ sessionCount: projectSessions.length,
238
+ lastUsed: project.time?.updated || project.time?.created || 0,
239
+ source: 'opencode'
240
+ });
241
+ }
242
+
243
+ return sortByOrder(
244
+ projects,
245
+ getProjectOrder(),
246
+ (a, b) => (b.lastUsed || 0) - (a.lastUsed || 0)
247
+ );
248
+ }
249
+
250
+ // 根据项目ID获取会话列表
251
+ function getSessionsByProjectId(projectId) {
252
+ const sessionsDir = path.join(getSessionsDir(), projectId);
253
+ const sessions = [];
254
+
255
+ if (!fs.existsSync(sessionsDir)) {
256
+ return sessions;
257
+ }
258
+
259
+ const files = fs.readdirSync(sessionsDir).filter(file => file.endsWith('.json'));
260
+ for (const file of files) {
261
+ const filePath = path.join(sessionsDir, file);
262
+ try {
263
+ const content = fs.readFileSync(filePath, 'utf8');
264
+ const session = JSON.parse(content);
265
+ sessions.push(normalizeSession(session, filePath, projectId));
266
+ } catch (err) {
267
+ console.error(`[OpenCode Sessions] Failed to parse session file ${file}:`, err);
268
+ }
269
+ }
270
+
271
+ const fallbackSorted = sessions.sort(
272
+ (a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime()
273
+ );
274
+
275
+ const order = getSessionOrder(projectId);
276
+ if (order.length === 0) {
277
+ return fallbackSorted;
278
+ }
279
+
280
+ const orderMap = new Map(order.map((id, idx) => [id, idx]));
281
+ return [...fallbackSorted].sort((a, b) => {
282
+ const aIndex = orderMap.has(a.sessionId) ? orderMap.get(a.sessionId) : Number.MAX_SAFE_INTEGER;
283
+ const bIndex = orderMap.has(b.sessionId) ? orderMap.get(b.sessionId) : Number.MAX_SAFE_INTEGER;
284
+ if (aIndex === bIndex) {
285
+ return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
286
+ }
287
+ return aIndex - bIndex;
288
+ });
289
+ }
290
+
291
+ // 归一化会话格式(与 Claude Code 格式一致)
292
+ function normalizeSession(session, filePath, projectId = null) {
293
+ const mtime = session.time?.updated
294
+ ? new Date(session.time.updated).toISOString()
295
+ : new Date().toISOString();
296
+
297
+ let size = 0;
298
+ try {
299
+ if (filePath && fs.existsSync(filePath)) {
300
+ const stats = fs.statSync(filePath);
301
+ size = stats.size;
302
+ }
303
+ } catch (err) {
304
+ // 忽略错误
305
+ }
306
+
307
+ return {
308
+ sessionId: session.id,
309
+ projectName: projectId,
310
+ mtime,
311
+ size,
312
+ filePath: filePath || '',
313
+ gitBranch: null,
314
+ firstMessage: session.title || session.slug || null,
315
+ forkedFrom: null,
316
+ directory: session.directory,
317
+ slug: session.slug,
318
+ source: 'opencode'
319
+ };
320
+ }
321
+
322
+ // 根据项目名获取会话列表
323
+ function getSessionsByProject(projectName) {
324
+ return getSessionsByProjectId(projectName);
325
+ }
326
+
327
+ function getSessionLocation(sessionId) {
328
+ const sessionsRoot = getSessionsDir();
329
+ if (!fs.existsSync(sessionsRoot)) {
330
+ return null;
331
+ }
332
+
333
+ const projectDirs = fs.readdirSync(sessionsRoot, { withFileTypes: true });
334
+ for (const projectDir of projectDirs) {
335
+ if (!projectDir.isDirectory()) continue;
336
+
337
+ const projectPath = path.join(sessionsRoot, projectDir.name);
338
+ const directPath = path.join(projectPath, `${sessionId}.json`);
339
+ if (fs.existsSync(directPath)) {
340
+ const sessionData = readJsonSafe(directPath, null);
341
+ if (sessionData && sessionData.id === sessionId) {
342
+ return { projectId: projectDir.name, sessionPath: directPath, sessionData };
343
+ }
344
+ }
345
+
346
+ const files = fs.readdirSync(projectPath).filter(file => file.endsWith('.json'));
347
+ for (const file of files) {
348
+ const sessionPath = path.join(projectPath, file);
349
+ const sessionData = readJsonSafe(sessionPath, null);
350
+ if (sessionData && sessionData.id === sessionId) {
351
+ return { projectId: projectDir.name, sessionPath, sessionData };
352
+ }
353
+ }
354
+ }
355
+
356
+ return null;
357
+ }
358
+
359
+ // 根据会话ID获取会话详情
360
+ function getSessionById(sessionId) {
361
+ const location = getSessionLocation(sessionId);
362
+ if (!location) {
363
+ return null;
364
+ }
365
+
366
+ return normalizeSession(location.sessionData, location.sessionPath, location.projectId);
367
+ }
368
+
369
+ // 获取项目和会话数量统计
370
+ function getProjectAndSessionCounts() {
371
+ try {
372
+ const projects = getProjects();
373
+ let sessionCount = 0;
374
+
375
+ for (const project of projects) {
376
+ sessionCount += project.sessionCount || 0;
377
+ }
378
+
379
+ return {
380
+ projectCount: projects.length,
381
+ sessionCount
382
+ };
383
+ } catch (err) {
384
+ console.error('[OpenCode Sessions] Failed to get counts:', err);
385
+ return { projectCount: 0, sessionCount: 0 };
386
+ }
387
+ }
388
+
389
+ function getRecentSessions(limit = 5) {
390
+ const projects = getProjects();
391
+ const { loadAliases } = require('./alias');
392
+ const { getForkRelations } = require('./sessions');
393
+ const aliases = loadAliases();
394
+ const forkRelations = getForkRelations();
395
+ const allSessions = [];
396
+
397
+ for (const project of projects) {
398
+ const sessions = getSessionsByProjectId(project.name);
399
+ sessions.forEach(session => {
400
+ allSessions.push({
401
+ ...session,
402
+ alias: aliases[session.sessionId] || null,
403
+ forkedFrom: forkRelations[session.sessionId] || null,
404
+ projectName: project.name,
405
+ projectDisplayName: project.displayName,
406
+ projectFullPath: project.fullPath
407
+ });
408
+ });
409
+ }
410
+
411
+ return allSessions
412
+ .sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime())
413
+ .slice(0, limit);
414
+ }
415
+
416
+ // 删除会话
417
+ function deleteSession(sessionId) {
418
+ const location = getSessionLocation(sessionId);
419
+ if (!location) {
420
+ throw new Error('Session not found');
421
+ }
422
+
423
+ fs.unlinkSync(location.sessionPath);
424
+
425
+ const messageDir = getMessageDir(sessionId);
426
+ if (fs.existsSync(messageDir)) {
427
+ fs.rmSync(messageDir, { recursive: true, force: true });
428
+ }
429
+
430
+ try {
431
+ const { deleteAlias } = require('./alias');
432
+ deleteAlias(sessionId);
433
+ } catch (err) {
434
+ // ignore alias cleanup errors
435
+ }
436
+
437
+ removeSessionFromOrder(location.projectId, sessionId);
438
+
439
+ try {
440
+ const { getForkRelations, saveForkRelations } = require('./sessions');
441
+ const relations = getForkRelations();
442
+ delete relations[sessionId];
443
+ Object.keys(relations).forEach((key) => {
444
+ if (relations[key] === sessionId) {
445
+ delete relations[key];
446
+ }
447
+ });
448
+ saveForkRelations(relations);
449
+ } catch (err) {
450
+ // ignore fork relation cleanup errors
451
+ }
452
+
453
+ return { success: true, projectName: location.projectId, sessionId };
454
+ }
455
+
456
+ function forkSession(sessionId) {
457
+ const location = getSessionLocation(sessionId);
458
+ if (!location) {
459
+ throw new Error('Session not found');
460
+ }
461
+
462
+ const now = new Date().toISOString();
463
+ const newSessionId = crypto.randomUUID();
464
+ const source = location.sessionData;
465
+ const nextSession = {
466
+ ...source,
467
+ id: newSessionId,
468
+ time: {
469
+ ...(source.time || {}),
470
+ created: now,
471
+ updated: now
472
+ }
473
+ };
474
+
475
+ const targetPath = path.join(path.dirname(location.sessionPath), `${newSessionId}.json`);
476
+ fs.writeFileSync(targetPath, JSON.stringify(nextSession, null, 2), 'utf8');
477
+
478
+ const sourceMessageDir = getMessageDir(sessionId);
479
+ const targetMessageDir = getMessageDir(newSessionId);
480
+ if (fs.existsSync(sourceMessageDir)) {
481
+ copyDirectoryRecursive(sourceMessageDir, targetMessageDir);
482
+ }
483
+
484
+ try {
485
+ const { getForkRelations, saveForkRelations } = require('./sessions');
486
+ const relations = getForkRelations();
487
+ relations[newSessionId] = sessionId;
488
+ saveForkRelations(relations);
489
+ } catch (err) {
490
+ // ignore fork relation save errors
491
+ }
492
+
493
+ const existingOrder = getSessionOrder(location.projectId);
494
+ saveSessionOrder(location.projectId, [newSessionId, ...existingOrder.filter(id => id !== newSessionId)]);
495
+
496
+ return {
497
+ success: true,
498
+ newSessionId,
499
+ forkedFrom: sessionId,
500
+ projectName: location.projectId,
501
+ newFilePath: targetPath
502
+ };
503
+ }
504
+
505
+ function deleteProject(projectId) {
506
+ const projectSessionDir = path.join(getSessionsDir(), projectId);
507
+ if (!fs.existsSync(projectSessionDir)) {
508
+ throw new Error('Project not found');
509
+ }
510
+
511
+ const sessionFiles = fs.readdirSync(projectSessionDir).filter(file => file.endsWith('.json'));
512
+ const deletedSessionIds = [];
513
+
514
+ for (const file of sessionFiles) {
515
+ const sessionPath = path.join(projectSessionDir, file);
516
+ const session = readJsonSafe(sessionPath, null);
517
+ const sessionId = session?.id || path.basename(file, '.json');
518
+ deletedSessionIds.push(sessionId);
519
+
520
+ const messageDir = getMessageDir(sessionId);
521
+ if (fs.existsSync(messageDir)) {
522
+ fs.rmSync(messageDir, { recursive: true, force: true });
523
+ }
524
+
525
+ try {
526
+ const { deleteAlias } = require('./alias');
527
+ deleteAlias(sessionId);
528
+ } catch (err) {
529
+ // ignore alias cleanup errors
530
+ }
531
+ }
532
+
533
+ fs.rmSync(projectSessionDir, { recursive: true, force: true });
534
+
535
+ const projectEntries = getProjectEntries();
536
+ for (const entry of projectEntries) {
537
+ if (entry.data.id === projectId) {
538
+ fs.rmSync(entry.filePath, { force: true });
539
+ }
540
+ }
541
+
542
+ removeProjectFromOrder(projectId);
543
+
544
+ try {
545
+ const { getForkRelations, saveForkRelations } = require('./sessions');
546
+ const deletedSet = new Set(deletedSessionIds);
547
+ const relations = getForkRelations();
548
+ Object.keys(relations).forEach((key) => {
549
+ if (deletedSet.has(key) || deletedSet.has(relations[key])) {
550
+ delete relations[key];
551
+ }
552
+ });
553
+ saveForkRelations(relations);
554
+ } catch (err) {
555
+ // ignore relation cleanup errors
556
+ }
557
+
558
+ return {
559
+ success: true,
560
+ projectName: projectId,
561
+ deletedCount: deletedSessionIds.length
562
+ };
563
+ }
564
+
565
+ // 搜索会话
566
+ function searchSessions(keyword, contextLength = 35, projectFilter = null) {
567
+ if (!keyword || !String(keyword).trim()) {
568
+ return [];
569
+ }
570
+
571
+ const searchKeyword = String(keyword).trim();
572
+ const projects = getProjects();
573
+ const { loadAliases } = require('./alias');
574
+ const aliases = loadAliases();
575
+ const results = [];
576
+
577
+ for (const project of projects) {
578
+ if (projectFilter && project.name !== projectFilter) {
579
+ continue;
580
+ }
581
+
582
+ const sessions = getSessionsByProjectId(project.name);
583
+ for (const session of sessions) {
584
+ const matches = [];
585
+
586
+ const quickChecks = [
587
+ session.sessionId,
588
+ session.firstMessage,
589
+ session.slug,
590
+ session.directory
591
+ ];
592
+ for (const text of quickChecks) {
593
+ const context = buildContext(text, searchKeyword, contextLength);
594
+ if (context) {
595
+ matches.push({
596
+ role: 'assistant',
597
+ context,
598
+ timestamp: session.mtime
599
+ });
600
+ }
601
+ }
602
+
603
+ const messageDir = getMessageDir(session.sessionId);
604
+ if (fs.existsSync(messageDir)) {
605
+ const messageFiles = fs.readdirSync(messageDir)
606
+ .filter(file => file.endsWith('.json'))
607
+ .sort();
608
+ for (const messageFile of messageFiles) {
609
+ const messagePath = path.join(messageDir, messageFile);
610
+ const message = readJsonSafe(messagePath, null);
611
+ if (!message) continue;
612
+
613
+ const text = extractTextContent(message.content);
614
+ const context = buildContext(text, searchKeyword, contextLength);
615
+ if (!context) continue;
616
+
617
+ matches.push({
618
+ role: message.role === 'user' ? 'user' : 'assistant',
619
+ context,
620
+ timestamp: message.time?.created || null
621
+ });
622
+ }
623
+ }
624
+
625
+ if (matches.length > 0) {
626
+ results.push({
627
+ sessionId: session.sessionId,
628
+ projectName: project.name,
629
+ projectDisplayName: project.displayName,
630
+ projectFullPath: project.fullPath,
631
+ alias: aliases[session.sessionId] || null,
632
+ matchCount: matches.length,
633
+ matches: matches.slice(0, 5),
634
+ source: 'opencode'
635
+ });
636
+ }
637
+ }
638
+ }
639
+
640
+ return results.sort((a, b) => b.matchCount - a.matchCount);
641
+ }
642
+
643
+ module.exports = {
644
+ isOpenCodeInstalled,
645
+ getOpenCodeDataDir,
646
+ getSessionsDir,
647
+ getProjectsDir,
648
+ getProjects,
649
+ getProjectOrder,
650
+ saveProjectOrder,
651
+ getSessionsByProject,
652
+ getSessionsByProjectId,
653
+ getSessionById,
654
+ getRecentSessions,
655
+ normalizeSession,
656
+ getProjectAndSessionCounts,
657
+ deleteSession,
658
+ deleteProject,
659
+ forkSession,
660
+ getSessionOrder,
661
+ saveSessionOrder,
662
+ searchSessions
663
+ };