@space3-npm/cybersoul-client 1.4.28 → 1.5.0

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.
@@ -0,0 +1,198 @@
1
+ import { CharacterState, DispatcherIntent, PersistedDynamicContext, SupportedLLMModel, WardrobeItem, LikedPicture, CoreMemory, UserCodex } from "../types.js";
2
+ import { CyberSoulError } from "../errors.js";
3
+ /**
4
+ * Configuration for the [CyberSoulApi] HTTP layer. Mirrors the relevant
5
+ * subset of [CyberSoulClientConfig] so the API module can be reused
6
+ * independently of the orchestration layer.
7
+ */
8
+ export interface CyberSoulApiConfig {
9
+ backendUrl: string;
10
+ characterKey: string;
11
+ requestTimeoutMs?: number;
12
+ maxRetries?: number;
13
+ /**
14
+ * Optional fetch override. When provided, the API layer uses this in
15
+ * place of the global `fetch` for every HTTP call. Intended for
16
+ * environments where the global fetch is suspended by the host
17
+ * platform — e.g. React Native on Samsung BBA / Doze — and a native
18
+ * HTTP path must be used instead. Must conform to the standard
19
+ * `fetch` signature.
20
+ */
21
+ fetchImpl?: typeof fetch;
22
+ }
23
+ /** Result of a successful image generation call. */
24
+ export interface GeneratedImage {
25
+ image_url: string;
26
+ id: string;
27
+ }
28
+ /** Result of a successful voice generation call. */
29
+ export interface GeneratedVoice {
30
+ audio_url: string;
31
+ id: string;
32
+ duration_sec?: number;
33
+ }
34
+ /** Payload accepted by PATCH /characters/dynamic-context. */
35
+ export interface DynamicContextPatchPayload {
36
+ temperature?: number;
37
+ temperatureAbsolute?: number;
38
+ ongoingScene?: {
39
+ scene: string;
40
+ outfit: string;
41
+ } | null;
42
+ userNickname?: string;
43
+ agentNickname?: string;
44
+ talkingStyle?: string;
45
+ userAnalysis?: DispatcherIntent["userAnalysis"];
46
+ }
47
+ /** Payload accepted by POST /characters/ondemand-event. */
48
+ export interface OndemandEventPayload {
49
+ eventTitle?: string;
50
+ eventDescription: string;
51
+ durationMins?: number;
52
+ outfitId?: string;
53
+ scheduledStartTimeStr?: string;
54
+ scheduledDateStr?: string;
55
+ }
56
+ /** Payload accepted by POST /characters/moments. */
57
+ export interface SaveMomentPayload {
58
+ summary: string;
59
+ date: string;
60
+ time: string;
61
+ likedPictures?: LikedPicture[];
62
+ }
63
+ /**
64
+ * Encapsulates every backend HTTP call used by the SDK. Owns the
65
+ * transport (timeout, retry, typed error mapping) and exposes one typed
66
+ * method per endpoint so the orchestration layer in `client.ts` never
67
+ * touches `fetch` or status codes directly.
68
+ *
69
+ * Error contract:
70
+ * - Transport-level failures are wrapped as [CyberSoulNetworkError] /
71
+ * [CyberSoulTimeoutError].
72
+ * - Backend typed failures (402 / wallet / sensitive-content / auth)
73
+ * are surfaced as the dedicated subclasses of [CyberSoulError] so
74
+ * callers can branch on `instanceof` instead of string-sniffing.
75
+ */
76
+ export declare class CyberSoulApi {
77
+ private backendUrl;
78
+ private characterKey;
79
+ private requestTimeoutMs;
80
+ private maxRetries;
81
+ private fetchImpl?;
82
+ constructor(config: CyberSoulApiConfig);
83
+ /**
84
+ * Internal wrapper for fetch that injects the backend URL and the
85
+ * Character Auth token, applies per-request timeout, and retries
86
+ * transient server-side failures (HTTP >= 500) for idempotent
87
+ * methods (GET / HEAD).
88
+ *
89
+ * Exposed as `public` so callers that need a raw `Response` (e.g. to
90
+ * branch on status without paying for an exception) can reuse the
91
+ * same transport path. Most consumers should prefer the typed
92
+ * helpers below.
93
+ */
94
+ apiFetch(endpoint: string, options?: RequestInit): Promise<Response>;
95
+ /**
96
+ * GET /api/v1/cyber-soul/state. Returns the full character state
97
+ * (identity, dynamic context, codex, memory, active event/wardrobe).
98
+ *
99
+ * 401/403 → [CyberSoulAuthError] (character may have been deleted).
100
+ * Other non-2xx → [CyberSoulApiError].
101
+ */
102
+ getState(): Promise<CharacterState>;
103
+ /**
104
+ * GET /api/v1/cyber-soul/wardrobe. Returns the raw wardrobe items;
105
+ * the caller is responsible for any caching / formatting. Resolves
106
+ * to an empty array when the backend returns no items or a non-2xx
107
+ * (failures are swallowed because wardrobe is best-effort context
108
+ * for prompt assembly — a missing list must not abort a chat turn).
109
+ */
110
+ getWardrobe(): Promise<WardrobeItem[]>;
111
+ /**
112
+ * POST /api/v1/cyber-soul/{type}/generate. Generates an image or a
113
+ * voice clip. Backend typed failures are surfaced as dedicated
114
+ * subclasses of [CyberSoulError] so callers can branch precisely:
115
+ * - 402 / INSUFFICIENT_POINTS → [CyberSoulInsufficientPointsError]
116
+ * - WALLET_DEDUCTION_ERROR → [CyberSoulWalletError]
117
+ * - E005 (sensitive content) → [CyberSoulSensitiveContentError]
118
+ * - 401 / 403 → [CyberSoulAuthError]
119
+ * - anything else → [CyberSoulApiError] (with legacy
120
+ * duck-typed `code` preserved)
121
+ */
122
+ generatePrimitive(type: "image" | "voice", payload: any): Promise<GeneratedImage & GeneratedVoice>;
123
+ /**
124
+ * PATCH /api/v1/cyber-soul/characters/dynamic-context.
125
+ *
126
+ * The server applies stage-based dampening, familiarity soft-caps,
127
+ * hard floor, and stage re-evaluation, then returns the
128
+ * *authoritative* persisted `temperature` and `relationshipStage`.
129
+ *
130
+ * Returns the post-write snapshot, or `null` when there's nothing
131
+ * to send / the request fails (failure is non-fatal for a chat
132
+ * turn — callers must treat `null` as "no fresh snapshot").
133
+ *
134
+ * The caller is responsible for any payload normalization (e.g.
135
+ * mapping `temperatureDelta` → `temperature`); this method sends
136
+ * `payload` as-is.
137
+ */
138
+ patchDynamicContext(payload: DynamicContextPatchPayload): Promise<PersistedDynamicContext | null>;
139
+ /**
140
+ * PATCH /api/v1/cyber-soul/characters/dynamic-context with an exact
141
+ * absolute temperature. Used by chat recall, where inverse deltas are
142
+ * not accurate once the backend has applied dampening, caps, and
143
+ * stage re-evaluation. The value is clamped to [0,100] with one
144
+ * decimal of precision before being sent.
145
+ *
146
+ * Returns `null` on transport failure or invalid input — strict,
147
+ * no silent fallback. Caller should treat `null` as "restore did
148
+ * not succeed" and surface the inconsistency.
149
+ */
150
+ restoreDynamicContextTemperature(temperatureAbsolute: number): Promise<PersistedDynamicContext | null>;
151
+ /**
152
+ * POST /api/v1/cyber-soul/characters/ondemand-event. Schedules an
153
+ * on-demand event. Throws when the backend rejects the schedule.
154
+ */
155
+ triggerOndemandEvent(payload: OndemandEventPayload): Promise<void>;
156
+ /**
157
+ * POST /api/v1/cyber-soul/characters/gift-outfit. Adds a new outfit
158
+ * (or several — the backend may expand a single description into
159
+ * multiple items) to the wardrobe inventory. Returns the number of
160
+ * items created, or `undefined` when the server didn't report a
161
+ * count (never fabricated).
162
+ */
163
+ giftOutfit(descriptionText: string): Promise<number | undefined>;
164
+ /**
165
+ * POST /api/v1/cyber-soul/characters/bootstrap. Bootstraps a
166
+ * character profile from OpenClaw workspace files.
167
+ */
168
+ bootstrapCharacter(workspaceFiles: Record<string, string>): Promise<void>;
169
+ /**
170
+ * POST /api/v1/cyber-soul/daily-script/generate. Triggers the
171
+ * backend to generate the daily script/plan for the character.
172
+ */
173
+ generateDailyScript(): Promise<void>;
174
+ /**
175
+ * GET /api/v1/cyber-soul/llm-models. Lists the public LLM models the
176
+ * backend currently supports, including each model's
177
+ * `customConfigDefinition` schema for `customSettings`.
178
+ */
179
+ listLLMModels(): Promise<SupportedLLMModel[]>;
180
+ /**
181
+ * POST /api/v1/cyber-soul/characters/moments. Saves a story moment
182
+ * so it can be picked up by the core-memory consolidation pass.
183
+ */
184
+ saveMoment(payload: SaveMomentPayload): Promise<void>;
185
+ /**
186
+ * PATCH /api/v1/cyber-soul/characters/core-memory. Replaces the
187
+ * consolidated core memory and user codex in one shot.
188
+ */
189
+ updateCoreMemory(payload: {
190
+ coreMemory: CoreMemory;
191
+ userCodex: UserCodex;
192
+ }): Promise<void>;
193
+ }
194
+ /**
195
+ * Convenience type guard re-exported so callers don't need to import
196
+ * from `errors.js` just to branch on a caught value.
197
+ */
198
+ export declare function isCyberSoulError(e: unknown): e is CyberSoulError;
@@ -0,0 +1,427 @@
1
+ import { CyberSoulApiError, CyberSoulAuthError, CyberSoulError, CyberSoulInsufficientPointsError, CyberSoulNetworkError, CyberSoulSensitiveContentError, CyberSoulTimeoutError, CyberSoulWalletError, } from "../errors.js";
2
+ /**
3
+ * Encapsulates every backend HTTP call used by the SDK. Owns the
4
+ * transport (timeout, retry, typed error mapping) and exposes one typed
5
+ * method per endpoint so the orchestration layer in `client.ts` never
6
+ * touches `fetch` or status codes directly.
7
+ *
8
+ * Error contract:
9
+ * - Transport-level failures are wrapped as [CyberSoulNetworkError] /
10
+ * [CyberSoulTimeoutError].
11
+ * - Backend typed failures (402 / wallet / sensitive-content / auth)
12
+ * are surfaced as the dedicated subclasses of [CyberSoulError] so
13
+ * callers can branch on `instanceof` instead of string-sniffing.
14
+ */
15
+ export class CyberSoulApi {
16
+ backendUrl;
17
+ characterKey;
18
+ requestTimeoutMs;
19
+ maxRetries;
20
+ fetchImpl;
21
+ constructor(config) {
22
+ this.backendUrl = config.backendUrl;
23
+ this.characterKey = config.characterKey;
24
+ this.requestTimeoutMs = config.requestTimeoutMs ?? 120000;
25
+ this.maxRetries = Math.max(0, config.maxRetries ?? 1);
26
+ this.fetchImpl = config.fetchImpl;
27
+ }
28
+ /* -------------------------------------------------------------------- */
29
+ /* Transport */
30
+ /* -------------------------------------------------------------------- */
31
+ /**
32
+ * Internal wrapper for fetch that injects the backend URL and the
33
+ * Character Auth token, applies per-request timeout, and retries
34
+ * transient server-side failures (HTTP >= 500) for idempotent
35
+ * methods (GET / HEAD).
36
+ *
37
+ * Exposed as `public` so callers that need a raw `Response` (e.g. to
38
+ * branch on status without paying for an exception) can reuse the
39
+ * same transport path. Most consumers should prefer the typed
40
+ * helpers below.
41
+ */
42
+ async apiFetch(endpoint, options = {}) {
43
+ const url = `${this.backendUrl}${endpoint}`;
44
+ const headers = {
45
+ Authorization: `Bearer ${this.characterKey}`,
46
+ "Content-Type": "application/json",
47
+ ...(options.headers || {}),
48
+ };
49
+ const method = (options.method || "GET").toUpperCase();
50
+ const isIdempotent = method === "GET" || method === "HEAD";
51
+ const retryLimit = isIdempotent ? this.maxRetries : 0;
52
+ let lastError;
53
+ for (let attempt = 0; attempt <= retryLimit; attempt++) {
54
+ const controller = new AbortController();
55
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
56
+ try {
57
+ // NOTE: When no custom fetchImpl is provided, fall back to the global
58
+ // `fetch` bound to `globalThis`. Browsers throw "Illegal invocation"
59
+ // if the global `fetch` is invoked while detached from its Window
60
+ // receiver (e.g. via a captured reference).
61
+ const fetchFn = this.fetchImpl ?? fetch.bind(globalThis);
62
+ const response = await fetchFn(url, {
63
+ ...options,
64
+ headers,
65
+ signal: controller.signal,
66
+ });
67
+ // Retry transient server-side failures only for idempotent methods.
68
+ if (response.status >= 500 && attempt < retryLimit) {
69
+ continue;
70
+ }
71
+ return response;
72
+ }
73
+ catch (error) {
74
+ if (error instanceof Error && error.name === "AbortError") {
75
+ lastError = new CyberSoulTimeoutError(endpoint, method, this.requestTimeoutMs);
76
+ }
77
+ else {
78
+ lastError = new CyberSoulNetworkError(endpoint, method, error instanceof Error
79
+ ? `Network request failed: ${method} ${endpoint}: ${error.message}`
80
+ : `Network request failed: ${method} ${endpoint}`, { cause: error });
81
+ }
82
+ if (attempt >= retryLimit) {
83
+ throw lastError;
84
+ }
85
+ }
86
+ finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+ // Defensive: the loop above either returns a Response, throws the
91
+ // wrapped network error, or continues to the next attempt. Reaching
92
+ // this point means the retry budget was exhausted without ever
93
+ // populating `lastError` (logically unreachable, but TypeScript
94
+ // cannot prove that).
95
+ throw lastError instanceof Error
96
+ ? lastError
97
+ : new CyberSoulNetworkError(endpoint, method, `Request failed unexpectedly: ${method} ${endpoint}`);
98
+ }
99
+ /* -------------------------------------------------------------------- */
100
+ /* State & wardrobe */
101
+ /* -------------------------------------------------------------------- */
102
+ /**
103
+ * GET /api/v1/cyber-soul/state. Returns the full character state
104
+ * (identity, dynamic context, codex, memory, active event/wardrobe).
105
+ *
106
+ * 401/403 → [CyberSoulAuthError] (character may have been deleted).
107
+ * Other non-2xx → [CyberSoulApiError].
108
+ */
109
+ async getState() {
110
+ const endpoint = "/api/v1/cyber-soul/state";
111
+ const res = await this.apiFetch(endpoint);
112
+ if (!res.ok) {
113
+ let body;
114
+ try {
115
+ body = await res.json();
116
+ }
117
+ catch {
118
+ body = undefined;
119
+ }
120
+ const detail = (body && typeof body === "object" && "error" in body
121
+ ? String(body.error)
122
+ : undefined) ?? `HTTP ${res.status}`;
123
+ if (res.status === 401 || res.status === 403) {
124
+ throw new CyberSoulAuthError(endpoint, "GET", res.status, `Character credential rejected by backend (${detail}). The character may have been deleted.`, body);
125
+ }
126
+ throw new CyberSoulApiError(endpoint, "GET", res.status, `Failed to fetch character state: ${detail}`, body);
127
+ }
128
+ const json = await res.json();
129
+ return json.data;
130
+ }
131
+ /**
132
+ * GET /api/v1/cyber-soul/wardrobe. Returns the raw wardrobe items;
133
+ * the caller is responsible for any caching / formatting. Resolves
134
+ * to an empty array when the backend returns no items or a non-2xx
135
+ * (failures are swallowed because wardrobe is best-effort context
136
+ * for prompt assembly — a missing list must not abort a chat turn).
137
+ */
138
+ async getWardrobe() {
139
+ const wardrobeRes = await this.apiFetch("/api/v1/cyber-soul/wardrobe");
140
+ if (!wardrobeRes.ok)
141
+ return [];
142
+ let payload = {};
143
+ try {
144
+ payload = await wardrobeRes.json();
145
+ }
146
+ catch (e) {
147
+ return [];
148
+ }
149
+ return Array.isArray(payload?.data) ? payload.data : [];
150
+ }
151
+ /* -------------------------------------------------------------------- */
152
+ /* Primitive media generation */
153
+ /* -------------------------------------------------------------------- */
154
+ /**
155
+ * POST /api/v1/cyber-soul/{type}/generate. Generates an image or a
156
+ * voice clip. Backend typed failures are surfaced as dedicated
157
+ * subclasses of [CyberSoulError] so callers can branch precisely:
158
+ * - 402 / INSUFFICIENT_POINTS → [CyberSoulInsufficientPointsError]
159
+ * - WALLET_DEDUCTION_ERROR → [CyberSoulWalletError]
160
+ * - E005 (sensitive content) → [CyberSoulSensitiveContentError]
161
+ * - 401 / 403 → [CyberSoulAuthError]
162
+ * - anything else → [CyberSoulApiError] (with legacy
163
+ * duck-typed `code` preserved)
164
+ */
165
+ async generatePrimitive(type, payload) {
166
+ const endpoint = `/api/v1/cyber-soul/${type}/generate`;
167
+ const res = await this.apiFetch(endpoint, {
168
+ method: "POST",
169
+ body: JSON.stringify(payload),
170
+ });
171
+ if (!res.ok) {
172
+ let errData;
173
+ try {
174
+ errData = await res.json();
175
+ }
176
+ catch (e) { }
177
+ const msg = errData?.message || errData?.error || `Status ${res.status}`;
178
+ const code = errData?.code || "UNKNOWN_ERROR";
179
+ const detailedMessage = `Failed to generate ${type}: ${msg}`;
180
+ if (res.status === 402 || code === "INSUFFICIENT_POINTS") {
181
+ throw new CyberSoulInsufficientPointsError(endpoint, "POST", res.status, detailedMessage, errData, code);
182
+ }
183
+ if (code === "WALLET_DEDUCTION_ERROR") {
184
+ throw new CyberSoulWalletError(endpoint, "POST", res.status, detailedMessage, errData, code);
185
+ }
186
+ if (code === "E005") {
187
+ throw new CyberSoulSensitiveContentError(endpoint, "POST", res.status, detailedMessage, errData, code);
188
+ }
189
+ if (res.status === 401 || res.status === 403) {
190
+ throw new CyberSoulAuthError(endpoint, "POST", res.status, detailedMessage, errData);
191
+ }
192
+ const apiErr = new CyberSoulApiError(endpoint, "POST", res.status, detailedMessage, errData);
193
+ // Preserve the legacy duck-typed `code` field so existing callers
194
+ // that branch on `e.code` (including this SDK's own `interact()`
195
+ // mediaTasks catch block) keep working unchanged.
196
+ apiErr.code = code;
197
+ throw apiErr;
198
+ }
199
+ return res.json();
200
+ }
201
+ /* -------------------------------------------------------------------- */
202
+ /* Dynamic context */
203
+ /* -------------------------------------------------------------------- */
204
+ /**
205
+ * PATCH /api/v1/cyber-soul/characters/dynamic-context.
206
+ *
207
+ * The server applies stage-based dampening, familiarity soft-caps,
208
+ * hard floor, and stage re-evaluation, then returns the
209
+ * *authoritative* persisted `temperature` and `relationshipStage`.
210
+ *
211
+ * Returns the post-write snapshot, or `null` when there's nothing
212
+ * to send / the request fails (failure is non-fatal for a chat
213
+ * turn — callers must treat `null` as "no fresh snapshot").
214
+ *
215
+ * The caller is responsible for any payload normalization (e.g.
216
+ * mapping `temperatureDelta` → `temperature`); this method sends
217
+ * `payload` as-is.
218
+ */
219
+ async patchDynamicContext(payload) {
220
+ if (payload.temperature === undefined &&
221
+ payload.temperatureAbsolute === undefined &&
222
+ payload.ongoingScene === undefined &&
223
+ payload.userNickname === undefined &&
224
+ payload.agentNickname === undefined &&
225
+ payload.talkingStyle === undefined &&
226
+ payload.userAnalysis === undefined) {
227
+ return null;
228
+ }
229
+ let res;
230
+ try {
231
+ res = await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
232
+ method: "PATCH",
233
+ body: JSON.stringify(payload),
234
+ });
235
+ }
236
+ catch (e) {
237
+ console.error("Failed to update dynamic context", e);
238
+ return null;
239
+ }
240
+ if (!res.ok) {
241
+ console.error(`Failed to update dynamic context: HTTP ${res.status}`);
242
+ return null;
243
+ }
244
+ try {
245
+ const body = (await res.json());
246
+ const temperature = typeof body.dynamicContext?.temperature === "number" &&
247
+ Number.isFinite(body.dynamicContext.temperature)
248
+ ? body.dynamicContext.temperature
249
+ : undefined;
250
+ const relationshipStage = typeof body.relationshipStage === "string"
251
+ ? body.relationshipStage
252
+ : undefined;
253
+ if (temperature === undefined && relationshipStage === undefined) {
254
+ return null;
255
+ }
256
+ return { temperature, relationshipStage };
257
+ }
258
+ catch (e) {
259
+ console.error("Failed to parse dynamic-context PATCH response", e);
260
+ return null;
261
+ }
262
+ }
263
+ /**
264
+ * PATCH /api/v1/cyber-soul/characters/dynamic-context with an exact
265
+ * absolute temperature. Used by chat recall, where inverse deltas are
266
+ * not accurate once the backend has applied dampening, caps, and
267
+ * stage re-evaluation. The value is clamped to [0,100] with one
268
+ * decimal of precision before being sent.
269
+ *
270
+ * Returns `null` on transport failure or invalid input — strict,
271
+ * no silent fallback. Caller should treat `null` as "restore did
272
+ * not succeed" and surface the inconsistency.
273
+ */
274
+ async restoreDynamicContextTemperature(temperatureAbsolute) {
275
+ if (!Number.isFinite(temperatureAbsolute))
276
+ return null;
277
+ const normalizedAbsolute = Math.max(0, Math.min(100, Math.round(temperatureAbsolute * 10) / 10));
278
+ try {
279
+ const res = await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
280
+ method: "PATCH",
281
+ body: JSON.stringify({ temperatureAbsolute: normalizedAbsolute }),
282
+ });
283
+ if (!res.ok)
284
+ return null;
285
+ const payload = (await res.json());
286
+ if (payload?.status !== "success")
287
+ return null;
288
+ if (typeof payload.dynamicContext?.temperature !== "number")
289
+ return null;
290
+ if (!Number.isFinite(payload.dynamicContext.temperature))
291
+ return null;
292
+ return {
293
+ temperature: payload.dynamicContext.temperature,
294
+ relationshipStage: typeof payload.relationshipStage === "string"
295
+ ? payload.relationshipStage
296
+ : undefined,
297
+ };
298
+ }
299
+ catch (e) {
300
+ console.error("restoreDynamicContextTemperature failed", e);
301
+ return null;
302
+ }
303
+ }
304
+ /* -------------------------------------------------------------------- */
305
+ /* Events, wardrobe writes, lifecycle */
306
+ /* -------------------------------------------------------------------- */
307
+ /**
308
+ * POST /api/v1/cyber-soul/characters/ondemand-event. Schedules an
309
+ * on-demand event. Throws when the backend rejects the schedule.
310
+ */
311
+ async triggerOndemandEvent(payload) {
312
+ const res = await this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
313
+ method: "POST",
314
+ body: JSON.stringify(payload),
315
+ });
316
+ if (!res.ok) {
317
+ throw new Error("Backend failed to schedule the on-demand event");
318
+ }
319
+ }
320
+ /**
321
+ * POST /api/v1/cyber-soul/characters/gift-outfit. Adds a new outfit
322
+ * (or several — the backend may expand a single description into
323
+ * multiple items) to the wardrobe inventory. Returns the number of
324
+ * items created, or `undefined` when the server didn't report a
325
+ * count (never fabricated).
326
+ */
327
+ async giftOutfit(descriptionText) {
328
+ const res = await this.apiFetch("/api/v1/cyber-soul/characters/gift-outfit", {
329
+ method: "POST",
330
+ body: JSON.stringify({ text: descriptionText }),
331
+ });
332
+ if (!res.ok)
333
+ throw new Error("Failed to gift outfit");
334
+ try {
335
+ const body = (await res.json());
336
+ return typeof body.count === "number" && Number.isFinite(body.count)
337
+ ? body.count
338
+ : undefined;
339
+ }
340
+ catch {
341
+ // The gift already succeeded server-side (res.ok); a missing/
342
+ // unparseable count is non-fatal — report "unknown" rather than
343
+ // fabricating a number.
344
+ return undefined;
345
+ }
346
+ }
347
+ /**
348
+ * POST /api/v1/cyber-soul/characters/bootstrap. Bootstraps a
349
+ * character profile from OpenClaw workspace files.
350
+ */
351
+ async bootstrapCharacter(workspaceFiles) {
352
+ const res = await this.apiFetch("/api/v1/cyber-soul/characters/bootstrap", {
353
+ method: "POST",
354
+ body: JSON.stringify({ workspace_files: workspaceFiles }),
355
+ });
356
+ if (!res.ok)
357
+ throw new Error("Failed to bootstrap character");
358
+ }
359
+ /**
360
+ * POST /api/v1/cyber-soul/daily-script/generate. Triggers the
361
+ * backend to generate the daily script/plan for the character.
362
+ */
363
+ async generateDailyScript() {
364
+ const res = await this.apiFetch("/api/v1/cyber-soul/daily-script/generate", {
365
+ method: "POST",
366
+ });
367
+ if (!res.ok)
368
+ throw new Error("Failed to generate daily script");
369
+ }
370
+ /* -------------------------------------------------------------------- */
371
+ /* LLM models */
372
+ /* -------------------------------------------------------------------- */
373
+ /**
374
+ * GET /api/v1/cyber-soul/llm-models. Lists the public LLM models the
375
+ * backend currently supports, including each model's
376
+ * `customConfigDefinition` schema for `customSettings`.
377
+ */
378
+ async listLLMModels() {
379
+ const res = await this.apiFetch("/api/v1/cyber-soul/llm-models");
380
+ if (!res.ok) {
381
+ throw new Error(`Failed to list supported LLMs: ${res.status}`);
382
+ }
383
+ const body = (await res.json());
384
+ if (Array.isArray(body))
385
+ return body;
386
+ if (body && typeof body === "object" && Array.isArray(body.data)) {
387
+ return body.data;
388
+ }
389
+ throw new Error("Unexpected response shape from /llm-models");
390
+ }
391
+ /* -------------------------------------------------------------------- */
392
+ /* Memory pipeline */
393
+ /* -------------------------------------------------------------------- */
394
+ /**
395
+ * POST /api/v1/cyber-soul/characters/moments. Saves a story moment
396
+ * so it can be picked up by the core-memory consolidation pass.
397
+ */
398
+ async saveMoment(payload) {
399
+ const res = await this.apiFetch("/api/v1/cyber-soul/characters/moments", {
400
+ method: "POST",
401
+ body: JSON.stringify(payload),
402
+ });
403
+ if (!res.ok) {
404
+ throw new Error("Failed to save character moment.");
405
+ }
406
+ }
407
+ /**
408
+ * PATCH /api/v1/cyber-soul/characters/core-memory. Replaces the
409
+ * consolidated core memory and user codex in one shot.
410
+ */
411
+ async updateCoreMemory(payload) {
412
+ const res = await this.apiFetch("/api/v1/cyber-soul/characters/core-memory", {
413
+ method: "PATCH",
414
+ body: JSON.stringify(payload),
415
+ });
416
+ if (!res.ok) {
417
+ throw new Error(`Failed to update core memory. Status: ${res.status}`);
418
+ }
419
+ }
420
+ }
421
+ /**
422
+ * Convenience type guard re-exported so callers don't need to import
423
+ * from `errors.js` just to branch on a caught value.
424
+ */
425
+ export function isCyberSoulError(e) {
426
+ return e instanceof CyberSoulError;
427
+ }