@yeaft/webchat-agent 0.1.772 → 0.1.774

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.772",
3
+ "version": "0.1.774",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -32,10 +32,19 @@
32
32
  * 'reserved'/'invalid_vp_id'/... — bubbled from ids.js validators
33
33
  */
34
34
 
35
- import { existsSync, renameSync, rmSync, readdirSync, statSync } from 'fs';
35
+ import {
36
+ existsSync,
37
+ renameSync,
38
+ rmSync,
39
+ readdirSync,
40
+ statSync,
41
+ mkdirSync,
42
+ readFileSync,
43
+ writeFileSync,
44
+ } from 'fs';
36
45
  import { randomBytes } from 'crypto';
37
46
  import { homedir } from 'os';
38
- import { join } from 'path';
47
+ import { isAbsolute, join, resolve } from 'path';
39
48
  import {
40
49
  openGroup, createGroup, listGroups, loadGroupMeta,
41
50
  } from './group-store.js';
@@ -87,10 +96,77 @@ export class GroupCrudError extends Error {
87
96
  }
88
97
  }
89
98
 
90
- function groupsRoot(yeaftDir) {
99
+ const GROUP_WORKDIR_REGISTRY = 'group-workdirs.json';
100
+
101
+ export function groupsRoot(yeaftDir) {
91
102
  return join(yeaftDir, 'groups');
92
103
  }
93
104
 
105
+ export function normalizeWorkDir(workDir) {
106
+ const raw = String(workDir || '').trim();
107
+ if (!raw) return '';
108
+ return isAbsolute(raw) ? raw : resolve(raw);
109
+ }
110
+
111
+ export function yeaftDirForWorkDir(workDir) {
112
+ const normalized = normalizeWorkDir(workDir);
113
+ return normalized ? join(normalized, '.yeaft') : '';
114
+ }
115
+
116
+ function registryPath(yeaftDir) {
117
+ return join(yeaftDir, GROUP_WORKDIR_REGISTRY);
118
+ }
119
+
120
+ export function readWorkDirRegistry(yeaftDir) {
121
+ if (!yeaftDir) return {};
122
+ const file = registryPath(yeaftDir);
123
+ if (!existsSync(file)) return {};
124
+ try {
125
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
126
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
127
+ } catch {
128
+ return {};
129
+ }
130
+ }
131
+
132
+ function writeWorkDirRegistry(yeaftDir, registry) {
133
+ if (!yeaftDir) return;
134
+ mkdirSync(yeaftDir, { recursive: true });
135
+ writeFileSync(registryPath(yeaftDir), `${JSON.stringify(registry, null, 2)}\n`);
136
+ }
137
+
138
+ export function registerGroupWorkDir(defaultYeaftDir, groupId, workDir) {
139
+ const normalized = normalizeWorkDir(workDir);
140
+ if (!defaultYeaftDir || !groupId || !normalized) return;
141
+ const registry = readWorkDirRegistry(defaultYeaftDir);
142
+ registry[groupId] = normalized;
143
+ writeWorkDirRegistry(defaultYeaftDir, registry);
144
+ }
145
+
146
+ export function unregisterGroupWorkDir(defaultYeaftDir, groupId) {
147
+ if (!defaultYeaftDir || !groupId) return;
148
+ const registry = readWorkDirRegistry(defaultYeaftDir);
149
+ if (!Object.prototype.hasOwnProperty.call(registry, groupId)) return;
150
+ delete registry[groupId];
151
+ writeWorkDirRegistry(defaultYeaftDir, registry);
152
+ }
153
+
154
+ export function resolveGroupYeaftDir(defaultYeaftDir, groupId) {
155
+ if (!defaultYeaftDir || !groupId) return defaultYeaftDir;
156
+ const defaultGroupDir = join(groupsRoot(defaultYeaftDir), groupId);
157
+ if (existsSync(defaultGroupDir) && loadGroupMeta(defaultGroupDir)) return defaultYeaftDir;
158
+
159
+ const registry = readWorkDirRegistry(defaultYeaftDir);
160
+ const workDir = normalizeWorkDir(registry[groupId]);
161
+ if (workDir) {
162
+ const candidate = yeaftDirForWorkDir(workDir);
163
+ const candidateDir = join(groupsRoot(candidate), groupId);
164
+ if (existsSync(candidateDir) && loadGroupMeta(candidateDir)) return candidate;
165
+ }
166
+
167
+ return defaultYeaftDir;
168
+ }
169
+
94
170
  /** Build a safe group id from a display name (slug + ulid-lite suffix). */
95
171
  export function makeGroupId(name) {
96
172
  const slug = String(name || 'group')
@@ -146,11 +222,13 @@ export function ensureDefaultGroupIfEmpty(yeaftDir, options = {}) {
146
222
  * we do NOT auto-expand to the full VP library here. That's D1's job only.
147
223
  *
148
224
  * @param {string} yeaftDir
149
- * @param {{name:string, roster?:string[], defaultVpId?:string|null}} spec
150
- * @returns {{id:string, name:string, roster:string[], defaultVpId:string|null}}
225
+ * @param {{name:string, roster?:string[], defaultVpId?:string|null, workDir?:string}} spec
226
+ * @returns {{id:string, name:string, roster:string[], defaultVpId:string|null, workDir?:string}}
151
227
  */
152
228
  export function createGroupFromSpec(yeaftDir, spec, options = {}) {
153
- const memoryRoot = options.memoryRoot || DEFAULT_MEMORY_ROOT;
229
+ const normalizedWorkDir = normalizeWorkDir(spec && spec.workDir);
230
+ const groupYeaftDir = normalizedWorkDir ? yeaftDirForWorkDir(normalizedWorkDir) : yeaftDir;
231
+ const memoryRoot = options.memoryRoot || (groupYeaftDir ? join(groupYeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
154
232
  const name = String(spec && spec.name || '').trim();
155
233
  if (!name) throw new GroupCrudError('invalid_name', null, 'group name required');
156
234
 
@@ -174,15 +252,16 @@ export function createGroupFromSpec(yeaftDir, spec, options = {}) {
174
252
  if (!defaultVpId) defaultVpId = roster[0] || null;
175
253
 
176
254
  const id = makeGroupId(name);
177
- const root = groupsRoot(yeaftDir);
255
+ const root = groupsRoot(groupYeaftDir);
178
256
  if (existsSync(join(root, id))) {
179
257
  // Extremely unlikely (ulid suffix), but surface deterministically.
180
258
  throw new GroupCrudError('duplicate', id);
181
259
  }
182
260
 
183
- const handle = createGroup(root, { id, name, roster, defaultVpId });
261
+ const handle = createGroup(root, { id, name, roster, defaultVpId, workDir: normalizedWorkDir });
184
262
  const meta = handle.getMeta();
185
263
  handle.close();
264
+ if (normalizedWorkDir) registerGroupWorkDir(yeaftDir, id, normalizedWorkDir);
186
265
 
187
266
  // Seed Layer-A resident summary so the first session has memory content
188
267
  // even before Dream-v2 has run. No-op if a summary.md already exists.
@@ -243,7 +322,8 @@ export function updateGroupAnnouncement(yeaftDir, groupId, text) {
243
322
  * own second-confirm modal (acceptance #4 in task-334-slice-specs.md 334m).
244
323
  */
245
324
  export function archiveGroup(yeaftDir, groupId) {
246
- const root = groupsRoot(yeaftDir);
325
+ const groupYeaftDir = resolveGroupYeaftDir(yeaftDir, groupId);
326
+ const root = groupsRoot(groupYeaftDir);
247
327
  const srcDir = join(root, groupId);
248
328
  if (!existsSync(srcDir) || !loadGroupMeta(srcDir)) {
249
329
  throw new GroupCrudError('not_found', groupId);
@@ -253,6 +333,7 @@ export function archiveGroup(yeaftDir, groupId) {
253
333
  const suffix = randomBytes(2).toString('hex');
254
334
  const dstDir = join(root, `.archived-${ts}-${suffix}-${groupId}`);
255
335
  renameSync(srcDir, dstDir);
336
+ unregisterGroupWorkDir(yeaftDir, groupId);
256
337
  return { groupId, archivedAs: dstDir };
257
338
  }
258
339
 
@@ -269,8 +350,9 @@ export function archiveGroup(yeaftDir, groupId) {
269
350
  * delete cleans up legacy state too.
270
351
  */
271
352
  export function deleteGroup(yeaftDir, groupId, options = {}) {
272
- const memoryRoot = options.memoryRoot || DEFAULT_MEMORY_ROOT;
273
- const root = groupsRoot(yeaftDir);
353
+ const groupYeaftDir = resolveGroupYeaftDir(yeaftDir, groupId);
354
+ const memoryRoot = options.memoryRoot || (groupYeaftDir ? join(groupYeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
355
+ const root = groupsRoot(groupYeaftDir);
274
356
  const srcDir = join(root, groupId);
275
357
  const liveExists = existsSync(srcDir) && !!loadGroupMeta(srcDir);
276
358
 
@@ -307,6 +389,7 @@ export function deleteGroup(yeaftDir, groupId, options = {}) {
307
389
  console.warn(`[group-crud] failed to remove memory dir for ${groupId}:`, err?.message || err);
308
390
  }
309
391
 
392
+ unregisterGroupWorkDir(yeaftDir, groupId);
310
393
  return { groupId, deleted: true, legacyCleanedUp: legacyDirs.length };
311
394
  }
312
395
 
@@ -382,8 +465,9 @@ export function setGroupDefaultVp(yeaftDir, groupId, vpId) {
382
465
  }
383
466
  }
384
467
 
385
- function requireGroup(yeaftDir, groupId) {
386
- const root = groupsRoot(yeaftDir);
468
+ export function requireGroup(yeaftDir, groupId) {
469
+ const groupYeaftDir = resolveGroupYeaftDir(yeaftDir, groupId);
470
+ const root = groupsRoot(groupYeaftDir);
387
471
  const dir = join(root, groupId);
388
472
  if (!existsSync(dir) || !loadGroupMeta(dir)) {
389
473
  throw new GroupCrudError('not_found', groupId);
@@ -393,7 +477,18 @@ function requireGroup(yeaftDir, groupId) {
393
477
 
394
478
  /** Convenience: snapshot all non-archived groups for WS broadcast. */
395
479
  export function snapshotGroups(yeaftDir) {
396
- return listGroups(groupsRoot(yeaftDir));
480
+ const byId = new Map();
481
+ for (const group of listGroups(groupsRoot(yeaftDir))) {
482
+ byId.set(group.id, group);
483
+ }
484
+ const registry = readWorkDirRegistry(yeaftDir);
485
+ for (const [groupId, workDir] of Object.entries(registry)) {
486
+ const groupYeaftDir = yeaftDirForWorkDir(workDir);
487
+ const dir = join(groupsRoot(groupYeaftDir), groupId);
488
+ const meta = existsSync(dir) ? loadGroupMeta(dir) : null;
489
+ if (meta) byId.set(meta.id, meta);
490
+ }
491
+ return Array.from(byId.values()).sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
397
492
  }
398
493
 
399
494
  export { DEFAULT_GROUP_ID };
@@ -139,6 +139,7 @@ export function createGroup(groupsRoot, spec) {
139
139
  roster,
140
140
  defaultVpId: spec.defaultVpId || null,
141
141
  announcement: typeof spec.announcement === 'string' ? spec.announcement : '',
142
+ workDir: typeof spec.workDir === 'string' ? spec.workDir.trim() : '',
142
143
  createdAt: spec.createdAt || new Date().toISOString(),
143
144
  };
144
145
  h.saveMeta(meta);
@@ -153,9 +154,10 @@ export function loadGroupMeta(dir) {
153
154
  const raw = readFileSync(path, 'utf8');
154
155
  const parsed = JSON.parse(raw);
155
156
  validateMeta(parsed);
156
- // Legacy groups created before the announcement field was added are
157
- // forward-compat: missing field reads back as empty string.
157
+ // Legacy groups created before optional fields were added are
158
+ // forward-compat: missing fields read back as safe empty strings.
158
159
  if (typeof parsed.announcement !== 'string') parsed.announcement = '';
160
+ if (typeof parsed.workDir !== 'string') parsed.workDir = '';
159
161
  return parsed;
160
162
  } catch {
161
163
  return null;
@@ -192,6 +194,9 @@ function validateMeta(meta) {
192
194
  if (meta.announcement != null && typeof meta.announcement !== 'string') {
193
195
  throw new Error('group.announcement must be string');
194
196
  }
197
+ if (meta.workDir != null && typeof meta.workDir !== 'string') {
198
+ throw new Error('group.workDir must be string');
199
+ }
195
200
  }
196
201
 
197
202
  /**
@@ -41,6 +41,8 @@ import {
41
41
  removeMember,
42
42
  setGroupDefaultVp,
43
43
  snapshotGroups,
44
+ resolveGroupYeaftDir,
45
+ groupsRoot,
44
46
  } from './groups/group-crud.js';
45
47
  import { openGroup, loadGroupMeta } from './groups/group-store.js';
46
48
  import { createCoordinator } from './groups/coordinator.js';
@@ -997,6 +999,7 @@ function sendGroupRosterChanged(group) {
997
999
  name: group.name,
998
1000
  roster: group.roster,
999
1001
  defaultVpId: group.defaultVpId,
1002
+ workDir: group.workDir || '',
1000
1003
  });
1001
1004
  }
1002
1005
 
@@ -1024,8 +1027,7 @@ export function handleUnifyCreateGroup(msg) {
1024
1027
  const payload = (msg && msg.payload) || {};
1025
1028
  try {
1026
1029
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1027
- const memoryRoot = yeaftDir ? join(yeaftDir, 'memory') : undefined;
1028
- const group = createGroupFromSpec(yeaftDir, payload, memoryRoot ? { memoryRoot } : {});
1030
+ const group = createGroupFromSpec(yeaftDir, payload);
1029
1031
  sendGroupCrudResult({ op: 'create', requestId, ok: true, group });
1030
1032
  sendGroupSnapshotBroadcast();
1031
1033
  } catch (err) {
@@ -1107,8 +1109,7 @@ export function handleUnifyDeleteGroup(msg) {
1107
1109
  const groupId = msg && msg.groupId;
1108
1110
  try {
1109
1111
  const yeaftDir = ctx.CONFIG?.yeaftDir;
1110
- const memoryRoot = yeaftDir ? join(yeaftDir, 'memory') : undefined;
1111
- const result = deleteGroup(yeaftDir, groupId, memoryRoot ? { memoryRoot } : {});
1112
+ const result = deleteGroup(yeaftDir, groupId);
1112
1113
  // Cascade: remove every persisted message stamped with this group id.
1113
1114
  // Hard delete (per user spec): no soft-archive, the bytes are gone.
1114
1115
  // Skipped silently if the session/store isn't initialized — the next
@@ -1632,15 +1633,17 @@ export async function handleUnifyGroupChat(msg) {
1632
1633
  // seedFailed separately so a seed crash surfaces a different message
1633
1634
  // than a genuinely-missing group.
1634
1635
  let groupHandle = null;
1636
+ let groupRoot = null;
1635
1637
  let seedFailed = false;
1636
1638
  try {
1637
- const root = join(yeaftDir, 'groups');
1638
- const dir = join(root, groupId);
1639
+ const groupYeaftDir = resolveGroupYeaftDir(yeaftDir, groupId);
1640
+ groupRoot = groupsRoot(groupYeaftDir);
1641
+ const dir = join(groupRoot, groupId);
1639
1642
  if (existsSync(dir) && loadGroupMeta(dir)) {
1640
- groupHandle = openGroup(root, groupId);
1643
+ groupHandle = openGroup(groupRoot, groupId);
1641
1644
  } else if (groupId === 'grp_default') {
1642
1645
  try {
1643
- const seeded = seedDefaultGroup(yeaftDir, { memoryRoot: join(yeaftDir, 'memory') });
1646
+ const seeded = seedDefaultGroup(groupYeaftDir, { memoryRoot: join(groupYeaftDir, 'memory') });
1644
1647
  groupHandle = seeded.group;
1645
1648
  } catch (seedErr) {
1646
1649
  seedFailed = true;
@@ -1655,7 +1658,7 @@ export async function handleUnifyGroupChat(msg) {
1655
1658
 
1656
1659
  if (!groupHandle) {
1657
1660
  const errText = seedFailed
1658
- ? `⚠️ Failed to seed default group ${groupId} — check ~/.yeaft/ permissions.`
1661
+ ? `⚠️ Failed to seed default group ${groupId} — check group .yeaft permissions.`
1659
1662
  : `⚠️ Group ${groupId} not found.`;
1660
1663
  sendUnifyOutput({
1661
1664
  type: 'assistant',
@@ -1683,7 +1686,7 @@ export async function handleUnifyGroupChat(msg) {
1683
1686
  }
1684
1687
  if (rosterMutated) {
1685
1688
  try { groupHandle.close && groupHandle.close(); } catch { /* best-effort */ }
1686
- groupHandle = openGroup(join(yeaftDir, 'groups'), groupId);
1689
+ groupHandle = openGroup(groupRoot, groupId);
1687
1690
  sendGroupRosterChanged(groupHandle.getMeta());
1688
1691
  }
1689
1692
  }
@@ -1692,7 +1695,7 @@ export async function handleUnifyGroupChat(msg) {
1692
1695
  try {
1693
1696
  setGroupDefaultVp(yeaftDir, groupId, meta2.roster[0]);
1694
1697
  try { groupHandle.close && groupHandle.close(); } catch { /* best-effort */ }
1695
- groupHandle = openGroup(join(yeaftDir, 'groups'), groupId);
1698
+ groupHandle = openGroup(groupRoot, groupId);
1696
1699
  sendGroupRosterChanged(groupHandle.getMeta());
1697
1700
  rosterMutated = true;
1698
1701
  } catch { /* best-effort */ }