miladyai 2.0.0-alpha.27
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/_virtual/_rolldown/runtime.js +7 -0
- package/dist/actions/emote.js +64 -0
- package/dist/actions/restart.js +81 -0
- package/dist/actions/send-message.js +152 -0
- package/dist/agent-admin-routes.js +82 -0
- package/dist/agent-lifecycle-routes.js +79 -0
- package/dist/agent-transfer-routes.js +102 -0
- package/dist/api/agent-admin-routes.js +82 -0
- package/dist/api/agent-lifecycle-routes.js +79 -0
- package/dist/api/agent-transfer-routes.js +102 -0
- package/dist/api/apps-hyperscape-routes.js +58 -0
- package/dist/api/apps-routes.js +114 -0
- package/dist/api/auth-routes.js +56 -0
- package/dist/api/autonomy-routes.js +44 -0
- package/dist/api/bug-report-routes.js +111 -0
- package/dist/api/character-routes.js +195 -0
- package/dist/api/cloud-routes.js +330 -0
- package/dist/api/cloud-status-routes.js +155 -0
- package/dist/api/compat-utils.js +111 -0
- package/dist/api/database.js +735 -0
- package/dist/api/diagnostics-routes.js +205 -0
- package/dist/api/drop-service.js +134 -0
- package/dist/api/early-logs.js +86 -0
- package/dist/api/http-helpers.js +131 -0
- package/dist/api/knowledge-routes.js +534 -0
- package/dist/api/memory-bounds.js +71 -0
- package/dist/api/models-routes.js +28 -0
- package/dist/api/og-tracker.js +36 -0
- package/dist/api/permissions-routes.js +109 -0
- package/dist/api/plugin-validation.js +198 -0
- package/dist/api/provider-switch-config.js +41 -0
- package/dist/api/registry-routes.js +86 -0
- package/dist/api/registry-service.js +164 -0
- package/dist/api/sandbox-routes.js +1112 -0
- package/dist/api/server.js +7949 -0
- package/dist/api/subscription-routes.js +172 -0
- package/dist/api/terminal-run-limits.js +24 -0
- package/dist/api/training-routes.js +158 -0
- package/dist/api/trajectory-routes.js +300 -0
- package/dist/api/trigger-routes.js +246 -0
- package/dist/api/twitter-verify.js +134 -0
- package/dist/api/tx-service.js +108 -0
- package/dist/api/wallet-routes.js +266 -0
- package/dist/api/wallet.js +568 -0
- package/dist/api/whatsapp-routes.js +182 -0
- package/dist/api/zip-utils.js +109 -0
- package/dist/apps-hyperscape-routes.js +58 -0
- package/dist/apps-routes.js +114 -0
- package/dist/ascii.js +20 -0
- package/dist/auth/anthropic.js +44 -0
- package/dist/auth/apply-stealth.js +41 -0
- package/dist/auth/claude-code-stealth.js +78 -0
- package/dist/auth/credentials.js +156 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/openai-codex.js +66 -0
- package/dist/auth/types.js +9 -0
- package/dist/auth-routes.js +56 -0
- package/dist/autonomy-routes.js +44 -0
- package/dist/bug-report-routes.js +111 -0
- package/dist/build-info.json +6 -0
- package/dist/character-routes.js +195 -0
- package/dist/cli/argv.js +63 -0
- package/dist/cli/banner.js +34 -0
- package/dist/cli/cli-name.js +21 -0
- package/dist/cli/cli-utils.js +16 -0
- package/dist/cli/git-commit.js +78 -0
- package/dist/cli/parse-duration.js +15 -0
- package/dist/cli/plugins-cli.js +590 -0
- package/dist/cli/profile-utils.js +9 -0
- package/dist/cli/profile.js +95 -0
- package/dist/cli/program/build-program.js +17 -0
- package/dist/cli/program/command-registry.js +23 -0
- package/dist/cli/program/help.js +47 -0
- package/dist/cli/program/preaction.js +33 -0
- package/dist/cli/program/register.config.js +106 -0
- package/dist/cli/program/register.configure.js +20 -0
- package/dist/cli/program/register.dashboard.js +124 -0
- package/dist/cli/program/register.models.js +23 -0
- package/dist/cli/program/register.setup.js +36 -0
- package/dist/cli/program/register.start.js +22 -0
- package/dist/cli/program/register.subclis.js +70 -0
- package/dist/cli/program/register.tui.js +163 -0
- package/dist/cli/program/register.update.js +154 -0
- package/dist/cli/program.js +3 -0
- package/dist/cli/run-main.js +37 -0
- package/dist/cli/version.js +7 -0
- package/dist/cloud/validate-url.js +93 -0
- package/dist/cloud-routes.js +330 -0
- package/dist/cloud-status-routes.js +155 -0
- package/dist/compat-utils.js +111 -0
- package/dist/config/config.js +69 -0
- package/dist/config/env-vars.js +19 -0
- package/dist/config/includes.js +121 -0
- package/dist/config/object-utils.js +7 -0
- package/dist/config/paths.js +38 -0
- package/dist/config/plugin-auto-enable.js +231 -0
- package/dist/config/schema.js +864 -0
- package/dist/config/telegram-custom-commands.js +76 -0
- package/dist/config/zod-schema.agent-runtime.js +519 -0
- package/dist/config/zod-schema.core.js +538 -0
- package/dist/config/zod-schema.hooks.js +103 -0
- package/dist/config/zod-schema.js +488 -0
- package/dist/config/zod-schema.providers-core.js +785 -0
- package/dist/config/zod-schema.session.js +73 -0
- package/dist/core-plugins.js +37 -0
- package/dist/custom-actions.js +250 -0
- package/dist/database.js +735 -0
- package/dist/diagnostics/integration-observability.js +57 -0
- package/dist/diagnostics-routes.js +205 -0
- package/dist/drop-service.js +134 -0
- package/dist/early-logs.js +24 -0
- package/dist/eliza.js +2061 -0
- package/dist/emotes/catalog.js +271 -0
- package/dist/entry.js +40 -0
- package/dist/hooks/discovery.js +167 -0
- package/dist/hooks/eligibility.js +64 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/loader.js +147 -0
- package/dist/hooks/registry.js +55 -0
- package/dist/http-helpers.js +131 -0
- package/dist/index.js +49 -0
- package/dist/knowledge-routes.js +534 -0
- package/dist/memory-bounds.js +71 -0
- package/dist/milady-plugin.js +90 -0
- package/dist/models-routes.js +28 -0
- package/dist/onboarding-names.js +78 -0
- package/dist/onboarding-presets.js +922 -0
- package/dist/package.json +1 -0
- package/dist/permissions-routes.js +109 -0
- package/dist/plugin-validation.js +107 -0
- package/dist/plugins/whatsapp/actions.js +91 -0
- package/dist/plugins/whatsapp/index.js +16 -0
- package/dist/plugins/whatsapp/service.js +270 -0
- package/dist/provider-switch-config.js +41 -0
- package/dist/providers/admin-trust.js +46 -0
- package/dist/providers/autonomous-state.js +101 -0
- package/dist/providers/session-bridge.js +86 -0
- package/dist/providers/session-utils.js +36 -0
- package/dist/providers/simple-mode.js +50 -0
- package/dist/providers/ui-catalog.js +15 -0
- package/dist/providers/workspace-provider.js +93 -0
- package/dist/providers/workspace.js +348 -0
- package/dist/registry-routes.js +86 -0
- package/dist/registry-service.js +164 -0
- package/dist/restart.js +40 -0
- package/dist/runtime/core-plugins.js +37 -0
- package/dist/runtime/custom-actions.js +250 -0
- package/dist/runtime/eliza.js +2061 -0
- package/dist/runtime/embedding-manager-support.js +185 -0
- package/dist/runtime/embedding-manager.js +193 -0
- package/dist/runtime/embedding-presets.js +54 -0
- package/dist/runtime/embedding-state.js +8 -0
- package/dist/runtime/milady-plugin.js +90 -0
- package/dist/runtime/onboarding-names.js +78 -0
- package/dist/runtime/restart.js +40 -0
- package/dist/runtime/version.js +7 -0
- package/dist/sandbox-routes.js +1112 -0
- package/dist/security/audit-log.js +149 -0
- package/dist/security/network-policy.js +70 -0
- package/dist/server.js +7949 -0
- package/dist/services/agent-export.js +559 -0
- package/dist/services/app-manager.js +389 -0
- package/dist/services/browser-capture.js +86 -0
- package/dist/services/fallback-training-service.js +128 -0
- package/dist/services/mcp-marketplace.js +134 -0
- package/dist/services/plugin-installer.js +396 -0
- package/dist/services/plugin-manager-types.js +15 -0
- package/dist/services/registry-client-app-meta.js +144 -0
- package/dist/services/registry-client-endpoints.js +166 -0
- package/dist/services/registry-client-local.js +271 -0
- package/dist/services/registry-client-network.js +93 -0
- package/dist/services/registry-client-queries.js +70 -0
- package/dist/services/registry-client.js +157 -0
- package/dist/services/sandbox-engine.js +511 -0
- package/dist/services/sandbox-manager.js +297 -0
- package/dist/services/self-updater.js +175 -0
- package/dist/services/skill-catalog-client.js +119 -0
- package/dist/services/skill-marketplace.js +521 -0
- package/dist/services/stream-manager.js +236 -0
- package/dist/services/update-checker.js +121 -0
- package/dist/services/update-notifier.js +29 -0
- package/dist/services/version-compat.js +78 -0
- package/dist/services/whatsapp-pairing.js +196 -0
- package/dist/shared/ui-catalog-prompt.js +728 -0
- package/dist/subscription-routes.js +172 -0
- package/dist/terminal/links.js +19 -0
- package/dist/terminal/palette.js +14 -0
- package/dist/terminal/theme.js +25 -0
- package/dist/terminal-run-limits.js +24 -0
- package/dist/training-routes.js +158 -0
- package/dist/trajectory-routes.js +300 -0
- package/dist/trigger-routes.js +246 -0
- package/dist/triggers/action.js +218 -0
- package/dist/triggers/runtime.js +281 -0
- package/dist/triggers/scheduling.js +295 -0
- package/dist/triggers/types.js +5 -0
- package/dist/tui/components/assistant-message.js +76 -0
- package/dist/tui/components/chat-editor.js +34 -0
- package/dist/tui/components/embeddings-overlay.js +46 -0
- package/dist/tui/components/footer.js +60 -0
- package/dist/tui/components/index.js +15 -0
- package/dist/tui/components/modal-frame.js +45 -0
- package/dist/tui/components/modal-style.js +15 -0
- package/dist/tui/components/model-selector.js +70 -0
- package/dist/tui/components/pinned-chat-layout.js +46 -0
- package/dist/tui/components/plugins-endpoints-tab.js +196 -0
- package/dist/tui/components/plugins-installed-tab-view.js +69 -0
- package/dist/tui/components/plugins-installed-tab.js +319 -0
- package/dist/tui/components/plugins-overlay-catalog.js +81 -0
- package/dist/tui/components/plugins-overlay-data-api.js +21 -0
- package/dist/tui/components/plugins-overlay-data-shared.js +20 -0
- package/dist/tui/components/plugins-overlay-data.js +323 -0
- package/dist/tui/components/plugins-overlay.js +117 -0
- package/dist/tui/components/plugins-store-tab.js +148 -0
- package/dist/tui/components/settings-overlay.js +61 -0
- package/dist/tui/components/status-bar.js +64 -0
- package/dist/tui/components/tool-execution.js +68 -0
- package/dist/tui/components/user-message.js +22 -0
- package/dist/tui/eliza-tui-bridge.js +606 -0
- package/dist/tui/index.js +370 -0
- package/dist/tui/modal-presets.js +33 -0
- package/dist/tui/model-spec.js +46 -0
- package/dist/tui/sse-parser.js +78 -0
- package/dist/tui/theme.js +110 -0
- package/dist/tui/titlebar-spinner.js +62 -0
- package/dist/tui/tui-app.js +311 -0
- package/dist/tui/ws-client.js +215 -0
- package/dist/twitter-verify.js +134 -0
- package/dist/tx-service.js +108 -0
- package/dist/utils/exec-safety.js +17 -0
- package/dist/utils/globals.js +20 -0
- package/dist/utils/milady-root.js +61 -0
- package/dist/utils/number-parsing.js +37 -0
- package/dist/version-resolver.js +37 -0
- package/dist/version.js +7 -0
- package/dist/wallet-routes.js +266 -0
- package/dist/wallet.js +568 -0
- package/dist/whatsapp-routes.js +182 -0
- package/dist/zip-utils.js +109 -0
- package/milady.mjs +14 -0
- package/package.json +111 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { CharacterSchema } from "../config/zod-schema.js";
|
|
2
|
+
import { ModelType, logger } from "@elizaos/core";
|
|
3
|
+
|
|
4
|
+
//#region src/api/character-routes.ts
|
|
5
|
+
function buildCharacterSummary(ctx) {
|
|
6
|
+
return [
|
|
7
|
+
ctx.name ? `Name: ${ctx.name}` : "",
|
|
8
|
+
ctx.system ? `System prompt: ${ctx.system}` : "",
|
|
9
|
+
ctx.bio ? `Bio: ${ctx.bio}` : "",
|
|
10
|
+
ctx.style?.all?.length ? `Style rules: ${ctx.style.all.join("; ")}` : ""
|
|
11
|
+
].filter(Boolean).join("\n");
|
|
12
|
+
}
|
|
13
|
+
function buildGeneratePrompt(field, context, mode) {
|
|
14
|
+
const charSummary = buildCharacterSummary(context);
|
|
15
|
+
if (field === "bio") return `Given this character:\n${charSummary}\n\nWrite a concise, compelling bio for this character (3-4 short paragraphs, one per line). Just output the bio lines, nothing else. Match the character's voice and personality.`;
|
|
16
|
+
if (field === "style") return `Given this character:\n${charSummary}${mode === "append" && context.style?.all?.length ? `\nExisting style rules (add to these, don't repeat):\n${context.style.all.join("\n")}` : ""}\n\nGenerate 4-6 communication style rules for this character. Output a JSON object with keys "all", "chat", "post", each containing an array of short rule strings. Just output the JSON, nothing else.`;
|
|
17
|
+
if (field === "chatExamples") return `Given this character:\n${charSummary}\n\nGenerate 3 example chat conversations showing how this character responds. Output a JSON array where each element is an array of message objects like [{"user":"{{user1}}","content":{"text":"..."}},{"user":"{{agentName}}","content":{"text":"..."}}]. Just output the JSON array, nothing else.`;
|
|
18
|
+
return `Given this character:\n${charSummary}${mode === "append" && context.postExamples?.length ? `\nExisting posts (add new ones, don't repeat):\n${context.postExamples.join("\n")}` : ""}\n\nGenerate 3-5 example social media posts this character would write. Output a JSON array of strings. Just output the JSON array, nothing else.`;
|
|
19
|
+
}
|
|
20
|
+
const CHARACTER_SCHEMA_FIELDS = [
|
|
21
|
+
{
|
|
22
|
+
key: "name",
|
|
23
|
+
type: "string",
|
|
24
|
+
label: "Name",
|
|
25
|
+
description: "Agent display name",
|
|
26
|
+
maxLength: 100
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "username",
|
|
30
|
+
type: "string",
|
|
31
|
+
label: "Username",
|
|
32
|
+
description: "Agent username for platforms",
|
|
33
|
+
maxLength: 50
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: "bio",
|
|
37
|
+
type: "string | string[]",
|
|
38
|
+
label: "Bio",
|
|
39
|
+
description: "Biography — single string or array of points"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "system",
|
|
43
|
+
type: "string",
|
|
44
|
+
label: "System Prompt",
|
|
45
|
+
description: "System prompt defining core behavior",
|
|
46
|
+
maxLength: 1e4
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "adjectives",
|
|
50
|
+
type: "string[]",
|
|
51
|
+
label: "Adjectives",
|
|
52
|
+
description: "Personality adjectives (e.g. curious, witty)"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "topics",
|
|
56
|
+
type: "string[]",
|
|
57
|
+
label: "Topics",
|
|
58
|
+
description: "Topics the agent is knowledgeable about"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "style",
|
|
62
|
+
type: "object",
|
|
63
|
+
label: "Style",
|
|
64
|
+
description: "Communication style guides",
|
|
65
|
+
children: [
|
|
66
|
+
{
|
|
67
|
+
key: "all",
|
|
68
|
+
type: "string[]",
|
|
69
|
+
label: "All",
|
|
70
|
+
description: "Style guidelines for all responses"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: "chat",
|
|
74
|
+
type: "string[]",
|
|
75
|
+
label: "Chat",
|
|
76
|
+
description: "Style guidelines for chat responses"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "post",
|
|
80
|
+
type: "string[]",
|
|
81
|
+
label: "Post",
|
|
82
|
+
description: "Style guidelines for social media posts"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "messageExamples",
|
|
88
|
+
type: "array",
|
|
89
|
+
label: "Message Examples",
|
|
90
|
+
description: "Example conversations demonstrating the agent's voice"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: "postExamples",
|
|
94
|
+
type: "string[]",
|
|
95
|
+
label: "Post Examples",
|
|
96
|
+
description: "Example social media posts"
|
|
97
|
+
}
|
|
98
|
+
];
|
|
99
|
+
async function handleCharacterRoutes(ctx) {
|
|
100
|
+
const { req, res, method, pathname, state, readJsonBody, json, error, pickRandomNames } = ctx;
|
|
101
|
+
if (method === "GET" && pathname === "/api/character") {
|
|
102
|
+
const runtime = state.runtime;
|
|
103
|
+
const merged = {};
|
|
104
|
+
if (runtime) {
|
|
105
|
+
const character = runtime.character;
|
|
106
|
+
if (character.name) merged.name = character.name;
|
|
107
|
+
if (character.bio) merged.bio = character.bio;
|
|
108
|
+
if (character.system) merged.system = character.system;
|
|
109
|
+
if (character.adjectives) merged.adjectives = character.adjectives;
|
|
110
|
+
if (character.topics) merged.topics = character.topics;
|
|
111
|
+
if (character.style) merged.style = character.style;
|
|
112
|
+
if (character.postExamples) merged.postExamples = character.postExamples;
|
|
113
|
+
}
|
|
114
|
+
json(res, {
|
|
115
|
+
character: merged,
|
|
116
|
+
agentName: state.agentName
|
|
117
|
+
});
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (method === "PUT" && pathname === "/api/character") {
|
|
121
|
+
const body = await readJsonBody(req, res);
|
|
122
|
+
if (!body) return true;
|
|
123
|
+
const result = CharacterSchema.safeParse(body);
|
|
124
|
+
if (!result.success) {
|
|
125
|
+
json(res, {
|
|
126
|
+
ok: false,
|
|
127
|
+
validationErrors: result.error.issues.map((issue) => ({
|
|
128
|
+
path: issue.path.join("."),
|
|
129
|
+
message: issue.message
|
|
130
|
+
}))
|
|
131
|
+
}, 422);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (state.runtime) {
|
|
135
|
+
const character = state.runtime.character;
|
|
136
|
+
if (body.name != null) character.name = String(body.name);
|
|
137
|
+
if (body.bio != null) character.bio = Array.isArray(body.bio) ? body.bio : [String(body.bio)];
|
|
138
|
+
if (body.system != null) character.system = String(body.system);
|
|
139
|
+
if (body.adjectives != null) character.adjectives = body.adjectives;
|
|
140
|
+
if (body.topics != null) character.topics = body.topics;
|
|
141
|
+
if (body.style != null) character.style = body.style;
|
|
142
|
+
if (body.postExamples != null) character.postExamples = body.postExamples;
|
|
143
|
+
}
|
|
144
|
+
if (body.name) state.agentName = String(body.name);
|
|
145
|
+
json(res, {
|
|
146
|
+
ok: true,
|
|
147
|
+
character: body,
|
|
148
|
+
agentName: state.agentName
|
|
149
|
+
});
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (method === "GET" && pathname === "/api/character/random-name") {
|
|
153
|
+
json(res, { name: pickRandomNames(1)[0] ?? "Reimu" });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (method === "POST" && pathname === "/api/character/generate") {
|
|
157
|
+
const body = await readJsonBody(req, res);
|
|
158
|
+
if (!body) return true;
|
|
159
|
+
if (!body.field || !body.context) {
|
|
160
|
+
error(res, "field and context are required", 400);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
const runtime = state.runtime;
|
|
164
|
+
if (!runtime) {
|
|
165
|
+
error(res, "Agent runtime not available. Start the agent first.", 503);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (body.field !== "bio" && body.field !== "style" && body.field !== "chatExamples" && body.field !== "postExamples") {
|
|
169
|
+
error(res, `Unknown field: ${body.field}`, 400);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
const prompt = buildGeneratePrompt(body.field, body.context, body.mode);
|
|
173
|
+
try {
|
|
174
|
+
const result = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
175
|
+
prompt,
|
|
176
|
+
temperature: .8,
|
|
177
|
+
maxTokens: 1500
|
|
178
|
+
});
|
|
179
|
+
json(res, { generated: String(result) });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
const message = err instanceof Error ? err.message : "generation failed";
|
|
182
|
+
logger.error(`[character-generate] ${message}`);
|
|
183
|
+
error(res, message, 500);
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (method === "GET" && pathname === "/api/character/schema") {
|
|
188
|
+
json(res, { fields: CHARACTER_SCHEMA_FIELDS });
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
//#endregion
|
|
195
|
+
export { handleCharacterRoutes };
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { saveMiladyConfig } from "../config/config.js";
|
|
2
|
+
import { createIntegrationTelemetrySpan } from "../diagnostics/integration-observability.js";
|
|
3
|
+
import { readJsonBody as readJsonBody$1, sendJson, sendJsonError } from "./http-helpers.js";
|
|
4
|
+
import { validateCloudBaseUrl } from "../cloud/validate-url.js";
|
|
5
|
+
import { logger } from "@elizaos/core";
|
|
6
|
+
|
|
7
|
+
//#region src/api/cloud-routes.ts
|
|
8
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
function extractAgentId(pathname) {
|
|
10
|
+
const id = pathname.split("/")[4];
|
|
11
|
+
return id && UUID_RE.test(id) ? id : null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Read and parse a JSON request body with size limits and error handling.
|
|
15
|
+
* Returns null (and sends a 4xx response) if reading or parsing fails.
|
|
16
|
+
*/
|
|
17
|
+
async function readJsonBody(req, res) {
|
|
18
|
+
return readJsonBody$1(req, res, {
|
|
19
|
+
maxBytes: 1048576,
|
|
20
|
+
tooLargeMessage: "Request body too large",
|
|
21
|
+
destroyOnTooLarge: true
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const CLOUD_LOGIN_CREATE_TIMEOUT_MS = 1e4;
|
|
25
|
+
const CLOUD_LOGIN_POLL_TIMEOUT_MS = 1e4;
|
|
26
|
+
function isRedirectResponse(response) {
|
|
27
|
+
return response.status >= 300 && response.status < 400;
|
|
28
|
+
}
|
|
29
|
+
function isTimeoutError(error) {
|
|
30
|
+
if (!(error instanceof Error)) return false;
|
|
31
|
+
if (error.name === "TimeoutError" || error.name === "AbortError") return true;
|
|
32
|
+
const message = error.message.toLowerCase();
|
|
33
|
+
return message.includes("timed out") || message.includes("timeout");
|
|
34
|
+
}
|
|
35
|
+
async function fetchWithTimeout(input, init, timeoutMs) {
|
|
36
|
+
return fetch(input, {
|
|
37
|
+
...init,
|
|
38
|
+
redirect: "manual",
|
|
39
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns true if the request was handled, false if path didn't match.
|
|
44
|
+
*/
|
|
45
|
+
async function handleCloudRoute(req, res, pathname, method, state) {
|
|
46
|
+
if (method === "POST" && pathname === "/api/cloud/login") {
|
|
47
|
+
const baseUrl = state.config.cloud?.baseUrl ?? "https://www.elizacloud.ai";
|
|
48
|
+
const urlError = await validateCloudBaseUrl(baseUrl);
|
|
49
|
+
if (urlError) {
|
|
50
|
+
sendJsonError(res, urlError);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
const sessionId = crypto.randomUUID();
|
|
54
|
+
const loginCreateSpan = createIntegrationTelemetrySpan({
|
|
55
|
+
boundary: "cloud",
|
|
56
|
+
operation: "login_create_session",
|
|
57
|
+
timeoutMs: CLOUD_LOGIN_CREATE_TIMEOUT_MS
|
|
58
|
+
});
|
|
59
|
+
let createRes;
|
|
60
|
+
try {
|
|
61
|
+
createRes = await fetchWithTimeout(`${baseUrl}/api/auth/cli-session`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({ sessionId })
|
|
65
|
+
}, CLOUD_LOGIN_CREATE_TIMEOUT_MS);
|
|
66
|
+
} catch (fetchErr) {
|
|
67
|
+
if (isTimeoutError(fetchErr)) {
|
|
68
|
+
loginCreateSpan.failure({
|
|
69
|
+
error: fetchErr,
|
|
70
|
+
statusCode: 504
|
|
71
|
+
});
|
|
72
|
+
sendJsonError(res, "Eliza Cloud login request timed out", 504);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
loginCreateSpan.failure({
|
|
76
|
+
error: fetchErr,
|
|
77
|
+
statusCode: 502
|
|
78
|
+
});
|
|
79
|
+
sendJsonError(res, "Failed to reach Eliza Cloud", 502);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (isRedirectResponse(createRes)) {
|
|
83
|
+
loginCreateSpan.failure({
|
|
84
|
+
statusCode: createRes.status,
|
|
85
|
+
errorKind: "redirect_response"
|
|
86
|
+
});
|
|
87
|
+
sendJsonError(res, "Eliza Cloud login request was redirected; redirects are not allowed", 502);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (!createRes.ok) {
|
|
91
|
+
loginCreateSpan.failure({
|
|
92
|
+
statusCode: createRes.status,
|
|
93
|
+
errorKind: "http_error"
|
|
94
|
+
});
|
|
95
|
+
sendJsonError(res, "Failed to create auth session with Eliza Cloud", 502);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
loginCreateSpan.success({ statusCode: createRes.status });
|
|
99
|
+
sendJson(res, {
|
|
100
|
+
ok: true,
|
|
101
|
+
sessionId,
|
|
102
|
+
browserUrl: `${baseUrl}/auth/cli-login?session=${encodeURIComponent(sessionId)}`
|
|
103
|
+
});
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (method === "GET" && pathname.startsWith("/api/cloud/login/status")) {
|
|
107
|
+
const sessionId = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`).searchParams.get("sessionId");
|
|
108
|
+
if (!sessionId) {
|
|
109
|
+
sendJsonError(res, "sessionId query parameter is required");
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const baseUrl = state.config.cloud?.baseUrl ?? "https://www.elizacloud.ai";
|
|
113
|
+
const urlError = await validateCloudBaseUrl(baseUrl);
|
|
114
|
+
if (urlError) {
|
|
115
|
+
sendJsonError(res, urlError);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
const loginPollSpan = createIntegrationTelemetrySpan({
|
|
119
|
+
boundary: "cloud",
|
|
120
|
+
operation: "login_poll_status",
|
|
121
|
+
timeoutMs: CLOUD_LOGIN_POLL_TIMEOUT_MS
|
|
122
|
+
});
|
|
123
|
+
let pollRes;
|
|
124
|
+
try {
|
|
125
|
+
pollRes = await fetchWithTimeout(`${baseUrl}/api/auth/cli-session/${encodeURIComponent(sessionId)}`, {}, CLOUD_LOGIN_POLL_TIMEOUT_MS);
|
|
126
|
+
} catch (fetchErr) {
|
|
127
|
+
if (isTimeoutError(fetchErr)) {
|
|
128
|
+
loginPollSpan.failure({
|
|
129
|
+
error: fetchErr,
|
|
130
|
+
statusCode: 504
|
|
131
|
+
});
|
|
132
|
+
sendJson(res, {
|
|
133
|
+
status: "error",
|
|
134
|
+
error: "Eliza Cloud status request timed out"
|
|
135
|
+
}, 504);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
loginPollSpan.failure({
|
|
139
|
+
error: fetchErr,
|
|
140
|
+
statusCode: 502
|
|
141
|
+
});
|
|
142
|
+
sendJson(res, {
|
|
143
|
+
status: "error",
|
|
144
|
+
error: "Failed to reach Eliza Cloud"
|
|
145
|
+
}, 502);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
if (isRedirectResponse(pollRes)) {
|
|
149
|
+
loginPollSpan.failure({
|
|
150
|
+
statusCode: pollRes.status,
|
|
151
|
+
errorKind: "redirect_response"
|
|
152
|
+
});
|
|
153
|
+
sendJson(res, {
|
|
154
|
+
status: "error",
|
|
155
|
+
error: "Eliza Cloud status request was redirected; redirects are not allowed"
|
|
156
|
+
}, 502);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (!pollRes.ok) {
|
|
160
|
+
loginPollSpan.failure({
|
|
161
|
+
statusCode: pollRes.status,
|
|
162
|
+
errorKind: "http_error"
|
|
163
|
+
});
|
|
164
|
+
sendJson(res, pollRes.status === 404 ? {
|
|
165
|
+
status: "expired",
|
|
166
|
+
error: "Session not found or expired"
|
|
167
|
+
} : {
|
|
168
|
+
status: "error",
|
|
169
|
+
error: `Eliza Cloud returned HTTP ${pollRes.status}`
|
|
170
|
+
});
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
let data;
|
|
174
|
+
try {
|
|
175
|
+
data = await pollRes.json();
|
|
176
|
+
} catch (parseErr) {
|
|
177
|
+
loginPollSpan.failure({
|
|
178
|
+
error: parseErr,
|
|
179
|
+
statusCode: pollRes.status
|
|
180
|
+
});
|
|
181
|
+
throw parseErr;
|
|
182
|
+
}
|
|
183
|
+
loginPollSpan.success({ statusCode: pollRes.status });
|
|
184
|
+
if (data.status === "authenticated" && data.apiKey) {
|
|
185
|
+
const cloud = state.config.cloud ?? {};
|
|
186
|
+
cloud.enabled = true;
|
|
187
|
+
cloud.apiKey = data.apiKey;
|
|
188
|
+
state.config.cloud = cloud;
|
|
189
|
+
try {
|
|
190
|
+
saveMiladyConfig(state.config);
|
|
191
|
+
logger.info("[cloud-login] API key saved to config file");
|
|
192
|
+
} catch (saveErr) {
|
|
193
|
+
logger.error(`[cloud-login] Failed to save config: ${saveErr instanceof Error ? saveErr.message : saveErr}`);
|
|
194
|
+
}
|
|
195
|
+
process.env.ELIZAOS_CLOUD_API_KEY = data.apiKey;
|
|
196
|
+
process.env.ELIZAOS_CLOUD_ENABLED = "true";
|
|
197
|
+
if (state.runtime) try {
|
|
198
|
+
if (!state.runtime.character.secrets) state.runtime.character.secrets = {};
|
|
199
|
+
const secrets = state.runtime.character.secrets;
|
|
200
|
+
secrets.ELIZAOS_CLOUD_API_KEY = data.apiKey;
|
|
201
|
+
secrets.ELIZAOS_CLOUD_ENABLED = "true";
|
|
202
|
+
await state.runtime.updateAgent(state.runtime.agentId, { secrets: { ...secrets } });
|
|
203
|
+
logger.info("[cloud-login] API key persisted to agent DB record");
|
|
204
|
+
} catch (dbErr) {
|
|
205
|
+
logger.warn(`[cloud-login] DB persistence failed (non-fatal): ${dbErr instanceof Error ? dbErr.message : dbErr}`);
|
|
206
|
+
}
|
|
207
|
+
if (state.cloudManager && !state.cloudManager.getClient()) await state.cloudManager.init();
|
|
208
|
+
sendJson(res, {
|
|
209
|
+
status: "authenticated",
|
|
210
|
+
keyPrefix: data.keyPrefix
|
|
211
|
+
});
|
|
212
|
+
} else sendJson(res, { status: data.status });
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
if (method === "GET" && pathname === "/api/cloud/agents") {
|
|
216
|
+
const client = state.cloudManager?.getClient();
|
|
217
|
+
if (!client) {
|
|
218
|
+
sendJsonError(res, "Not connected to Eliza Cloud", 401);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
sendJson(res, {
|
|
222
|
+
ok: true,
|
|
223
|
+
agents: await client.listAgents()
|
|
224
|
+
});
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (method === "POST" && pathname === "/api/cloud/agents") {
|
|
228
|
+
const client = state.cloudManager?.getClient();
|
|
229
|
+
if (!client) {
|
|
230
|
+
sendJsonError(res, "Not connected to Eliza Cloud", 401);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
const body = await readJsonBody(req, res);
|
|
234
|
+
if (!body) return true;
|
|
235
|
+
if (!body.agentName?.trim()) {
|
|
236
|
+
sendJsonError(res, "agentName is required");
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
sendJson(res, {
|
|
240
|
+
ok: true,
|
|
241
|
+
agent: await client.createAgent({
|
|
242
|
+
agentName: body.agentName,
|
|
243
|
+
agentConfig: body.agentConfig,
|
|
244
|
+
environmentVars: body.environmentVars
|
|
245
|
+
})
|
|
246
|
+
}, 201);
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
if (method === "POST" && pathname.startsWith("/api/cloud/agents/") && pathname.endsWith("/provision")) {
|
|
250
|
+
const agentId = extractAgentId(pathname);
|
|
251
|
+
if (!agentId || !state.cloudManager) {
|
|
252
|
+
sendJsonError(res, "Invalid agent ID or cloud not connected", 400);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
sendJson(res, {
|
|
256
|
+
ok: true,
|
|
257
|
+
agentId,
|
|
258
|
+
agentName: (await state.cloudManager.connect(agentId)).agentName,
|
|
259
|
+
status: state.cloudManager.getStatus()
|
|
260
|
+
});
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
if (method === "POST" && pathname.startsWith("/api/cloud/agents/") && pathname.endsWith("/shutdown")) {
|
|
264
|
+
const agentId = extractAgentId(pathname);
|
|
265
|
+
if (!agentId || !state.cloudManager) {
|
|
266
|
+
sendJsonError(res, "Invalid agent ID or cloud not connected", 400);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
const client = state.cloudManager.getClient();
|
|
270
|
+
if (!client) {
|
|
271
|
+
sendJsonError(res, "Not connected to Eliza Cloud", 401);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
if (state.cloudManager.getActiveAgentId() === agentId) await state.cloudManager.disconnect();
|
|
275
|
+
await client.deleteAgent(agentId);
|
|
276
|
+
sendJson(res, {
|
|
277
|
+
ok: true,
|
|
278
|
+
agentId,
|
|
279
|
+
status: "stopped"
|
|
280
|
+
});
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (method === "POST" && pathname.startsWith("/api/cloud/agents/") && pathname.endsWith("/connect")) {
|
|
284
|
+
const agentId = extractAgentId(pathname);
|
|
285
|
+
if (!agentId || !state.cloudManager) {
|
|
286
|
+
sendJsonError(res, "Invalid agent ID or cloud not connected", 400);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
if (state.cloudManager.getActiveAgentId()) await state.cloudManager.disconnect();
|
|
290
|
+
sendJson(res, {
|
|
291
|
+
ok: true,
|
|
292
|
+
agentId,
|
|
293
|
+
agentName: (await state.cloudManager.connect(agentId)).agentName,
|
|
294
|
+
status: state.cloudManager.getStatus()
|
|
295
|
+
});
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
if (method === "POST" && pathname === "/api/cloud/disconnect") {
|
|
299
|
+
if (state.cloudManager) await state.cloudManager.disconnect();
|
|
300
|
+
const cloud = state.config.cloud ?? {};
|
|
301
|
+
cloud.enabled = false;
|
|
302
|
+
delete cloud.apiKey;
|
|
303
|
+
state.config.cloud = cloud;
|
|
304
|
+
try {
|
|
305
|
+
saveMiladyConfig(state.config);
|
|
306
|
+
} catch (saveErr) {
|
|
307
|
+
logger.warn(`[cloud-login] Failed to save cloud disconnect state: ${saveErr instanceof Error ? saveErr.message : saveErr}`);
|
|
308
|
+
}
|
|
309
|
+
delete process.env.ELIZAOS_CLOUD_API_KEY;
|
|
310
|
+
delete process.env.ELIZAOS_CLOUD_ENABLED;
|
|
311
|
+
if (state.runtime) try {
|
|
312
|
+
if (!state.runtime.character.secrets) state.runtime.character.secrets = {};
|
|
313
|
+
const secrets = state.runtime.character.secrets;
|
|
314
|
+
delete secrets.ELIZAOS_CLOUD_API_KEY;
|
|
315
|
+
delete secrets.ELIZAOS_CLOUD_ENABLED;
|
|
316
|
+
await state.runtime.updateAgent(state.runtime.agentId, { secrets: { ...secrets } });
|
|
317
|
+
} catch (dbErr) {
|
|
318
|
+
logger.warn(`[cloud-login] Failed to clear cloud secrets from agent DB: ${dbErr instanceof Error ? dbErr.message : dbErr}`);
|
|
319
|
+
}
|
|
320
|
+
sendJson(res, {
|
|
321
|
+
ok: true,
|
|
322
|
+
status: "disconnected"
|
|
323
|
+
});
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
//#endregion
|
|
330
|
+
export { handleCloudRoute };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { validateCloudBaseUrl } from "../cloud/validate-url.js";
|
|
2
|
+
import { logger } from "@elizaos/core";
|
|
3
|
+
|
|
4
|
+
//#region src/api/cloud-status-routes.ts
|
|
5
|
+
const DEFAULT_CLOUD_API_BASE_URL = "https://www.elizacloud.ai/api/v1";
|
|
6
|
+
const CLOUD_BILLING_URL = "https://www.elizacloud.ai/dashboard/settings?tab=billing";
|
|
7
|
+
function resolveCloudApiBaseUrl(rawBaseUrl) {
|
|
8
|
+
const base = (rawBaseUrl ?? DEFAULT_CLOUD_API_BASE_URL).trim().replace(/\/+$/, "");
|
|
9
|
+
if (base.endsWith("/api/v1")) return base;
|
|
10
|
+
return `${base}/api/v1`;
|
|
11
|
+
}
|
|
12
|
+
async function fetchCloudCreditsByApiKey(baseUrl, apiKey) {
|
|
13
|
+
const response = await fetch(`${baseUrl}/credits/balance`, {
|
|
14
|
+
headers: {
|
|
15
|
+
Accept: "application/json",
|
|
16
|
+
Authorization: `Bearer ${apiKey}`
|
|
17
|
+
},
|
|
18
|
+
redirect: "manual",
|
|
19
|
+
signal: AbortSignal.timeout(1e4)
|
|
20
|
+
});
|
|
21
|
+
if (response.status >= 300 && response.status < 400) throw new Error("Cloud credits request was redirected; redirects are not allowed");
|
|
22
|
+
const creditResponse = await response.json().catch(() => ({}));
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const message = typeof creditResponse.error === "string" && creditResponse.error.trim() ? creditResponse.error : `HTTP ${response.status}`;
|
|
25
|
+
throw new Error(message);
|
|
26
|
+
}
|
|
27
|
+
const rawBalance = typeof creditResponse.balance === "number" ? creditResponse.balance : typeof creditResponse.data?.balance === "number" ? creditResponse.data.balance : void 0;
|
|
28
|
+
return typeof rawBalance === "number" ? rawBalance : null;
|
|
29
|
+
}
|
|
30
|
+
async function handleCloudStatusRoutes(ctx) {
|
|
31
|
+
const { res, method, pathname, config, runtime, json } = ctx;
|
|
32
|
+
if (method === "GET" && pathname === "/api/cloud/status") {
|
|
33
|
+
const cloudMode = config.cloud?.enabled;
|
|
34
|
+
const cloudEnabled = cloudMode === true;
|
|
35
|
+
const hasApiKey = Boolean(config.cloud?.apiKey?.trim());
|
|
36
|
+
const effectivelyEnabled = cloudEnabled || cloudMode !== false && hasApiKey;
|
|
37
|
+
const cloudAuth = runtime ? runtime.getService("CLOUD_AUTH") : null;
|
|
38
|
+
const authConnected = Boolean(cloudAuth?.isAuthenticated());
|
|
39
|
+
if (authConnected || hasApiKey) {
|
|
40
|
+
json(res, {
|
|
41
|
+
connected: true,
|
|
42
|
+
enabled: effectivelyEnabled,
|
|
43
|
+
hasApiKey,
|
|
44
|
+
userId: authConnected ? cloudAuth?.getUserId?.() : void 0,
|
|
45
|
+
organizationId: authConnected ? cloudAuth?.getOrganizationId?.() : void 0,
|
|
46
|
+
topUpUrl: CLOUD_BILLING_URL,
|
|
47
|
+
reason: authConnected ? void 0 : runtime ? "api_key_present_not_authenticated" : "api_key_present_runtime_not_started"
|
|
48
|
+
});
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (!runtime) {
|
|
52
|
+
json(res, {
|
|
53
|
+
connected: false,
|
|
54
|
+
enabled: effectivelyEnabled,
|
|
55
|
+
hasApiKey,
|
|
56
|
+
reason: "runtime_not_started"
|
|
57
|
+
});
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
json(res, {
|
|
61
|
+
connected: false,
|
|
62
|
+
enabled: effectivelyEnabled,
|
|
63
|
+
hasApiKey,
|
|
64
|
+
reason: "not_authenticated"
|
|
65
|
+
});
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (method === "GET" && pathname === "/api/cloud/credits") {
|
|
69
|
+
const cloudAuth = runtime ? runtime.getService("CLOUD_AUTH") : null;
|
|
70
|
+
const configApiKey = config.cloud?.apiKey?.trim();
|
|
71
|
+
if (!cloudAuth || !cloudAuth.isAuthenticated()) {
|
|
72
|
+
if (!configApiKey) {
|
|
73
|
+
json(res, {
|
|
74
|
+
balance: null,
|
|
75
|
+
connected: false
|
|
76
|
+
});
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
const resolvedBaseUrl = resolveCloudApiBaseUrl(config.cloud?.baseUrl);
|
|
80
|
+
const baseUrlRejection = await validateCloudBaseUrl(resolvedBaseUrl);
|
|
81
|
+
if (baseUrlRejection) {
|
|
82
|
+
json(res, {
|
|
83
|
+
balance: null,
|
|
84
|
+
connected: true,
|
|
85
|
+
error: baseUrlRejection
|
|
86
|
+
});
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const balance = await fetchCloudCreditsByApiKey(resolvedBaseUrl, configApiKey);
|
|
91
|
+
if (typeof balance !== "number") {
|
|
92
|
+
json(res, {
|
|
93
|
+
balance: null,
|
|
94
|
+
connected: true,
|
|
95
|
+
error: "unexpected response"
|
|
96
|
+
});
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
json(res, {
|
|
100
|
+
connected: true,
|
|
101
|
+
balance,
|
|
102
|
+
low: balance < 2,
|
|
103
|
+
critical: balance < .5,
|
|
104
|
+
topUpUrl: CLOUD_BILLING_URL
|
|
105
|
+
});
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const msg = err instanceof Error ? err.message : "cloud API unreachable";
|
|
108
|
+
logger.debug(`[cloud/credits] Failed to fetch balance via API key: ${msg}`);
|
|
109
|
+
json(res, {
|
|
110
|
+
balance: null,
|
|
111
|
+
connected: true,
|
|
112
|
+
error: msg
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
let balance;
|
|
118
|
+
const client = cloudAuth.getClient();
|
|
119
|
+
try {
|
|
120
|
+
const creditResponse = await client.get("/credits/balance");
|
|
121
|
+
const rawBalance = typeof creditResponse?.balance === "number" ? creditResponse.balance : typeof (creditResponse?.data)?.balance === "number" ? creditResponse.data.balance : void 0;
|
|
122
|
+
if (typeof rawBalance !== "number") {
|
|
123
|
+
logger.debug(`[cloud/credits] Unexpected response shape: ${JSON.stringify(creditResponse)}`);
|
|
124
|
+
json(res, {
|
|
125
|
+
balance: null,
|
|
126
|
+
connected: true,
|
|
127
|
+
error: "unexpected response"
|
|
128
|
+
});
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
balance = rawBalance;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
const msg = err instanceof Error ? err.message : "cloud API unreachable";
|
|
134
|
+
logger.debug(`[cloud/credits] Failed to fetch balance: ${msg}`);
|
|
135
|
+
json(res, {
|
|
136
|
+
balance: null,
|
|
137
|
+
connected: true,
|
|
138
|
+
error: msg
|
|
139
|
+
});
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
json(res, {
|
|
143
|
+
connected: true,
|
|
144
|
+
balance,
|
|
145
|
+
low: balance < 2,
|
|
146
|
+
critical: balance < .5,
|
|
147
|
+
topUpUrl: CLOUD_BILLING_URL
|
|
148
|
+
});
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
export { handleCloudStatusRoutes };
|