@space3-npm/cybersoul-client 1.4.22 → 1.4.24

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/dist/client.d.ts CHANGED
@@ -83,6 +83,15 @@ export declare class CyberSoulClient {
83
83
  * reply is already in flight.
84
84
  */
85
85
  private buildMediaError;
86
+ /**
87
+ * Shared giftOutfit handler for `interact()` / `proactiveInteract()`.
88
+ * Validates the LLM's `giftOutfit` intent, performs the wardrobe write,
89
+ * fires the `onOutfitGifted` callback, and resolves to the
90
+ * [OutfitGiftedPayload] (or `undefined` when there was nothing to gift
91
+ * or the write failed). Failures are swallowed (logged) so a wardrobe
92
+ * hiccup never aborts the chat turn.
93
+ */
94
+ private processGiftOutfit;
86
95
  private formatHistoryEntries;
87
96
  private buildHistoryTranscript;
88
97
  interact(params: InteractParams): Promise<InteractResponse>;
@@ -144,8 +153,11 @@ export declare class CyberSoulClient {
144
153
  updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"], userAnalysis?: DispatcherIntent["userAnalysis"]): Promise<PersistedDynamicContext | null>;
145
154
  /**
146
155
  * Gift a new outfit to the character's wardrobe inventory.
156
+ * Returns the number of wardrobe items the backend created (the
157
+ * backend may expand a single description into multiple items), or
158
+ * `undefined` when the server did not report a count.
147
159
  */
148
- giftOutfit(descriptionText: string): Promise<void>;
160
+ giftOutfit(descriptionText: string): Promise<number | undefined>;
149
161
  /**
150
162
  * Bootstrap character profile from OpenClaw workspace files.
151
163
  */
package/dist/client.js CHANGED
@@ -311,7 +311,18 @@ Interaction Boundaries: ${state.interaction_boundaries || "None"}`);
311
311
  // [2] SITUATIONAL CONTEXT
312
312
  const currentTimeMs = state.current_time ? new Date(state.current_time).getTime() : Date.now();
313
313
  const timePeriod = this.getTimePeriodInfo(currentTimeMs);
314
- const timeStr = new Date(currentTimeMs).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
314
+ const currentDate = new Date(currentTimeMs);
315
+ const timeStr = currentDate.toLocaleString("en-US", {
316
+ timeZone: "Asia/Shanghai",
317
+ weekday: "long",
318
+ year: "numeric",
319
+ month: "long",
320
+ day: "numeric",
321
+ hour: "2-digit",
322
+ minute: "2-digit",
323
+ second: "2-digit",
324
+ hour12: false,
325
+ });
315
326
  contextParts.push(`\n[SITUATIONAL CONTEXT]
316
327
  Current time: ${timeStr} (${timePeriod.period})`);
317
328
  if (dyn.lastInteractionAt) {
@@ -471,7 +482,10 @@ ${isProactive
471
482
  return `- Include 'triggerEvent' only if the VERY LAST USER MESSAGE proposes a new activity/hangout AND you accept the invitation, explicitly requests an outfit change AND you agree, or proposes intimate/romantic actions AND you agree; ignore older history. DO NOT include it if you decline or reject the proposal. ${this.getOutfitSelectionPrompt()}`;
472
483
  }
473
484
  getOutfitAcquisitionPolicyPrompt() {
474
- return `- Outfit acquisition (VERY LAST USER MESSAGE only): set giftOutfit for gift/buy/add-clothes intent; otherwise null. giftOutfit format: { "descriptionText": "short outfit description" }.`;
485
+ return `- Outfit acquisition (giftOutfit): set 'giftOutfit' to { "descriptionText": "short outfit description" } when a genuinely NEW outfit (one that is NOT already in the Available Wardrobe) is obtained THIS turn, triggered by EITHER:
486
+ (a) USER-GIFTED: the VERY LAST USER MESSAGE expresses gift/buy/add-clothes intent for you (e.g. "I bought you a dress", "here, wear this new outfit", "adding some lingerie to your closet").
487
+ (b) CHARACTER-ACQUIRED: the conversation or active event naturally leads YOU to acquire a new outfit you don't already own (e.g. you went shopping, received/made clothes, or the scene requires changing into a brand-new outfit that is absent from your Available Wardrobe).
488
+ Keep 'descriptionText' to a concise English-or-matching-language description of the single new outfit. Otherwise set 'giftOutfit' to null. Do NOT fire it for outfits already present in the Available Wardrobe, and do NOT fire it just because you changed into an existing outfit.`;
475
489
  }
476
490
  getEventSchemaParams(userName) {
477
491
  const name = userName || "the user";
@@ -596,6 +610,45 @@ ${isProactive
596
610
  affected,
597
611
  };
598
612
  }
613
+ /**
614
+ * Shared giftOutfit handler for `interact()` / `proactiveInteract()`.
615
+ * Validates the LLM's `giftOutfit` intent, performs the wardrobe write,
616
+ * fires the `onOutfitGifted` callback, and resolves to the
617
+ * [OutfitGiftedPayload] (or `undefined` when there was nothing to gift
618
+ * or the write failed). Failures are swallowed (logged) so a wardrobe
619
+ * hiccup never aborts the chat turn.
620
+ */
621
+ async processGiftOutfit(giftOutfitIntent, onOutfitGifted) {
622
+ if (!giftOutfitIntent ||
623
+ typeof giftOutfitIntent !== "object" ||
624
+ typeof giftOutfitIntent.descriptionText !== "string" ||
625
+ giftOutfitIntent.descriptionText.trim().length === 0) {
626
+ return undefined;
627
+ }
628
+ const outfitDescription = giftOutfitIntent.descriptionText.trim();
629
+ try {
630
+ const count = await this.giftOutfit(outfitDescription);
631
+ const giftedOutfit = {
632
+ descriptionText: outfitDescription,
633
+ };
634
+ if (typeof count === "number") {
635
+ giftedOutfit.count = count;
636
+ }
637
+ if (onOutfitGifted) {
638
+ try {
639
+ onOutfitGifted(giftedOutfit);
640
+ }
641
+ catch (cbErr) {
642
+ console.warn("[CyberSoulClient] onOutfitGifted callback threw:", cbErr);
643
+ }
644
+ }
645
+ return giftedOutfit;
646
+ }
647
+ catch (e) {
648
+ console.error("[CyberSoulClient] giftOutfit failed:", e);
649
+ return undefined;
650
+ }
651
+ }
599
652
  formatHistoryEntries(history, userName, agentName, promptDirective = "") {
600
653
  const contextLines = [];
601
654
  for (let i = 0; i < history.length; i++) {
@@ -835,6 +888,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
835
888
  let finalAudioUrl = undefined;
836
889
  let finalAudioMediaId = undefined;
837
890
  let finalDurationSec = undefined;
891
+ let giftedOutfit = undefined;
838
892
  // Partial-failure capture: text was already produced and emitted
839
893
  // via [onTextReady], so a wallet / insufficient-points failure on
840
894
  // image or voice MUST NOT abort the whole turn. We collect the
@@ -875,7 +929,10 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
875
929
  typeof parsedIntent.giftOutfit === "object" &&
876
930
  typeof parsedIntent.giftOutfit.descriptionText === "string" &&
877
931
  parsedIntent.giftOutfit.descriptionText.trim().length > 0) {
878
- mediaTasks.push(this.giftOutfit(parsedIntent.giftOutfit.descriptionText.trim()).catch((e) => console.error("[CyberSoulClient] Auto giftOutfit failed:", e)));
932
+ mediaTasks.push(this.processGiftOutfit(parsedIntent.giftOutfit, params.onOutfitGifted).then((result) => {
933
+ if (result)
934
+ giftedOutfit = result;
935
+ }));
879
936
  }
880
937
  const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) &&
881
938
  (!isAuto || !!parsedIntent.imageParams);
@@ -978,6 +1035,7 @@ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent",
978
1035
  isEndTurn: parsedIntent.isEndTurn,
979
1036
  persistedDynamicContext,
980
1037
  mediaError,
1038
+ giftedOutfit,
981
1039
  };
982
1040
  }
983
1041
  catch (error) {
@@ -1179,6 +1237,7 @@ Modalities:
1179
1237
  ? "'imageParams' may be included only if sending a photo right now would feel natural for this character in this relationship — otherwise set null. Do not attach a photo just because you can."
1180
1238
  : "ALWAYS set 'imageParams' to null."}
1181
1239
  - ALWAYS set 'voiceArgs' to null.
1240
+ ${this.getOutfitAcquisitionPolicyPrompt()}
1182
1241
 
1183
1242
  Output ONLY a valid JSON object matching exactly this structure (no markdown wrappers).
1184
1243
  If "shouldSkipProactive" is true, set "skipReason" to one short sentence and set every other field to null.
@@ -1189,6 +1248,7 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1189
1248
  "actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY.",
1190
1249
  "textResponse": "Spoken dialogue ONLY.",
1191
1250
  "stateUpdate": { "temperatureDelta": 0, "ongoingScene": { "scene": "...", "outfit": "..." } },
1251
+ "giftOutfit": { "descriptionText": "Concise description of the newly acquired outfit to add into wardrobe." },
1192
1252
  ${this.getImageSchemaParams(imageAllowed)},
1193
1253
  "voiceArgs": null
1194
1254
  }`;
@@ -1254,6 +1314,11 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1254
1314
  stateUpdate: parsedIntent.stateUpdate,
1255
1315
  });
