@yeaft/webchat-agent 0.1.123 → 0.1.125

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/conversation.js CHANGED
@@ -2,13 +2,6 @@ import ctx from './context.js';
2
2
  import { loadSessionHistory } from './history.js';
3
3
  import { startClaudeQuery } from './claude.js';
4
4
  import { crewSessions, loadCrewIndex } from './crew.js';
5
- import { rolePlaySessions, saveRolePlayIndex, removeRolePlaySession, loadRolePlayIndex, validateRolePlayConfig, initRolePlayRouteState, loadCrewContext, refreshCrewContext, initCrewContextMtimes } from './roleplay.js';
6
- import { loadRolePlaySessionIndex } from './roleplay-session.js';
7
- import { initRolePlayDir, writeSessionClaudeMd, generateSessionName, getDefaultRoles, getSessionDir } from './roleplay-dir.js';
8
- import { addRolePlaySession, findRolePlaySessionByConversationId, setActiveRolePlaySession } from './roleplay-session.js';
9
-
10
- // Restore persisted roleplay sessions on module load (agent startup)
11
- loadRolePlayIndex();
12
5
 
13
6
  // 不支持的斜杠命令(真正需要交互式 CLI 的命令)
14
7
  const UNSUPPORTED_SLASH_COMMANDS = ['/help', '/bug', '/login', '/logout', '/terminal-setup', '/vim', '/config'];
@@ -41,7 +34,7 @@ export function parseSlashCommand(message) {
41
34
  return { type: null, message };
42
35
  }
43
36
 
