@space3-npm/cybersoul-client 1.4.8 → 1.4.11

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
@@ -79,7 +79,15 @@ export declare class CyberSoulClient {
79
79
  ondemandEvent(params: OndemandEventParams): Promise<OndemandEventResponse>;
80
80
  /**
81
81
  * Generates a proactive message when the user hasn't responded.
82
- * Safely prevents spamming, and adjusts its approach based on relationship dynamics.
82
+ *
83
+ * Design:
84
+ * - Code owns ONE objective rule: don't spam (cap consecutive un-replied
85
+ * messages). Everything else is a social judgment.
86
+ * - The LLM owns the social judgment — given full character context
87
+ * (stage, temperature, traits, ongoing scene, time since last
88
+ * interaction, recent history), it answers a single question:
89
+ * "Would I, as this person right now, actually reach out?"
90
+ * Skip is the default; speaking is the exception.
83
91
  */
84
92
  proactiveInteract(params: ProactiveParams): Promise<ProactiveResponse>;
85
93
  /**
package/dist/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { InteractRequestType, } from "./types.js";
2
2
  import { robustJsonParse } from "./utils/json.utils.js";
3
3
  import { GenericLLMProvider } from "./llm.provider.js";
4
+ import { CyberSoulApiError, CyberSoulAuthError, CyberSoulInsufficientPointsError, CyberSoulNetworkError, CyberSoulTimeoutError, CyberSoulWalletError, } from "./errors.js";
4
5
  export class CyberSoulClient {
5
6
  config;
6
7
  llm;
@@ -47,10 +48,12 @@ export class CyberSoulClient {
47
48
  }
48
49
  catch (error) {
49
50
  if (error instanceof Error && error.name === "AbortError") {
50
- lastError = new Error(`Request timed out after ${this.requestTimeoutMs}ms: ${method} ${endpoint}`);
51
+ lastError = new CyberSoulTimeoutError(endpoint, method, this.requestTimeoutMs);
51
52
  }
52
53
  else {
53
- lastError = error;
54
+ lastError = new CyberSoulNetworkError(endpoint, method, error instanceof Error
55
+ ? `Network request failed: ${method} ${endpoint}: ${error.message}`
56
+ : `Network request failed: ${method} ${endpoint}`, { cause: error });
54
57
  }
55
58
  if (attempt >= retryLimit) {
56
59
  throw lastError;
@@ -60,14 +63,34 @@ export class CyberSoulClient {
60
63
  clearTimeout(timeout);
61
64
  }
62
65
  }
66
+ // Defensive: the loop above either returns a Response, throws the
67
+ // wrapped network error, or continues to the next attempt. Reaching
68
+ // this point means the retry budget was exhausted without ever
69
+ // populating `lastError` (logically unreachable, but TypeScript
70
+ // cannot prove that).
63
71
  throw lastError instanceof Error
64
72
  ? lastError
65
- : new Error("Request failed unexpectedly");
73
+ : new CyberSoulNetworkError(endpoint, method, `Request failed unexpectedly: ${method} ${endpoint}`);
66
74
  }
67
75
  async fetchRemoteState() {
68
- const res = await this.apiFetch("/api/v1/cyber-soul/state");
69
- if (!res.ok)
70
- throw new Error("Failed to fetch character state");
76
+ const endpoint = "/api/v1/cyber-soul/state";
77
+ const res = await this.apiFetch(endpoint);
78
+ if (!res.ok) {
79
+ let body;
80
+ try {
81
+ body = await res.json();
82
+ }
83
+ catch {
84
+ body = undefined;
85
+ }
86
+ const detail = (body && typeof body === "object" && "error" in body
87
+ ? String(body.error)
88
+ : undefined) ?? `HTTP ${res.status}`;
89
+ if (res.status === 401 || res.status === 403) {
90
+ throw new CyberSoulAuthError(endpoint, "GET", res.status, `Character credential rejected by backend (${detail}). The character may have been deleted.`, body);
91
+ }
92
+ throw new CyberSoulApiError(endpoint, "GET", res.status, `Failed to fetch character state: ${detail}`, body);
93
+ }
71
94
  const json = await res.json();
72
95
  return json.data;
73
96
  }
@@ -99,7 +122,8 @@ export class CyberSoulClient {
99
122
  return availableOutfits;
100
123
  }
101
124
  async generatePrimitive(type, payload) {
102
- const res = await this.apiFetch(`/api/v1/cyber-soul/${type}/generate`, {
125
+ const endpoint = `/api/v1/cyber-soul/${type}/generate`;
126
+ const res = await this.apiFetch(endpoint, {
103
127
  method: "POST",
104
128
  body: JSON.stringify(payload),
105
129
  });
@@ -110,9 +134,23 @@ export class CyberSoulClient {
110
134
  }
111
135
  catch (e) { }
112
136
  const msg = errData?.message || errData?.error || `Status ${res.status}`;
113
- const err = new Error(`Failed to generate ${type}: ${msg}`);
114
- err.code = errData?.code || "UNKNOWN_ERROR";
115
- throw err;
137
+ const code = errData?.code || "UNKNOWN_ERROR";
138
+ const detailedMessage = `Failed to generate ${type}: ${msg}`;
139
+ if (res.status === 402 || code === "INSUFFICIENT_POINTS") {
140
+ throw new CyberSoulInsufficientPointsError(endpoint, "POST", res.status, detailedMessage, errData, code);
141
+ }
142
+ if (code === "WALLET_DEDUCTION_ERROR") {
143
+ throw new CyberSoulWalletError(endpoint, "POST", res.status, detailedMessage, errData, code);
144
+ }
145
+ if (res.status === 401 || res.status === 403) {
146
+ throw new CyberSoulAuthError(endpoint, "POST", res.status, detailedMessage, errData);
147
+ }
148
+ const apiErr = new CyberSoulApiError(endpoint, "POST", res.status, detailedMessage, errData);
149
+ // Preserve the legacy duck-typed `code` field so existing callers
150
+ // that branch on `e.code` (including this SDK's own `interact()`
151
+ // mediaTasks catch block) keep working unchanged.
152
+ apiErr.code = code;
153
+ throw apiErr;
116
154
  }
117
155
  return res.json();
118
156
  }
@@ -861,137 +899,139 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
861
899
  }
862
900
  /**
863
901
  * Generates a proactive message when the user hasn't responded.
864
- * Safely prevents spamming, and adjusts its approach based on relationship dynamics.
902
+ *
903
+ * Design:
904
+ * - Code owns ONE objective rule: don't spam (cap consecutive un-replied
905
+ * messages). Everything else is a social judgment.
906
+ * - The LLM owns the social judgment — given full character context
907
+ * (stage, temperature, traits, ongoing scene, time since last
908
+ * interaction, recent history), it answers a single question:
909
+ * "Would I, as this person right now, actually reach out?"
910
+ * Skip is the default; speaking is the exception.
865
911
  */
866
912
  async proactiveInteract(params) {
867
913
  try {
868
- // 1. Cold Interaction Protection (Logic-based fallback)
914
+ // 1. Spam guard (the only hard-coded gate). Counts assistant messages
915
+ // since the last user reply; bails out if the user has clearly
916
+ // stopped responding.
869
917
  const history = params.history || [];
870
918
  const maxUnreplied = params.maxUnreplied ?? 2;
871
919
  let consecutiveProactive = 0;
872
- // Start from the most recent message
873
920
  for (let i = history.length - 1; i >= 0; i--) {
874
921
  const msg = history[i];
875
- if (msg.role === 'user') {
876
- break; // User responded, streak broken
877
- }
878
- if (msg.role === 'assistant') {
922
+ if (msg.role === "user")
923
+ break;
924
+ if (msg.role === "assistant")
879
925
  consecutiveProactive++;
880
- }
881
926
  }
882
927
  if (consecutiveProactive >= maxUnreplied) {
883
928
  return {
884
929
  status: "skipped",
885
- reason: `User is busy. ${consecutiveProactive} consecutive proactive messages ignored.`
930
+ reason: `Spam guard: ${consecutiveProactive} consecutive un-replied messages already sent.`,
886
931
  };
887
932
  }
888
- // 2. Fetch current character state
933
+ // 2. Fetch state. baseContext below already includes stage,
934
+ // temperature, traits, ongoing scene, active/next event, current
935
+ // time, and lastInteractionAt — the LLM has everything it needs to
936
+ // make the social call without us restating it.
889
937
  const [state, availableOutfits] = await Promise.all([
890
938
  this.fetchRemoteState(),
891
- this.getWardrobePromptStr()
939
+ this.getWardrobePromptStr(),
892
940
  ]);
893
- // 3. Evaluate behavioral approach based on relationship and personality
894
- const dyn = state.dynamic_context || {};
895
- const stage = state.relationship_stage || "STRANGER";
896
- const temperature = dyn.temperature ?? 0;
897
- const userTraits = state.user_codex?.psychological?.traits?.join(", ") || "";
898
- let interrogationStrategy = "Do not ask 'are you there?' or 'why aren't you answering?'. Just share your current status, a passing thought, complain whimsically, or tease the user naturally like a real partner.";
899
- if (stage === "PARTNER" || (stage === "INTIMATE" && temperature > 70)) {
900
- // PARTNER (>85) or High INTIMATE (>70)
901
- interrogationStrategy = "Because you are deeply intimate and highly affectionate, you MISS them. You MAY organically 'interrogate' or pout playfully about why they are ignoring you (e.g., 'Are you too busy for me?', 'Still ignoring your girl?'). Act like a real, slightly needy/attached partner.";
902
- }
903
- else if (stage === "INTIMATE" || stage === "WARM") {
904
- // Low INTIMATE (60-70) or WARM (40-60)
905
- interrogationStrategy = "Because you are close but currently feeling neglected or cold, you notice they are ignoring you. You MAY be passive-aggressive or cross-examine them coldly (e.g., 'So we're just not talking today?', 'Fine, keep ignoring me.').";
906
- }
907
- else if (stage === "COLD" || stage === "ACQUAINTANCE" || stage === "STRANGER") {
908
- // COLD (<40)
909
- interrogationStrategy = "You are distant. Do NOT double-text with neediness. If you must speak, make it a detached observation or a cold administrative remark.";
910
- }
911
- // History/Context awareness prompt
912
- const historyAwarenessPrompt = `CRITICAL CONTEXT AWARENESS: Read the CHAT HISTORY above carefully. Remember that YOU sent the last message. Your new message MUST feel organically connected to the flow of what you two were previously talking about, or naturally bring up a known event/topic from your [CORE MEMORY]. Do not sound like a robot reading a log.`;
913
- // 4. Build a Proactive-specific System Prompt
914
941
  const baseContext = this.buildStateContextPrompt(state, true);
915
942
  const types = this.normalizeRequestTypes(params.requestTypes);
916
943
  const requestedOthers = types.filter((t) => t !== InteractRequestType.AUTO && t !== InteractRequestType.TEXT);
917
- // Determine modalities (reusing logic from interact)
918
- let modalitiesInstruction = "You are initiating conversation without a preceding user message.\\n";
919
- if (requestedOthers.includes(InteractRequestType.IMAGE)) {
920
- modalitiesInstruction += " - Include 'imageParams' for visual/photo moments. CRITICAL POLICY: NEVER send pictures to strangers! If Stage is STRANGER or COLD, or Familiarity is very low (< 10), ALWAYS set 'imageParams' to null.\\n";
921
- }
922
- else {
923
- modalitiesInstruction += " - ALWAYS set 'imageParams' to null.\\n";
924
- }
944
+ const imageAllowed = requestedOthers.includes(InteractRequestType.IMAGE);
945
+ // 3. Build the prompt. We deliberately ask ONE coherent question
946
+ // framed in-character ("would I text right now?") rather than
947
+ // handing the LLM a checklist. The character's own traits,
948
+ // relationship state, and recent transcript are the inputs.
925
949
  const systemPrompt = `${baseContext}
926
950
 
927
- [PROACTIVE INITIATION TASK]
928
- The user has NOT spoken to you recently. You sent the last message in the chat history, and they haven't replied. You are deciding to follow up proactively.
929
- If you decide that based on your current mood and the relationship stage it's better not to send a message right now (e.g. you are cold and giving them space), you can skip this proactive message by setting "shouldSkipProactive" to true.
930
- ${interrogationStrategy}
931
- ${historyAwarenessPrompt}
932
- Consider the user's known traits (${userTraits}) when choosing how to act. Need to keep it strictly under 2-3 sentences max.
951
+ [PROACTIVE OPPORTUNITY]
952
+ Time has passed since the last message in [CHAT HISTORY] and the user has not replied. You have an OPPORTUNITY (not an obligation) to send them a message. Decide, in character, whether you would actually do that.
953
+
954
+ [HOW TO DECIDE — THINK LIKE THE PERSON YOU ARE]
955
+ Real humans rarely send unprompted messages. Most of the time, silence is the right answer. Reach out ONLY if a real person with YOUR personality, in YOUR relationship to this user, at THIS moment, would genuinely feel moved to text.
956
+
957
+ Reasons NOT to reach out (set "shouldSkipProactive": true):
958
+ - The last exchange ended on a note that closes the door — a farewell, a brush-off, a fight, a "talk later", an explicit dismissal — from either side. If YOU pushed them away last turn (because of your traits or a fight), staying quiet IS the in-character choice; flipping to friendly now makes you look bipolar.
959
+ - Your relationship is too distant for unsolicited contact (e.g. STRANGER, COLD) or your current mood is too low to want to reach out.
960
+ - Too little time has passed since the last message for a follow-up to feel natural. Use the time gap shown in [CHAT HISTORY] — minutes after the last turn is almost always too soon.
961
+ - There is no genuine reason to text — no shared thread, no event, no thought that would actually push a real person to pick up the phone.
962
+ - It's the wrong time of day for this relationship.
963
+
964
+ When in doubt: SKIP. The bar for reaching out is high.
965
+
966
+ [IF YOU DO DECIDE TO REACH OUT]
967
+ Speak strictly in character — your traits, communication style, and current mood dictate the tone. Do NOT default to needy/cheerful unless that's who you are. Connect naturally to the last topic or to your current scene/event. Keep it to 2-3 short sentences. Never ask "are you there?" or "why aren't you answering?".
933
968
 
934
969
  Available Wardrobe Outfits:
935
970
  ${availableOutfits}
936
971
 
937
- ${modalitiesInstruction}
938
- You MUST output ONLY a valid JSON object matching exactly this structure:
972
+ Modalities:
973
+ - 'textResponse' is required when you proceed.
974
+ - ${imageAllowed
975
+ ? "'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."
976
+ : "ALWAYS set 'imageParams' to null."}
977
+ - ALWAYS set 'voiceArgs' to null.
978
+
979
+ Output ONLY a valid JSON object matching exactly this structure (no markdown wrappers).
980
+ If "shouldSkipProactive" is true, set "skipReason" to one short sentence and set every other field to null.
981
+ If "shouldSkipProactive" is false, "textResponse" is required and "stateUpdate" must be provided; include "ongoingScene" only if your scene/outfit actually changed, otherwise omit it.
939
982
  {
940
983
  "shouldSkipProactive": false,
941
- "skipReason": "(Optional. Reason for skipping if shouldSkipProactive is true)",
984
+ "skipReason": null,
942
985
  "actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY.",
943
986
  "textResponse": "Spoken dialogue ONLY.",
944
- "stateUpdate": { "temperatureDelta": 1, "ongoingScene": { "scene": "...", "outfit": "..." } },
945
- ${this.getImageSchemaParams(requestedOthers.includes(InteractRequestType.IMAGE))},
987
+ "stateUpdate": { "temperatureDelta": 0, "ongoingScene": { "scene": "...", "outfit": "..." } },
988
+ ${this.getImageSchemaParams(imageAllowed)},
946
989
  "voiceArgs": null
947
990
  }`;
948
- const transcript = params.history && params.history.length > 0 ? this.buildHistoryTranscript(params.history, state) : "";
949
- const harnessContext = params.localContext ? `[ADDITIONAL SCENE CONTEXT]\n${params.localContext}\n\n` : "";
991
+ const transcript = params.history && params.history.length > 0
992
+ ? this.buildHistoryTranscript(params.history, state)
993
+ : "";
994
+ const harnessContext = params.localContext
995
+ ? `[ADDITIONAL SCENE CONTEXT]\n${params.localContext}\n\n`
996
+ : "";
950
997
  const promptMessages = [
951
998
  { role: "system", content: systemPrompt },
952
999
  {
953
1000
  role: "user",
954
- content: `${harnessContext}${transcript}\n[TRIGGER PROACTIVE MESSAGE]\nBased on your active event and environment, send a new message to the user.\n\nCRITICAL: Output ONLY valid JSON matching the schema. DO NOT wrap the JSON in \`\`\`json.`
955
- }
1001
+ content: `${harnessContext}${transcript}\n[DECIDE NOW]\nWould you, as this character, actually send a message right now? Answer in the JSON schema above.`,
1002
+ },
956
1003
  ];
957
- // 5. Generate with LLM using a confident temperature
958
- const rawLlmResponse = await this.llm.generate(promptMessages, 800, 0.7);
959
- let parsedIntent;
960
- try {
961
- parsedIntent = robustJsonParse(rawLlmResponse, "Proactive fallback");
962
- }
963
- catch (e) {
964
- parsedIntent = { textResponse: rawLlmResponse.replace(/^[\`\s]+|[\`\s]+$/g, "").trim() };
965
- }
1004
+ // 4. LLM decides. Lower temperature than `interact` because this is a
1005
+ // judgment call, not creative reply.
1006
+ const rawLlmResponse = await this.llm.generate(promptMessages, 800, 0.5);
1007
+ // Fail fast on parse error. A proactive message is opt-in by design;
1008
+ // if the LLM produced unparseable output we'd rather skip than ship
1009
+ // raw scaffolding to the user.
1010
+ const parsedIntent = robustJsonParse(rawLlmResponse, "Proactive fallback");
966
1011
  if (parsedIntent.shouldSkipProactive) {
967
1012
  return {
968
1013
  status: "skipped",
969
- reason: parsedIntent.skipReason || "Character decided to skip proactive message based on mood/stage."
1014
+ reason: parsedIntent.skipReason || "Character chose not to reach out.",
1015
+ };
1016
+ }
1017
+ if (typeof parsedIntent.textResponse !== "string" || parsedIntent.textResponse.trim().length === 0) {
1018
+ return {
1019
+ status: "skipped",
1020
+ reason: "LLM produced no textResponse (treated as implicit skip).",
970
1021
  };
971
1022
  }
972
- // Update Remote state if needed (capture promise for authoritative
973
- // server snapshot — see notes in interact()).
1023
+ // 5. Persist state and optionally generate image, in parallel.
974
1024
  let persistedStatePromise = Promise.resolve(null);
975
1025
  if (parsedIntent.stateUpdate) {
976
1026
  persistedStatePromise = this._updateDynamicContextInternal(parsedIntent.stateUpdate);
977
1027
  }
978
- const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
979
- parsedIntent.textResponse.trim().length > 0
980
- ? parsedIntent.textResponse
981
- : "...";
982
- // Fire text ready callback if provided
983
- if (params.onTextReady && (resolvedTextResponse || parsedIntent.actionText)) {
984
- params.onTextReady(resolvedTextResponse, parsedIntent.actionText, {
1028
+ if (params.onTextReady) {
1029
+ params.onTextReady(parsedIntent.textResponse, parsedIntent.actionText, {
985
1030
  stateUpdate: parsedIntent.stateUpdate,
986
- userAnalysis: parsedIntent.userAnalysis,
987
- isEndTurn: parsedIntent.isEndTurn,
988
- triggerEvent: parsedIntent.triggerEvent,
989
- likePreviousPicture: parsedIntent.likePreviousPicture,
990
1031
  });
991
1032
  }
992
- // Handle Optional Media (Image only for proactive to save compute normally, but you can extend)
993
- let finalImageUrl = undefined;
994
- let finalImageMediaId = undefined;
1033
+ let finalImageUrl;
1034
+ let finalImageMediaId;
995
1035
  if (parsedIntent.imageParams) {
996
1036
  try {
997
1037
  const res = await this.generatePrimitive("image", parsedIntent.imageParams);
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Typed error hierarchy thrown by the CyberSoul SDK.
3
+ *
4
+ * Consumers should branch on `instanceof` rather than parsing error
5
+ * messages — message strings are not a stable API.
6
+ */
7
+ export declare class CyberSoulError extends Error {
8
+ /** Stable discriminator usable in serialized logs / event payloads. */
9
+ readonly kind: string;
10
+ constructor(kind: string, message: string, options?: {
11
+ cause?: unknown;
12
+ });
13
+ }
14
+ /**
15
+ * The underlying fetch call never produced a response (DNS failure,
16
+ * connection refused, TLS error, offline, etc.). The remote service may
17
+ * or may not have processed the request; callers should treat this as a
18
+ * transient failure and retry later when connectivity is restored.
19
+ */
20
+ export declare class CyberSoulNetworkError extends CyberSoulError {
21
+ readonly endpoint: string;
22
+ readonly method: string;
23
+ constructor(endpoint: string, method: string, message: string, options?: {
24
+ cause?: unknown;
25
+ });
26
+ }
27
+ /**
28
+ * The request was aborted because it exceeded the configured
29
+ * `requestTimeoutMs`. A specialization of network error — callers that
30
+ * want to distinguish "no connection" from "slow connection" can use
31
+ * `instanceof CyberSoulTimeoutError`.
32
+ */
33
+ export declare class CyberSoulTimeoutError extends CyberSoulNetworkError {
34
+ readonly timeoutMs: number;
35
+ constructor(endpoint: string, method: string, timeoutMs: number);
36
+ }
37
+ /**
38
+ * The backend returned a non-2xx HTTP response. Callers can inspect
39
+ * `status` to decide how to react (e.g. surface a user-facing message
40
+ * for 4xx vs. retry on 5xx).
41
+ */
42
+ export declare class CyberSoulApiError extends CyberSoulError {
43
+ readonly status: number;
44
+ readonly endpoint: string;
45
+ readonly method: string;
46
+ /** Parsed JSON body when available, otherwise `undefined`. */
47
+ readonly body?: unknown;
48
+ constructor(endpoint: string, method: string, status: number, message: string, body?: unknown, kind?: string);
49
+ }
50
+ /**
51
+ * The backend rejected the SDK's character credential (HTTP 401/403 on
52
+ * a character-scoped endpoint). The most common cause is that the
53
+ * character profile bound to this `characterKey` has been deleted on
54
+ * the backend; the binding is effectively terminal for this client.
55
+ */
56
+ export declare class CyberSoulAuthError extends CyberSoulApiError {
57
+ constructor(endpoint: string, method: string, status: number, message: string, body?: unknown);
58
+ }
59
+ /**
60
+ * The backend rejected a paid action because the user's wallet does
61
+ * not have enough points to cover it (HTTP 402 / `INSUFFICIENT_POINTS`).
62
+ * Callers should surface a top-up prompt rather than retrying.
63
+ */
64
+ export declare class CyberSoulInsufficientPointsError extends CyberSoulApiError {
65
+ /**
66
+ * Backend-supplied machine code. Always `"INSUFFICIENT_POINTS"` today;
67
+ * kept as a field so future variants (e.g. per-feature paywalls) can
68
+ * piggy-back on the same class.
69
+ */
70
+ readonly code: string;
71
+ constructor(endpoint: string, method: string, status: number, message: string, body?: unknown, code?: string);
72
+ }
73
+ /**
74
+ * The wallet-deduction call failed for a reason *other than* insufficient
75
+ * balance (e.g. wallet service unavailable, accounting bug). Distinct from
76
+ * `CyberSoulInsufficientPointsError` because the user can't fix this by
77
+ * topping up — it's an upstream infrastructure issue.
78
+ */
79
+ export declare class CyberSoulWalletError extends CyberSoulApiError {
80
+ readonly code: string;
81
+ constructor(endpoint: string, method: string, status: number, message: string, body?: unknown, code?: string);
82
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Typed error hierarchy thrown by the CyberSoul SDK.
3
+ *
4
+ * Consumers should branch on `instanceof` rather than parsing error
5
+ * messages — message strings are not a stable API.
6
+ */
7
+ export class CyberSoulError extends Error {
8
+ /** Stable discriminator usable in serialized logs / event payloads. */
9
+ kind;
10
+ constructor(kind, message, options) {
11
+ super(message);
12
+ this.name = new.target.name;
13
+ this.kind = kind;
14
+ if (options?.cause !== undefined) {
15
+ // ES2022 `cause` field. We assign defensively so older targets
16
+ // don't drop it on the floor.
17
+ this.cause = options.cause;
18
+ }
19
+ // Preserve prototype chain for downlevel-transpiled `extends`.
20
+ Object.setPrototypeOf(this, new.target.prototype);
21
+ }
22
+ }
23
+ /**
24
+ * The underlying fetch call never produced a response (DNS failure,
25
+ * connection refused, TLS error, offline, etc.). The remote service may
26
+ * or may not have processed the request; callers should treat this as a
27
+ * transient failure and retry later when connectivity is restored.
28
+ */
29
+ export class CyberSoulNetworkError extends CyberSoulError {
30
+ endpoint;
31
+ method;
32
+ constructor(endpoint, method, message, options) {
33
+ super("network", message, options);
34
+ this.endpoint = endpoint;
35
+ this.method = method;
36
+ }
37
+ }
38
+ /**
39
+ * The request was aborted because it exceeded the configured
40
+ * `requestTimeoutMs`. A specialization of network error — callers that
41
+ * want to distinguish "no connection" from "slow connection" can use
42
+ * `instanceof CyberSoulTimeoutError`.
43
+ */
44
+ export class CyberSoulTimeoutError extends CyberSoulNetworkError {
45
+ timeoutMs;
46
+ constructor(endpoint, method, timeoutMs) {
47
+ super(endpoint, method, `Request timed out after ${timeoutMs}ms: ${method} ${endpoint}`);
48
+ this.timeoutMs = timeoutMs;
49
+ }
50
+ }
51
+ /**
52
+ * The backend returned a non-2xx HTTP response. Callers can inspect
53
+ * `status` to decide how to react (e.g. surface a user-facing message
54
+ * for 4xx vs. retry on 5xx).
55
+ */
56
+ export class CyberSoulApiError extends CyberSoulError {
57
+ status;
58
+ endpoint;
59
+ method;
60
+ /** Parsed JSON body when available, otherwise `undefined`. */
61
+ body;
62
+ constructor(endpoint, method, status, message, body, kind = "api") {
63
+ super(kind, message);
64
+ this.status = status;
65
+ this.endpoint = endpoint;
66
+ this.method = method;
67
+ this.body = body;
68
+ }
69
+ }
70
+ /**
71
+ * The backend rejected the SDK's character credential (HTTP 401/403 on
72
+ * a character-scoped endpoint). The most common cause is that the
73
+ * character profile bound to this `characterKey` has been deleted on
74
+ * the backend; the binding is effectively terminal for this client.
75
+ */
76
+ export class CyberSoulAuthError extends CyberSoulApiError {
77
+ constructor(endpoint, method, status, message, body) {
78
+ super(endpoint, method, status, message, body, "auth");
79
+ }
80
+ }
81
+ /**
82
+ * The backend rejected a paid action because the user's wallet does
83
+ * not have enough points to cover it (HTTP 402 / `INSUFFICIENT_POINTS`).
84
+ * Callers should surface a top-up prompt rather than retrying.
85
+ */
86
+ export class CyberSoulInsufficientPointsError extends CyberSoulApiError {
87
+ /**
88
+ * Backend-supplied machine code. Always `"INSUFFICIENT_POINTS"` today;
89
+ * kept as a field so future variants (e.g. per-feature paywalls) can
90
+ * piggy-back on the same class.
91
+ */
92
+ code;
93
+ constructor(endpoint, method, status, message, body, code = "INSUFFICIENT_POINTS") {
94
+ super(endpoint, method, status, message, body, "insufficient-points");
95
+ this.code = code;
96
+ }
97
+ }
98
+ /**
99
+ * The wallet-deduction call failed for a reason *other than* insufficient
100
+ * balance (e.g. wallet service unavailable, accounting bug). Distinct from
101
+ * `CyberSoulInsufficientPointsError` because the user can't fix this by
102
+ * topping up — it's an upstream infrastructure issue.
103
+ */
104
+ export class CyberSoulWalletError extends CyberSoulApiError {
105
+ code;
106
+ constructor(endpoint, method, status, message, body, code = "WALLET_DEDUCTION_ERROR") {
107
+ super(endpoint, method, status, message, body, "wallet");
108
+ this.code = code;
109
+ }
110
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './types.js';
2
2
  export * from './client.js';
3
3
  export * from './llm.provider.js';
4
+ export * from './errors.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './types.js';
2
2
  export * from './client.js';
3
3
  export * from './llm.provider.js';
4
+ export * from './errors.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.4.8",
3
+ "version": "1.4.11",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",