1256
1316
  }
1317
+ // Outfit acquisition: the character may decide, on its own, to pick
1318
+ // up a brand-new outfit while reaching out. Fire-and-capture in
1319
+ // parallel with image generation; surface it via callback + the
1320
+ // final response so upstream can show "New outfit added".
1321
+ const giftOutfitPromise = this.processGiftOutfit(parsedIntent.giftOutfit, params.onOutfitGifted);
1257
1322
  let finalImageUrl;
1258
1323
  let finalImageMediaId;
1259
1324
  let proactiveMediaError = null;
@@ -1289,6 +1354,7 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1289
1354
  }
1290
1355
  }
1291
1356
  const persistedDynamicContext = (await persistedStatePromise) ?? undefined;
1357
+ const giftedOutfit = (await giftOutfitPromise) ?? undefined;
1292
1358
  const proactiveMediaErrorEnv = proactiveMediaError
1293
1359
  ? this.buildMediaError(proactiveMediaError, proactiveAffected)
1294
1360
  : undefined;
@@ -1301,6 +1367,7 @@ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate"
1301
1367
  stateUpdate: parsedIntent.stateUpdate,
1302
1368
  persistedDynamicContext,
1303
1369
  mediaError: proactiveMediaErrorEnv,
1370
+ giftedOutfit,
1304
1371
  };
