@yeaft/webchat-agent 0.1.772 → 0.1.773
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 +1 -1
- package/unify/groups/group-crud.js +109 -14
- package/unify/groups/group-store.js +7 -2
- package/unify/web-bridge.js +14 -11
package/package.json
CHANGED
|
@@ -32,10 +32,19 @@
|
|
|
32
32
|
* 'reserved'/'invalid_vp_id'/... — bubbled from ids.js validators
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import {
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
273
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
157
|
-
// forward-compat: missing
|
|
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
|
/**
|
package/unify/web-bridge.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
1638
|
-
|
|
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(
|
|
1643
|
+
groupHandle = openGroup(groupRoot, groupId);
|
|
1641
1644
|
} else if (groupId === 'grp_default') {
|
|
1642
1645
|
try {
|
|
1643
|
-
const seeded = seedDefaultGroup(
|
|
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
|
|
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(
|
|
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(
|
|
1698
|
+
groupHandle = openGroup(groupRoot, groupId);
|
|
1696
1699
|
sendGroupRosterChanged(groupHandle.getMeta());
|
|
1697
1700
|
rosterMutated = true;
|
|
1698
1701
|
} catch { /* best-effort */ }
|