@yeaft/webchat-agent 1.0.78 → 1.0.79
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/connection/buffer.js +6 -2
- package/connection/message-router.js +64 -1
- package/conversation.js +32 -4
- package/crew/builtin-actions.js +154 -0
- package/crew/context-loader.js +171 -0
- package/crew/control.js +444 -0
- package/crew/human-interaction.js +195 -0
- package/crew/persistence.js +295 -0
- package/crew/role-management.js +182 -0
- package/crew/role-output.js +461 -0
- package/crew/role-query.js +406 -0
- package/crew/role-states.js +180 -0
- package/crew/routing-fallback.js +64 -0
- package/crew/routing-metrics.js +215 -0
- package/crew/routing.js +951 -0
- package/crew/session.js +648 -0
- package/crew/shared-dir.js +266 -0
- package/crew/task-files.js +554 -0
- package/crew/ui-messages.js +274 -0
- package/crew/worktree.js +130 -0
- package/crew-i18n.js +579 -0
- package/crew.js +48 -0
- package/history.js +2 -2
- package/index.js +6 -0
- package/package.json +1 -1
- package/providers/claude-code.js +1 -1
- package/yeaft/attachments.js +2 -2
- package/yeaft/config.js +1 -1
- package/yeaft/conversation/persist.js +112 -458
- package/yeaft/conversation/search.js +15 -51
- package/yeaft/session.js +18 -27
- package/yeaft/web-bridge.js +15 -27
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew — 共享目录和 CLAUDE.md 管理
|
|
3
|
+
* initSharedDir, initRoleDir, writeSharedClaudeMd, writeRoleClaudeMd, updateSharedClaudeMd
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { getMessages, getAllMemoryTitles } from '../crew-i18n.js';
|
|
8
|
+
|
|
9
|
+
/** Format role label: "icon displayName" or just "displayName" if no icon */
|
|
10
|
+
function roleLabel(r) {
|
|
11
|
+
return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MEMORY_BACKUP_FILE = '.memory-backup.json';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract user-written content after a memory section title.
|
|
18
|
+
* Searches for any known locale's title (e.g. "# 共享记忆" or "# Shared Memory"),
|
|
19
|
+
* returns the trimmed content after the title line until EOF or next top-level heading.
|
|
20
|
+
* Returns null if section not found or content is only the default placeholder.
|
|
21
|
+
*/
|
|
22
|
+
function extractMemorySection(fileContent, titles, defaults) {
|
|
23
|
+
for (const title of titles) {
|
|
24
|
+
const idx = fileContent.indexOf(title);
|
|
25
|
+
if (idx === -1) continue;
|
|
26
|
+
// Content starts after the title line
|
|
27
|
+
const afterTitle = fileContent.slice(idx + title.length);
|
|
28
|
+
// Find next top-level heading (# at start of line) — that's where memory ends
|
|
29
|
+
const nextHeading = afterTitle.search(/\n#\s/);
|
|
30
|
+
const raw = nextHeading === -1 ? afterTitle : afterTitle.slice(0, nextHeading);
|
|
31
|
+
const trimmed = raw.trim();
|
|
32
|
+
// Skip if empty or is just the default placeholder
|
|
33
|
+
if (!trimmed) return null;
|
|
34
|
+
for (const d of defaults) {
|
|
35
|
+
if (trimmed === d.trim()) return null;
|
|
36
|
+
}
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Backup memory content from .crew/CLAUDE.md and .crew/roles/<role>/CLAUDE.md
|
|
44
|
+
* before deletion. Writes .crew/.memory-backup.json.
|
|
45
|
+
*/
|
|
46
|
+
export async function backupMemoryContent(crewDir) {
|
|
47
|
+
const { sharedTitles, sharedDefaults, personalTitles, personalDefaults } = getAllMemoryTitles();
|
|
48
|
+
const backup = { shared: null, roles: {} };
|
|
49
|
+
|
|
50
|
+
// Extract shared memory from .crew/CLAUDE.md
|
|
51
|
+
try {
|
|
52
|
+
const sharedContent = await fs.readFile(join(crewDir, 'CLAUDE.md'), 'utf-8');
|
|
53
|
+
backup.shared = extractMemorySection(sharedContent, sharedTitles, sharedDefaults);
|
|
54
|
+
} catch { /* CLAUDE.md doesn't exist — skip */ }
|
|
55
|
+
|
|
56
|
+
// Extract personal memory from each role's CLAUDE.md
|
|
57
|
+
try {
|
|
58
|
+
const rolesDir = join(crewDir, 'roles');
|
|
59
|
+
const roleDirs = await fs.readdir(rolesDir);
|
|
60
|
+
for (const roleName of roleDirs) {
|
|
61
|
+
try {
|
|
62
|
+
const roleClaudeMd = await fs.readFile(join(rolesDir, roleName, 'CLAUDE.md'), 'utf-8');
|
|
63
|
+
const memory = extractMemorySection(roleClaudeMd, personalTitles, personalDefaults);
|
|
64
|
+
if (memory) {
|
|
65
|
+
backup.roles[roleName] = memory;
|
|
66
|
+
}
|
|
67
|
+
} catch { /* Role dir or file missing — skip */ }
|
|
68
|
+
}
|
|
69
|
+
} catch { /* roles/ doesn't exist — skip */ }
|
|
70
|
+
|
|
71
|
+
// Only write backup if there's something to preserve
|
|
72
|
+
if (backup.shared || Object.keys(backup.roles).length > 0) {
|
|
73
|
+
await fs.writeFile(join(crewDir, MEMORY_BACKUP_FILE), JSON.stringify(backup, null, 2));
|
|
74
|
+
console.log(`[Crew] Memory backup saved: shared=${!!backup.shared}, roles=${Object.keys(backup.roles).join(',') || 'none'}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load memory backup from .crew/.memory-backup.json, returns null if not found.
|
|
80
|
+
*/
|
|
81
|
+
async function loadMemoryBackup(sharedDir) {
|
|
82
|
+
try {
|
|
83
|
+
const data = await fs.readFile(join(sharedDir, MEMORY_BACKUP_FILE), 'utf-8');
|
|
84
|
+
return JSON.parse(data);
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Delete memory backup file after successful restore.
|
|
92
|
+
*/
|
|
93
|
+
async function cleanupMemoryBackup(sharedDir) {
|
|
94
|
+
try {
|
|
95
|
+
await fs.rm(join(sharedDir, MEMORY_BACKUP_FILE), { force: true });
|
|
96
|
+
} catch { /* ignore */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 初始化共享目录
|
|
101
|
+
*/
|
|
102
|
+
export async function initSharedDir(sharedDir, roles, projectDir, language = 'zh-CN') {
|
|
103
|
+
await fs.mkdir(sharedDir, { recursive: true });
|
|
104
|
+
await fs.mkdir(join(sharedDir, 'context'), { recursive: true });
|
|
105
|
+
await fs.mkdir(join(sharedDir, 'sessions'), { recursive: true });
|
|
106
|
+
await fs.mkdir(join(sharedDir, 'roles'), { recursive: true });
|
|
107
|
+
|
|
108
|
+
// 初始化每个角色的目录
|
|
109
|
+
for (const role of roles) {
|
|
110
|
+
await initRoleDir(sharedDir, role, language, roles);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 生成 .crew/CLAUDE.md(共享级)
|
|
114
|
+
await writeSharedClaudeMd(sharedDir, roles, projectDir, language);
|
|
115
|
+
|
|
116
|
+
// 清理记忆备份文件(已在 write 阶段恢复)
|
|
117
|
+
await cleanupMemoryBackup(sharedDir);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 初始化角色目录: .crew/roles/{roleName}/CLAUDE.md
|
|
122
|
+
*/
|
|
123
|
+
export async function initRoleDir(sharedDir, role, language = 'zh-CN', allRoles = []) {
|
|
124
|
+
const roleDir = join(sharedDir, 'roles', role.name);
|
|
125
|
+
await fs.mkdir(roleDir, { recursive: true });
|
|
126
|
+
|
|
127
|
+
// 角色 CLAUDE.md(仅首次创建,后续角色自己维护记忆内容)
|
|
128
|
+
const claudeMdPath = join(roleDir, 'CLAUDE.md');
|
|
129
|
+
try {
|
|
130
|
+
await fs.access(claudeMdPath);
|
|
131
|
+
// 已存在,不覆盖(保留角色自己写入的记忆)
|
|
132
|
+
} catch {
|
|
133
|
+
await writeRoleClaudeMd(sharedDir, role, language, allRoles);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 写入 .crew/CLAUDE.md — 共享级(所有角色自动继承)
|
|
139
|
+
*/
|
|
140
|
+
export async function writeSharedClaudeMd(sharedDir, roles, projectDir, language = 'zh-CN') {
|
|
141
|
+
const m = getMessages(language);
|
|
142
|
+
|
|
143
|
+
// Check for memory backup to restore
|
|
144
|
+
const backup = await loadMemoryBackup(sharedDir);
|
|
145
|
+
const sharedMemoryContent = (backup && backup.shared) ? backup.shared : m.sharedMemoryDefault;
|
|
146
|
+
|
|
147
|
+
const claudeMd = `${m.projectGoal}
|
|
148
|
+
|
|
149
|
+
${m.projectCodePath}
|
|
150
|
+
${projectDir}
|
|
151
|
+
${m.useAbsolutePath}
|
|
152
|
+
|
|
153
|
+
${m.teamMembersTitle}
|
|
154
|
+
${roles.length > 0 ? roles.map(r => `- ${roleLabel(r)}(${r.name}): ${r.description}${r.isDecisionMaker ? ` (${m.decisionMakerTag})` : ''}`).join('\n') : m.noMembers}
|
|
155
|
+
|
|
156
|
+
${m.workConventions}
|
|
157
|
+
${m.workConventionsContent}
|
|
158
|
+
|
|
159
|
+
${m.mergeRules}
|
|
160
|
+
${m.mergeRulesContent}
|
|
161
|
+
|
|
162
|
+
${m.taskSplitRules}
|
|
163
|
+
${m.taskSplitRulesContent}
|
|
164
|
+
|
|
165
|
+
${m.stuckRules}
|
|
166
|
+
${m.stuckRulesContent}
|
|
167
|
+
|
|
168
|
+
${m.worktreeRules}
|
|
169
|
+
${m.worktreeRulesContent}
|
|
170
|
+
|
|
171
|
+
${m.featureRecordShared}
|
|
172
|
+
|
|
173
|
+
${m.sharedMemoryTitle}
|
|
174
|
+
${sharedMemoryContent}
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
await fs.writeFile(join(sharedDir, 'CLAUDE.md'), claudeMd);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Replace generic role names in ROUTE examples with actual instance names.
|
|
182
|
+
*
|
|
183
|
+
* Given a role with groupIndex=2 and the full role list containing
|
|
184
|
+
* dev-1, dev-2, rev-1, rev-2, test-1, test-2, the function rewrites:
|
|
185
|
+
* "to: reviewer" → "to: rev-2"
|
|
186
|
+
* "to: developer" → "to: dev-2"
|
|
187
|
+
* "to: tester" → "to: test-2"
|
|
188
|
+
*
|
|
189
|
+
* For roles without a groupIndex (pm, designer, etc.), or when no matching
|
|
190
|
+
* instance exists, the generic name is left untouched.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} text - claudeMd content with generic ROUTE targets
|
|
193
|
+
* @param {object} role - the role being written (must have roleType, groupIndex)
|
|
194
|
+
* @param {Array} allRoles - full expanded role list
|
|
195
|
+
* @returns {string} text with generic names replaced by instance names
|
|
196
|
+
*/
|
|
197
|
+
export function resolveRouteTargets(text, role, allRoles) {
|
|
198
|
+
if (!allRoles || allRoles.length === 0 || !role.groupIndex) return text;
|
|
199
|
+
|
|
200
|
+
// Build a lookup: generic roleType → instance name at this groupIndex
|
|
201
|
+
// e.g. { developer: 'dev-2', reviewer: 'rev-2', tester: 'test-2' }
|
|
202
|
+
const instanceMap = {};
|
|
203
|
+
for (const r of allRoles) {
|
|
204
|
+
if (r.groupIndex === role.groupIndex && r.roleType && r.name !== r.roleType) {
|
|
205
|
+
instanceMap[r.roleType] = r.name;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (Object.keys(instanceMap).length === 0) return text;
|
|
210
|
+
|
|
211
|
+
// Replace "to: <genericName>" inside ROUTE blocks
|
|
212
|
+
// Use a careful regex that only touches the `to:` field value
|
|
213
|
+
return text.replace(/(to:\s*)(developer|reviewer|tester)\b/gi, (match, prefix, genericName) => {
|
|
214
|
+
const resolved = instanceMap[genericName.toLowerCase()];
|
|
215
|
+
return resolved ? `${prefix}${resolved}` : match;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 写入 .crew/roles/{roleName}/CLAUDE.md — 角色级
|
|
221
|
+
* @param {string} sharedDir
|
|
222
|
+
* @param {object} role
|
|
223
|
+
* @param {string} language
|
|
224
|
+
* @param {Array} [allRoles] - full expanded role list for ROUTE target resolution
|
|
225
|
+
*/
|
|
226
|
+
export async function writeRoleClaudeMd(sharedDir, role, language = 'zh-CN', allRoles = []) {
|
|
227
|
+
const roleDir = join(sharedDir, 'roles', role.name);
|
|
228
|
+
const m = getMessages(language);
|
|
229
|
+
|
|
230
|
+
// Check for memory backup to restore
|
|
231
|
+
const backup = await loadMemoryBackup(sharedDir);
|
|
232
|
+
const personalMemoryContent = (backup && backup.roles && backup.roles[role.name])
|
|
233
|
+
? backup.roles[role.name]
|
|
234
|
+
: m.personalMemoryDefault;
|
|
235
|
+
|
|
236
|
+
// Resolve generic ROUTE targets to actual instance names
|
|
237
|
+
const resolvedClaudeMd = resolveRouteTargets(role.claudeMd || role.description, role, allRoles);
|
|
238
|
+
|
|
239
|
+
let claudeMd = `${m.roleTitle(roleLabel(role))}
|
|
240
|
+
${resolvedClaudeMd}
|
|
241
|
+
`;
|
|
242
|
+
|
|
243
|
+
// 有独立 worktree 的角色,覆盖代码工作目录
|
|
244
|
+
if (role.workDir) {
|
|
245
|
+
claudeMd += `
|
|
246
|
+
${m.codeWorkDir}
|
|
247
|
+
${role.workDir}
|
|
248
|
+
${m.codeWorkDirNote}
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
claudeMd += `
|
|
253
|
+
${m.personalMemory}
|
|
254
|
+
${personalMemoryContent}
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
await fs.writeFile(join(roleDir, 'CLAUDE.md'), claudeMd);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 角色变动时更新 .crew/CLAUDE.md
|
|
262
|
+
*/
|
|
263
|
+
export async function updateSharedClaudeMd(session) {
|
|
264
|
+
const roles = Array.from(session.roles.values());
|
|
265
|
+
await writeSharedClaudeMd(session.sharedDir, roles, session.projectDir, session.language || 'zh-CN');
|
|
266
|
+
}
|