1305
1372
  }
1306
1373
  catch (error) {
@@ -1424,6 +1491,9 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
1424
1491
  }
1425
1492
  /**
1426
1493
  * Gift a new outfit to the character's wardrobe inventory.
1494
+ * Returns the number of wardrobe items the backend created (the
1495
+ * backend may expand a single description into multiple items), or
1496
+ * `undefined` when the server did not report a count.
1427
1497
  */
1428
1498
  async giftOutfit(descriptionText) {
1429
1499
  const res = await this.apiFetch("/api/v1/cyber-soul/characters/gift-outfit", {
@@ -1432,6 +1502,18 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
1432
1502
  });
1433
1503
  if (!res.ok)
1434
1504
  throw new Error("Failed to gift outfit");
1505
+ try {
1506
+ const body = (await res.json());
1507
+ return typeof body.count === "number" && Number.isFinite(body.count)
1508
+ ? body.count
1509
+ : undefined;
1510
+ }
1511
+ catch {
1512
+ // The gift already succeeded server-side (res.ok); a missing/
1513
+ // unparseable count is non-fatal — report "unknown" rather than
1514
+ // fabricating a number.
1515
+ return undefined;
1516
+ }
1435
1517
  }
1436
1518
  /**
1437
1519
  * Bootstrap character profile from OpenClaw workspace files.
package/dist/types.d.ts CHANGED
@@ -80,6 +80,26 @@ export interface MediaReadyPayload {
80
80
  /** Voice only — TTS duration in seconds when known. */
