@yeaft/webchat-agent 0.1.792 → 0.1.794

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.792",
3
+ "version": "0.1.794",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -31,6 +31,7 @@ import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
31
31
  import { join } from 'path';
32
32
  import { createVp, VpCrudError } from './vp-crud.js';
33
33
  import { DEFAULT_VP_LIB_DIR } from './vp-store.js';
34
+ import { STOCK_VP_IDS } from './stock-ids.js';
34
35
 
35
36
  /**
36
37
  * The 33 default VPs. Each entry is a valid `createVp` payload.
@@ -855,6 +856,33 @@ Bad for: requests that require pretending to be a licensed professional, bypassi
855
856
  },
856
857
  ]);
857
858
 
859
+ /**
860
+ * Self-check: every seed persona's vpId must appear in STOCK_VP_IDS, and
861
+ * vice versa. The two lists live in separate modules to break a circular
862
+ * import (see stock-ids.js header), so the only thing keeping them in
863
+ * sync is this load-time assertion. If you add a new seed VP and forget
864
+ * to add its id to stock-ids.js#STOCK_VP_ID_LIST (or vice versa), the
865
+ * agent will refuse to start with a clear error.
866
+ */
867
+ const _seedIds = new Set(DEFAULT_VPS.map(v => v.vpId));
868
+ {
869
+ const missingInStockIds = [];
870
+ for (const id of _seedIds) {
871
+ if (!STOCK_VP_IDS.has(id)) missingInStockIds.push(id);
872
+ }
873
+ const missingInSeeds = [];
874
+ for (const id of STOCK_VP_IDS) {
875
+ if (!_seedIds.has(id)) missingInSeeds.push(id);
876
+ }
877
+ if (missingInStockIds.length || missingInSeeds.length) {
878
+ throw new Error(
879
+ '[seed-defaults] DEFAULT_VPS / STOCK_VP_IDS mismatch — '
880
+ + `add to stock-ids.js: [${missingInStockIds.join(', ')}]; `
881
+ + `add to DEFAULT_VPS: [${missingInSeeds.join(', ')}]`,
882
+ );
883
+ }
884
+ }
885
+
858
886
  /**
859
887
  * True iff `libDir` exists and contains at least one subdirectory that
860
888
  * looks like a VP entry (has a `role.md` file). A stray empty directory
@@ -0,0 +1,53 @@
1
+ /**
2
+ * stock-ids.js — single source of truth for "is this vpId a stock seed VP?".
3
+ *
4
+ * Owns the canonical list of seed vpIds and exposes `STOCK_VP_IDS` (a Set
5
+ * for O(1) lookup) plus `isStockVpId(vpId)`.
6
+ *
7
+ * Why this lives in its own tiny module instead of in seed-defaults.js:
8
+ * `seed-defaults.js` imports `createVp / VpCrudError` from `vp-crud.js`,
9
+ * so if `vp-crud.js` were to import `STOCK_VP_IDS` from `seed-defaults`
10
+ * directly, the module graph would be circular (vp-crud → seed-defaults
11
+ * → vp-crud). The cycle "works" today only because every consumer reads
12
+ * STOCK_VP_IDS inside a function body (live binding), but a future
13
+ * refactor that moves the check to module top level — say, to validate
14
+ * input on import — would crash with `STOCK_VP_IDS is undefined`. Moving
15
+ * the Set into a leaf module with zero inbound deps from elsewhere in
16
+ * the package breaks the cycle for good.
17
+ *
18
+ * Authoritative-vs-derived: this file is the SOURCE OF TRUTH for stock
19
+ * ids. seed-defaults.js asserts at module load that every entry in
20
+ * DEFAULT_VPS appears here, and vice versa — see the self-check at the
21
+ * bottom of seed-defaults.js. So adding a new seed VP only requires
22
+ * adding both the persona object *and* its id here (two-file change,
23
+ * caught by the assertion if you forget either).
24
+ */
25
+
26
+ const STOCK_VP_ID_LIST = Object.freeze([
27
+ // engineering
28
+ 'steve', 'linus', 'martin', 'dieter', 'ada', 'grace', 'alice', 'ken',
29
+ 'margaret', 'shannon', 'alan', 'norman',
30
+ // philosophy / psychology
31
+ 'kongzi', 'socrates', 'nietzsche', 'kahneman', 'jung',
32
+ // strategy / business
33
+ 'sunzi', 'clausewitz', 'simaqian', 'harari',
34
+ 'buffett', 'munger', 'dalio', 'bezos', 'drucker',
35
+ // arts / culture
36
+ 'luxun', 'sudongpo', 'borges', 'einstein', 'kubrick', 'miyazaki',
37
+ // assistant
38
+ 'omni',
39
+ ]);
40
+
41
+ /** @type {ReadonlySet<string>} */
42
+ export const STOCK_VP_IDS = new Set(STOCK_VP_ID_LIST);
43
+
44
+ /**
45
+ * True iff `vpId` is a stock seed VP that ships with the agent.
46
+ * Pure function of the string — undefined / non-string input returns false.
47
+ *
48
+ * @param {unknown} vpId
49
+ * @returns {boolean}
50
+ */
51
+ export function isStockVpId(vpId) {
52
+ return typeof vpId === 'string' && STOCK_VP_IDS.has(vpId);
53
+ }
@@ -23,6 +23,7 @@
23
23
 
24
24
  import { defaultRegistry } from './registry.js';
25
25
  import { VpLoader } from './vp-loader.js';
26
+ import { STOCK_VP_IDS } from './stock-ids.js';
26
27
 
27
28
  /** Process-singleton VpLoader; lazily started on first subscribe. */
28
29
  let _loaderStarted = false;
