@yeaft/webchat-agent 0.1.77 → 0.1.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/cli.js CHANGED
@@ -223,6 +223,7 @@ function upgradeWindows(latestVersion) {
223
223
 
224
224
  writeFileSync(batPath, batLines.join('\r\n'));
225
225
  const child = spawn('cmd.exe', ['/c', batPath], {
226
+ detached: true,
226
227
  stdio: 'ignore',
227
228
  windowsHide: true,
228
229
  });
@@ -76,7 +76,7 @@ try {
76
76
  log('Temp dir: ' + tmpDir);
77
77
 
78
78
  const packOutput = execFileSync('npm', ['pack', PKG, '--pack-destination', tmpDir], {
79
- shell: true, encoding: 'utf8', cwd: tmpDir, timeout: 120000
79
+ shell: process.platform === 'win32', encoding: 'utf8', cwd: tmpDir, timeout: 120000
80
80
  }).trim();
81
81
  const tgzName = packOutput.split('\n').pop().trim();
82
82
  const tgzPath = path.join(tmpDir, tgzName);
@@ -100,7 +100,7 @@ try {
100
100
  log('Installing dependencies...');
101
101
  try {
102
102
  execFileSync('npm', ['install', '--omit=dev'], {
103
- shell: true, cwd: TARGET, encoding: 'utf8', timeout: 120000
103
+ shell: process.platform === 'win32', cwd: TARGET, encoding: 'utf8', timeout: 120000
104
104
  });
105
105
  log('Dependencies installed');
106
106
  } catch (depErr) {
@@ -46,7 +46,7 @@ export async function handleUpgradeAgent() {
46
46
  const pkgName = ctx.pkgName || '@yeaft/webchat-agent';
47
47
  // Check latest version (async to avoid blocking heartbeat)
48
48
  const latestVersion = await new Promise((resolve, reject) => {
49
- execFile('npm', ['view', pkgName, 'version'], { stdio: 'pipe', shell: true }, (err, stdout) => {
49
+ execFile('npm', ['view', pkgName, 'version'], { stdio: 'pipe', shell: process.platform === 'win32' }, (err, stdout) => {
50
50
  if (err) reject(err); else resolve(stdout.toString().trim());
51
51
  });
52
52
  });
@@ -74,7 +74,7 @@ export async function handleUpgradeAgent() {
74
74
 
75
75
  // 判断全局安装 vs 局部安装
76
76
  const isGlobalInstall = await new Promise((resolve) => {
77
- execFile('npm', ['prefix', '-g'], { shell: true }, (err, stdout) => {
77
+ execFile('npm', ['prefix', '-g'], { shell: process.platform === 'win32' }, (err, stdout) => {
78
78
  if (err) { resolve(false); return; }
79
79
  const globalPrefix = stdout.toString().trim().replace(/\\/g, '/');
80
80
  resolve(installDir === globalPrefix || installDir === globalPrefix + '/lib');
@@ -94,7 +94,7 @@ export async function handleUpgradeAgent() {
94
94
  const isPm2 = !!process.env.pm_id;
95
95
  if (isPm2) {
96
96
  try {
97
- execFileSync('pm2', ['delete', PM2_APP_NAME], { shell: true, stdio: 'pipe' });
97
+ execFileSync('pm2', ['delete', PM2_APP_NAME], { shell: process.platform === 'win32', stdio: 'pipe' });
98
98
  console.log(`[Agent] PM2 app deleted to prevent auto-restart during upgrade`);
99
99
  } catch {
100
100
  console.log(`[Agent] PM2 delete skipped (app may not be registered)`);
@@ -201,8 +201,9 @@ function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestV
201
201
 
202
202
  writeFileSync(batPath, batLines.join('\r\n'));
203
203
  const child = spawn('cmd.exe', ['/c', batPath], {
204
+ detached: true,
204
205
  stdio: 'ignore',
205
- windowsHide: true
206
+ windowsHide: true,
206
207
  });
207
208
  child.unref();
208
209
  console.log(`[Agent] Spawned upgrade script (PID wait for ${pid}, pm2=${isPm2}, dir=${installDir}): ${batPath}`);
package/crew/session.js CHANGED
@@ -6,7 +6,7 @@ import { join, isAbsolute } from 'path';
6
6
  import ctx from '../context.js';
7
7
  import { getMessages } from '../crew-i18n.js';
8
8
  import { initWorktrees } from './worktree.js';
9
- import { initSharedDir, writeRoleClaudeMd, updateSharedClaudeMd } from './shared-dir.js';
9
+ import { initSharedDir, writeRoleClaudeMd, updateSharedClaudeMd, backupMemoryContent } from './shared-dir.js';
10
10
  import {
11
11
  loadCrewIndex, upsertCrewIndex, removeFromCrewIndex,
12
12
  loadSessionMeta, saveSessionMeta, loadSessionMessages, getMaxShardIndex
@@ -366,6 +366,9 @@ export async function handleDeleteCrewDir(msg) {
366
366
  if (!isValidProjectDir(projectDir)) return;
367
367
  const crewDir = join(projectDir, '.crew');
368
368
  try {
369
+ // 提取并备份记忆内容(删除前)
370
+ await backupMemoryContent(crewDir);
371
+
369
372
  // 删除 Crew 模板定义
370
373
  await fs.rm(join(crewDir, 'CLAUDE.md'), { force: true }).catch(() => {});
371
374
  await fs.rm(join(crewDir, 'roles'), { recursive: true, force: true }).catch(() => {});
@@ -4,13 +4,98 @@
4
4
  */
5
5
  import { promises as fs } from 'fs';
6
6
  import { join } from 'path';
7
- import { getMessages } from '../crew-i18n.js';
7
+ import { getMessages, getAllMemoryTitles } from '../crew-i18n.js';
8
8
 
9
9
  /** Format role label: "icon displayName" or just "displayName" if no icon */
10
10
  function roleLabel(r) {
11
11
  return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
12
12
  }
13
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/*/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
+
14
99
  /**
15
100
  * 初始化共享目录
16
101
  */
@@ -27,6 +112,9 @@ export async function initSharedDir(sharedDir, roles, projectDir, language = 'zh
27
112
 
28
113
  // 生成 .crew/CLAUDE.md(共享级)
29
114
  await writeSharedClaudeMd(sharedDir, roles, projectDir, language);
115
+
116
+ // 清理记忆备份文件(已在 write 阶段恢复)
117
+ await cleanupMemoryBackup(sharedDir);
30
118
  }
31
119
 
32
120
  /**
@@ -52,6 +140,10 @@ export async function initRoleDir(sharedDir, role, language = 'zh-CN', allRoles
52
140
  export async function writeSharedClaudeMd(sharedDir, roles, projectDir, language = 'zh-CN') {
53
141
  const m = getMessages(language);
54
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
+
55
147
  const claudeMd = `${m.projectGoal}
56
148
 
57
149
  ${m.projectCodePath}
@@ -73,7 +165,7 @@ ${m.worktreeRulesContent}
73
165
  ${m.featureRecordShared}
74
166
 
75
167
  ${m.sharedMemoryTitle}
76
- ${m.sharedMemoryDefault}
168
+ ${sharedMemoryContent}
77
169
  `;
78
170
 
79
171
  await fs.writeFile(join(sharedDir, 'CLAUDE.md'), claudeMd);
@@ -129,6 +221,12 @@ export async function writeRoleClaudeMd(sharedDir, role, language = 'zh-CN', all
129
221
  const roleDir = join(sharedDir, 'roles', role.name);
130
222
  const m = getMessages(language);
131
223
 
224
+ // Check for memory backup to restore
225
+ const backup = await loadMemoryBackup(sharedDir);
226
+ const personalMemoryContent = (backup && backup.roles && backup.roles[role.name])
227
+ ? backup.roles[role.name]
228
+ : m.personalMemoryDefault;
229
+
132
230
  // Resolve generic ROUTE targets to actual instance names
133
231
  const resolvedClaudeMd = resolveRouteTargets(role.claudeMd || role.description, role, allRoles);
134
232
 
@@ -147,7 +245,7 @@ ${m.codeWorkDirNote}
147
245
 
148
246
  claudeMd += `
149
247
  ${m.personalMemory}
150
- ${m.personalMemoryDefault}
248
+ ${personalMemoryContent}
151
249
  `;
152
250
 
153
251
  await fs.writeFile(join(roleDir, 'CLAUDE.md'), claudeMd);
package/crew-i18n.js CHANGED
@@ -498,3 +498,23 @@ Roles don't need to manually create or update these files.`,
498
498
  export function getMessages(language) {
499
499
  return messages[language] || messages['zh-CN'];
500
500
  }
501
+
502
+ /**
503
+ * Get all memory-related titles and defaults across all locales.
504
+ * Used to detect and extract user-written memory content regardless of language.
505
+ * @returns {{ sharedTitles: string[], sharedDefaults: string[], personalTitles: string[], personalDefaults: string[] }}
506
+ */
507
+ export function getAllMemoryTitles() {
508
+ const sharedTitles = [];
509
+ const sharedDefaults = [];
510
+ const personalTitles = [];
511
+ const personalDefaults = [];
512
+ for (const lang of Object.keys(messages)) {
513
+ const m = messages[lang];
514
+ if (m.sharedMemoryTitle) sharedTitles.push(m.sharedMemoryTitle);
515
+ if (m.sharedMemoryDefault) sharedDefaults.push(m.sharedMemoryDefault);
516
+ if (m.personalMemory) personalTitles.push(m.personalMemory);
517
+ if (m.personalMemoryDefault) personalDefaults.push(m.personalMemoryDefault);
518
+ }
519
+ return { sharedTitles, sharedDefaults, personalTitles, personalDefaults };
520
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",