evolclaw 2.1.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +59 -30
  2. package/data/evolclaw.sample.json +15 -4
  3. package/dist/agents/claude-runner.js +685 -0
  4. package/dist/agents/codex-runner.js +315 -0
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +580 -10
  7. package/dist/channels/feishu.js +888 -135
  8. package/dist/channels/wechat.js +127 -21
  9. package/dist/cli.js +519 -136
  10. package/dist/config.js +277 -25
  11. package/dist/core/agent-loader.js +39 -0
  12. package/dist/core/channel-loader.js +67 -0
  13. package/dist/core/command-handler.js +1537 -392
  14. package/dist/core/event-bus.js +32 -0
  15. package/dist/core/interaction-router.js +68 -0
  16. package/dist/core/message/message-bridge.js +216 -0
  17. package/dist/core/message/message-processor.js +1028 -0
  18. package/dist/core/message/message-queue.js +240 -0
  19. package/dist/core/message/stream-debouncer.js +122 -0
  20. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  21. package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
  22. package/dist/core/permission.js +259 -0
  23. package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
  24. package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
  25. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  26. package/dist/core/session/session-file-adapter.js +7 -0
  27. package/dist/core/session/session-file-health.js +45 -0
  28. package/dist/core/session/session-manager.js +1072 -0
  29. package/dist/index.js +402 -252
  30. package/dist/ipc.js +106 -0
  31. package/dist/paths.js +1 -0
  32. package/dist/types.js +3 -0
  33. package/dist/utils/{platform.js → cross-platform.js} +38 -1
  34. package/dist/utils/error-utils.js +130 -5
  35. package/dist/utils/init-channel.js +649 -0
  36. package/dist/utils/init.js +190 -53
  37. package/dist/utils/logger.js +8 -3
  38. package/dist/utils/media-cache.js +207 -0
  39. package/dist/utils/migrate-project.js +122 -0
  40. package/dist/utils/rich-content-renderer.js +228 -0
  41. package/dist/utils/stats-collector.js +102 -0
  42. package/package.json +4 -2
  43. package/dist/core/agent-runner.js +0 -348
  44. package/dist/core/message-processor.js +0 -604
  45. package/dist/core/message-queue.js +0 -116
  46. package/dist/core/message-stream.js +0 -59
  47. package/dist/core/session-manager.js +0 -664
  48. package/dist/index.js.bak +0 -340
  49. package/dist/utils/init-feishu.js +0 -261
  50. package/dist/utils/init-wechat.js +0 -170
  51. package/dist/utils/markdown-to-feishu.js +0 -94
  52. package/dist/utils/permission.js +0 -43
  53. package/dist/utils/session-file-health.js +0 -68
  54. /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Gemini SessionFileAdapter
