@yeaft/webchat-agent 0.1.124 → 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/claude.js +0 -224
- package/connection/message-router.js +2 -6
- package/conversation.js +8 -317
- package/crew/context-loader.js +171 -0
- package/crew.js +3 -0
- package/package.json +1 -1
- package/roleplay-dir.js +0 -305
- package/roleplay-i18n.js +0 -574
- package/roleplay-session.js +0 -184
- package/roleplay.js +0 -1061
package/roleplay-dir.js
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RolePlay — .roleplay/ directory and CLAUDE.md management.
|
|
3
|
-
*
|
|
4
|
-
* Analogous to agent/crew/shared-dir.js but for the single-process
|
|
5
|
-
* RolePlay collaboration mode.
|
|
6
|
-
*
|
|
7
|
-
* Directory structure:
|
|
8
|
-
* .roleplay/
|
|
9
|
-
* ├── CLAUDE.md (shared instructions, inherited by all sessions)
|
|
10
|
-
* ├── session.json (session index)
|
|
11
|
-
* ├── roles/ (one subdirectory per session)
|
|
12
|
-
* │ └── {session-name}/
|
|
13
|
-
* │ └── CLAUDE.md (Claude Code cwd points here)
|
|
14
|
-
* └── context/
|
|
15
|
-
* ├── kanban.md
|
|
16
|
-
* └── features/
|
|
17
|
-
*/
|
|
18
|
-
import { promises as fs } from 'fs';
|
|
19
|
-
import { join } from 'path';
|
|
20
|
-
import { existsSync, readdirSync } from 'fs';
|
|
21
|
-
import { getRolePlayMessages } from './roleplay-i18n.js';
|
|
22
|
-
|
|
23
|
-
// ─── Team type → default role set mapping ───────────────────────────
|
|
24
|
-
// Each entry is a list of role names (keys into roleTemplates in i18n).
|
|
25
|
-
const TEAM_ROLES = {
|
|
26
|
-
dev: ['pm', 'dev', 'reviewer', 'tester'],
|
|
27
|
-
writing: ['editor', 'writer', 'proofreader'],
|
|
28
|
-
trading: ['quant', 'strategist', 'risk', 'macro'],
|
|
29
|
-
video: ['director', 'writer', 'producer'],
|
|
30
|
-
custom: ['pm', 'dev', 'reviewer', 'tester'], // default same as dev
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// ─── Session name constraints ───────────────────────────────────────
|
|
34
|
-
const SESSION_NAME_RE = /^[a-z0-9-]+$/;
|
|
35
|
-
const MAX_SESSION_NAME_LEN = 64;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Generate a session directory name.
|
|
39
|
-
*
|
|
40
|
-
* Format: `{teamType}-{customName|"team"}-{YYYYMMDD}[-{seq}]`
|
|
41
|
-
*
|
|
42
|
-
* If a session with the same name already exists under .roleplay/roles/,
|
|
43
|
-
* a sequence number is appended (e.g. `-2`, `-3`).
|
|
44
|
-
*
|
|
45
|
-
* @param {string} projectDir - absolute path to project root
|
|
46
|
-
* @param {string} teamType - dev | writing | trading | video | custom
|
|
47
|
-
* @param {string} [customName] - user-provided name (optional)
|
|
48
|
-
* @returns {string} unique session name
|
|
49
|
-
*/
|
|
50
|
-
export function generateSessionName(projectDir, teamType, customName) {
|
|
51
|
-
const now = new Date();
|
|
52
|
-
const datePart = [
|
|
53
|
-
now.getFullYear(),
|
|
54
|
-
String(now.getMonth() + 1).padStart(2, '0'),
|
|
55
|
-
String(now.getDate()).padStart(2, '0'),
|
|
56
|
-
].join('');
|
|
57
|
-
|
|
58
|
-
const namePart = sanitizeNamePart(customName) || 'team';
|
|
59
|
-
const base = `${teamType}-${namePart}-${datePart}`;
|
|
60
|
-
|
|
61
|
-
// Check for duplicates under .roleplay/roles/
|
|
62
|
-
const rolesDir = join(projectDir, '.roleplay', 'roles');
|
|
63
|
-
const existing = new Set();
|
|
64
|
-
if (existsSync(rolesDir)) {
|
|
65
|
-
try {
|
|
66
|
-
for (const d of readdirSync(rolesDir, { withFileTypes: true })) {
|
|
67
|
-
if (d.isDirectory()) existing.add(d.name);
|
|
68
|
-
}
|
|
69
|
-
} catch {
|
|
70
|
-
// permission error — treat as empty
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!existing.has(base)) return base;
|
|
75
|
-
|
|
76
|
-
// Append sequence number
|
|
77
|
-
for (let seq = 2; seq <= 999; seq++) {
|
|
78
|
-
const candidate = `${base}-${seq}`;
|
|
79
|
-
if (!existing.has(candidate)) return candidate;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Extremely unlikely: fall back to timestamp suffix
|
|
83
|
-
return `${base}-${Date.now()}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Sanitize user-provided name part:
|
|
88
|
-
* - lowercase
|
|
89
|
-
* - replace non-alphanumeric with hyphens
|
|
90
|
-
* - collapse consecutive hyphens
|
|
91
|
-
* - trim leading/trailing hyphens
|
|
92
|
-
* - enforce max length
|
|
93
|
-
*
|
|
94
|
-
* Returns empty string if input is empty/invalid.
|
|
95
|
-
*/
|
|
96
|
-
function sanitizeNamePart(raw) {
|
|
97
|
-
if (!raw || typeof raw !== 'string') return '';
|
|
98
|
-
let s = raw.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
99
|
-
if (s.length > 30) s = s.substring(0, 30).replace(/-$/, '');
|
|
100
|
-
return SESSION_NAME_RE.test(s) ? s : '';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ─── Directory initialization ───────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Ensure .roleplay/ directory structure exists.
|
|
107
|
-
* Creates the top-level dirs if absent, writes shared CLAUDE.md
|
|
108
|
-
* only on first creation (preserves user edits after that).
|
|
109
|
-
*
|
|
110
|
-
* @param {string} projectDir - absolute path to project root
|
|
111
|
-
* @param {string} [language='zh-CN']
|
|
112
|
-
*/
|
|
113
|
-
export async function initRolePlayDir(projectDir, language = 'zh-CN') {
|
|
114
|
-
const rpDir = join(projectDir, '.roleplay');
|
|
115
|
-
|
|
116
|
-
await fs.mkdir(rpDir, { recursive: true });
|
|
117
|
-
await fs.mkdir(join(rpDir, 'roles'), { recursive: true });
|
|
118
|
-
await fs.mkdir(join(rpDir, 'context'), { recursive: true });
|
|
119
|
-
await fs.mkdir(join(rpDir, 'context', 'features'), { recursive: true });
|
|
120
|
-
|
|
121
|
-
// Write shared CLAUDE.md only if it doesn't exist
|
|
122
|
-
const sharedMdPath = join(rpDir, 'CLAUDE.md');
|
|
123
|
-
try {
|
|
124
|
-
await fs.access(sharedMdPath);
|
|
125
|
-
// Already exists — don't overwrite (user may have edited it)
|
|
126
|
-
} catch {
|
|
127
|
-
await writeRolePlaySharedClaudeMd(projectDir, language);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Write (or overwrite) .roleplay/CLAUDE.md — the shared-level instructions
|
|
133
|
-
* inherited by all sessions via Claude Code's CLAUDE.md lookup chain.
|
|
134
|
-
*
|
|
135
|
-
* @param {string} projectDir
|
|
136
|
-
* @param {string} [language='zh-CN']
|
|
137
|
-
*/
|
|
138
|
-
export async function writeRolePlaySharedClaudeMd(projectDir, language = 'zh-CN') {
|
|
139
|
-
const m = getRolePlayMessages(language);
|
|
140
|
-
const rpDir = join(projectDir, '.roleplay');
|
|
141
|
-
|
|
142
|
-
const content = `${m.sharedTitle}
|
|
143
|
-
|
|
144
|
-
${m.projectPath}
|
|
145
|
-
${projectDir}
|
|
146
|
-
${m.useAbsolutePath}
|
|
147
|
-
|
|
148
|
-
${m.workMode}
|
|
149
|
-
${m.workModeContent}
|
|
150
|
-
|
|
151
|
-
${m.workConventions}
|
|
152
|
-
${m.workConventionsContent}
|
|
153
|
-
|
|
154
|
-
${m.crewRelation}
|
|
155
|
-
${m.crewRelationContent}
|
|
156
|
-
|
|
157
|
-
${m.sharedMemory}
|
|
158
|
-
${m.sharedMemoryDefault}
|
|
159
|
-
`;
|
|
160
|
-
|
|
161
|
-
await fs.writeFile(join(rpDir, 'CLAUDE.md'), content);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ─── Session CLAUDE.md generation ───────────────────────────────────
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Write .roleplay/roles/{sessionName}/CLAUDE.md — session-level config.
|
|
168
|
-
*
|
|
169
|
-
* This file is the core configuration that Claude Code reads automatically
|
|
170
|
-
* (cwd is set to this directory). It contains:
|
|
171
|
-
* - Session metadata (name, teamType, language)
|
|
172
|
-
* - Full role list with descriptions (generated from teamType or custom roles)
|
|
173
|
-
* - ROUTE protocol reference
|
|
174
|
-
* - Workflow for this team type
|
|
175
|
-
* - Project path reference
|
|
176
|
-
* - Session memory section
|
|
177
|
-
*
|
|
178
|
-
* @param {string} projectDir - project root
|
|
179
|
-
* @param {string} sessionName - session directory name
|
|
180
|
-
* @param {object} config - { teamType, language, roles?, projectDir? }
|
|
181
|
-
* - config.roles: optional custom role array from RolePlay config.
|
|
182
|
-
* If provided and non-empty, these are used instead of default team roles.
|
|
183
|
-
* Each role: { name, displayName, icon?, description?, claudeMd? }
|
|
184
|
-
*/
|
|
185
|
-
export async function writeSessionClaudeMd(projectDir, sessionName, config) {
|
|
186
|
-
const { teamType = 'dev', language = 'zh-CN', roles: customRoles } = config;
|
|
187
|
-
const m = getRolePlayMessages(language);
|
|
188
|
-
|
|
189
|
-
const sessionDir = join(projectDir, '.roleplay', 'roles', sessionName);
|
|
190
|
-
await fs.mkdir(sessionDir, { recursive: true });
|
|
191
|
-
|
|
192
|
-
// Build role list section
|
|
193
|
-
const roleSection = buildRoleSection(teamType, language, customRoles);
|
|
194
|
-
|
|
195
|
-
// Build workflow section
|
|
196
|
-
const workflowMap = {
|
|
197
|
-
dev: m.devWorkflow,
|
|
198
|
-
writing: m.writingWorkflow,
|
|
199
|
-
trading: m.tradingWorkflow,
|
|
200
|
-
video: m.videoWorkflow,
|
|
201
|
-
};
|
|
202
|
-
const workflow = workflowMap[teamType] || m.genericWorkflow;
|
|
203
|
-
|
|
204
|
-
const content = `${m.sessionTitle(sessionName)}
|
|
205
|
-
|
|
206
|
-
${m.teamTypeLabel}
|
|
207
|
-
${teamType}
|
|
208
|
-
|
|
209
|
-
${m.languageLabel}
|
|
210
|
-
${language}
|
|
211
|
-
|
|
212
|
-
${m.roleListTitle}
|
|
213
|
-
|
|
214
|
-
${roleSection}
|
|
215
|
-
|
|
216
|
-
${m.routeProtocol}
|
|
217
|
-
|
|
218
|
-
${m.workflowTitle}
|
|
219
|
-
|
|
220
|
-
${workflow}
|
|
221
|
-
|
|
222
|
-
${m.projectPathTitle}
|
|
223
|
-
${projectDir}
|
|
224
|
-
${m.useAbsolutePath}
|
|
225
|
-
|
|
226
|
-
${m.sessionMemory}
|
|
227
|
-
${m.sessionMemoryDefault}
|
|
228
|
-
`;
|
|
229
|
-
|
|
230
|
-
await fs.writeFile(join(sessionDir, 'CLAUDE.md'), content);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Build the role list section for a session CLAUDE.md.
|
|
235
|
-
*
|
|
236
|
-
* Strategy:
|
|
237
|
-
* 1. If custom roles are provided (from RolePlay config), use them directly.
|
|
238
|
-
* For each custom role, look up a matching roleTemplate for extra detail,
|
|
239
|
-
* but prefer the custom role's claudeMd/description if provided.
|
|
240
|
-
* 2. Otherwise, use the default role set for the teamType from TEAM_ROLES.
|
|
241
|
-
*/
|
|
242
|
-
function buildRoleSection(teamType, language, customRoles) {
|
|
243
|
-
const m = getRolePlayMessages(language);
|
|
244
|
-
const templates = m.roleTemplates;
|
|
245
|
-
|
|
246
|
-
if (customRoles && customRoles.length > 0) {
|
|
247
|
-
return customRoles.map(r => {
|
|
248
|
-
const tmpl = templates[r.name];
|
|
249
|
-
// Prefer custom claudeMd, then custom description, then template
|
|
250
|
-
const body = r.claudeMd || r.description || (tmpl ? tmpl.content : '');
|
|
251
|
-
const heading = tmpl
|
|
252
|
-
? tmpl.heading
|
|
253
|
-
: `## ${r.icon || ''} ${r.displayName || r.name} (${r.name})`.replace(/\s{2,}/g, ' ');
|
|
254
|
-
return `${heading}\n${body}`;
|
|
255
|
-
}).join('\n\n');
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Default: use TEAM_ROLES mapping
|
|
259
|
-
const roleNames = TEAM_ROLES[teamType] || TEAM_ROLES.dev;
|
|
260
|
-
return roleNames.map(name => {
|
|
261
|
-
const tmpl = templates[name];
|
|
262
|
-
if (!tmpl) return `## ${name}\n(No template available)`;
|
|
263
|
-
return `${tmpl.heading}\n${tmpl.content}`;
|
|
264
|
-
}).join('\n\n');
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Get the default role list for a team type.
|
|
269
|
-
* Used by session creation to populate session.json roles snapshot.
|
|
270
|
-
*
|
|
271
|
-
* @param {string} teamType
|
|
272
|
-
* @param {string} language
|
|
273
|
-
* @returns {Array<{name: string, displayName: string, icon: string}>}
|
|
274
|
-
*/
|
|
275
|
-
export function getDefaultRoles(teamType, language = 'zh-CN') {
|
|
276
|
-
const m = getRolePlayMessages(language);
|
|
277
|
-
const templates = m.roleTemplates;
|
|
278
|
-
const roleNames = TEAM_ROLES[teamType] || TEAM_ROLES.dev;
|
|
279
|
-
|
|
280
|
-
return roleNames.map(name => {
|
|
281
|
-
const tmpl = templates[name];
|
|
282
|
-
if (!tmpl) return { name, displayName: name, icon: '' };
|
|
283
|
-
|
|
284
|
-
// Extract icon and displayName from heading: "## 📋 PM-乔布斯 (pm)"
|
|
285
|
-
const headingMatch = tmpl.heading.match(/^##\s*(\S+)\s+(.+?)\s*\(([\w-]+)\)\s*$/);
|
|
286
|
-
if (headingMatch) {
|
|
287
|
-
return {
|
|
288
|
-
name: headingMatch[3],
|
|
289
|
-
displayName: headingMatch[2],
|
|
290
|
-
icon: headingMatch[1],
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
return { name, displayName: name, icon: '' };
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Get the session directory path.
|
|
299
|
-
* @param {string} projectDir
|
|
300
|
-
* @param {string} sessionName
|
|
301
|
-
* @returns {string} absolute path to .roleplay/roles/{sessionName}/
|
|
302
|
-
*/
|
|
303
|
-
export function getSessionDir(projectDir, sessionName) {
|
|
304
|
-
return join(projectDir, '.roleplay', 'roles', sessionName);
|
|
305
|
-
}
|