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.
Files changed (241) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +7 -0
  2. package/dist/actions/emote.js +64 -0
  3. package/dist/actions/restart.js +81 -0
  4. package/dist/actions/send-message.js +152 -0
  5. package/dist/agent-admin-routes.js +82 -0
  6. package/dist/agent-lifecycle-routes.js +79 -0
  7. package/dist/agent-transfer-routes.js +102 -0
  8. package/dist/api/agent-admin-routes.js +82 -0
  9. package/dist/api/agent-lifecycle-routes.js +79 -0
  10. package/dist/api/agent-transfer-routes.js +102 -0
  11. package/dist/api/apps-hyperscape-routes.js +58 -0
  12. package/dist/api/apps-routes.js +114 -0
  13. package/dist/api/auth-routes.js +56 -0
  14. package/dist/api/autonomy-routes.js +44 -0
  15. package/dist/api/bug-report-routes.js +111 -0
  16. package/dist/api/character-routes.js +195 -0
  17. package/dist/api/cloud-routes.js +330 -0
  18. package/dist/api/cloud-status-routes.js +155 -0
  19. package/dist/api/compat-utils.js +111 -0
  20. package/dist/api/database.js +735 -0
  21. package/dist/api/diagnostics-routes.js +205 -0
  22. package/dist/api/drop-service.js +134 -0
  23. package/dist/api/early-logs.js +86 -0
  24. package/dist/api/http-helpers.js +131 -0
  25. package/dist/api/knowledge-routes.js +534 -0
  26. package/dist/api/memory-bounds.js +71 -0
  27. package/dist/api/models-routes.js +28 -0
  28. package/dist/api/og-tracker.js +36 -0
  29. package/dist/api/permissions-routes.js +109 -0
  30. package/dist/api/plugin-validation.js +198 -0
  31. package/dist/api/provider-switch-config.js +41 -0
  32. package/dist/api/registry-routes.js +86 -0
  33. package/dist/api/registry-service.js +164 -0
  34. package/dist/api/sandbox-routes.js +1112 -0
  35. package/dist/api/server.js +7949 -0
  36. package/dist/api/subscription-routes.js +172 -0
  37. package/dist/api/terminal-run-limits.js +24 -0
  38. package/dist/api/training-routes.js +158 -0
  39. package/dist/api/trajectory-routes.js +300 -0
  40. package/dist/api/trigger-routes.js +246 -0
  41. package/dist/api/twitter-verify.js +134 -0
  42. package/dist/api/tx-service.js +108 -0
  43. package/dist/api/wallet-routes.js +266 -0
  44. package/dist/api/wallet.js +568 -0
  45. package/dist/api/whatsapp-routes.js +182 -0
  46. package/dist/api/zip-utils.js +109 -0
  47. package/dist/apps-hyperscape-routes.js +58 -0
  48. package/dist/apps-routes.js +114 -0
  49. package/dist/ascii.js +20 -0
  50. package/dist/auth/anthropic.js +44 -0
  51. package/dist/auth/apply-stealth.js +41 -0
  52. package/dist/auth/claude-code-stealth.js +78 -0
  53. package/dist/auth/credentials.js +156 -0
  54. package/dist/auth/index.js +5 -0
  55. package/dist/auth/openai-codex.js +66 -0
  56. package/dist/auth/types.js +9 -0
  57. package/dist/auth-routes.js +56 -0
  58. package/dist/autonomy-routes.js +44 -0
  59. package/dist/bug-report-routes.js +111 -0
  60. package/dist/build-info.json +6 -0
  61. package/dist/character-routes.js +195 -0
  62. package/dist/cli/argv.js +63 -0
  63. package/dist/cli/banner.js +34 -0
  64. package/dist/cli/cli-name.js +21 -0
  65. package/dist/cli/cli-utils.js +16 -0
  66. package/dist/cli/git-commit.js +78 -0
  67. package/dist/cli/parse-duration.js +15 -0
  68. package/dist/cli/plugins-cli.js +590 -0
  69. package/dist/cli/profile-utils.js +9 -0
  70. package/dist/cli/profile.js +95 -0
  71. package/dist/cli/program/build-program.js +17 -0
  72. package/dist/cli/program/command-registry.js +23 -0
  73. package/dist/cli/program/help.js +47 -0
  74. package/dist/cli/program/preaction.js +33 -0
  75. package/dist/cli/program/register.config.js +106 -0
  76. package/dist/cli/program/register.configure.js +20 -0
  77. package/dist/cli/program/register.dashboard.js +124 -0
  78. package/dist/cli/program/register.models.js +23 -0
  79. package/dist/cli/program/register.setup.js +36 -0
  80. package/dist/cli/program/register.start.js +22 -0
  81. package/dist/cli/program/register.subclis.js +70 -0
  82. package/dist/cli/program/register.tui.js +163 -0
  83. package/dist/cli/program/register.update.js +154 -0
  84. package/dist/cli/program.js +3 -0
  85. package/dist/cli/run-main.js +37 -0
  86. package/dist/cli/version.js +7 -0
  87. package/dist/cloud/validate-url.js +93 -0
  88. package/dist/cloud-routes.js +330 -0
  89. package/dist/cloud-status-routes.js +155 -0
  90. package/dist/compat-utils.js +111 -0
  91. package/dist/config/config.js +69 -0
  92. package/dist/config/env-vars.js +19 -0
  93. package/dist/config/includes.js +121 -0
  94. package/dist/config/object-utils.js +7 -0
  95. package/dist/config/paths.js +38 -0
  96. package/dist/config/plugin-auto-enable.js +231 -0
  97. package/dist/config/schema.js +864 -0
  98. package/dist/config/telegram-custom-commands.js +76 -0
  99. package/dist/config/zod-schema.agent-runtime.js +519 -0
  100. package/dist/config/zod-schema.core.js +538 -0
  101. package/dist/config/zod-schema.hooks.js +103 -0
  102. package/dist/config/zod-schema.js +488 -0
  103. package/dist/config/zod-schema.providers-core.js +785 -0
  104. package/dist/config/zod-schema.session.js +73 -0
  105. package/dist/core-plugins.js +37 -0
  106. package/dist/custom-actions.js +250 -0
  107. package/dist/database.js +735 -0
  108. package/dist/diagnostics/integration-observability.js +57 -0
  109. package/dist/diagnostics-routes.js +205 -0
  110. package/dist/drop-service.js +134 -0
  111. package/dist/early-logs.js +24 -0
  112. package/dist/eliza.js +2061 -0
  113. package/dist/emotes/catalog.js +271 -0
  114. package/dist/entry.js +40 -0
  115. package/dist/hooks/discovery.js +167 -0
  116. package/dist/hooks/eligibility.js +64 -0
  117. package/dist/hooks/index.js +4 -0
  118. package/dist/hooks/loader.js +147 -0
  119. package/dist/hooks/registry.js +55 -0
  120. package/dist/http-helpers.js +131 -0
  121. package/dist/index.js +49 -0
  122. package/dist/knowledge-routes.js +534 -0
  123. package/dist/memory-bounds.js +71 -0
  124. package/dist/milady-plugin.js +90 -0
  125. package/dist/models-routes.js +28 -0
  126. package/dist/onboarding-names.js +78 -0
  127. package/dist/onboarding-presets.js +922 -0
  128. package/dist/package.json +1 -0
  129. package/dist/permissions-routes.js +109 -0
  130. package/dist/plugin-validation.js +107 -0
  131. package/dist/plugins/whatsapp/actions.js +91 -0
  132. package/dist/plugins/whatsapp/index.js +16 -0
  133. package/dist/plugins/whatsapp/service.js +270 -0
  134. package/dist/provider-switch-config.js +41 -0
  135. package/dist/providers/admin-trust.js +46 -0
  136. package/dist/providers/autonomous-state.js +101 -0
  137. package/dist/providers/session-bridge.js +86 -0
  138. package/dist/providers/session-utils.js +36 -0
  139. package/dist/providers/simple-mode.js +50 -0
  140. package/dist/providers/ui-catalog.js +15 -0
  141. package/dist/providers/workspace-provider.js +93 -0
  142. package/dist/providers/workspace.js +348 -0
  143. package/dist/registry-routes.js +86 -0
  144. package/dist/registry-service.js +164 -0
  145. package/dist/restart.js +40 -0
  146. package/dist/runtime/core-plugins.js +37 -0
  147. package/dist/runtime/custom-actions.js +250 -0
  148. package/dist/runtime/eliza.js +2061 -0
  149. package/dist/runtime/embedding-manager-support.js +185 -0
  150. package/dist/runtime/embedding-manager.js +193 -0
  151. package/dist/runtime/embedding-presets.js +54 -0
  152. package/dist/runtime/embedding-state.js +8 -0
  153. package/dist/runtime/milady-plugin.js +90 -0
  154. package/dist/runtime/onboarding-names.js +78 -0
  155. package/dist/runtime/restart.js +40 -0
  156. package/dist/runtime/version.js +7 -0
  157. package/dist/sandbox-routes.js +1112 -0
  158. package/dist/security/audit-log.js +149 -0
  159. package/dist/security/network-policy.js +70 -0
  160. package/dist/server.js +7949 -0
  161. package/dist/services/agent-export.js +559 -0
  162. package/dist/services/app-manager.js +389 -0
  163. package/dist/services/browser-capture.js +86 -0
  164. package/dist/services/fallback-training-service.js +128 -0
  165. package/dist/services/mcp-marketplace.js +134 -0
  166. package/dist/services/plugin-installer.js +396 -0
  167. package/dist/services/plugin-manager-types.js +15 -0
  168. package/dist/services/registry-client-app-meta.js +144 -0
  169. package/dist/services/registry-client-endpoints.js +166 -0
  170. package/dist/services/registry-client-local.js +271 -0
  171. package/dist/services/registry-client-network.js +93 -0
  172. package/dist/services/registry-client-queries.js +70 -0
  173. package/dist/services/registry-client.js +157 -0
  174. package/dist/services/sandbox-engine.js +511 -0
  175. package/dist/services/sandbox-manager.js +297 -0
  176. package/dist/services/self-updater.js +175 -0
  177. package/dist/services/skill-catalog-client.js +119 -0
  178. package/dist/services/skill-marketplace.js +521 -0
  179. package/dist/services/stream-manager.js +236 -0
  180. package/dist/services/update-checker.js +121 -0
  181. package/dist/services/update-notifier.js +29 -0
  182. package/dist/services/version-compat.js +78 -0
  183. package/dist/services/whatsapp-pairing.js +196 -0
  184. package/dist/shared/ui-catalog-prompt.js +728 -0
  185. package/dist/subscription-routes.js +172 -0
  186. package/dist/terminal/links.js +19 -0
  187. package/dist/terminal/palette.js +14 -0
  188. package/dist/terminal/theme.js +25 -0
  189. package/dist/terminal-run-limits.js +24 -0
  190. package/dist/training-routes.js +158 -0
  191. package/dist/trajectory-routes.js +300 -0
  192. package/dist/trigger-routes.js +246 -0
  193. package/dist/triggers/action.js +218 -0
  194. package/dist/triggers/runtime.js +281 -0
  195. package/dist/triggers/scheduling.js +295 -0
  196. package/dist/triggers/types.js +5 -0
  197. package/dist/tui/components/assistant-message.js +76 -0
  198. package/dist/tui/components/chat-editor.js +34 -0
  199. package/dist/tui/components/embeddings-overlay.js +46 -0
  200. package/dist/tui/components/footer.js +60 -0
  201. package/dist/tui/components/index.js +15 -0
  202. package/dist/tui/components/modal-frame.js +45 -0
  203. package/dist/tui/components/modal-style.js +15 -0
  204. package/dist/tui/components/model-selector.js +70 -0
  205. package/dist/tui/components/pinned-chat-layout.js +46 -0
  206. package/dist/tui/components/plugins-endpoints-tab.js +196 -0
  207. package/dist/tui/components/plugins-installed-tab-view.js +69 -0
  208. package/dist/tui/components/plugins-installed-tab.js +319 -0
  209. package/dist/tui/components/plugins-overlay-catalog.js +81 -0
  210. package/dist/tui/components/plugins-overlay-data-api.js +21 -0
  211. package/dist/tui/components/plugins-overlay-data-shared.js +20 -0
  212. package/dist/tui/components/plugins-overlay-data.js +323 -0
  213. package/dist/tui/components/plugins-overlay.js +117 -0
  214. package/dist/tui/components/plugins-store-tab.js +148 -0
  215. package/dist/tui/components/settings-overlay.js +61 -0
  216. package/dist/tui/components/status-bar.js +64 -0
  217. package/dist/tui/components/tool-execution.js +68 -0
  218. package/dist/tui/components/user-message.js +22 -0
  219. package/dist/tui/eliza-tui-bridge.js +606 -0
  220. package/dist/tui/index.js +370 -0
  221. package/dist/tui/modal-presets.js +33 -0
  222. package/dist/tui/model-spec.js +46 -0
  223. package/dist/tui/sse-parser.js +78 -0
  224. package/dist/tui/theme.js +110 -0
  225. package/dist/tui/titlebar-spinner.js +62 -0
  226. package/dist/tui/tui-app.js +311 -0
  227. package/dist/tui/ws-client.js +215 -0
  228. package/dist/twitter-verify.js +134 -0
  229. package/dist/tx-service.js +108 -0
  230. package/dist/utils/exec-safety.js +17 -0
  231. package/dist/utils/globals.js +20 -0
  232. package/dist/utils/milady-root.js +61 -0
  233. package/dist/utils/number-parsing.js +37 -0
  234. package/dist/version-resolver.js +37 -0
  235. package/dist/version.js +7 -0
  236. package/dist/wallet-routes.js +266 -0
  237. package/dist/wallet.js +568 -0
  238. package/dist/whatsapp-routes.js +182 -0
  239. package/dist/zip-utils.js +109 -0
  240. package/milady.mjs +14 -0
  241. package/package.json +111 -0
