@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/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
- }