3
+ *
4
+ * Reads Gemini CLI session files from ~/.gemini/tmp/{project}/chats/.
5
+ * Gemini stores sessions as JSON files with naming convention:
6
+ * session-{YYYY-MM-DDTHH-mm}-{uuid-prefix}.json
7
+ *
8
+ * Each file contains:
9
+ * { sessionId, projectHash, startTime, lastUpdated, messages, kind }
10
+ *
11
+ * Messages have type: 'user' | 'gemini' (not 'role').
12
+ */
13
+ import { logger } from '../../../utils/logger.js';
14
+ import path from 'path';
15
+ import fs from 'fs';
16
+ import os from 'os';
17
+ export class GeminiSessionFileAdapter {
18
+ agentId = 'gemini';
19
+ getGeminiHome() {
20
+ return path.join(os.homedir(), '.gemini');
21
+ }
22
+ /**
23
+ * Resolve Gemini project key for a project path.
24
+ * Priority:
25
+ * 1. ~/.gemini/projects.json explicit mapping
26
+ * 2. .project_root file content match under ~/.gemini/tmp/<project-key>/
27
+ * 3. basename(projectPath) fallback
28
+ */
29
+ resolveProjectKey(projectPath) {
30
+ const geminiHome = this.getGeminiHome();
31
+ const projectsPath = path.join(geminiHome, 'projects.json');
32
+ try {
33
+ if (fs.existsSync(projectsPath)) {
34
+ const data = JSON.parse(fs.readFileSync(projectsPath, 'utf-8'));
35
+ const mapped = data?.projects?.[projectPath];
36
+ if (typeof mapped === 'string' && mapped.trim())
37
+ return mapped;
38
+ }
39
+ }
40
+ catch (error) {
41
+ logger.debug('[GeminiAdapter] Failed to read projects.json:', error);
42
+ }
43
+ const tmpDir = path.join(geminiHome, 'tmp');
44
+ if (fs.existsSync(tmpDir)) {
45
+ try {
46
+ for (const entry of fs.readdirSync(tmpDir, { withFileTypes: true })) {
47
+ if (!entry.isDirectory())
48
+ continue;
49
+ const rootFile = path.join(tmpDir, entry.name, '.project_root');
50
+ if (!fs.existsSync(rootFile))
51
+ continue;
52
+ try {
53
+ const content = fs.readFileSync(rootFile, 'utf-8').trim();
54
+ if (content === projectPath)
55
+ return entry.name;
56
+ }
57
+ catch {
58
+ // ignore broken .project_root
59
+ }
60
+ }
61
+ }
62
+ catch (error) {
63
+ logger.debug('[GeminiAdapter] Failed to scan tmp project roots:', error);
64
+ }
65
+ }
66
+ return path.basename(projectPath);
67
+ }
68
+ /**
69
+ * Resolve the Gemini chats directory for a given project path.
70
+ */
71
+ resolveChatsDir(projectPath) {
72
+ const geminiHome = this.getGeminiHome();
73
+ const projectKey = this.resolveProjectKey(projectPath);
74
+ return path.join(geminiHome, 'tmp', projectKey, 'chats');
75
+ }
76
+ /**
77
+ * Find the session file matching a given session UUID.
78
+ * Gemini files are named session-{date}-{uuid-prefix}.json where
79
+ * uuid-prefix is the first 8 chars of the session UUID.
80
+ */
81
+ findSessionFile(projectPath, agentSessionId) {
82
+ const chatsDir = this.resolveChatsDir(projectPath);
83
+ if (!fs.existsSync(chatsDir))
84
+ return null;
85
+ const uuidPrefix = agentSessionId.substring(0, 8);
86
+ try {
87
+ const files = fs.readdirSync(chatsDir);
88
+ const match = files.find(f => f.includes(uuidPrefix) && f.endsWith('.json'));
89
+ return match ? path.join(chatsDir, match) : null;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ readSessionData(projectPath, agentSessionId) {
96
+ const filePath = this.findSessionFile(projectPath, agentSessionId);
97
+ if (!filePath)
98
+ return null;
99
+ try {
100
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
101
+ }
102
+ catch (error) {
103
+ logger.warn(`[GeminiAdapter] Failed to read session file: ${filePath}`, error);
104
+ return null;
105
+ }
106
+ }
107
+ checkExists(projectPath, agentSessionId) {
108
+ return this.findSessionFile(projectPath, agentSessionId) !== null;
109
+ }
110
+ getFileInfo(projectPath, agentSessionId) {
111
+ const data = this.readSessionData(projectPath, agentSessionId);
112
+ if (!data)
113
+ return { turns: 0 };
114
+ const msgs = data.messages || [];
115
+ const userTurns = msgs.filter((m) => m.type === 'user').length;
116
+ // Extract title from first user message
117
+ let title;
118
+ const firstUser = msgs.find((m) => m.type === 'user');
119
+ if (firstUser) {
120
+ const text = Array.isArray(firstUser.content)
121
+ ? firstUser.content[0]?.text || ''
122
+ : String(firstUser.content || '');
123
+ title = text.substring(0, 50).trim() || undefined;
124
+ }
125
+ return { turns: userTurns, title };
126
+ }
127
+ readFirstMessage(projectPath, agentSessionId) {
128
+ return this.readUserMessage(projectPath, agentSessionId, 'first');
129
+ }
130
+ readLastUserMessage(projectPath, agentSessionId) {
131
+ return this.readUserMessage(projectPath, agentSessionId, 'last');
132
+ }
133
+ scanCliSessions(projectPath) {
134
+ const chatsDir = this.resolveChatsDir(projectPath);
135
+ if (!fs.existsSync(chatsDir))
136
+ return [];
137
+ try {
138
+ const files = fs.readdirSync(chatsDir)
139
+ .filter(f => f.startsWith('session-') && f.endsWith('.json'))
140
+ .map(f => {
141
+ const filePath = path.join(chatsDir, f);
142
+ const stat = fs.statSync(filePath);
143
+ // Extract UUID from filename: session-{date}-{uuid-prefix}.json
144
+ // Read the file to get full UUID
145
+ try {
146
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
147
+ return { uuid: data.sessionId, mtime: stat.mtimeMs };
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ })
153
+ .filter((e) => e !== null)
154
+ .sort((a, b) => b.mtime - a.mtime)
155
+ .slice(0, 10);
156
+ return files;
157
+ }
158
+ catch (error) {
159
+ logger.warn('[GeminiAdapter] scanCliSessions failed:', error);
160
+ return [];
161
+ }
162
+ }
163
+ readUserMessage(projectPath, agentSessionId, which) {
164
+ const data = this.readSessionData(projectPath, agentSessionId);
165
+ if (!data)
166
+ return null;
167
+ const msgs = (data.messages || []).filter((m) => m.type === 'user');
168
+ if (msgs.length === 0)
169
+ return null;
170
+ const msg = which === 'first' ? msgs[0] : msgs[msgs.length - 1];
171
+ const text = Array.isArray(msg.content)
172
+ ? msg.content[0]?.text || ''
173
+ : String(msg.content || '');
174
+ const trimmed = text.trim().replace(/\s+/g, ' ');
175
+ return trimmed.substring(0, 50) + (trimmed.length > 50 ? '...' : '');
176
+ }
177
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * SessionFileAdapter — 会话文件操作的 Agent 抽象层
3
+ *
4
+ * 不同 Agent 后端(Claude / Codex)使用不同的会话文件存储格式和路径。
5
+ * 此接口将文件操作抽象为统一 API,由各 Agent 适配器分别实现。
6
+ */
7
+ export {};
@@ -0,0 +1,45 @@
1
+ import fsPromises from 'fs/promises';
2
+ import { logger } from '../../utils/logger.js';
3
+ /**
4
+ * 检查会话文件健康度(接收完整文件路径)
5
+ */
6
+ export async function checkSessionFile(sessionFile) {
7
+ const issues = [];
8
+ try {
9
+ const stats = await fsPromises.stat(sessionFile);
10
+ const sizeMB = stats.size / (1024 * 1024);
11
+ if (stats.size > 50 * 1024 * 1024) {
12
+ issues.push(`会话文件过大: ${sizeMB.toFixed(1)}MB`);
13
+ }
14
+ const content = await fsPromises.readFile(sessionFile, 'utf-8');
15
+ const lines = content.split('\n').filter(l => l.trim());
16
+ for (let i = 0; i < lines.length; i++) {
17
+ try {
18
+ JSON.parse(lines[i]);
19
+ }
20
+ catch (e) {
21
+ issues.push(`会话文件格式损坏(第 ${i + 1} 行)`);
22
+ return { healthy: false, issues, corrupt: true, fileSize: stats.size };
23
+ }
24
+ }
25
+ return {
26
+ healthy: issues.length === 0,
27
+ issues,
28
+ fileSize: stats.size
29
+ };
30
+ }
31
+ catch (error) {
32
+ logger.error('[SessionFileHealth] Check failed:', error);
33
+ issues.push(`文件读取失败: ${error.message}`);
34
+ return { healthy: false, issues, corrupt: true };
35
+ }
36
+ }
37
+ /**
38
+ * 备份单个会话文件(在同目录下创建 .bak 副本)
39
+ */
40
+ export async function backupSessionFile(sessionFile) {
41
+ const backupPath = `${sessionFile}.bak-${Date.now()}`;
42
+ await fsPromises.copyFile(sessionFile, backupPath);
43
+ logger.info(`[SessionFileHealth] Backup created: ${backupPath}`);
44
+ return backupPath;
45
+ }