44
- // 发送 conversation 列表(含活跃 crew sessions + 索引中已停止的 crew sessions + roleplay sessions
37
+ // 发送 conversation 列表(含活跃 crew sessions + 索引中已停止的 crew sessions)
45
38
  export async function sendConversationList() {
46
39
  const list = [];
47
40
  for (const [id, state] of ctx.conversations) {
@@ -54,21 +47,6 @@ export async function sendConversationList() {
54
47
  userId: state.userId,
55
48
  username: state.username
56
49
  };
57
- // roleplay conversations are stored in ctx.conversations but also tracked in rolePlaySessions
58
- if (rolePlaySessions.has(id)) {
59
- entry.type = 'rolePlay';
60
- const rpSession = rolePlaySessions.get(id);
61
- entry.rolePlayRoles = rpSession.roles;
62
- // Include route state if initialized
63
- if (rpSession._routeInitialized) {
64
- entry.rolePlayState = {
65
- currentRole: rpSession.currentRole,
66
- round: rpSession.round,
67
- features: rpSession.features ? Array.from(rpSession.features.values()) : [],
68
- waitingHuman: rpSession.waitingHuman || false
69
- };
70
- }
71
- }
72
50
  list.push(entry);
73
51
  }
74
52
  // 追加活跃 crew sessions
@@ -131,92 +109,15 @@ export async function createConversation(msg) {
131
109
  const { conversationId, workDir, userId, username, disallowedTools } = msg;
132
110
  const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
133
111
 
134
- // Validate and sanitize rolePlayConfig if provided
135
- let rolePlayConfig = null;
136
- if (msg.rolePlayConfig) {
137
- const result = validateRolePlayConfig(msg.rolePlayConfig);
138
- if (!result.valid) {
139
- console.warn(`[createConversation] Invalid rolePlayConfig: ${result.error}`);
140
- sendError(conversationId, `Invalid rolePlayConfig: ${result.error}`);
141
- return;
142
- }
143
- rolePlayConfig = result.config;
144
- }
145
-
146
- // Load .crew context if rolePlay and projectDir contains .crew directory
147
- if (rolePlayConfig) {
148
- const crewContext = loadCrewContext(effectiveWorkDir);
149
- if (crewContext) {
150
- rolePlayConfig.crewContext = crewContext;
151
- console.log(` RolePlay: loaded .crew context (${crewContext.roles.length} roles, ${crewContext.features.length} features)`);
152
- }
153
- }
154
-
155
- // ★ RolePlay: initialize .roleplay/ directory, generate session, set cwd
156
- let rpSessionName = null;
157
- let rpSessionWorkDir = effectiveWorkDir; // default: project root
158
- if (rolePlayConfig) {
159
- try {
160
- const language = rolePlayConfig.language || 'zh-CN';
161
-
162
- // 1. Ensure .roleplay/ directory structure exists
163
- await initRolePlayDir(effectiveWorkDir, language);
164
-
165
- // 2. Generate unique session name
166
- rpSessionName = generateSessionName(
167
- effectiveWorkDir,
168
- rolePlayConfig.teamType,
169
- msg.rolePlayConfig?.sessionName || null
170
- );
171
-
172
- // 3. Write session CLAUDE.md (role list, ROUTE protocol, workflow)
173
- await writeSessionClaudeMd(effectiveWorkDir, rpSessionName, {
174
- teamType: rolePlayConfig.teamType,
175
- language,
176
- roles: rolePlayConfig.roles,
177
- });
178
-
179
- // 4. Set cwd to session directory so Claude Code auto-reads its CLAUDE.md
180
- rpSessionWorkDir = getSessionDir(effectiveWorkDir, rpSessionName);
181
-
182
- // 5. Build roles snapshot for session.json
183
- const rolesSnapshot = (rolePlayConfig.roles && rolePlayConfig.roles.length > 0)
184
- ? rolePlayConfig.roles.map(r => ({
185
- name: r.name,
186
- displayName: r.displayName || r.name,
187
- icon: r.icon || '',
188
- }))
189
- : getDefaultRoles(rolePlayConfig.teamType, language);
190
-
191
- // 6. Persist to .roleplay/session.json
192
- await addRolePlaySession(effectiveWorkDir, {
193
- name: rpSessionName,
194
- teamType: rolePlayConfig.teamType,
195
- language,
196
- projectDir: effectiveWorkDir,
197
- conversationId,
198
- roles: rolesSnapshot,
199
- createdAt: Date.now(),
200
- });
201
-
202
- console.log(` RolePlay: initialized .roleplay/${rpSessionName}, cwd=${rpSessionWorkDir}`);
203
- } catch (e) {
204
- console.error('[createConversation] Failed to init .roleplay/ dir:', e);
205
- // Non-fatal: fall back to project root as cwd
206
- rpSessionWorkDir = effectiveWorkDir;
207
- }
208
- }
209
-
210
- console.log(`Creating conversation: ${conversationId} in ${rpSessionWorkDir} (lazy start)`);
112
+ console.log(`Creating conversation: ${conversationId} in ${effectiveWorkDir} (lazy start)`);
211
113
  if (username) console.log(` User: ${username} (${userId})`);
212
- if (rolePlayConfig) console.log(` RolePlay: teamType=${rolePlayConfig.teamType}, roles=${rolePlayConfig.roles?.length}, session=${rpSessionName}`);
213
114
 
214
115
  // 只创建 conversation 状态,不启动 Claude 进程
215
116
  // Claude 进程会在用户发送第一条消息时启动 (见 handleUserInput)
216
117
  ctx.conversations.set(conversationId, {
217
118
  query: null,
218
119
  inputStream: null,
219
- workDir: rpSessionWorkDir,
120
+ workDir: effectiveWorkDir,
220
121
  claudeSessionId: null,
221
122
  createdAt: Date.now(),
222
123
  abortController: null,
@@ -226,10 +127,6 @@ export async function createConversation(msg) {
226
127
  userId,
227
128
  username,
228
129
  disallowedTools: disallowedTools || null, // null = 使用全局默认
229
- rolePlayConfig: rolePlayConfig || null,
230
- // Track the original project dir and session name for .roleplay/ operations
231
- _rpProjectDir: rolePlayConfig ? effectiveWorkDir : null,
232
- _rpSessionName: rpSessionName,
233
130
  usage: {
234
131
  inputTokens: 0,
235
132
  outputTokens: 0,
@@ -239,31 +136,13 @@ export async function createConversation(msg) {
239
136
  }
240
137
  });
241
138
 
242
- // Register in rolePlaySessions for type inference in sendConversationList
243
- if (rolePlayConfig) {
244
- const rpSession = {
245
- roles: rolePlayConfig.roles,
246
- teamType: rolePlayConfig.teamType,
247
- language: rolePlayConfig.language,
248
- projectDir: effectiveWorkDir,
249
- createdAt: Date.now(),
250
- userId,
251
- username,
252
- };
253
- rolePlaySessions.set(conversationId, rpSession);
254
- // Initialize route state eagerly so it's ready when Claude starts
255
- initRolePlayRouteState(rpSession, ctx.conversations.get(conversationId));
256
- saveRolePlayIndex();
257
- }
258
-
259
139
  ctx.sendToServer({
260
140
  type: 'conversation_created',
261
141
  conversationId,
262
- workDir: rpSessionWorkDir,
142
+ workDir: effectiveWorkDir,
263
143
  userId,
264
144
  username,
265
- disallowedTools: disallowedTools || null,
266
- rolePlayConfig: rolePlayConfig || null
145
+ disallowedTools: disallowedTools || null
267
146
  });
268
147
 
269
148
  // 立即发送 agent 级别的 MCP servers 列表(从 ~/.claude.json 读取的)
@@ -315,42 +194,10 @@ export async function resumeConversation(msg) {
315
194
 
316
195
  // 只创建 conversation 状态并保存 claudeSessionId,不启动 Claude 进程
317
196
  // Claude 进程会在用户发送第一条消息时启动 (见 handleUserInput)
318
- // Restore rolePlayConfig from persisted rolePlaySessions if available
319
- const rolePlayEntry = rolePlaySessions.get(conversationId);
320
- let rolePlayConfig = rolePlayEntry
321
- ? { roles: rolePlayEntry.roles, teamType: rolePlayEntry.teamType, language: rolePlayEntry.language }
322
- : null;
323
-
324
- // ★ RolePlay resume: look up session in .roleplay/session.json to restore cwd
325
- let rpResumeWorkDir = effectiveWorkDir;
326
- if (rolePlayConfig && rolePlayEntry) {
327
- const rpProjectDir = rolePlayEntry.projectDir || effectiveWorkDir;
328
- const rpDiskSession = findRolePlaySessionByConversationId(rpProjectDir, conversationId);
329
- if (rpDiskSession && rpDiskSession.name) {
330
- rpResumeWorkDir = getSessionDir(rpProjectDir, rpDiskSession.name);
331
- // Re-activate the session
332
- setActiveRolePlaySession(rpProjectDir, rpDiskSession.name).catch(e => {
333
- console.warn('[Resume] Failed to update activeSession:', e.message);
334
- });
335
- console.log(`[Resume] RolePlay: restored session cwd=${rpResumeWorkDir}`);
336
- }
337
- }
338
-
339
- // ★ RolePlay resume: refresh .crew context to get latest kanban/features
340
- if (rolePlayConfig && rolePlayEntry) {
341
- const crewContext = loadCrewContext(effectiveWorkDir);
342
- if (crewContext) {
343
- rolePlayConfig.crewContext = crewContext;
344
- // Initialize mtime snapshot (without re-loading) so subsequent refreshes can detect changes
345
- initCrewContextMtimes(effectiveWorkDir, rolePlayEntry);
346
- console.log(`[Resume] RolePlay: refreshed .crew context (${crewContext.features.length} features)`);
347
- }
348
- }
349
-
350
197
  ctx.conversations.set(conversationId, {
351
198
  query: null,
352
199
  inputStream: null,
353
- workDir: rpResumeWorkDir,
200
+ workDir: effectiveWorkDir,
354
201
  claudeSessionId: claudeSessionId, // 保存要恢复的 session ID
355
202
  createdAt: Date.now(),
356
203
  abortController: null,
@@ -360,7 +207,6 @@ export async function resumeConversation(msg) {
360
207
  userId,
361
208
  username,
362
209
  disallowedTools: disallowedTools || null, // null = 使用全局默认
363
- rolePlayConfig,
364
210
  usage: {
365
211
  inputTokens: 0,
366
212
  outputTokens: 0,
@@ -426,11 +272,6 @@ export function deleteConversation(msg) {
426
272
  ctx.conversations.delete(conversationId);
427
273
  }
428
274
 
429
- // Clean up roleplay session if applicable
430
- if (rolePlaySessions.has(conversationId)) {
431
- removeRolePlaySession(conversationId);
432
- }
433
-
434
275
  ctx.sendToServer({
435
276
  type: 'conversation_deleted',
436
277
  conversationId
@@ -570,18 +411,6 @@ export async function handleUserInput(msg) {
570
411
  const resumeSessionId = claudeSessionId || state?.claudeSessionId || null;
571
412
  const effectiveWorkDir = workDir || state?.workDir || ctx.CONFIG.workDir;
572
413
 
573
- // ★ RolePlay: refresh .crew context before starting a new query
574
- // so the appendSystemPrompt has the latest kanban/features
575
- if (state?.rolePlayConfig) {
576
- const rpSession = rolePlaySessions.get(conversationId);
577
- if (rpSession) {
578
- const refreshed = refreshCrewContext(effectiveWorkDir, rpSession, state);
579
- if (refreshed) {
580
- console.log(`[SDK] RolePlay: .crew context refreshed before query start`);
581
- }
582
- }
583
- }
584
-
585
414
  console.log(`[SDK] Starting Claude for ${conversationId}, resume: ${resumeSessionId || 'none'}`);
586
415
  state = await startClaudeQuery(conversationId, effectiveWorkDir, resumeSessionId);
587
416
  }
@@ -590,77 +419,11 @@ export async function handleUserInput(msg) {
590
419
  // Claude stream-json 模式支持在回复过程中接收新消息(写入 stdin)
591
420
  let effectivePrompt = prompt;
592
421
 
593
- // ★ RolePlay: if session was waiting for human input, clear the flag and
594
- // wrap the user message with context about which role was asking
595
- const rpSession = rolePlaySessions.get(conversationId);
596
- if (rpSession && rpSession.waitingHuman && rpSession.waitingHumanContext) {
597
- const { fromRole, message: requestMessage } = rpSession.waitingHumanContext;
598
- const fromRoleConfig = rpSession.roles.find?.(r => r.name === fromRole) ||
599
- (Array.isArray(rpSession.roles) ? rpSession.roles.find(r => r.name === fromRole) : null);
600
- const fromLabel = fromRoleConfig
601
- ? (fromRoleConfig.icon ? `${fromRoleConfig.icon} ${fromRoleConfig.displayName}` : fromRoleConfig.displayName)
602
- : fromRole;
603
-
604
- effectivePrompt = `人工回复(回应 ${fromLabel} 的请求: "${requestMessage}"):\n\n${prompt}`;
605
-
606
- rpSession.waitingHuman = false;
607
- rpSession.waitingHumanContext = null;
608
- console.log(`[RolePlay] Human responded, resuming from ${fromRole}'s request`);
609
- }
610
-
611
- // ★ Save displayPrompt before rolePrefix injection (preserves waitingHuman prefix but excludes ROLE signal)
422
+ // ★ Save displayPrompt before any modification (preserves original user input)
612
423
  const displayPrompt = effectivePrompt;
613
424
 
614
- // ★ RolePlay: handle @mention targetRole routing
615
- const targetRole = msg.targetRole;
616
- if (targetRole && rpSession && rpSession._routeInitialized) {
617
- const roleNames = new Set(rpSession.roles.map(r => r.name));
618
- if (roleNames.has(targetRole)) {
619
- const targetRoleConfig = rpSession.roles.find(r => r.name === targetRole);
620
- const targetLabel = targetRoleConfig
621
- ? (targetRoleConfig.icon ? `${targetRoleConfig.icon} ${targetRoleConfig.displayName}` : targetRoleConfig.displayName)
622
- : targetRole;
623
- const targetClaudeMd = targetRoleConfig?.claudeMd || '';
624
-
625
- // Prepend ROLE signal so Claude responds as the target role
626
- let rolePrefix = `---ROLE: ${targetRole}---\n\n`;
627
- rolePrefix += `用户指定由 ${targetLabel} 来回复。\n`;
628
- if (targetClaudeMd) {
629
- rolePrefix += `<role-context>\n${targetClaudeMd}\n</role-context>\n\n`;
630
- }
631
- rolePrefix += `请以 ${targetLabel} 的身份回复以下消息:\n\n`;
632
- effectivePrompt = rolePrefix + effectivePrompt;
633
-
634
- // Update rpSession state
635
- const prevRole = rpSession.currentRole;
636
- rpSession.currentRole = targetRole;
637
- if (rpSession.roleStates[targetRole]) {
638
- rpSession.roleStates[targetRole].status = 'active';
639
- }
640
- if (prevRole && prevRole !== targetRole && rpSession.roleStates[prevRole]) {
641
- rpSession.roleStates[prevRole].status = 'idle';
642
- }
643
-
644
- // Send roleplay_status update to frontend
645
- ctx.sendToServer({
646
- type: 'roleplay_status',
647
- conversationId,
648
- currentRole: rpSession.currentRole,
649
- round: rpSession.round,
650
- features: rpSession.features ? Array.from(rpSession.features.values()) : [],
651
- roleStates: rpSession.roleStates || {},
652
- waitingHuman: false
653
- });
654
-
655
- console.log(`[RolePlay] @mention routing: ${prevRole || 'none'} -> ${targetRole}`);
656
- } else {
657
- console.warn(`[RolePlay] @mention target role not found: ${targetRole}`);
658
- }
659
- }
660
-
661
425
  // ★ Separate display message (shown to user) from Claude message (sent to model)
662
- // displayPrompt: user's original text + waitingHuman prefix (no ROLE signal)
663
- // effectivePrompt: may include ROLE signal prefix for @mention routing
426
+ // displayPrompt: user's original text (no modifications)
664
427
  const userMessage = {
665
428
  type: 'user',
666
429
  message: { role: 'user', content: effectivePrompt }
@@ -786,75 +549,3 @@ export function handleAskUserAnswer(msg) {
786
549
  console.log(`[AskUser] No pending question for requestId: ${msg.requestId}`);
787
550
  }
788
551
  }
789
-
790
- /**
791
- * Handle check_crew_context request — check if a directory has .crew context
792
- * and return role/context info for RolePlay auto-import.
793
- */
794
- export function handleCheckCrewContext(msg) {
795
- const { projectDir, requestId } = msg;
796
- if (!projectDir) {
797
- ctx.sendToServer({ type: 'crew_context_result', requestId, found: false });
798
- return;
799
- }
800
- const crewContext = loadCrewContext(projectDir);
801
- if (!crewContext) {
802
- ctx.sendToServer({ type: 'crew_context_result', requestId, found: false });
803
- return;
804
- }
805
- // Return a safe subset for the frontend (no full claudeMd content, just metadata)
806
- ctx.sendToServer({
807
- type: 'crew_context_result',
808
- requestId,
809
- found: true,
810
- roles: crewContext.roles.map(r => ({
811
- name: r.name,
812
- displayName: r.displayName,
813
- icon: r.icon,
814
- description: r.description,
815
- roleType: r.roleType,
816
- isDecisionMaker: r.isDecisionMaker,
817
- hasClaudeMd: !!(r.claudeMd && r.claudeMd.length > 0),
818
- })),
819
- teamType: crewContext.teamType,
820
- language: crewContext.language,
821
- featureCount: crewContext.features.length,
822
- });
823
- }
824
-
825
- /**
826
- * Handle check_roleplay_sessions request — check if a directory has .roleplay/session.json
827
- * and return the list of existing sessions for restore.
828
- */
829
- export function handleCheckRolePlaySessions(msg) {
830
- const { projectDir, requestId } = msg;
831
- if (!projectDir) {
832
- ctx.sendToServer({ type: 'roleplay_sessions_result', requestId, found: false, sessions: [] });
833
- return;
834
- }
835
- const index = loadRolePlaySessionIndex(projectDir);
836
- const activeSessions = index.sessions.filter(s => s.status === 'active');
837
- if (activeSessions.length === 0) {
838
- ctx.sendToServer({ type: 'roleplay_sessions_result', requestId, found: false, sessions: [] });
839
- return;
840
- }
841
- ctx.sendToServer({
842
- type: 'roleplay_sessions_result',
843
- requestId,
844
- found: true,
845
- sessions: activeSessions.map(s => ({
846
- name: s.name,
847
- teamType: s.teamType,
848
- language: s.language,
849
- conversationId: s.conversationId,
850
- roles: (s.roles || []).map(r => ({
851
- name: r.name,
852
- displayName: r.displayName,
853
- icon: r.icon || '',
854
- description: r.description || '',
855
- })),
856
- createdAt: s.createdAt,
857
- updatedAt: s.updatedAt,
858
- })),
859
- });
860
- }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Crew Context Loader — detect and parse .crew/ directory structure.
3
+ *
4
+ * Reads .crew/CLAUDE.md, session.json, per-role CLAUDE.md files,
5
+ * kanban, and feature files. Used by the frontend to detect whether
6
+ * a project has a Crew setup and display role metadata.
7
+ */
8
+
9
+ import { join } from 'path';
10
+ import { readFileSync, existsSync, readdirSync } from 'fs';
11
+ import ctx from '../context.js';
12
+
13
+ const MAX_CLAUDE_MD_LEN = 8192;
14
+
15
+ /**
16
+ * Read a file, returning null on any error.
17
+ * @param {string} filePath
18
+ * @returns {string|null}
19
+ */
20
+ function readFileOrNull(filePath) {
21
+ try {
22
+ if (!existsSync(filePath)) return null;
23
+ return readFileSync(filePath, 'utf-8');
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Deduplicate Crew roles by roleType.
31
+ * Crew may have dev-1, dev-2, dev-3 — collapse to a single "dev" entry.
32
+ * Attaches per-role CLAUDE.md content.
33
+ */
34
+ function deduplicateRoles(sessionRoles, roleClaudes) {
35
+ const byType = new Map();
36
+ const merged = [];
37
+
38
+ for (const r of sessionRoles) {
39
+ const type = r.roleType || r.name;
40
+ const claudeMd = roleClaudes[r.name] || '';
41
+
42
+ if (byType.has(type)) continue;
43
+ byType.set(type, true);
44
+
45
+ const name = type;
46
+ let displayName = r.displayName || name;
47
+ displayName = displayName.replace(/-\d+$/, '');
48
+
49
+ merged.push({
50
+ name,
51
+ displayName,
52
+ icon: r.icon || '',
53
+ description: r.description || '',
54
+ claudeMd: claudeMd.substring(0, MAX_CLAUDE_MD_LEN),
55
+ roleType: type,
56
+ isDecisionMaker: !!r.isDecisionMaker,
57
+ });
58
+ }
59
+
60
+ return merged;
61
+ }
62
+
63
+ /**
64
+ * Load .crew context from a project directory.
65
+ * Returns null if .crew/ doesn't exist.
66
+ */
67
+ export function loadCrewContext(projectDir) {
68
+ const crewDir = join(projectDir, '.crew');
69
+ if (!existsSync(crewDir)) return null;
70
+
71
+ // 1. Shared CLAUDE.md
72
+ const sharedClaudeMd = readFileOrNull(join(crewDir, 'CLAUDE.md')) || '';
73
+
74
+ // 2. session.json → roles, teamType, language, features
75
+ let sessionRoles = [];
76
+ let teamType = 'dev';
77
+ let language = 'zh-CN';
78
+ let sessionFeatures = [];
79
+ const sessionPath = join(crewDir, 'session.json');
80
+ const sessionJson = readFileOrNull(sessionPath);
81
+ if (sessionJson) {
82
+ try {
83
+ const session = JSON.parse(sessionJson);
84
+ if (Array.isArray(session.roles)) sessionRoles = session.roles;
85
+ if (session.teamType) teamType = session.teamType;
86
+ if (session.language) language = session.language;
87
+ if (Array.isArray(session.features)) sessionFeatures = session.features;
88
+ } catch {
89
+ // Invalid JSON — ignore
90
+ }
91
+ }
92
+
93
+ // 3. Per-role CLAUDE.md from .crew/roles/*/CLAUDE.md
94
+ const roleClaudes = {};
95
+ const rolesDir = join(crewDir, 'roles');
96
+ if (existsSync(rolesDir)) {
97
+ try {
98
+ const roleDirs = readdirSync(rolesDir, { withFileTypes: true })
99
+ .filter(d => d.isDirectory())
100
+ .map(d => d.name);
101
+ for (const dirName of roleDirs) {
102
+ const md = readFileOrNull(join(rolesDir, dirName, 'CLAUDE.md'));
103
+ if (md) roleClaudes[dirName] = md;
104
+ }
105
+ } catch {
106
+ // Permission error or similar — ignore
107
+ }
108
+ }
109
+
110
+ // 4. Merge roles: deduplicate by roleType, attach claudeMd
111
+ const roles = deduplicateRoles(sessionRoles, roleClaudes);
112
+
113
+ // 5. Kanban
114
+ const kanban = readFileOrNull(join(crewDir, 'context', 'kanban.md')) || '';
115
+
116
+ // 6. Feature files from context/features/*.md
117
+ const features = [];
118
+ const featuresDir = join(crewDir, 'context', 'features');
119
+ if (existsSync(featuresDir)) {
120
+ try {
121
+ const files = readdirSync(featuresDir)
122
+ .filter(f => f.endsWith('.md') && f !== 'index.md')
123
+ .sort();
124
+ for (const f of files) {
125
+ const content = readFileOrNull(join(featuresDir, f));
126
+ if (content) {
127
+ features.push({ name: f.replace('.md', ''), content });
128
+ }
129
+ }
130
+ } catch {
131
+ // ignore
132
+ }
133
+ }
134
+
135
+ return { sharedClaudeMd, roles, kanban, features, teamType, language, sessionFeatures };
136
+ }
137
+
138
+ /**
139
+ * Handle check_crew_context message — check if a project has .crew/ setup
140
+ * and return role metadata for the frontend.
141
+ */
142
+ export function handleCheckCrewContext(msg) {
143
+ const { projectDir, requestId } = msg;
144
+ if (!projectDir) {
145
+ ctx.sendToServer({ type: 'crew_context_result', requestId, found: false });
146
+ return;
147
+ }
148
+ const crewContext = loadCrewContext(projectDir);
149
+ if (!crewContext) {
150
+ ctx.sendToServer({ type: 'crew_context_result', requestId, found: false });
151
+ return;
152
+ }
153
+ // Return a safe subset for the frontend (no full claudeMd content, just metadata)
154
+ ctx.sendToServer({
155
+ type: 'crew_context_result',
156
+ requestId,
157
+ found: true,
158
+ roles: crewContext.roles.map(r => ({
159
+ name: r.name,
160
+ displayName: r.displayName,
161
+ icon: r.icon,
162
+ description: r.description,
163
+ roleType: r.roleType,
164
+ isDecisionMaker: r.isDecisionMaker,
165
+ hasClaudeMd: !!(r.claudeMd && r.claudeMd.length > 0),
166
+ })),
167
+ teamType: crewContext.teamType,
168
+ language: crewContext.language,
169
+ featureCount: crewContext.features.length,
170
+ });
171
+ }
package/crew.js CHANGED
@@ -44,3 +44,6 @@ export {
44
44
  addRoleToSession,
45
45
  removeRoleFromSession
46
46
  } from './crew/role-management.js';
47
+
48
+ // Crew 上下文检测
49
+ export { handleCheckCrewContext } from './crew/context-loader.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.123",
3
+ "version": "0.1.125",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",