81
81
  durationSec?: number;
82
82
  }
83
+ /**
84
+ * Payload delivered by [InteractParams.onOutfitGifted] /
85
+ * [ProactiveParams.onOutfitGifted] when a new outfit is successfully
86
+ * added to the character's wardrobe during a turn. Fires for BOTH
87
+ * trigger paths: (a) the user explicitly gifts/buys an outfit, and
88
+ * (b) the conversation or an active event leads the character to
89
+ * acquire a brand-new outfit. Lets upstream consumers (e.g.
90
+ * cybersoul-chat) render a system message like
91
+ * "New outfit added to wardrobe".
92
+ */
93
+ export interface OutfitGiftedPayload {
94
+ /** Human-readable description of the newly acquired outfit. */
95
+ descriptionText: string;
96
+ /**
97
+ * Number of wardrobe items the backend created for this gift, when
98
+ * the server reported it. Omitted when the count is unknown — never
99
+ * fabricated.
100
+ */
101
+ count?: number;
102
+ }
83
103
  export interface ProactiveParams {
84
104
  history?: HistoryEntry[];
85
105
  maxUnreplied?: number;
@@ -95,6 +115,13 @@ export interface ProactiveParams {
95
115
  onStateReady?: (persisted: PersistedDynamicContext) => void;
96
116
  /** Fires per modality as each media task settles successfully. */
97
117
  onMediaReady?: (payload: MediaReadyPayload) => void;
118
+ /**
119
+ * Fires when an outfit has been successfully added to the wardrobe
120
+ * during this turn (user-initiated gift OR character-initiated
121
+ * acquisition). Lets the UI render a system message like
122
+ * "New outfit added to wardrobe" in real time.
123
+ */
124
+ onOutfitGifted?: (payload: OutfitGiftedPayload) => void;
98
125
  }
99
126
  export interface ProactiveResponse {
100
127
  status: "success" | "skipped" | "error";
@@ -113,6 +140,10 @@ export interface ProactiveResponse {
113
140
  * can still render the text reply and explain the missing media
114
141
  * without losing the conversation. See [InteractMediaError]. */
115
142
  mediaError?: InteractMediaError;
143
+ /** Set when an outfit was successfully added to the wardrobe this turn.
144
+ * Mirrors the [ProactiveParams.onOutfitGifted] callback for consumers
145
+ * that only read the final response. See [OutfitGiftedPayload]. */
146
+ giftedOutfit?: OutfitGiftedPayload;
116
147
  error?: string;
117
148
  }
118
149
  export interface InteractParams {
@@ -132,6 +163,13 @@ export interface InteractParams {
132
163
  onStateReady?: (persisted: PersistedDynamicContext) => void;
133
164
  /** Fires per modality as each media task settles successfully. */
134
165
  onMediaReady?: (payload: MediaReadyPayload) => void;
166
+ /**
167
+ * Fires when an outfit has been successfully added to the wardrobe
168
+ * during this turn (user-initiated gift OR character-initiated
169
+ * acquisition). Lets the UI render a system message like
170
+ * "New outfit added to wardrobe" in real time.
171
+ */
172
+ onOutfitGifted?: (payload: OutfitGiftedPayload) => void;
135
173
  }
136
174
  export interface OndemandEventParams {
137
175
  eventDescription: string;
@@ -173,6 +211,10 @@ export interface InteractResponse {
173
211
  imageMediaId?: string;
174
212
  audioUrl?: string;
175
213
  audioMediaId?: string;
214
+ /** Set when an outfit was successfully added to the wardrobe this turn.
215
+ * Mirrors the [InteractParams.onOutfitGifted] callback for consumers
216
+ * that only read the final response. See [OutfitGiftedPayload]. */
217
+ giftedOutfit?: OutfitGiftedPayload;
176
218
  likePreviousPicture?: boolean;
177
219
  durationSec?: number;
178
220
  triggeredEvent?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.22",
3
+ "version": "1.4.24",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",