@@ -175,8 +176,8 @@ function ensureLoader(registry = defaultRegistry) {
175
176
  * Serialise a VP (entity layer shape) to the wire-format the web layer
176
177
  * expects (spec §2.1). Pure; no IO.
177
178
  *
178
- * @param {{id:string,name:string,role:string,traits?:string[],modelHint?:string,personaHash?:string}} vp
179
- * @returns {{vpId:string,displayName:string,subtitle:string,role:string,traits:string[],modelHint:?string,personaHash:?string}}
179
+ * @param {{id:string,name:string,role:string,nameZh?:string,aliases?:string[],traits?:string[],modelHint?:string,personaHash?:string}} vp
180
+ * @returns {{vpId:string,displayName:string,displayNameZh:string,aliases:string[],subtitle:string,role:string,traits:string[],modelHint:?string,personaHash:?string,isStock:boolean}}
180
181
  */
181
182
  export function serializeVpForWire(vp) {
182
183
  return {
@@ -191,6 +192,13 @@ export function serializeVpForWire(vp) {
191
192
  traits: Array.isArray(vp.traits) ? vp.traits.slice() : [],
192
193
  modelHint: vp.modelHint ?? null,
193
194
  personaHash: vp.personaHash ?? null,
195
+ // task-vp-customize: mark seed VPs so the frontend can disable
196
+ // Edit/Delete and surface a "Stock" badge. Pure id check — see
197
+ // stock-ids.js#STOCK_VP_IDS for the contract. `Set.has(undefined)`
198
+ // is fine, but we still coerce to plain boolean so the wire field
199
+ // is strictly `true | false` (never `undefined`) and downstream
200
+ // `!!` reads can collapse cleanly.
201
+ isStock: STOCK_VP_IDS.has(vp.id) === true,
194
202
  };
195
203
  }
196
204
 
@@ -14,6 +14,7 @@
14
14
  * Error codes returned to the caller (wire-visible):
15
15
  * 'duplicate' — vpId already exists (on create)
16
16
  * 'not_found' — vpId does not exist on disk (on update/delete)
17
+ * 'stock_readonly' — vpId is one of the seed/stock VPs (mutation refused)
17
18
  * <reason from validateVpId> — invalid shape
18
19
  */
19
20
 
@@ -24,6 +25,7 @@ import { validateVpId } from '../groups/ids.js';
24
25
  import { DEFAULT_VP_LIB_DIR, parseRoleMd } from './vp-store.js';
25
26
  import { seedSummaryIfMissingSync, removeScopeDirSync } from '../memory/store-v2.js';
26
27
  import { VP_STUB_MARKER } from '../memory/seed-backfill.js';
28
+ import { STOCK_VP_IDS } from './stock-ids.js';
27
29
 
28
30
  /**
29
31
  * Default memory root used when callers don't pass `options.memoryRoot`.
@@ -34,13 +36,6 @@ import { VP_STUB_MARKER } from '../memory/seed-backfill.js';
34
36
  */
35
37
  const DEFAULT_MEMORY_ROOT = join(homedir(), '.yeaft', 'memory');
36
38
 
37
- /**
38
- * Build the seed summary body for a freshly-created VP. Pulled into a
39
- * helper so tests can pin the exact format.
40
- *
41
- * @param {object} payload same shape as createVp
42
- * @returns {string}
43
- */
44
39
  /**
45
40
  * Build the seed body for a freshly-created VP's `<root>/vp/<id>/summary.md`.
46
41
  *
@@ -218,6 +213,11 @@ export function updateVp(payload, options = {}) {
218
213
  const v = validateVpId(vpId);
219
214
  if (!v.ok) throw new VpCrudError(v.reason, vpId);
220
215
 
216
+ // Stock VPs ship with the agent and are immutable from the CRUD path.
217
+ // The UI also disables Edit/Delete on these, but a misbehaving WS client
218
+ // could bypass the UI — refuse here so the file on disk stays canonical.
219
+ if (STOCK_VP_IDS.has(vpId)) throw new VpCrudError('stock_readonly', vpId);
220
+
221
221
  const dir = vpDirFor(libDir, vpId);
222
222
  if (!existsSync(dir) || !existsSync(vpRolePathFor(libDir, vpId))) {
223
223
  throw new VpCrudError('not_found', vpId);
@@ -249,6 +249,10 @@ export function deleteVp(vpId, options = {}) {
249
249
  if (!vpId || typeof vpId !== 'string' || vpId.includes('/') || vpId.includes('\\') || vpId === '..' || vpId === '.') {
250
250
  throw new VpCrudError('illegal_character', vpId);
251
251
  }
252
+ // Stock VPs ship with the agent. Refuse the delete server-side so a
253
+ // misbehaving WS client cannot wipe `~/.yeaft/virtual-persons/steve/`
254
+ // even if the UI's delete button is missing or disabled.
255
+ if (STOCK_VP_IDS.has(vpId)) throw new VpCrudError('stock_readonly', vpId);
252
256
  const dir = vpDirFor(libDir, vpId);
253
257
  if (!existsSync(dir)) {
254
258
  throw new VpCrudError('not_found', vpId);
@@ -298,5 +302,11 @@ export function readVp(vpId, options = {}) {
298
302
  modelHint,
299
303
  persona: body,
300
304
  planInstruction: typeof meta.planInstruction === 'string' ? String(meta.planInstruction) : '',
305
+ // Echo the authoritative stock flag on the read response so the UI's
306
+ // detail view doesn't have to trust the (potentially stale) list
307
+ // snapshot it was launched from. Defence-in-depth: same Set, two
308
+ // call sites; if either disagrees, the agent guard in updateVp /
309
+ // deleteVp is still the final word.
310
+ isStock: STOCK_VP_IDS.has(id) === true,
301
311
  };
302
312
  }