@@ -0,0 +1,735 @@
1
+ import { loadMiladyConfig, saveMiladyConfig } from "../config/config.js";
2
+ import { isLoopbackHost, normalizeHostLike, normalizeIpForPolicy } from "../security/network-policy.js";
3
+ import { readJsonBody as readJsonBody$1, sendJson, sendJsonError } from "./http-helpers.js";
4
+ import { logger } from "@elizaos/core";
5
+ import net from "node:net";
6
+ import { promisify } from "node:util";
7
+ import dns from "node:dns";
8
+
9
+ //#region src/api/database.ts
10
+ /**
11
+ * Database management API handlers for the Milady Control UI.
12
+ *
13
+ * Provides endpoints for:
14
+ * - Database provider configuration (PGLite vs Postgres)
15
+ * - Connection testing for remote Postgres
16
+ * - Table browsing and introspection
17
+ * - Row-level CRUD operations
18
+ * - Raw SQL query execution
19
+ * - Database status and health
20
+ *
21
+ * All data endpoints use the active runtime's database adapter (Drizzle ORM)
22
+ * so they work identically for both PGLite and Postgres.
23
+ */
24
+ async function readJsonBody(req, res) {
25
+ return readJsonBody$1(req, res, { maxBytes: 2 * 1024 * 1024 });
26
+ }
27
+ /**
28
+ * Safely quote a SQL identifier (table or column name).
29
+ * Postgres uses double-quote escaping: embedded " becomes "".
30
+ */
31
+ function quoteIdent(name) {
32
+ return `"${name.replace(/"/g, "\"\"")}"`;
33
+ }
34
+ /**
35
+ * Build a Postgres connection string from individual credential fields.
36
+ */
37
+ function buildConnectionString(creds) {
38
+ if (creds.connectionString) return creds.connectionString;
39
+ const host = creds.host ?? "localhost";
40
+ const port = creds.port ?? 5432;
41
+ const user = encodeURIComponent(creds.user ?? "postgres");
42
+ const password = creds.password ? encodeURIComponent(creds.password) : "";
43
+ const database = creds.database ?? "postgres";
44
+ return `postgresql://${password ? `${user}:${password}` : user}@${host}:${port}/${database}${creds.ssl ? "?sslmode=require" : ""}`;
45
+ }
46
+ /**
47
+ * Return a copy of credentials with host pinned to a validated IP address.
48
+ * For connection strings, rewrites URL hostname to avoid re-resolution later.
49
+ */
50
+ function withPinnedHost(creds, pinnedHost) {
51
+ const normalizedPinned = pinnedHost.replace(/^::ffff:/i, "");
52
+ const next = {
53
+ ...creds,
54
+ host: normalizedPinned
55
+ };
56
+ if (next.connectionString) try {
57
+ const parsed = new URL(next.connectionString);
58
+ parsed.hostname = normalizedPinned;
59
+ parsed.searchParams.set("host", normalizedPinned);
60
+ parsed.searchParams.set("hostaddr", normalizedPinned);
61
+ next.connectionString = parsed.toString();
62
+ } catch {
63
+ delete next.connectionString;
64
+ }
65
+ return next;
66
+ }
67
+ const dnsLookupAll = promisify(dns.lookup);
68
+ /**
69
+ * IP ranges that are ALWAYS blocked regardless of bind address.
70
+ * Cloud metadata and "this" network are never legitimate Postgres targets.
71
+ */
72
+ const ALWAYS_BLOCKED_IP_PATTERNS = [
73
+ /^169\.254\./,
74
+ /^0\./,
75
+ /^fe[89ab][0-9a-f]:/i
76
+ ];
77
+ /**
78
+ * Private/internal IP ranges — blocked only when the API is bound to a
79
+ * non-loopback address (i.e. remotely reachable). When bound to 127.0.0.1
80
+ * (the default), these are allowed since local Postgres is the most common
81
+ * setup and an attacker who can reach the loopback API already has local
82
+ * network access.
83
+ */
84
+ const PRIVATE_IP_PATTERNS = [
85
+ /^127\./,
86
+ /^10\./,
87
+ /^172\.(1[6-9]|2\d|3[01])\./,
88
+ /^192\.168\./,
89
+ /^::1$/,
90
+ /^f[cd][0-9a-f]{2}:/i
91
+ ];
92
+ /**
93
+ * Returns true when the API server is bound to a loopback-only address.
94
+ * In that case, private/internal IP ranges are allowed for DB connections
95
+ * since only local processes can reach the API.
96
+ */
97
+ function isApiLoopbackOnly() {
98
+ let bind = (process.env.MILADY_API_BIND ?? "127.0.0.1").trim().toLowerCase();
99
+ if (!bind) bind = "127.0.0.1";
100
+ if (bind.startsWith("http://") || bind.startsWith("https://")) try {
101
+ bind = new URL(bind).hostname.toLowerCase();
102
+ } catch {}
103
+ const bracketedIpv6 = /^\[([^\]]+)\](?::\d+)?$/.exec(bind);
104
+ if (bracketedIpv6?.[1]) bind = bracketedIpv6[1];
105
+ else {
106
+ const singleColonHostPort = /^([^:]+):(\d+)$/.exec(bind);
107
+ if (singleColonHostPort?.[1]) bind = singleColonHostPort[1];
108
+ }
109
+ bind = bind.replace(/^\[|\]$/g, "");
110
+ return isLoopbackHost(bind);
111
+ }
112
+ /**
113
+ * Extract all potential hosts from a Postgres connection string or credentials object.
114
+ * Includes query params like ?host= and ?hostaddr= which Postgres clients honor.
115
+ * Returns empty array if no host can be determined.
116
+ */
117
+ function extractHosts(creds) {
118
+ if (creds.connectionString) try {
119
+ const url = new URL(creds.connectionString);
120
+ const hosts = [];
121
+ const hostParam = url.searchParams.get("host");
122
+ if (hostParam) hosts.push(...hostParam.split(",").map((h) => normalizeHostLike(h)).filter(Boolean));
123
+ const hostAddrParam = url.searchParams.get("hostaddr");
124
+ if (hostAddrParam) hosts.push(...hostAddrParam.split(",").map((h) => normalizeHostLike(h)).filter(Boolean));
125
+ if (url.hostname) hosts.push(normalizeHostLike(url.hostname));
126
+ return [...new Set(hosts)];
127
+ } catch {
128
+ return [];
129
+ }
130
+ if (creds.host) {
131
+ const host = normalizeHostLike(creds.host);
132
+ return host ? [host] : [];
133
+ }
134
+ return [];
135
+ }
136
+ /**
137
+ * Check whether an IP address falls in a blocked range.
138
+ * When the API is remotely reachable, private ranges are also blocked.
139
+ */
140
+ function isBlockedIp(ip) {
141
+ const normalized = normalizeIpForPolicy(ip);
142
+ if (ALWAYS_BLOCKED_IP_PATTERNS.some((p) => p.test(normalized))) return true;
143
+ if (!isApiLoopbackOnly() && PRIVATE_IP_PATTERNS.some((p) => p.test(normalized))) return true;
144
+ return false;
145
+ }
146
+ /**
147
+ * Validate that all target hosts do not resolve to blocked addresses.
148
+ *
149
+ * Performs DNS resolution to catch hostnames like `metadata.google.internal`
150
+ * or `169.254.169.254.nip.io` that resolve to link-local / cloud metadata
151
+ * IPs. Also handles IPv6-mapped IPv4 addresses (e.g. `::ffff:169.254.x.y`).
152
+ *
153
+ * Returns a validation result including a pinned host IP when successful.
154
+ */
155
+ async function validateDbHost(creds, opts = {}) {
156
+ const hosts = extractHosts(creds);
157
+ if (hosts.length === 0) return {
158
+ error: "Could not determine target host from the provided credentials.",
159
+ pinnedHost: null
160
+ };
161
+ let pinnedHost = null;
162
+ for (const host of hosts) {
163
+ const literalNormalized = normalizeIpForPolicy(host);
164
+ if (isBlockedIp(literalNormalized)) return {
165
+ error: `Connection to "${host}" is blocked: link-local and metadata addresses are not allowed.`,
166
+ pinnedHost: null
167
+ };
168
+ if (net.isIP(literalNormalized)) {
169
+ if (!pinnedHost) pinnedHost = literalNormalized;
170
+ continue;
171
+ }
172
+ try {
173
+ const results = await dnsLookupAll(host, { all: true });
174
+ const addresses = Array.isArray(results) ? results : [results];
175
+ for (const entry of addresses) {
176
+ const ip = typeof entry === "string" ? entry : entry.address;
177
+ const normalized = normalizeIpForPolicy(ip);
178
+ if (isBlockedIp(normalized)) return {
179
+ error: `Connection to "${host}" is blocked: it resolves to ${ip} which is a link-local or metadata address.`,
180
+ pinnedHost: null
181
+ };
182
+ if (!pinnedHost) pinnedHost = normalized;
183
+ }
184
+ } catch {
185
+ if (!opts.allowUnresolvedHostnames) return {
186
+ error: `Connection to "${host}" failed DNS resolution during validation. Use a resolvable hostname or a literal IP address.`,
187
+ pinnedHost: null
188
+ };
189
+ }
190
+ }
191
+ if (!pinnedHost) {
192
+ if (opts.allowUnresolvedHostnames) return {
193
+ error: null,
194
+ pinnedHost: null
195
+ };
196
+ return {
197
+ error: "Could not validate any host to a concrete IP address.",
198
+ pinnedHost: null
199
+ };
200
+ }
201
+ return {
202
+ error: null,
203
+ pinnedHost
204
+ };
205
+ }
206
+ /** Convert a JS value to a SQL literal for use in raw queries. */
207
+ function sqlLiteral(v) {
208
+ if (v === null || v === void 0) return "NULL";
209
+ if (typeof v === "number") return String(v);
210
+ if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
211
+ if (typeof v === "object") return `'${JSON.stringify(v).replace(/'/g, "''")}'::jsonb`;
212
+ return `'${String(v).replace(/'/g, "''")}'`;
213
+ }
214
+ /** Build a "col = val" SQL assignment clause. */
215
+ function sqlAssign(col, val) {
216
+ if (val === null || val === void 0) return `${quoteIdent(col)} = NULL`;
217
+ return `${quoteIdent(col)} = ${sqlLiteral(val)}`;
218
+ }
219
+ /** Build a "col = val" or "col IS NULL" SQL WHERE predicate. */
220
+ function sqlPredicate(col, val) {
221
+ if (val === null || val === void 0) return `${quoteIdent(col)} IS NULL`;
222
+ return `${quoteIdent(col)} = ${sqlLiteral(val)}`;
223
+ }
224
+ let _sqlHelper = null;
225
+ async function getDrizzleSql() {
226
+ if (!_sqlHelper) _sqlHelper = (await import("drizzle-orm")).sql;
227
+ return _sqlHelper;
228
+ }
229
+ /** Execute raw SQL via the runtime's Drizzle adapter. */
230
+ async function executeRawSql(runtime, sqlText) {
231
+ const drizzleSql = await getDrizzleSql();
232
+ const db = runtime.adapter.db;
233
+ const rawQuery = drizzleSql?.raw(sqlText);
234
+ if (!rawQuery) throw new Error("SQL module not available");
235
+ const result = await db.execute(rawQuery);
236
+ const rows = Array.isArray(result.rows) ? result.rows : result;
237
+ let columns = [];
238
+ if (result.fields && Array.isArray(result.fields)) columns = result.fields.map((f) => f.name);
239
+ else if (rows.length > 0) columns = Object.keys(rows[0]);
240
+ return {
241
+ rows,
242
+ columns
243
+ };
244
+ }
245
+ /**
246
+ * Detect the current database provider from environment / runtime state.
247
+ */
248
+ function detectCurrentProvider() {
249
+ return process.env.POSTGRES_URL ? "postgres" : "pglite";
250
+ }
251
+ /** Verify a table name refers to a real user table. */
252
+ async function assertTableExists(runtime, tableName) {
253
+ const { rows } = await executeRawSql(runtime, `SELECT 1 FROM information_schema.tables
254
+ WHERE table_name = '${tableName.replace(/'/g, "''")}'
255
+ AND table_schema NOT IN ('pg_catalog', 'information_schema')
256
+ AND table_type = 'BASE TABLE'
257
+ LIMIT 1`);
258
+ return rows.length > 0;
259
+ }
260
+ /**
261
+ * GET /api/database/status
262
+ * Returns current connection status, provider, table count, version.
263
+ */
264
+ async function handleGetStatus(_req, res, runtime) {
265
+ const provider = detectCurrentProvider();
266
+ if (!runtime?.adapter) {
267
+ sendJson(res, {
268
+ provider,
269
+ connected: false,
270
+ serverVersion: null,
271
+ tableCount: 0,
272
+ pgliteDataDir: process.env.PGLITE_DATA_DIR ?? null,
273
+ postgresHost: null
274
+ });
275
+ return;
276
+ }
277
+ const { rows } = await executeRawSql(runtime, "SELECT version()");
278
+ const serverVersion = rows.length > 0 ? String(rows[0].version ?? "") : null;
279
+ const tableResult = await executeRawSql(runtime, `SELECT count(*) AS cnt
280
+ FROM information_schema.tables
281
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
282
+ AND table_type = 'BASE TABLE'`);
283
+ sendJson(res, {
284
+ provider,
285
+ connected: true,
286
+ serverVersion,
287
+ tableCount: tableResult.rows.length > 0 ? Number(tableResult.rows[0].cnt ?? 0) : 0,
288
+ pgliteDataDir: provider === "pglite" ? process.env.PGLITE_DATA_DIR ?? null : null,
289
+ postgresHost: provider === "postgres" ? process.env.POSTGRES_URL?.replace(/^postgresql:\/\/[^@]*@/, "").replace(/\/.*$/, "") ?? null : null
290
+ });
291
+ }
292
+ /**
293
+ * GET /api/database/config
294
+ * Returns the persisted database configuration from milady.json.
295
+ */
296
+ function handleGetConfig(_req, res) {
297
+ const dbConfig = loadMiladyConfig().database ?? { provider: "pglite" };
298
+ const sanitized = { ...dbConfig };
299
+ if (sanitized.postgres?.password) sanitized.postgres = {
300
+ ...sanitized.postgres,
301
+ password: "••••••••"
302
+ };
303
+ if (sanitized.postgres?.connectionString) sanitized.postgres = {
304
+ ...sanitized.postgres,
305
+ connectionString: sanitized.postgres.connectionString.replace(/:([^@]+)@/, ":••••••••@")
306
+ };
307
+ sendJson(res, {
308
+ config: sanitized,
309
+ activeProvider: detectCurrentProvider(),
310
+ needsRestart: (dbConfig.provider ?? "pglite") !== detectCurrentProvider()
311
+ });
312
+ }
313
+ /**
314
+ * PUT /api/database/config
315
+ * Saves new database configuration. Does NOT restart the agent automatically;
316
+ * the UI prompts the user to restart.
317
+ */
318
+ async function handlePutConfig(req, res) {
319
+ const body = await readJsonBody(req, res);
320
+ if (!body) return;
321
+ if (body.provider && body.provider !== "pglite" && body.provider !== "postgres") {
322
+ sendJsonError(res, `Invalid provider: ${String(body.provider)}. Must be "pglite" or "postgres".`);
323
+ return;
324
+ }
325
+ const config = loadMiladyConfig();
326
+ const existingDb = config.database ?? {};
327
+ const effectiveProvider = body.provider ?? existingDb.provider ?? "pglite";
328
+ let validatedPostgres = null;
329
+ if (body.postgres) {
330
+ const pg = body.postgres;
331
+ if (effectiveProvider === "postgres" && !pg.connectionString && !pg.host) {
332
+ sendJsonError(res, "Postgres configuration requires either a connectionString or at least a host.");
333
+ return;
334
+ }
335
+ const validation = await validateDbHost(pg, { allowUnresolvedHostnames: Boolean(pg.connectionString) });
336
+ if (validation.error) {
337
+ sendJsonError(res, validation.error);
338
+ return;
339
+ }
340
+ validatedPostgres = validation.pinnedHost ? withPinnedHost(pg, validation.pinnedHost) : pg;
341
+ }
342
+ const merged = {
343
+ ...existingDb,
344
+ ...body
345
+ };
346
+ if (merged.provider === "postgres" && body.postgres) merged.postgres = {
347
+ ...existingDb.postgres,
348
+ ...validatedPostgres ?? body.postgres
349
+ };
350
+ if (merged.provider === "pglite" && body.pglite) merged.pglite = {
351
+ ...existingDb.pglite,
352
+ ...body.pglite
353
+ };
354
+ config.database = merged;
355
+ saveMiladyConfig(config);
356
+ logger.info({
357
+ src: "database-api",
358
+ provider: merged.provider
359
+ }, "Database configuration saved");
360
+ sendJson(res, {
361
+ saved: true,
362
+ config: merged,
363
+ needsRestart: (merged.provider ?? "pglite") !== detectCurrentProvider()
364
+ });
365
+ }
366
+ /**
367
+ * POST /api/database/test
368
+ * Tests a Postgres connection without persisting anything.
369
+ * Body: { connectionString?, host?, port?, user?, password?, database?, ssl? }
370
+ */
371
+ async function handleTestConnection(req, res) {
372
+ const body = await readJsonBody(req, res);
373
+ if (!body) return;
374
+ const validation = await validateDbHost(body);
375
+ if (validation.error) {
376
+ sendJsonError(res, validation.error);
377
+ return;
378
+ }
379
+ const connectionString = buildConnectionString(validation.pinnedHost ? withPinnedHost(body, validation.pinnedHost) : body);
380
+ const start = Date.now();
381
+ let Pool;
382
+ try {
383
+ const pgModule = await import("pg");
384
+ Pool = pgModule.default?.Pool ?? pgModule.Pool;
385
+ } catch {
386
+ sendJson(res, {
387
+ success: false,
388
+ serverVersion: null,
389
+ error: "PostgreSQL client library (pg) is not available. Ensure @elizaos/plugin-sql is installed.",
390
+ durationMs: Date.now() - start
391
+ });
392
+ return;
393
+ }
394
+ const pool = new Pool({
395
+ connectionString,
396
+ max: 1,
397
+ connectionTimeoutMillis: 1e4,
398
+ idleTimeoutMillis: 5e3
399
+ });
400
+ let client = null;
401
+ try {
402
+ client = await pool.connect();
403
+ const versionResult = await client.query("SELECT version()");
404
+ sendJson(res, {
405
+ success: true,
406
+ serverVersion: String(versionResult.rows[0]?.version ?? ""),
407
+ error: null,
408
+ durationMs: Date.now() - start
409
+ });
410
+ } catch (err) {
411
+ const durationMs = Date.now() - start;
412
+ sendJson(res, {
413
+ success: false,
414
+ serverVersion: null,
415
+ error: err instanceof Error ? err.message : String(err),
416
+ durationMs
417
+ });
418
+ } finally {
419
+ if (client) client.release();
420
+ await pool.end();
421
+ }
422
+ }
423
+ /**
424
+ * GET /api/database/tables
425
+ * Lists all user tables with column metadata and approximate row counts.
426
+ */
427
+ async function handleGetTables(_req, res, runtime) {
428
+ const tablesResult = await executeRawSql(runtime, `SELECT
429
+ t.table_schema AS schema,
430
+ t.table_name AS name,
431
+ COALESCE(s.n_live_tup, 0) AS row_count
432
+ FROM information_schema.tables t
433
+ LEFT JOIN pg_stat_user_tables s
434
+ ON s.schemaname = t.table_schema
435
+ AND s.relname = t.table_name
436
+ WHERE t.table_schema NOT IN ('pg_catalog', 'information_schema')
437
+ AND t.table_type = 'BASE TABLE'
438
+ ORDER BY t.table_schema, t.table_name`);
439
+ const columnsResult = await executeRawSql(runtime, `SELECT
440
+ c.table_schema AS schema,
441
+ c.table_name AS table_name,
442
+ c.column_name AS name,
443
+ c.data_type AS type,
444
+ (c.is_nullable = 'YES') AS nullable,
445
+ c.column_default AS default_value,
446
+ COALESCE(
447
+ (SELECT true
448
+ FROM information_schema.table_constraints tc
449
+ JOIN information_schema.key_column_usage kcu
450
+ ON tc.constraint_name = kcu.constraint_name
451
+ AND tc.table_schema = kcu.table_schema
452
+ WHERE tc.constraint_type = 'PRIMARY KEY'
453
+ AND tc.table_schema = c.table_schema
454
+ AND tc.table_name = c.table_name
455
+ AND kcu.column_name = c.column_name),
456
+ false
457
+ ) AS is_primary_key
458
+ FROM information_schema.columns c
459
+ WHERE c.table_schema NOT IN ('pg_catalog', 'information_schema')
460
+ ORDER BY c.table_schema, c.table_name, c.ordinal_position`);
461
+ const columnsByTable = /* @__PURE__ */ new Map();
462
+ for (const row of columnsResult.rows) {
463
+ const key = `${String(row.schema)}.${String(row.table_name)}`;
464
+ const cols = columnsByTable.get(key) ?? [];
465
+ cols.push({
466
+ name: String(row.name),
467
+ type: String(row.type),
468
+ nullable: Boolean(row.nullable),
469
+ defaultValue: row.default_value != null ? String(row.default_value) : null,
470
+ isPrimaryKey: Boolean(row.is_primary_key)
471
+ });
472
+ columnsByTable.set(key, cols);
473
+ }
474
+ sendJson(res, { tables: tablesResult.rows.map((row) => {
475
+ const key = `${String(row.schema)}.${String(row.name)}`;
476
+ return {
477
+ name: String(row.name),
478
+ schema: String(row.schema),
479
+ rowCount: Number(row.row_count ?? 0),
480
+ columns: columnsByTable.get(key) ?? []
481
+ };
482
+ }) });
483
+ }
484
+ /**
485
+ * GET /api/database/tables/:table/rows?offset=0&limit=50&sort=col&order=asc&search=term
486
+ * Paginated row retrieval for a specific table.
487
+ */
488
+ async function handleGetRows(req, res, runtime, tableName) {
489
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
490
+ const offset = Math.max(0, Number(url.searchParams.get("offset") ?? "0"));
491
+ const limit = Math.min(500, Math.max(1, Number(url.searchParams.get("limit") ?? "50")));
492
+ const sortCol = url.searchParams.get("sort") ?? "";
493
+ const sortOrder = url.searchParams.get("order") === "desc" ? "DESC" : "ASC";
494
+ const search = url.searchParams.get("search") ?? "";
495
+ if (!await assertTableExists(runtime, tableName)) {
496
+ sendJsonError(res, `Table "${tableName}" not found`, 404);
497
+ return;
498
+ }
499
+ const colResult = await executeRawSql(runtime, `SELECT column_name, data_type
500
+ FROM information_schema.columns
501
+ WHERE table_name = '${tableName.replace(/'/g, "''")}'
502
+ AND table_schema NOT IN ('pg_catalog', 'information_schema')
503
+ ORDER BY ordinal_position`);
504
+ const columnNames = colResult.rows.map((r) => String(r.column_name));
505
+ const columnTypes = new Map(colResult.rows.map((r) => [String(r.column_name), String(r.data_type)]));
506
+ const validSort = sortCol && columnNames.includes(sortCol) ? sortCol : "";
507
+ let whereClause = "";
508
+ if (search.trim()) {
509
+ const escapedSearch = search.replace(/'/g, "''").replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
510
+ const textColumns = columnNames.filter((col) => {
511
+ const t = columnTypes.get(col) ?? "";
512
+ return t.includes("char") || t.includes("text") || t === "uuid" || t === "jsonb" || t === "json" || t === "integer" || t === "bigint" || t === "numeric" || t === "timestamp" || t.includes("timestamp");
513
+ });
514
+ if (textColumns.length > 0) whereClause = `WHERE (${textColumns.map((col) => `${quoteIdent(col)}::text ILIKE '%${escapedSearch}%' ESCAPE '\\'`).join(" OR ")})`;
515
+ }
516
+ const countResult = await executeRawSql(runtime, `SELECT count(*) AS total FROM ${quoteIdent(tableName)} ${whereClause}`);
517
+ const total = Number(countResult.rows[0]?.total ?? 0);
518
+ const orderClause = validSort ? `ORDER BY ${quoteIdent(validSort)} ${sortOrder}` : "";
519
+ const result = await executeRawSql(runtime, `SELECT * FROM ${quoteIdent(tableName)} ${whereClause} ${orderClause} LIMIT ${limit} OFFSET ${offset}`);
520
+ sendJson(res, {
521
+ table: tableName,
522
+ rows: result.rows,
523
+ columns: result.columns,
524
+ total,
525
+ offset,
526
+ limit
527
+ });
528
+ }
529
+ /**
530
+ * POST /api/database/tables/:table/rows
531
+ * Insert a new row. Body: { data: Record<string, unknown> }
532
+ */
533
+ async function handleInsertRow(req, res, runtime, tableName) {
534
+ const body = await readJsonBody(req, res);
535
+ if (!body) return;
536
+ if (!body.data || typeof body.data !== "object" || Object.keys(body.data).length === 0) {
537
+ sendJsonError(res, "Request body must include a non-empty 'data' object.");
538
+ return;
539
+ }
540
+ if (!await assertTableExists(runtime, tableName)) {
541
+ sendJsonError(res, `Table "${tableName}" not found`, 404);
542
+ return;
543
+ }
544
+ const columns = Object.keys(body.data);
545
+ const values = Object.values(body.data);
546
+ const colList = columns.map((c) => quoteIdent(c)).join(", ");
547
+ const valList = values.map(sqlLiteral).join(", ");
548
+ sendJson(res, {
549
+ inserted: true,
550
+ row: (await executeRawSql(runtime, `INSERT INTO ${quoteIdent(tableName)} (${colList}) VALUES (${valList}) RETURNING *`)).rows[0] ?? null
551
+ }, 201);
552
+ }
553
+ /**
554
+ * PUT /api/database/tables/:table/rows
555
+ * Update a row. Body: { where: Record<string, unknown>, data: Record<string, unknown> }
556
+ */
557
+ async function handleUpdateRow(req, res, runtime, tableName) {
558
+ const body = await readJsonBody(req, res);
559
+ if (!body) return;
560
+ if (!body.where || Object.keys(body.where).length === 0) {
561
+ sendJsonError(res, "Request body must include a non-empty 'where' object for row identification.");
562
+ return;
563
+ }
564
+ if (!body.data || Object.keys(body.data).length === 0) {
565
+ sendJsonError(res, "Request body must include a non-empty 'data' object with fields to update.");
566
+ return;
567
+ }
568
+ const setClauses = Object.entries(body.data).map(([col, val]) => sqlAssign(col, val));
569
+ const whereClauses = Object.entries(body.where).map(([col, val]) => sqlPredicate(col, val));
570
+ const result = await executeRawSql(runtime, `UPDATE ${quoteIdent(tableName)}
571
+ SET ${setClauses.join(", ")}
572
+ WHERE ${whereClauses.join(" AND ")}
573
+ RETURNING *`);
574
+ if (result.rows.length === 0) {
575
+ sendJsonError(res, "No matching row found to update.", 404);
576
+ return;
577
+ }
578
+ sendJson(res, {
579
+ updated: true,
580
+ row: result.rows[0]
581
+ });
582
+ }
583
+ /**
584
+ * DELETE /api/database/tables/:table/rows
585
+ * Delete a row. Body: { where: Record<string, unknown> }
586
+ */
587
+ async function handleDeleteRow(req, res, runtime, tableName) {
588
+ const body = await readJsonBody(req, res);
589
+ if (!body) return;
590
+ if (!body.where || Object.keys(body.where).length === 0) {
591
+ sendJsonError(res, "Request body must include a non-empty 'where' object for row identification.");
592
+ return;
593
+ }
594
+ const whereClauses = Object.entries(body.where).map(([col, val]) => sqlPredicate(col, val));
595
+ const result = await executeRawSql(runtime, `DELETE FROM ${quoteIdent(tableName)}
596
+ WHERE ${whereClauses.join(" AND ")}
597
+ RETURNING *`);
598
+ if (result.rows.length === 0) {
599
+ sendJsonError(res, "No matching row found to delete.", 404);
600
+ return;
601
+ }
602
+ sendJson(res, {
603
+ deleted: true,
604
+ row: result.rows[0]
605
+ });
606
+ }
607
+ /**
608
+ * POST /api/database/query
609
+ * Execute a raw SQL query. Body: { sql: string, readOnly?: boolean }
610
+ */
611
+ async function handleQuery(req, res, runtime) {
612
+ const body = await readJsonBody(req, res);
613
+ if (!body) return;
614
+ if (!body.sql || typeof body.sql !== "string" || body.sql.trim().length === 0) {
615
+ sendJsonError(res, "Request body must include a non-empty 'sql' string.");
616
+ return;
617
+ }
618
+ const sqlText = body.sql.trim();
619
+ if (body.readOnly !== false) {
620
+ const stripped = sqlText.replace(/\/\*[\s\S]*?\*\//g, "").replace(/--.*$/gm, "").trim();
621
+ const noLiterals = stripped.replace(/\$([A-Za-z0-9_]*)\$[\s\S]*?\$\1\$/g, " ").replace(/'(?:[^']|'')*'/g, " ");
622
+ const noStrings = noLiterals.replace(/"(?:[^"]|"")*"/g, " ");
623
+ const match = new RegExp(`\\b(${[
624
+ "INSERT",
625
+ "UPDATE",
626
+ "DELETE",
627
+ "INTO",
628
+ "DROP",
629
+ "ALTER",
630
+ "TRUNCATE",
631
+ "CREATE",
632
+ "COPY",
633
+ "MERGE",
634
+ "CALL",
635
+ "DO",
636
+ "REFRESH",
637
+ "REINDEX",
638
+ "VACUUM",
639
+ "GRANT",
640
+ "REVOKE"
641
+ ].join("|")})\\b`, "i").exec(noStrings);
642
+ if (match) {
643
+ sendJsonError(res, `Query rejected: "${match[1].toUpperCase()}" is a mutation keyword. Set readOnly: false to execute mutations.`);
644
+ return;
645
+ }
646
+ const mutatingFunctionMatch = /(?:^|[^\w$])"?((?:nextval|setval))"?\s*\(/i.exec(noLiterals);
647
+ if (mutatingFunctionMatch) {
648
+ sendJsonError(res, `Query rejected: "${mutatingFunctionMatch[1].toUpperCase()}" is a mutating function. Set readOnly: false to execute mutations.`);
649
+ return;
650
+ }
651
+ if (stripped.replace(/;\s*$/, "").includes(";")) {
652
+ sendJsonError(res, "Query rejected: multi-statement queries are not allowed in read-only mode.");
653
+ return;
654
+ }
655
+ }
656
+ const start = Date.now();
657
+ const result = await executeRawSql(runtime, sqlText);
658
+ const durationMs = Date.now() - start;
659
+ sendJson(res, {
660
+ columns: result.columns,
661
+ rows: result.rows,
662
+ rowCount: result.rows.length,
663
+ durationMs
664
+ });
665
+ }
666
+ /**
667
+ * Route a database API request. Returns true if handled, false if not matched.
668
+ *
669
+ * Expected URL patterns:
670
+ * GET /api/database/status
671
+ * GET /api/database/config
672
+ * PUT /api/database/config
673
+ * POST /api/database/test
674
+ * GET /api/database/tables
675
+ * GET /api/database/tables/:table/rows
676
+ * POST /api/database/tables/:table/rows
677
+ * PUT /api/database/tables/:table/rows
678
+ * DELETE /api/database/tables/:table/rows
679
+ * POST /api/database/query
680
+ */
681
+ async function handleDatabaseRoute(req, res, runtime, pathname) {
682
+ const method = req.method ?? "GET";
683
+ if (method === "GET" && pathname === "/api/database/status") {
684
+ await handleGetStatus(req, res, runtime);
685
+ return true;
686
+ }
687
+ if (method === "GET" && pathname === "/api/database/config") {
688
+ handleGetConfig(req, res);
689
+ return true;
690
+ }
691
+ if (method === "PUT" && pathname === "/api/database/config") {
692
+ await handlePutConfig(req, res);
693
+ return true;
694
+ }
695
+ if (method === "POST" && pathname === "/api/database/test") {
696
+ await handleTestConnection(req, res);
697
+ return true;
698
+ }
699
+ if (!runtime?.adapter) {
700
+ sendJsonError(res, "Database not available. The agent may not be running or the database adapter is not initialized.", 503);
701
+ return true;
702
+ }
703
+ if (method === "GET" && pathname === "/api/database/tables") {
704
+ await handleGetTables(req, res, runtime);
705
+ return true;
706
+ }
707
+ if (method === "POST" && pathname === "/api/database/query") {
708
+ await handleQuery(req, res, runtime);
709
+ return true;
710
+ }
711
+ const rowsMatch = pathname.match(/^\/api\/database\/tables\/([^/]+)\/rows$/);
712
+ if (rowsMatch) {
713
+ const tableNameDecoded = decodeURIComponent(rowsMatch[1]);
714
+ if (method === "GET") {
715
+ await handleGetRows(req, res, runtime, tableNameDecoded);
716
+ return true;
717
+ }
718
+ if (method === "POST") {
719
+ await handleInsertRow(req, res, runtime, tableNameDecoded);
720
+ return true;
721
+ }
722
+ if (method === "PUT") {
723
+ await handleUpdateRow(req, res, runtime, tableNameDecoded);
724
+ return true;
725
+ }
726
+ if (method === "DELETE") {
727
+ await handleDeleteRow(req, res, runtime, tableNameDecoded);
728
+ return true;
729
+ }
730
+ }
731
+ return false;
732
+ }
733
+
734
+ //#endregion
735
+ export { handleDatabaseRoute };