@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 +9 -1
- package/dist/client.js +131 -91
- package/dist/errors.d.ts +82 -0
- package/dist/errors.js +110 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
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
|
-
*
|
|
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
|
|
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
|
|
73
|
+
: new CyberSoulNetworkError(endpoint, method, `Request failed unexpectedly: ${method} ${endpoint}`);
|
|
66
74
|
}
|
|
67
75
|
async fetchRemoteState() {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
*
|
|
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.
|
|
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 ===
|
|
876
|
-
break;
|
|
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: `
|
|
930
|
+
reason: `Spam guard: ${consecutiveProactive} consecutive un-replied messages already sent.`,
|
|
886
931
|
};
|
|
887
932
|
}
|
|
888
|
-
// 2. Fetch
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
938
|
-
|
|
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":
|
|
984
|
+
"skipReason": null,
|
|
942
985
|
"actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY.",
|
|
943
986
|
"textResponse": "Spoken dialogue ONLY.",
|
|
944
|
-
"stateUpdate": { "temperatureDelta":
|
|
945
|
-
${this.getImageSchemaParams(
|
|
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
|
|
949
|
-
|
|
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[
|
|
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
|
-
//
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
979
|
-
parsedIntent.textResponse.
|
|
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
|
-
|
|
993
|
-
let
|
|
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);
|
package/dist/errors.d.ts
ADDED
|
@@ -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
package/dist/index.js
CHANGED