@yeaft/webchat-agent 0.1.63 → 0.1.65

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.
@@ -12,7 +12,7 @@ import {
12
12
  createConversation, resumeConversation, deleteConversation,
13
13
  handleRefreshConversation, handleCancelExecution,
14
14
  handleUserInput, handleUpdateConversationSettings, handleAskUserAnswer,
15
- sendConversationList
15
+ sendConversationList, handleCheckCrewContext
16
16
  } from '../conversation.js';
17
17
  import {
18
18
  createCrewSession, handleCrewHumanInput, handleCrewControl,
@@ -66,6 +66,10 @@ export async function handleMessage(msg) {
66
66
  await createConversation(msg);
67
67
  break;
68
68
 
69
+ case 'check_crew_context':
70
+ handleCheckCrewContext(msg);
71
+ break;
72
+
69
73
  case 'resume_conversation':
70
74
  await resumeConversation(msg);
71
75
  break;
package/conversation.js CHANGED
@@ -2,7 +2,7 @@ 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 } from './roleplay.js';
5
+ import { rolePlaySessions, saveRolePlayIndex, removeRolePlaySession, loadRolePlayIndex, validateRolePlayConfig, initRolePlayRouteState, loadCrewContext } from './roleplay.js';
6
6
 
7
7
  // Restore persisted roleplay sessions on module load (agent startup)
8
8
  loadRolePlayIndex();
@@ -140,6 +140,15 @@ export async function createConversation(msg) {
140
140
  rolePlayConfig = result.config;
141
141
  }
142
142
 
143
+ // Load .crew context if rolePlay and projectDir contains .crew directory
144
+ if (rolePlayConfig) {
145
+ const crewContext = loadCrewContext(effectiveWorkDir);
146
+ if (crewContext) {
147
+ rolePlayConfig.crewContext = crewContext;
148
+ console.log(` RolePlay: loaded .crew context (${crewContext.roles.length} roles, ${crewContext.features.length} features)`);
149
+ }
150
+ }
151
+
143
152
  console.log(`Creating conversation: ${conversationId} in ${effectiveWorkDir} (lazy start)`);
144
153
  if (username) console.log(` User: ${username} (${userId})`);
145
154
  if (rolePlayConfig) console.log(` RolePlay: teamType=${rolePlayConfig.teamType}, roles=${rolePlayConfig.roles?.length}`);
@@ -589,3 +598,38 @@ export function handleAskUserAnswer(msg) {
589
598
  console.log(`[AskUser] No pending question for requestId: ${msg.requestId}`);
590
599
  }
591
600
  }
601
+
602
+ /**
603
+ * Handle check_crew_context request — check if a directory has .crew context
604
+ * and return role/context info for RolePlay auto-import.
605
+ */
606
+ export function handleCheckCrewContext(msg) {
607
+ const { projectDir, requestId } = msg;
608
+ if (!projectDir) {
609
+ ctx.sendToServer({ type: 'crew_context_result', requestId, found: false });
610
+ return;
611
+ }
612
+ const crewContext = loadCrewContext(projectDir);
613
+ if (!crewContext) {
614
+ ctx.sendToServer({ type: 'crew_context_result', requestId, found: false });
615
+ return;
616
+ }
617
+ // Return a safe subset for the frontend (no full claudeMd content, just metadata)
618
+ ctx.sendToServer({
619
+ type: 'crew_context_result',
620
+ requestId,
621
+ found: true,
622
+ roles: crewContext.roles.map(r => ({
623
+ name: r.name,
624
+ displayName: r.displayName,
625
+ icon: r.icon,
626
+ description: r.description,
627
+ roleType: r.roleType,
628
+ isDecisionMaker: r.isDecisionMaker,
629
+ hasClaudeMd: !!(r.claudeMd && r.claudeMd.length > 0),
630
+ })),
631
+ teamType: crewContext.teamType,
632
+ language: crewContext.language,
633
+ featureCount: crewContext.features.length,
634
+ });
635
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/roleplay.js CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { join } from 'path';
11
11
  import { homedir } from 'os';
12
- import { readFileSync, writeFileSync, existsSync, renameSync } from 'fs';
12
+ import { readFileSync, writeFileSync, existsSync, renameSync, readdirSync } from 'fs';
13
13
  import { parseRoutes } from './crew/routing.js';
14
14
 
15
15
  const ROLEPLAY_INDEX_PATH = join(homedir(), '.claude', 'roleplay-sessions.json');
@@ -70,6 +70,147 @@ export function removeRolePlaySession(conversationId) {
70
70
  saveRolePlayIndex();
71
71
  }
72
72
 
73
+ // ---------------------------------------------------------------------------
74
+ // .crew context import
75
+ // ---------------------------------------------------------------------------
76
+
77
+ /**
78
+ * Read a file if it exists, otherwise return null.
79
+ * @param {string} filePath
80
+ * @returns {string|null}
81
+ */
82
+ function readFileOrNull(filePath) {
83
+ try {
84
+ if (!existsSync(filePath)) return null;
85
+ return readFileSync(filePath, 'utf-8');
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Load .crew context from a project directory.
93
+ * Returns null if .crew/ doesn't exist.
94
+ *
95
+ * @param {string} projectDir - absolute path to project root
96
+ * @returns {{ sharedClaudeMd: string, roles: Array, kanban: string, features: Array, teamType: string, language: string } | null}
97
+ */
98
+ export function loadCrewContext(projectDir) {
99
+ const crewDir = join(projectDir, '.crew');
100
+ if (!existsSync(crewDir)) return null;
101
+
102
+ // 1. Shared CLAUDE.md
103
+ const sharedClaudeMd = readFileOrNull(join(crewDir, 'CLAUDE.md')) || '';
104
+
105
+ // 2. session.json → roles, teamType, language, features
106
+ let sessionRoles = [];
107
+ let teamType = 'dev';
108
+ let language = 'zh-CN';
109
+ let sessionFeatures = [];
110
+ const sessionPath = join(crewDir, 'session.json');
111
+ const sessionJson = readFileOrNull(sessionPath);
112
+ if (sessionJson) {
113
+ try {
114
+ const session = JSON.parse(sessionJson);
115
+ if (Array.isArray(session.roles)) {
116
+ sessionRoles = session.roles;
117
+ }
118
+ if (session.teamType) teamType = session.teamType;
119
+ if (session.language) language = session.language;
120
+ if (Array.isArray(session.features)) {
121
+ sessionFeatures = session.features;
122
+ }
123
+ } catch {
124
+ // Invalid JSON — ignore
125
+ }
126
+ }
127
+
128
+ // 3. Per-role CLAUDE.md from .crew/roles/*/CLAUDE.md
129
+ const roleClaudes = {};
130
+ const rolesDir = join(crewDir, 'roles');
131
+ if (existsSync(rolesDir)) {
132
+ try {
133
+ const roleDirs = readdirSync(rolesDir, { withFileTypes: true })
134
+ .filter(d => d.isDirectory())
135
+ .map(d => d.name);
136
+ for (const dirName of roleDirs) {
137
+ const md = readFileOrNull(join(rolesDir, dirName, 'CLAUDE.md'));
138
+ if (md) roleClaudes[dirName] = md;
139
+ }
140
+ } catch {
141
+ // Permission error or similar — ignore
142
+ }
143
+ }
144
+
145
+ // 4. Merge roles: deduplicate by roleType, attach claudeMd
146
+ const roles = deduplicateRoles(sessionRoles, roleClaudes);
147
+
148
+ // 5. Kanban
149
+ const kanban = readFileOrNull(join(crewDir, 'context', 'kanban.md')) || '';
150
+
151
+ // 6. Feature files from context/features/*.md
152
+ const features = [];
153
+ const featuresDir = join(crewDir, 'context', 'features');
154
+ if (existsSync(featuresDir)) {
155
+ try {
156
+ const files = readdirSync(featuresDir)
157
+ .filter(f => f.endsWith('.md') && f !== 'index.md')
158
+ .sort();
159
+ for (const f of files) {
160
+ const content = readFileOrNull(join(featuresDir, f));
161
+ if (content) {
162
+ features.push({ name: f.replace('.md', ''), content });
163
+ }
164
+ }
165
+ } catch {
166
+ // ignore
167
+ }
168
+ }
169
+
170
+ return { sharedClaudeMd, roles, kanban, features, teamType, language, sessionFeatures };
171
+ }
172
+
173
+ /**
174
+ * Deduplicate Crew roles by roleType.
175
+ * Crew may have dev-1, dev-2, dev-3 — collapse to a single "dev" for RolePlay.
176
+ * Attaches per-role CLAUDE.md content.
177
+ */
178
+ function deduplicateRoles(sessionRoles, roleClaudes) {
179
+ const byType = new Map(); // roleType -> first role seen
180
+ const merged = [];
181
+
182
+ for (const r of sessionRoles) {
183
+ const type = r.roleType || r.name;
184
+ const claudeMd = roleClaudes[r.name] || '';
185
+
186
+ if (byType.has(type)) {
187
+ // Already have this roleType — skip duplicate instance
188
+ continue;
189
+ }
190
+
191
+ byType.set(type, true);
192
+
193
+ // Use roleType as the RolePlay name (e.g. "developer" instead of "dev-1")
194
+ // But keep "pm" and "designer" as-is since they're typically single-instance
195
+ const name = type;
196
+ // Strip instance suffix from displayName (e.g. "开发者-托瓦兹-1" → "开发者-托瓦兹")
197
+ let displayName = r.displayName || name;
198
+ displayName = displayName.replace(/-\d+$/, '');
199
+
200
+ merged.push({
201
+ name,
202
+ displayName,
203
+ icon: r.icon || '',
204
+ description: r.description || '',
205
+ claudeMd: claudeMd.substring(0, MAX_CLAUDE_MD_LEN),
206
+ roleType: type,
207
+ isDecisionMaker: !!r.isDecisionMaker,
208
+ });
209
+ }
210
+
211
+ return merged;
212
+ }
213
+
73
214
  // ---------------------------------------------------------------------------
74
215
  // Input validation
75
216
  // ---------------------------------------------------------------------------
@@ -165,11 +306,11 @@ export function validateRolePlayConfig(config) {
165
306
  * Build the appendSystemPrompt that tells Claude about the role play roles
166
307
  * and how to switch between them.
167
308
  *
168
- * @param {{ roles: Array, teamType: string, language: string }} config
309
+ * @param {{ roles: Array, teamType: string, language: string, crewContext?: object }} config
169
310
  * @returns {string}
170
311
  */
171
312
  export function buildRolePlaySystemPrompt(config) {
172
- const { roles, teamType, language } = config;
313
+ const { roles, teamType, language, crewContext } = config;
173
314
  const isZh = language === 'zh-CN';
174
315
 
175
316
  // Build role list
@@ -186,6 +327,14 @@ export function buildRolePlaySystemPrompt(config) {
186
327
  ? buildZhPrompt(roleList, workflow)
187
328
  : buildEnPrompt(roleList, workflow);
188
329
 
330
+ // Append .crew context if available
331
+ if (crewContext) {
332
+ const contextBlock = buildCrewContextBlock(crewContext, isZh);
333
+ if (contextBlock) {
334
+ return (prompt + '\n\n' + contextBlock).trim();
335
+ }
336
+ }
337
+
189
338
  return prompt.trim();
190
339
  }
191
340
 
@@ -531,3 +680,39 @@ export function getRolePlayRouteState(conversationId) {
531
680
  waitingHumanContext: session.waitingHumanContext || null
532
681
  };
533
682
  }
683
+
684
+ // ---------------------------------------------------------------------------
685
+ // .crew context block for system prompt
686
+ // ---------------------------------------------------------------------------
687
+
688
+ /**
689
+ * Build the .crew context block to append to the system prompt.
690
+ * Includes shared instructions, kanban, and feature history.
691
+ */
692
+ function buildCrewContextBlock(crewContext, isZh) {
693
+ const sections = [];
694
+
695
+ if (crewContext.sharedClaudeMd) {
696
+ const header = isZh ? '## 项目共享指令(来自 .crew)' : '## Shared Project Instructions (from .crew)';
697
+ sections.push(`${header}\n\n${crewContext.sharedClaudeMd}`);
698
+ }
699
+
700
+ if (crewContext.kanban) {
701
+ const header = isZh ? '## 当前任务看板' : '## Current Task Board';
702
+ sections.push(`${header}\n\n${crewContext.kanban}`);
703
+ }
704
+
705
+ if (crewContext.features && crewContext.features.length > 0) {
706
+ const header = isZh ? '## 历史工作记录' : '## Work History';
707
+ // Only include the last few features to avoid blowing up context
708
+ const recentFeatures = crewContext.features.slice(-5);
709
+ const featureTexts = recentFeatures.map(f => {
710
+ // Truncate each feature to keep total size reasonable
711
+ const content = f.content.length > 2000 ? f.content.substring(0, 2000) + '\n...(truncated)' : f.content;
712
+ return `### ${f.name}\n${content}`;
713
+ }).join('\n\n');
714
+ sections.push(`${header}\n\n${featureTexts}`);
715
+ }
716
+
717
+ return sections.join('\n\n');
718
+ }