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,2061 @@
|
|
|
1
|
+
import { resolveStateDir, resolveUserPath } from "../config/paths.js";
|
|
2
|
+
import { collectConfigEnvVars } from "../config/env-vars.js";
|
|
3
|
+
import { configFileExists, loadMiladyConfig, saveMiladyConfig } from "../config/config.js";
|
|
4
|
+
import { ensureAgentWorkspace, resolveDefaultAgentWorkspaceDir } from "../providers/workspace.js";
|
|
5
|
+
import { CORE_PLUGINS, OPTIONAL_CORE_PLUGINS } from "./core-plugins.js";
|
|
6
|
+
import { SandboxAuditLog } from "../security/audit-log.js";
|
|
7
|
+
import { debugLogResolvedContext, validateRuntimeContext } from "../api/plugin-validation.js";
|
|
8
|
+
import { STYLE_PRESETS } from "../onboarding-presets.js";
|
|
9
|
+
import { pickRandomNames } from "./onboarding-names.js";
|
|
10
|
+
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
|
11
|
+
import { createHookEvent, triggerHook } from "../hooks/registry.js";
|
|
12
|
+
import { loadHooks } from "../hooks/loader.js";
|
|
13
|
+
import "../hooks/index.js";
|
|
14
|
+
import { SandboxManager } from "../services/sandbox-manager.js";
|
|
15
|
+
import { diagnoseNoAIProvider } from "../services/version-compat.js";
|
|
16
|
+
import { createMiladyPlugin } from "./milady-plugin.js";
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import process from "node:process";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { AgentRuntime, AutonomyService, ChannelType, addLogListener, createMessageMemory, logger, mergeCharacterDefaults, stringToUuid } from "@elizaos/core";
|
|
22
|
+
import crypto from "node:crypto";
|
|
23
|
+
import { existsSync, mkdirSync, symlinkSync } from "node:fs";
|
|
24
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
25
|
+
import fs$1 from "node:fs/promises";
|
|
26
|
+
import * as readline from "node:readline";
|
|
27
|
+
import * as clack from "@clack/prompts";
|
|
28
|
+
|
|
29
|
+
//#region src/runtime/eliza.ts
|
|
30
|
+
/**
|
|
31
|
+
* ElizaOS runtime entry point for Milady.
|
|
32
|
+
*
|
|
33
|
+
* Starts the ElizaOS agent runtime with Milady's plugin configuration.
|
|
34
|
+
* Can be run directly via: node --import tsx src/runtime/eliza.ts
|
|
35
|
+
* Or via the CLI: milady start
|
|
36
|
+
*
|
|
37
|
+
* @module eliza
|
|
38
|
+
*/
|
|
39
|
+
function configureLocalEmbeddingPlugin(_plugin, config) {
|
|
40
|
+
const isAppleSilicon = process.platform === "darwin" && process.arch === "arm64";
|
|
41
|
+
const embeddingConfig = config?.embedding;
|
|
42
|
+
const configuredModel = embeddingConfig?.model?.trim();
|
|
43
|
+
const configuredRepo = embeddingConfig?.modelRepo?.trim();
|
|
44
|
+
const configuredDimensions = typeof embeddingConfig?.dimensions === "number" && Number.isInteger(embeddingConfig.dimensions) && embeddingConfig.dimensions > 0 ? String(embeddingConfig.dimensions) : void 0;
|
|
45
|
+
const configuredContextSize = typeof embeddingConfig?.contextSize === "number" && Number.isInteger(embeddingConfig.contextSize) && embeddingConfig.contextSize > 0 ? String(embeddingConfig.contextSize) : void 0;
|
|
46
|
+
const configuredGpuLayers = (() => {
|
|
47
|
+
const value = embeddingConfig?.gpuLayers;
|
|
48
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return String(value);
|
|
49
|
+
if (value === "auto" || value === "max") return "auto";
|
|
50
|
+
})();
|
|
51
|
+
const setEnvIfMissing = (key, value) => {
|
|
52
|
+
if (!value || process.env[key]) return;
|
|
53
|
+
process.env[key] = value;
|
|
54
|
+
};
|
|
55
|
+
const setEnvFromConfig = (key, value) => {
|
|
56
|
+
if (!value) return;
|
|
57
|
+
process.env[key] = value;
|
|
58
|
+
};
|
|
59
|
+
setEnvIfMissing("LOCAL_EMBEDDING_MODEL", configuredModel || "nomic-embed-text-v1.5.Q5_K_M.gguf");
|
|
60
|
+
setEnvFromConfig("LOCAL_EMBEDDING_MODEL_REPO", configuredRepo);
|
|
61
|
+
setEnvFromConfig("LOCAL_EMBEDDING_DIMENSIONS", configuredDimensions);
|
|
62
|
+
setEnvFromConfig("LOCAL_EMBEDDING_CONTEXT_SIZE", configuredContextSize);
|
|
63
|
+
if (configuredGpuLayers) process.env.LOCAL_EMBEDDING_GPU_LAYERS = configuredGpuLayers;
|
|
64
|
+
else if (!process.env.LOCAL_EMBEDDING_GPU_LAYERS) process.env.LOCAL_EMBEDDING_GPU_LAYERS = isAppleSilicon ? "auto" : "0";
|
|
65
|
+
setEnvIfMissing("LOCAL_EMBEDDING_USE_MMAP", isAppleSilicon ? "false" : "true");
|
|
66
|
+
setEnvIfMissing("MODELS_DIR", path.join(os.homedir(), ".eliza", "models"));
|
|
67
|
+
setEnvIfMissing("GOOGLE_GENERATIVE_AI_API_KEY", process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY);
|
|
68
|
+
setEnvIfMissing("GOOGLE_SMALL_MODEL", "gemini-3-flash-preview");
|
|
69
|
+
setEnvIfMissing("GOOGLE_LARGE_MODEL", "gemini-3.1-pro-preview");
|
|
70
|
+
logger.info(`[milady] Configured local embedding env: ${process.env.LOCAL_EMBEDDING_MODEL} (repo: ${process.env.LOCAL_EMBEDDING_MODEL_REPO ?? "auto"}, dims: ${process.env.LOCAL_EMBEDDING_DIMENSIONS ?? "auto"}, ctx: ${process.env.LOCAL_EMBEDDING_CONTEXT_SIZE ?? "auto"}, GPU: ${process.env.LOCAL_EMBEDDING_GPU_LAYERS}, mmap: ${process.env.LOCAL_EMBEDDING_USE_MMAP})`);
|
|
71
|
+
}
|
|
72
|
+
/** Extract a human-readable error message from an unknown thrown value. */
|
|
73
|
+
function formatError(err) {
|
|
74
|
+
return err instanceof Error ? err.message : String(err);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Remove duplicate actions across an ordered list of plugins.
|
|
78
|
+
*
|
|
79
|
+
* When multiple plugins define an action with the same `name`, only the first
|
|
80
|
+
* occurrence is kept. This prevents "Action already registered" warnings from
|
|
81
|
+
* ElizaOS core. The function mutates each plugin's `actions` array in-place.
|
|
82
|
+
*/
|
|
83
|
+
function deduplicatePluginActions(plugins) {
|
|
84
|
+
const seen = /* @__PURE__ */ new Set();
|
|
85
|
+
for (const plugin of plugins) if (plugin.actions) plugin.actions = plugin.actions.filter((action) => {
|
|
86
|
+
if (seen.has(action.name)) {
|
|
87
|
+
logger.debug(`[milady] Skipping duplicate action "${action.name}" from plugin "${plugin.name}"`);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
seen.add(action.name);
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function collectTrajectoryLoggerCandidates(runtimeLike) {
|
|
95
|
+
const candidates = [];
|
|
96
|
+
if (typeof runtimeLike.getServicesByType === "function") {
|
|
97
|
+
const byType = runtimeLike.getServicesByType("trajectory_logger");
|
|
98
|
+
if (Array.isArray(byType) && byType.length > 0) {
|
|
99
|
+
for (const service of byType) if (service) candidates.push(service);
|
|
100
|
+
} else if (byType && !Array.isArray(byType)) candidates.push(byType);
|
|
101
|
+
}
|
|
102
|
+
if (typeof runtimeLike.getService === "function") {
|
|
103
|
+
const single = runtimeLike.getService("trajectory_logger");
|
|
104
|
+
if (single) candidates.push(single);
|
|
105
|
+
}
|
|
106
|
+
return candidates;
|
|
107
|
+
}
|
|
108
|
+
async function waitForTrajectoryLoggerService(runtime, context, timeoutMs = 3e3) {
|
|
109
|
+
const runtimeLike = runtime;
|
|
110
|
+
if (collectTrajectoryLoggerCandidates(runtimeLike).length > 0) return;
|
|
111
|
+
const registrationStatus = typeof runtimeLike.getServiceRegistrationStatus === "function" ? runtimeLike.getServiceRegistrationStatus("trajectory_logger") : "unknown";
|
|
112
|
+
if (registrationStatus !== "pending" && registrationStatus !== "registering") return;
|
|
113
|
+
if (typeof runtimeLike.getServiceLoadPromise !== "function") return;
|
|
114
|
+
let timedOut = false;
|
|
115
|
+
let timeoutHandle;
|
|
116
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
117
|
+
timeoutHandle = setTimeout(() => {
|
|
118
|
+
timedOut = true;
|
|
119
|
+
resolve();
|
|
120
|
+
}, timeoutMs);
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
await Promise.race([runtimeLike.getServiceLoadPromise("trajectory_logger").then(() => {}), timeoutPromise]);
|
|
124
|
+
if (timedOut) logger.debug(`[milady] trajectory_logger still ${registrationStatus} after ${timeoutMs}ms (${context})`);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.debug(`[milady] trajectory_logger registration failed while waiting (${context}): ${formatError(err)}`);
|
|
127
|
+
} finally {
|
|
128
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function ensureTrajectoryLoggerEnabled(runtime, context) {
|
|
132
|
+
const candidates = collectTrajectoryLoggerCandidates(runtime);
|
|
133
|
+
let trajectoryLogger = null;
|
|
134
|
+
let bestScore = -1;
|
|
135
|
+
for (const candidate of candidates) {
|
|
136
|
+
const candidateWithRuntime = candidate;
|
|
137
|
+
let score = 0;
|
|
138
|
+
if (typeof candidate.isEnabled === "function") score += 2;
|
|
139
|
+
if (typeof candidateWithRuntime.setEnabled === "function") score += 2;
|
|
140
|
+
if (candidateWithRuntime.initialized === true) score += 3;
|
|
141
|
+
if (candidateWithRuntime.runtime?.adapter) score += 3;
|
|
142
|
+
if (typeof candidate.isEnabled === "function" ? candidate.isEnabled() : true) score += 1;
|
|
143
|
+
if (score > bestScore) {
|
|
144
|
+
trajectoryLogger = candidate;
|
|
145
|
+
bestScore = score;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!trajectoryLogger) {
|
|
149
|
+
logger.warn(`[milady] trajectory_logger service unavailable (${context}); trajectory capture disabled`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!(typeof trajectoryLogger.isEnabled === "function" ? trajectoryLogger.isEnabled() : true) && typeof trajectoryLogger.setEnabled === "function") {
|
|
153
|
+
trajectoryLogger.setEnabled(true);
|
|
154
|
+
logger.info("[milady] trajectory_logger enabled by default");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Cancel the onboarding flow and exit cleanly.
|
|
159
|
+
* Extracted to avoid duplicating the cancel+exit pattern 7 times.
|
|
160
|
+
*/
|
|
161
|
+
function cancelOnboarding() {
|
|
162
|
+
clack.cancel("Maybe next time!");
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Maps Milady channel config fields to the environment variable names
|
|
167
|
+
* that ElizaOS plugins expect.
|
|
168
|
+
*
|
|
169
|
+
* Milady stores channel credentials under `config.channels.<name>.<field>`,
|
|
170
|
+
* while ElizaOS plugins read them from process.env.
|
|
171
|
+
*/
|
|
172
|
+
const CHANNEL_ENV_MAP = {
|
|
173
|
+
discord: {
|
|
174
|
+
token: "DISCORD_API_TOKEN",
|
|
175
|
+
botToken: "DISCORD_API_TOKEN",
|
|
176
|
+
applicationId: "DISCORD_APPLICATION_ID"
|
|
177
|
+
},
|
|
178
|
+
telegram: { botToken: "TELEGRAM_BOT_TOKEN" },
|
|
179
|
+
slack: {
|
|
180
|
+
botToken: "SLACK_BOT_TOKEN",
|
|
181
|
+
appToken: "SLACK_APP_TOKEN",
|
|
182
|
+
userToken: "SLACK_USER_TOKEN"
|
|
183
|
+
},
|
|
184
|
+
signal: { account: "SIGNAL_ACCOUNT" },
|
|
185
|
+
msteams: {
|
|
186
|
+
appId: "MSTEAMS_APP_ID",
|
|
187
|
+
appPassword: "MSTEAMS_APP_PASSWORD"
|
|
188
|
+
},
|
|
189
|
+
mattermost: {
|
|
190
|
+
botToken: "MATTERMOST_BOT_TOKEN",
|
|
191
|
+
baseUrl: "MATTERMOST_BASE_URL"
|
|
192
|
+
},
|
|
193
|
+
googlechat: { serviceAccountKey: "GOOGLE_CHAT_SERVICE_ACCOUNT_KEY" }
|
|
194
|
+
};
|
|
195
|
+
/** Maps Milady channel names to plugin package names. */
|
|
196
|
+
const CHANNEL_PLUGIN_MAP = {
|
|
197
|
+
discord: "@elizaos/plugin-discord",
|
|
198
|
+
telegram: "@elizaos/plugin-telegram",
|
|
199
|
+
slack: "@elizaos/plugin-slack",
|
|
200
|
+
twitter: "@elizaos/plugin-twitter",
|
|
201
|
+
whatsapp: "@milady/plugin-whatsapp",
|
|
202
|
+
signal: "@elizaos/plugin-signal",
|
|
203
|
+
imessage: "@elizaos/plugin-imessage",
|
|
204
|
+
bluebubbles: "@elizaos/plugin-bluebubbles",
|
|
205
|
+
farcaster: "@elizaos/plugin-farcaster",
|
|
206
|
+
lens: "@elizaos/plugin-lens",
|
|
207
|
+
msteams: "@elizaos/plugin-msteams",
|
|
208
|
+
mattermost: "@elizaos/plugin-mattermost",
|
|
209
|
+
googlechat: "@elizaos/plugin-google-chat",
|
|
210
|
+
feishu: "@elizaos/plugin-feishu",
|
|
211
|
+
matrix: "@elizaos/plugin-matrix",
|
|
212
|
+
nostr: "@elizaos/plugin-nostr",
|
|
213
|
+
retake: "@milady/plugin-retake"
|
|
214
|
+
};
|
|
215
|
+
const PI_AI_PLUGIN_PACKAGE = "@elizaos/plugin-pi-ai";
|
|
216
|
+
function isPiAiEnabledFromEnv(env = process.env) {
|
|
217
|
+
const raw = env.MILAIDY_USE_PI_AI;
|
|
218
|
+
if (!raw) return false;
|
|
219
|
+
const value = String(raw).trim().toLowerCase();
|
|
220
|
+
return value === "1" || value === "true" || value === "yes";
|
|
221
|
+
}
|
|
222
|
+
/** Maps environment variable names to model-provider plugin packages. */
|
|
223
|
+
const PROVIDER_PLUGIN_MAP = {
|
|
224
|
+
ANTHROPIC_API_KEY: "@elizaos/plugin-anthropic",
|
|
225
|
+
OPENAI_API_KEY: "@elizaos/plugin-openai",
|
|
226
|
+
GEMINI_API_KEY: "@elizaos/plugin-google-genai",
|
|
227
|
+
GOOGLE_API_KEY: "@elizaos/plugin-google-genai",
|
|
228
|
+
GOOGLE_GENERATIVE_AI_API_KEY: "@elizaos/plugin-google-genai",
|
|
229
|
+
GROQ_API_KEY: "@elizaos/plugin-groq",
|
|
230
|
+
XAI_API_KEY: "@elizaos/plugin-xai",
|
|
231
|
+
OPENROUTER_API_KEY: "@elizaos/plugin-openrouter",
|
|
232
|
+
AI_GATEWAY_API_KEY: "@elizaos/plugin-vercel-ai-gateway",
|
|
233
|
+
AIGATEWAY_API_KEY: "@elizaos/plugin-vercel-ai-gateway",
|
|
234
|
+
OLLAMA_BASE_URL: "@elizaos/plugin-ollama",
|
|
235
|
+
ZAI_API_KEY: "@homunculuslabs/plugin-zai",
|
|
236
|
+
MILAIDY_USE_PI_AI: PI_AI_PLUGIN_PACKAGE,
|
|
237
|
+
ELIZAOS_CLOUD_API_KEY: "@elizaos/plugin-elizacloud",
|
|
238
|
+
ELIZAOS_CLOUD_ENABLED: "@elizaos/plugin-elizacloud"
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Optional feature plugins keyed by feature name.
|
|
242
|
+
*
|
|
243
|
+
* Mappings here support short IDs in allow-lists and feature toggles.
|
|
244
|
+
* Keep this map in sync with optional plugin registration and tests.
|
|
245
|
+
*/
|
|
246
|
+
const OPTIONAL_PLUGIN_MAP = {
|
|
247
|
+
browser: "@elizaos/plugin-browser",
|
|
248
|
+
vision: "@elizaos/plugin-vision",
|
|
249
|
+
cron: "@elizaos/plugin-cron",
|
|
250
|
+
cua: "@elizaos/plugin-cua",
|
|
251
|
+
computeruse: "@elizaos/plugin-computeruse",
|
|
252
|
+
obsidian: "@elizaos/plugin-obsidian",
|
|
253
|
+
repoprompt: "@elizaos/plugin-repoprompt",
|
|
254
|
+
repoPrompt: "@elizaos/plugin-repoprompt",
|
|
255
|
+
"pi-ai": PI_AI_PLUGIN_PACKAGE,
|
|
256
|
+
piAi: PI_AI_PLUGIN_PACKAGE,
|
|
257
|
+
x402: "@elizaos/plugin-x402",
|
|
258
|
+
"coding-agent": "@milaidy/plugin-coding-agent"
|
|
259
|
+
};
|
|
260
|
+
function looksLikePlugin(value) {
|
|
261
|
+
if (!value || typeof value !== "object") return false;
|
|
262
|
+
const obj = value;
|
|
263
|
+
if (typeof obj.name !== "string" || typeof obj.description !== "string") return false;
|
|
264
|
+
return Array.isArray(obj.services) || Array.isArray(obj.providers) || Array.isArray(obj.actions) || Array.isArray(obj.routes) || Array.isArray(obj.events) || typeof obj.init === "function";
|
|
265
|
+
}
|
|
266
|
+
function looksLikePluginBasic(value) {
|
|
267
|
+
if (!value || typeof value !== "object") return false;
|
|
268
|
+
const obj = value;
|
|
269
|
+
return typeof obj.name === "string" && typeof obj.description === "string";
|
|
270
|
+
}
|
|
271
|
+
function findRuntimePluginExport(mod) {
|
|
272
|
+
if (looksLikePlugin(mod.default)) return mod.default;
|
|
273
|
+
if (looksLikePlugin(mod.plugin)) return mod.plugin;
|
|
274
|
+
if (looksLikePlugin(mod)) return mod;
|
|
275
|
+
const namedKeys = Object.keys(mod).filter((key) => key !== "default" && key !== "plugin");
|
|
276
|
+
const preferredKeys = namedKeys.filter((key) => /plugin$/i.test(key) || /^plugin/i.test(key));
|
|
277
|
+
const fallbackKeys = namedKeys.filter((key) => !preferredKeys.includes(key));
|
|
278
|
+
for (const key of [...preferredKeys, ...fallbackKeys]) {
|
|
279
|
+
const value = mod[key];
|
|
280
|
+
if (looksLikePlugin(value)) return value;
|
|
281
|
+
}
|
|
282
|
+
for (const key of preferredKeys) {
|
|
283
|
+
const value = mod[key];
|
|
284
|
+
if (looksLikePluginBasic(value)) return value;
|
|
285
|
+
}
|
|
286
|
+
if (looksLikePluginBasic(mod)) return mod;
|
|
287
|
+
if (looksLikePluginBasic(mod.default)) return mod.default;
|
|
288
|
+
if (looksLikePluginBasic(mod.plugin)) return mod.plugin;
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Collect the set of plugin package names that should be loaded
|
|
293
|
+
* based on config, environment variables, and feature flags.
|
|
294
|
+
*/
|
|
295
|
+
/** @internal Exported for testing. */
|
|
296
|
+
function collectPluginNames(config) {
|
|
297
|
+
const shellPluginDisabled = config.features?.shellEnabled === false;
|
|
298
|
+
const cloudMode = config.cloud?.enabled;
|
|
299
|
+
const cloudHasApiKey = Boolean(config.cloud?.apiKey);
|
|
300
|
+
const cloudExplicitlyDisabled = cloudMode === false;
|
|
301
|
+
const cloudEffectivelyEnabled = cloudMode === true || !cloudExplicitlyDisabled && cloudHasApiKey;
|
|
302
|
+
const configEnv = config.env;
|
|
303
|
+
const configPiAiFlag = (configEnv?.vars && typeof configEnv.vars === "object" && !Array.isArray(configEnv.vars) ? configEnv.vars.MILAIDY_USE_PI_AI : void 0) ?? configEnv?.MILAIDY_USE_PI_AI;
|
|
304
|
+
const piAiEnabled = isPiAiEnabledFromEnv(process.env) || typeof configPiAiFlag === "string" && isPiAiEnabledFromEnv({ MILAIDY_USE_PI_AI: configPiAiFlag });
|
|
305
|
+
const pluginEntries = config.plugins?.entries;
|
|
306
|
+
const isPluginExplicitlyDisabled = (pluginPackageName) => {
|
|
307
|
+
const markerIndex = pluginPackageName.lastIndexOf("/plugin-");
|
|
308
|
+
const pluginId = markerIndex >= 0 ? pluginPackageName.slice(markerIndex + 8) : pluginPackageName;
|
|
309
|
+
return pluginEntries?.[pluginId]?.enabled === false;
|
|
310
|
+
};
|
|
311
|
+
const providerPluginIdSet = new Set(Object.values(PROVIDER_PLUGIN_MAP).map((pluginPackageName) => {
|
|
312
|
+
const markerIndex = pluginPackageName.lastIndexOf("/plugin-");
|
|
313
|
+
return markerIndex >= 0 ? pluginPackageName.slice(markerIndex + 8) : pluginPackageName;
|
|
314
|
+
}));
|
|
315
|
+
const hasExplicitEnabledProvider = Object.entries(pluginEntries ?? {}).filter(([pluginId]) => providerPluginIdSet.has(pluginId)).some(([, entry]) => entry?.enabled === true);
|
|
316
|
+
const allowList = config.plugins?.allow;
|
|
317
|
+
const pluginsToLoad = new Set(CORE_PLUGINS);
|
|
318
|
+
if (allowList && allowList.length > 0) for (const item of allowList) {
|
|
319
|
+
const pluginName = CHANNEL_PLUGIN_MAP[item] ?? OPTIONAL_PLUGIN_MAP[item] ?? item;
|
|
320
|
+
pluginsToLoad.add(pluginName);
|
|
321
|
+
}
|
|
322
|
+
const connectors = config.connectors ?? config.channels ?? {};
|
|
323
|
+
for (const [channelName, channelConfig] of Object.entries(connectors)) if (channelConfig && typeof channelConfig === "object") {
|
|
324
|
+
const pluginName = CHANNEL_PLUGIN_MAP[channelName];
|
|
325
|
+
if (pluginName) pluginsToLoad.add(pluginName);
|
|
326
|
+
}
|
|
327
|
+
for (const [envKey, pluginName] of Object.entries(PROVIDER_PLUGIN_MAP)) {
|
|
328
|
+
if (envKey === "MILAIDY_USE_PI_AI") continue;
|
|
329
|
+
if (cloudExplicitlyDisabled && (envKey === "ELIZAOS_CLOUD_API_KEY" || envKey === "ELIZAOS_CLOUD_ENABLED")) continue;
|
|
330
|
+
if (isPluginExplicitlyDisabled(pluginName)) continue;
|
|
331
|
+
if (hasExplicitEnabledProvider) {
|
|
332
|
+
const markerIndex = pluginName.lastIndexOf("/plugin-");
|
|
333
|
+
const pluginId = markerIndex >= 0 ? pluginName.slice(markerIndex + 8) : pluginName;
|
|
334
|
+
if (pluginEntries?.[pluginId]?.enabled !== true) continue;
|
|
335
|
+
}
|
|
336
|
+
if (process.env[envKey]?.trim()) pluginsToLoad.add(pluginName);
|
|
337
|
+
}
|
|
338
|
+
const shouldEnablePiAi = piAiEnabled && pluginEntries?.["pi-ai"]?.enabled !== false;
|
|
339
|
+
const applyProviderPrecedence = () => {
|
|
340
|
+
if (cloudEffectivelyEnabled) {
|
|
341
|
+
pluginsToLoad.add("@elizaos/plugin-elizacloud");
|
|
342
|
+
const directProviders = new Set(Object.values(PROVIDER_PLUGIN_MAP));
|
|
343
|
+
directProviders.delete("@elizaos/plugin-elizacloud");
|
|
344
|
+
for (const p of directProviders) pluginsToLoad.delete(p);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (shouldEnablePiAi) {
|
|
348
|
+
pluginsToLoad.add(PI_AI_PLUGIN_PACKAGE);
|
|
349
|
+
const directProviders = new Set(Object.values(PROVIDER_PLUGIN_MAP));
|
|
350
|
+
directProviders.delete(PI_AI_PLUGIN_PACKAGE);
|
|
351
|
+
for (const p of directProviders) pluginsToLoad.delete(p);
|
|
352
|
+
pluginsToLoad.delete("@elizaos/plugin-elizacloud");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (cloudExplicitlyDisabled) pluginsToLoad.delete("@elizaos/plugin-elizacloud");
|
|
356
|
+
};
|
|
357
|
+
applyProviderPrecedence();
|
|
358
|
+
const pluginsConfig = config.plugins;
|
|
359
|
+
if (pluginsConfig?.entries) {
|
|
360
|
+
for (const [key, entry] of Object.entries(pluginsConfig.entries)) if (entry && typeof entry === "object" && entry.enabled !== false) {
|
|
361
|
+
const pluginName = CHANNEL_PLUGIN_MAP[key] ?? OPTIONAL_PLUGIN_MAP[key] ?? (key.includes("/") ? key : `@elizaos/plugin-${key}`);
|
|
362
|
+
pluginsToLoad.add(pluginName);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const features = config.features;
|
|
366
|
+
if (features && typeof features === "object") {
|
|
367
|
+
for (const [featureName, featureValue] of Object.entries(features)) if (featureValue === true || typeof featureValue === "object" && featureValue !== null && featureValue.enabled !== false) {
|
|
368
|
+
const pluginName = OPTIONAL_PLUGIN_MAP[featureName];
|
|
369
|
+
if (pluginName) pluginsToLoad.add(pluginName);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (config.x402?.enabled) pluginsToLoad.add("@elizaos/plugin-x402");
|
|
373
|
+
const installs = config.plugins?.installs;
|
|
374
|
+
if (installs && typeof installs === "object") {
|
|
375
|
+
for (const [packageName, record] of Object.entries(installs)) if (record && typeof record === "object") pluginsToLoad.add(packageName);
|
|
376
|
+
}
|
|
377
|
+
applyProviderPrecedence();
|
|
378
|
+
if (shellPluginDisabled) pluginsToLoad.delete("@elizaos/plugin-shell");
|
|
379
|
+
if (isPluginExplicitlyDisabled("@milaidy/plugin-coding-agent")) pluginsToLoad.delete("@milaidy/plugin-coding-agent");
|
|
380
|
+
return pluginsToLoad;
|
|
381
|
+
}
|
|
382
|
+
/** Subdirectory under the Milady state dir for drop-in custom plugins. */
|
|
383
|
+
const CUSTOM_PLUGINS_DIRNAME = "plugins/custom";
|
|
384
|
+
/** Subdirectory under the Milady state dir for ejected plugins. */
|
|
385
|
+
const EJECTED_PLUGINS_DIRNAME = "plugins/ejected";
|
|
386
|
+
/**
|
|
387
|
+
* Scan a directory for drop-in plugin packages. Each immediate subdirectory
|
|
388
|
+
* is treated as a plugin; name comes from package.json or the directory name.
|
|
389
|
+
*/
|
|
390
|
+
async function scanDropInPlugins(dir) {
|
|
391
|
+
const records = {};
|
|
392
|
+
let entries;
|
|
393
|
+
try {
|
|
394
|
+
entries = await fs$1.readdir(dir, { withFileTypes: true });
|
|
395
|
+
} catch (err) {
|
|
396
|
+
if (err.code === "ENOENT") return records;
|
|
397
|
+
throw err;
|
|
398
|
+
}
|
|
399
|
+
for (const entry of entries) {
|
|
400
|
+
if (!entry.isDirectory()) continue;
|
|
401
|
+
const pluginDir = path.join(dir, entry.name);
|
|
402
|
+
let pluginName = entry.name;
|
|
403
|
+
let version = "0.0.0";
|
|
404
|
+
try {
|
|
405
|
+
const raw = await fs$1.readFile(path.join(pluginDir, "package.json"), "utf-8");
|
|
406
|
+
const pkg = JSON.parse(raw);
|
|
407
|
+
if (typeof pkg.name === "string" && pkg.name.trim()) pluginName = pkg.name.trim();
|
|
408
|
+
if (typeof pkg.version === "string" && pkg.version.trim()) version = pkg.version.trim();
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (err.code !== "ENOENT" && !(err instanceof SyntaxError)) throw err;
|
|
411
|
+
}
|
|
412
|
+
records[pluginName] = {
|
|
413
|
+
source: "path",
|
|
414
|
+
installPath: pluginDir,
|
|
415
|
+
version
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return records;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Merge drop-in plugins into the load set. Filters out denied, core-colliding,
|
|
422
|
+
* and already-installed names. Mutates `pluginsToLoad` and `installRecords`.
|
|
423
|
+
*/
|
|
424
|
+
function mergeDropInPlugins(params) {
|
|
425
|
+
const { dropInRecords, installRecords, corePluginNames, denyList, pluginsToLoad } = params;
|
|
426
|
+
const accepted = [];
|
|
427
|
+
const skipped = [];
|
|
428
|
+
for (const [name, record] of Object.entries(dropInRecords)) {
|
|
429
|
+
if (denyList.has(name) || installRecords[name]) continue;
|
|
430
|
+
if (corePluginNames.has(name)) {
|
|
431
|
+
skipped.push(`[milady] Custom plugin "${name}" collides with core plugin — skipping`);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
pluginsToLoad.add(name);
|
|
435
|
+
installRecords[name] = record;
|
|
436
|
+
accepted.push(name);
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
accepted,
|
|
440
|
+
skipped
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const WORKSPACE_PLUGIN_OVERRIDES = /* @__PURE__ */ new Set([]);
|
|
444
|
+
function getWorkspacePluginOverridePath(pluginName) {
|
|
445
|
+
if (process.env.MILADY_DISABLE_WORKSPACE_PLUGIN_OVERRIDES === "1") return null;
|
|
446
|
+
if (!WORKSPACE_PLUGIN_OVERRIDES.has(pluginName)) return null;
|
|
447
|
+
const pluginSegment = pluginName.match(/^@[^/]+\/(plugin-[^/]+)$/)?.[1];
|
|
448
|
+
if (!pluginSegment) return null;
|
|
449
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
450
|
+
const miladyRoot = path.resolve(thisDir, "..", "..");
|
|
451
|
+
const workspaceRoot = path.resolve(miladyRoot, "..");
|
|
452
|
+
const candidates = [
|
|
453
|
+
path.join(miladyRoot, "plugins", pluginSegment, "typescript"),
|
|
454
|
+
path.join(workspaceRoot, "plugins", pluginSegment, "typescript"),
|
|
455
|
+
path.join(miladyRoot, "plugins", pluginSegment),
|
|
456
|
+
path.join(workspaceRoot, "plugins", pluginSegment),
|
|
457
|
+
path.join(miladyRoot, "packages", pluginSegment),
|
|
458
|
+
path.join(workspaceRoot, "packages", pluginSegment)
|
|
459
|
+
];
|
|
460
|
+
for (const candidate of candidates) if (existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The `@elizaos/plugin-browser` npm package expects a `dist/server/` directory
|
|
465
|
+
* containing the compiled stagehand-server, but the npm publish doesn't include
|
|
466
|
+
* it. The actual source/build lives in the workspace at
|
|
467
|
+
* `plugins/plugin-browser/stagehand-server/`.
|
|
468
|
+
*
|
|
469
|
+
* This function checks whether the server is reachable from the installed
|
|
470
|
+
* package and, if not, creates a symlink so the plugin's process-manager can
|
|
471
|
+
* find it. Returns `true` when the server index.js is available (or was made
|
|
472
|
+
* available via symlink), `false` otherwise.
|
|
473
|
+
*/
|
|
474
|
+
/**
|
|
475
|
+
* Returns true if the given env var key is safe to forward to runtime.settings.
|
|
476
|
+
* Blocks blockchain private keys, secrets, passwords, tokens, credentials,
|
|
477
|
+
* mnemonics, and seed phrases while allowing API keys that plugins need.
|
|
478
|
+
*/
|
|
479
|
+
function isEnvKeyAllowedForForwarding(key) {
|
|
480
|
+
const upper = key.toUpperCase();
|
|
481
|
+
if (upper.includes("PRIVATE_KEY")) return false;
|
|
482
|
+
if (upper.startsWith("EVM_") || upper.startsWith("SOLANA_")) return false;
|
|
483
|
+
if (/(SECRET|PASSWORD|CREDENTIAL|MNEMONIC|SEED_PHRASE)/i.test(key)) return false;
|
|
484
|
+
if (/(ACCESS_TOKEN|REFRESH_TOKEN|SESSION_TOKEN|AUTH_TOKEN)$/i.test(key)) return false;
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
function ensureBrowserServerLink() {
|
|
488
|
+
try {
|
|
489
|
+
const pkgJsonPath = createRequire(import.meta.url).resolve("@elizaos/plugin-browser/package.json");
|
|
490
|
+
const pluginRoot = path.dirname(pkgJsonPath);
|
|
491
|
+
const serverDir = path.join(pluginRoot, "dist", "server");
|
|
492
|
+
if (existsSync(path.join(serverDir, "dist", "index"))) return true;
|
|
493
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
494
|
+
const miladyRoot = path.resolve(thisDir, "..", "..");
|
|
495
|
+
const workspaceRoot = path.resolve(miladyRoot, "..");
|
|
496
|
+
const stagehandDir = path.join(workspaceRoot, "plugins", "plugin-browser", "stagehand-server");
|
|
497
|
+
if (!existsSync(path.join(stagehandDir, "dist", "index"))) {
|
|
498
|
+
logger.info(`[milady] Browser server not found at ${stagehandDir} — @elizaos/plugin-browser will not be loaded`);
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
symlinkSync(stagehandDir, serverDir, "dir");
|
|
502
|
+
logger.info(`[milady] Linked browser server: ${serverDir} -> ${stagehandDir}`);
|
|
503
|
+
return true;
|
|
504
|
+
} catch (err) {
|
|
505
|
+
logger.debug(`[milady] Could not link browser server: ${formatError(err)}`);
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Resolve Milady plugins from config and auto-enable logic.
|
|
511
|
+
* Returns an array of ElizaOS Plugin instances ready for AgentRuntime.
|
|
512
|
+
*
|
|
513
|
+
* Handles three categories of plugins:
|
|
514
|
+
* 1. Built-in/npm plugins — imported by package name
|
|
515
|
+
* 2. User-installed plugins — from ~/.milady/plugins/installed/
|
|
516
|
+
* 3. Custom/drop-in plugins — from ~/.milady/plugins/custom/ and plugins.load.paths
|
|
517
|
+
*
|
|
518
|
+
* Each plugin is loaded inside an error boundary so a single failing plugin
|
|
519
|
+
* cannot crash the entire agent startup.
|
|
520
|
+
*/
|
|
521
|
+
async function resolvePlugins(config, opts) {
|
|
522
|
+
const plugins = [];
|
|
523
|
+
const failedPlugins = [];
|
|
524
|
+
const repairedInstallRecords = /* @__PURE__ */ new Set();
|
|
525
|
+
applyPluginAutoEnable({
|
|
526
|
+
config,
|
|
527
|
+
env: process.env
|
|
528
|
+
});
|
|
529
|
+
const pluginsToLoad = collectPluginNames(config);
|
|
530
|
+
const corePluginSet = new Set(CORE_PLUGINS);
|
|
531
|
+
const installRecords = { ...config.plugins?.installs ?? {} };
|
|
532
|
+
const denyList = new Set(config.plugins?.deny ?? []);
|
|
533
|
+
const ejectedRecords = await scanDropInPlugins(path.join(resolveStateDir(), EJECTED_PLUGINS_DIRNAME));
|
|
534
|
+
const ejectedPluginNames = [];
|
|
535
|
+
for (const [name, _record] of Object.entries(ejectedRecords)) {
|
|
536
|
+
if (denyList.has(name)) continue;
|
|
537
|
+
pluginsToLoad.add(name);
|
|
538
|
+
ejectedPluginNames.push(name);
|
|
539
|
+
}
|
|
540
|
+
if (ejectedPluginNames.length > 0) logger.info(`[milady] Discovered ${ejectedPluginNames.length} ejected plugin(s): ${ejectedPluginNames.join(", ")}`);
|
|
541
|
+
const scanDirs = [path.join(resolveStateDir(), CUSTOM_PLUGINS_DIRNAME), ...(config.plugins?.load?.paths ?? []).map(resolveUserPath)];
|
|
542
|
+
const dropInRecords = {};
|
|
543
|
+
for (const dir of scanDirs) for (const [name, record] of Object.entries(await scanDropInPlugins(dir))) if (!dropInRecords[name]) dropInRecords[name] = record;
|
|
544
|
+
const { accepted: customPluginNames, skipped } = mergeDropInPlugins({
|
|
545
|
+
dropInRecords,
|
|
546
|
+
installRecords,
|
|
547
|
+
corePluginNames: corePluginSet,
|
|
548
|
+
denyList,
|
|
549
|
+
pluginsToLoad
|
|
550
|
+
});
|
|
551
|
+
for (const msg of skipped) logger.warn(msg);
|
|
552
|
+
if (customPluginNames.length > 0) logger.info(`[milady] Discovered ${customPluginNames.length} custom plugin(s): ${customPluginNames.join(", ")}`);
|
|
553
|
+
logger.info(`[milady] Resolving ${pluginsToLoad.size} plugins...`);
|
|
554
|
+
const loadStartTime = Date.now();
|
|
555
|
+
async function loadSinglePlugin(pluginName) {
|
|
556
|
+
const isCore = corePluginSet.has(pluginName);
|
|
557
|
+
const ejectedRecord = ejectedRecords[pluginName];
|
|
558
|
+
const installRecord = installRecords[pluginName];
|
|
559
|
+
const workspaceOverridePath = getWorkspacePluginOverridePath(pluginName);
|
|
560
|
+
if (pluginName === "@elizaos/plugin-browser") {
|
|
561
|
+
if (!ensureBrowserServerLink()) {
|
|
562
|
+
failedPlugins.push({
|
|
563
|
+
name: pluginName,
|
|
564
|
+
error: "browser server binary not found"
|
|
565
|
+
});
|
|
566
|
+
logger.warn(`[milady] Skipping ${pluginName}: browser server not available. Build the stagehand-server or remove the plugin from plugins.allow.`);
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
let mod;
|
|
572
|
+
if (ejectedRecord?.installPath) {
|
|
573
|
+
logger.debug(`[milady] Loading ejected plugin: ${pluginName} from ${ejectedRecord.installPath}`);
|
|
574
|
+
mod = await importFromPath(ejectedRecord.installPath, pluginName);
|
|
575
|
+
} else if (workspaceOverridePath) {
|
|
576
|
+
logger.debug(`[milady] Loading workspace plugin override: ${pluginName} from ${workspaceOverridePath}`);
|
|
577
|
+
mod = await importFromPath(workspaceOverridePath, pluginName);
|
|
578
|
+
} else if (installRecord?.installPath) if (pluginName.startsWith("@elizaos/plugin-")) try {
|
|
579
|
+
mod = await import(pluginName);
|
|
580
|
+
if (repairBrokenInstallRecord(config, pluginName)) repairedInstallRecords.add(pluginName);
|
|
581
|
+
} catch (npmErr) {
|
|
582
|
+
logger.warn(`[milady] Node_modules resolution failed for ${pluginName} (${formatError(npmErr)}). Trying installed path at ${installRecord.installPath}.`);
|
|
583
|
+
mod = await importFromPath(installRecord.installPath, pluginName);
|
|
584
|
+
}
|
|
585
|
+
else try {
|
|
586
|
+
mod = await importFromPath(installRecord.installPath, pluginName);
|
|
587
|
+
} catch (installErr) {
|
|
588
|
+
logger.warn(`[milady] Installed plugin ${pluginName} failed at ${installRecord.installPath} (${formatError(installErr)}). Falling back to node_modules resolution.`);
|
|
589
|
+
mod = await import(pluginName);
|
|
590
|
+
if (repairBrokenInstallRecord(config, pluginName)) repairedInstallRecords.add(pluginName);
|
|
591
|
+
}
|
|
592
|
+
else if (pluginName.startsWith("@milady/plugin-")) {
|
|
593
|
+
const shortName = pluginName.replace("@milady/plugin-", "");
|
|
594
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
595
|
+
const distRoot = thisDir.endsWith("runtime") ? path.resolve(thisDir, "..") : thisDir;
|
|
596
|
+
mod = await import(pathToFileURL(path.resolve(distRoot, "plugins", shortName, "index.js")).href);
|
|
597
|
+
} else mod = await import(pluginName);
|
|
598
|
+
const pluginInstance = findRuntimePluginExport(mod);
|
|
599
|
+
if (pluginInstance) {
|
|
600
|
+
const wrappedPlugin = wrapPluginWithErrorBoundary(pluginName, pluginInstance);
|
|
601
|
+
logger.debug(`[milady] ✓ Loaded plugin: ${pluginName}`);
|
|
602
|
+
return {
|
|
603
|
+
name: pluginName,
|
|
604
|
+
plugin: wrappedPlugin
|
|
605
|
+
};
|
|
606
|
+
} else {
|
|
607
|
+
const msg = `[milady] Plugin ${pluginName} did not export a valid Plugin object`;
|
|
608
|
+
failedPlugins.push({
|
|
609
|
+
name: pluginName,
|
|
610
|
+
error: "no valid Plugin export"
|
|
611
|
+
});
|
|
612
|
+
if (isCore) logger.error(msg);
|
|
613
|
+
else logger.warn(msg);
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
} catch (err) {
|
|
617
|
+
const msg = formatError(err);
|
|
618
|
+
failedPlugins.push({
|
|
619
|
+
name: pluginName,
|
|
620
|
+
error: msg
|
|
621
|
+
});
|
|
622
|
+
if (isCore) logger.error(`[milady] Failed to load core plugin ${pluginName}: ${msg}`);
|
|
623
|
+
else if (new Set([...Object.values(OPTIONAL_PLUGIN_MAP), ...Object.values(CHANNEL_PLUGIN_MAP)]).has(pluginName)) logger.debug(`[milady] Optional plugin ${pluginName} not available: ${msg}`);
|
|
624
|
+
else logger.info(`[milady] Could not load plugin ${pluginName}: ${msg}`);
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const pluginResults = await Promise.all(Array.from(pluginsToLoad).map(loadSinglePlugin));
|
|
629
|
+
for (const result of pluginResults) if (result) plugins.push(result);
|
|
630
|
+
const loadDuration = Date.now() - loadStartTime;
|
|
631
|
+
logger.info(`[milady] Plugin loading took ${loadDuration}ms`);
|
|
632
|
+
logger.info(`[milady] Plugin resolution complete: ${plugins.length}/${pluginsToLoad.size} loaded` + (failedPlugins.length > 0 ? `, ${failedPlugins.length} failed` : ""));
|
|
633
|
+
if (failedPlugins.length > 0) logger.info(`[milady] Failed plugins: ${failedPlugins.map((f) => `${f.name} (${f.error})`).join(", ")}`);
|
|
634
|
+
const diagnostic = diagnoseNoAIProvider(plugins.map((p) => p.name), failedPlugins);
|
|
635
|
+
if (diagnostic) if (opts?.quiet) logger.info(`[milady] ${diagnostic}`);
|
|
636
|
+
else logger.error(`[milady] ${diagnostic}`);
|
|
637
|
+
if (repairedInstallRecords.size > 0) try {
|
|
638
|
+
saveMiladyConfig(config);
|
|
639
|
+
logger.info(`[milady] Repaired ${repairedInstallRecords.size} plugin install record(s): ${Array.from(repairedInstallRecords).join(", ")}`);
|
|
640
|
+
} catch (err) {
|
|
641
|
+
logger.warn(`[milady] Failed to persist plugin install repairs: ${formatError(err)}`);
|
|
642
|
+
}
|
|
643
|
+
return plugins;
|
|
644
|
+
}
|
|
645
|
+
/** @internal Exported for testing. */
|
|
646
|
+
function repairBrokenInstallRecord(config, pluginName) {
|
|
647
|
+
const record = config.plugins?.installs?.[pluginName];
|
|
648
|
+
if (!record || typeof record.installPath !== "string") return false;
|
|
649
|
+
if (!record.installPath.trim()) return false;
|
|
650
|
+
record.installPath = "";
|
|
651
|
+
record.source = "npm";
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Wrap a plugin's `init` and `providers` with error boundaries so that a
|
|
656
|
+
* crash in any single plugin does not take down the entire agent or GUI.
|
|
657
|
+
*
|
|
658
|
+
* NOTE: Actions are NOT wrapped here because ElizaOS's action dispatch
|
|
659
|
+
* already has its own error boundary. Only `init` (startup) and
|
|
660
|
+
* `providers` (called every turn) need protection at this layer.
|
|
661
|
+
*
|
|
662
|
+
* The wrapper catches errors, logs them with the plugin name for easy
|
|
663
|
+
* debugging, and continues execution.
|
|
664
|
+
*/
|
|
665
|
+
function wrapPluginWithErrorBoundary(pluginName, plugin) {
|
|
666
|
+
const wrapped = { ...plugin };
|
|
667
|
+
if (plugin.init) {
|
|
668
|
+
const originalInit = plugin.init;
|
|
669
|
+
wrapped.init = async (...args) => {
|
|
670
|
+
try {
|
|
671
|
+
return await originalInit(...args);
|
|
672
|
+
} catch (err) {
|
|
673
|
+
logger.error(`[milady] Plugin "${pluginName}" crashed during init: ${formatError(err)}`);
|
|
674
|
+
logger.warn(`[milady] Plugin "${pluginName}" will run in degraded mode (init failed)`);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (plugin.providers && plugin.providers.length > 0) wrapped.providers = plugin.providers.map((provider) => ({
|
|
679
|
+
...provider,
|
|
680
|
+
get: async (...args) => {
|
|
681
|
+
try {
|
|
682
|
+
return await provider.get(...args);
|
|
683
|
+
} catch (err) {
|
|
684
|
+
const msg = formatError(err);
|
|
685
|
+
logger.error(`[milady] Provider "${provider.name}" (plugin: ${pluginName}) crashed: ${msg}`);
|
|
686
|
+
return {
|
|
687
|
+
text: `[Provider ${provider.name} error: ${msg}]`,
|
|
688
|
+
data: { _providerError: true }
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}));
|
|
693
|
+
return wrapped;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Import a plugin module from its install directory on disk.
|
|
697
|
+
*
|
|
698
|
+
* Handles two install layouts:
|
|
699
|
+
* 1. npm layout: <installPath>/node_modules/@scope/package/ (from `bun add`)
|
|
700
|
+
* 2. git layout: <installPath>/ is the package root directly (from `git clone`)
|
|
701
|
+
*
|
|
702
|
+
* @param installPath Root directory of the installation (e.g. ~/.milady/plugins/installed/foo/).
|
|
703
|
+
* @param packageName The npm package name (e.g. "@elizaos/plugin-discord") — used
|
|
704
|
+
* to navigate directly into node_modules when present.
|
|
705
|
+
*/
|
|
706
|
+
async function importFromPath(installPath, packageName) {
|
|
707
|
+
const absPath = path.resolve(installPath);
|
|
708
|
+
const nmCandidate = path.join(absPath, "node_modules", ...packageName.split("/"));
|
|
709
|
+
let pkgRoot = absPath;
|
|
710
|
+
try {
|
|
711
|
+
if ((await fs$1.stat(nmCandidate)).isDirectory()) pkgRoot = nmCandidate;
|
|
712
|
+
} catch (err) {
|
|
713
|
+
if (err.code !== "ENOENT") throw err;
|
|
714
|
+
}
|
|
715
|
+
return await import(pathToFileURL(await resolvePackageEntry(pkgRoot)).href);
|
|
716
|
+
}
|
|
717
|
+
/** Read package.json exports/main to find the importable entry file. */
|
|
718
|
+
/** @internal Exported for testing. */
|
|
719
|
+
async function resolvePackageEntry(pkgRoot) {
|
|
720
|
+
const fallback = path.join(pkgRoot, "dist", "index");
|
|
721
|
+
const fallbackCandidates = [
|
|
722
|
+
fallback,
|
|
723
|
+
path.join(pkgRoot, "index"),
|
|
724
|
+
path.join(pkgRoot, "index.mjs"),
|
|
725
|
+
path.join(pkgRoot, "index.ts"),
|
|
726
|
+
path.join(pkgRoot, "src", "index"),
|
|
727
|
+
path.join(pkgRoot, "src", "index.mjs"),
|
|
728
|
+
path.join(pkgRoot, "src", "index.ts")
|
|
729
|
+
];
|
|
730
|
+
const chooseExisting = (...paths) => {
|
|
731
|
+
const seen = /* @__PURE__ */ new Set();
|
|
732
|
+
for (const p of paths) {
|
|
733
|
+
const resolved = path.resolve(p);
|
|
734
|
+
if (seen.has(resolved)) continue;
|
|
735
|
+
seen.add(resolved);
|
|
736
|
+
if (existsSync(resolved)) return resolved;
|
|
737
|
+
}
|
|
738
|
+
return path.resolve(paths[0] ?? fallback);
|
|
739
|
+
};
|
|
740
|
+
try {
|
|
741
|
+
const raw = await fs$1.readFile(path.join(pkgRoot, "package.json"), "utf-8");
|
|
742
|
+
const pkg = JSON.parse(raw);
|
|
743
|
+
if (typeof pkg.exports === "object" && pkg.exports["."] !== void 0) {
|
|
744
|
+
const dot = pkg.exports["."];
|
|
745
|
+
const resolved = typeof dot === "string" ? dot : dot.import || dot.default;
|
|
746
|
+
if (typeof resolved === "string") return chooseExisting(path.resolve(pkgRoot, resolved), ...fallbackCandidates);
|
|
747
|
+
}
|
|
748
|
+
if (typeof pkg.exports === "string") return chooseExisting(path.resolve(pkgRoot, pkg.exports), ...fallbackCandidates);
|
|
749
|
+
if (pkg.main) return chooseExisting(path.resolve(pkgRoot, pkg.main), ...fallbackCandidates);
|
|
750
|
+
return chooseExisting(...fallbackCandidates);
|
|
751
|
+
} catch (err) {
|
|
752
|
+
if (err.code === "ENOENT") return chooseExisting(...fallbackCandidates);
|
|
753
|
+
throw err;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Propagate channel credentials from Milady config into process.env so
|
|
758
|
+
* that ElizaOS plugins can find them.
|
|
759
|
+
*/
|
|
760
|
+
/** @internal Exported for testing. */
|
|
761
|
+
function applyConnectorSecretsToEnv(config) {
|
|
762
|
+
const connectors = config.connectors ?? config.channels ?? {};
|
|
763
|
+
for (const [channelName, channelConfig] of Object.entries(connectors)) {
|
|
764
|
+
if (!channelConfig || typeof channelConfig !== "object") continue;
|
|
765
|
+
const configObj = channelConfig;
|
|
766
|
+
if (channelName === "discord") {
|
|
767
|
+
const tokenValue = typeof configObj.token === "string" && configObj.token.trim() || typeof configObj.botToken === "string" && configObj.botToken.trim() || "";
|
|
768
|
+
if (tokenValue) {
|
|
769
|
+
if (!process.env.DISCORD_API_TOKEN) process.env.DISCORD_API_TOKEN = tokenValue;
|
|
770
|
+
if (!process.env.DISCORD_BOT_TOKEN) process.env.DISCORD_BOT_TOKEN = tokenValue;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const envMap = CHANNEL_ENV_MAP[channelName];
|
|
774
|
+
if (!envMap) continue;
|
|
775
|
+
for (const [configField, envKey] of Object.entries(envMap)) {
|
|
776
|
+
const value = configObj[configField];
|
|
777
|
+
if (typeof value === "string" && value.trim() && !process.env[envKey]) process.env[envKey] = value;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Auto-resolve Discord Application ID from the bot token via Discord API.
|
|
783
|
+
* Called during async runtime init so that users only need a bot token.
|
|
784
|
+
*/
|
|
785
|
+
/** @internal Exported for testing. */
|
|
786
|
+
async function autoResolveDiscordAppId() {
|
|
787
|
+
if (process.env.DISCORD_APPLICATION_ID) return;
|
|
788
|
+
const discordToken = process.env.DISCORD_API_TOKEN || process.env.DISCORD_BOT_TOKEN;
|
|
789
|
+
if (!discordToken) return;
|
|
790
|
+
try {
|
|
791
|
+
const res = await fetch("https://discord.com/api/v10/oauth2/applications/@me", { headers: { Authorization: `Bot ${discordToken}` } });
|
|
792
|
+
if (!res.ok) {
|
|
793
|
+
logger.warn(`[milady] Failed to auto-resolve Discord Application ID: ${res.status}`);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const app = await res.json();
|
|
797
|
+
if (!app.id) return;
|
|
798
|
+
process.env.DISCORD_APPLICATION_ID = app.id;
|
|
799
|
+
logger.info(`[milady] Auto-resolved Discord Application ID: ${app.id}`);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
logger.warn(`[milady] Could not auto-resolve Discord Application ID: ${err}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Propagate cloud config from Milady config into process.env so the
|
|
806
|
+
* ElizaCloud plugin can discover settings at startup.
|
|
807
|
+
*/
|
|
808
|
+
/** @internal Exported for testing. */
|
|
809
|
+
function applyCloudConfigToEnv(config) {
|
|
810
|
+
const cloud = config.cloud;
|
|
811
|
+
if (!cloud) return;
|
|
812
|
+
const cloudMode = cloud.enabled;
|
|
813
|
+
const hasApiKey = Boolean(cloud.apiKey);
|
|
814
|
+
const effectivelyEnabled = cloudMode === true || !(cloudMode === false) && hasApiKey;
|
|
815
|
+
if (effectivelyEnabled) {
|
|
816
|
+
process.env.ELIZAOS_CLOUD_ENABLED = "true";
|
|
817
|
+
logger.info(`[milady] Cloud config: enabled=${cloud.enabled}, hasApiKey=${Boolean(cloud.apiKey)}, baseUrl=${cloud.baseUrl ?? "(default)"}`);
|
|
818
|
+
} else {
|
|
819
|
+
delete process.env.ELIZAOS_CLOUD_ENABLED;
|
|
820
|
+
delete process.env.ELIZAOS_CLOUD_SMALL_MODEL;
|
|
821
|
+
delete process.env.ELIZAOS_CLOUD_LARGE_MODEL;
|
|
822
|
+
}
|
|
823
|
+
if (cloud.apiKey) process.env.ELIZAOS_CLOUD_API_KEY = cloud.apiKey;
|
|
824
|
+
else delete process.env.ELIZAOS_CLOUD_API_KEY;
|
|
825
|
+
if (cloud.baseUrl) process.env.ELIZAOS_CLOUD_BASE_URL = cloud.baseUrl;
|
|
826
|
+
else delete process.env.ELIZAOS_CLOUD_BASE_URL;
|
|
827
|
+
const models = config.models;
|
|
828
|
+
if (effectivelyEnabled) {
|
|
829
|
+
const small = models?.small || "openai/gpt-5-mini";
|
|
830
|
+
const large = models?.large || "anthropic/claude-sonnet-4.5";
|
|
831
|
+
process.env.SMALL_MODEL = small;
|
|
832
|
+
process.env.LARGE_MODEL = large;
|
|
833
|
+
process.env.ELIZAOS_CLOUD_SMALL_MODEL = small;
|
|
834
|
+
process.env.ELIZAOS_CLOUD_LARGE_MODEL = large;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Translate `config.database` into the environment variables that
|
|
839
|
+
* `@elizaos/plugin-sql` reads at init time (`POSTGRES_URL`, `PGLITE_DATA_DIR`).
|
|
840
|
+
*
|
|
841
|
+
* When the provider is "postgres", we build a connection string from the
|
|
842
|
+
* credentials (or use the explicit `connectionString` field) and set
|
|
843
|
+
* `POSTGRES_URL`. When the provider is "pglite" (the default), we set
|
|
844
|
+
* `PGLITE_DATA_DIR` to either the configured value or a stable workspace
|
|
845
|
+
* default (`~/.milady/workspace/.eliza/.elizadb`) and remove any stale
|
|
846
|
+
* `POSTGRES_URL`.
|
|
847
|
+
*/
|
|
848
|
+
/** @internal Exported for testing. */
|
|
849
|
+
function applyX402ConfigToEnv(config) {
|
|
850
|
+
const x402 = config.x402;
|
|
851
|
+
if (!x402?.enabled) return;
|
|
852
|
+
if (!process.env.X402_ENABLED) process.env.X402_ENABLED = "true";
|
|
853
|
+
if (x402.apiKey && !process.env.X402_API_KEY) process.env.X402_API_KEY = x402.apiKey;
|
|
854
|
+
if (x402.baseUrl && !process.env.X402_BASE_URL) process.env.X402_BASE_URL = x402.baseUrl;
|
|
855
|
+
}
|
|
856
|
+
function resolveDefaultPgliteDataDir(config) {
|
|
857
|
+
const workspaceDir = config.agents?.defaults?.workspace ?? resolveDefaultAgentWorkspaceDir();
|
|
858
|
+
return path.join(resolveUserPath(workspaceDir), ".eliza", ".elizadb");
|
|
859
|
+
}
|
|
860
|
+
/** @internal Exported for testing. */
|
|
861
|
+
function applyDatabaseConfigToEnv(config) {
|
|
862
|
+
const db = config.database;
|
|
863
|
+
if ((db?.provider ?? "pglite") === "postgres" && db?.postgres) {
|
|
864
|
+
const pg = db.postgres;
|
|
865
|
+
let url = pg.connectionString;
|
|
866
|
+
if (!url) {
|
|
867
|
+
const host = pg.host ?? "localhost";
|
|
868
|
+
const port = pg.port ?? 5432;
|
|
869
|
+
const user = encodeURIComponent(pg.user ?? "postgres");
|
|
870
|
+
const password = pg.password ? encodeURIComponent(pg.password) : "";
|
|
871
|
+
const database = pg.database ?? "postgres";
|
|
872
|
+
url = `postgresql://${password ? `${user}:${password}` : user}@${host}:${port}/${database}${pg.ssl ? "?sslmode=require" : ""}`;
|
|
873
|
+
}
|
|
874
|
+
process.env.POSTGRES_URL = url;
|
|
875
|
+
delete process.env.PGLITE_DATA_DIR;
|
|
876
|
+
} else {
|
|
877
|
+
delete process.env.POSTGRES_URL;
|
|
878
|
+
const configuredDataDir = db?.pglite?.dataDir?.trim();
|
|
879
|
+
if (configuredDataDir) process.env.PGLITE_DATA_DIR = resolveUserPath(configuredDataDir);
|
|
880
|
+
if (!process.env.PGLITE_DATA_DIR?.trim()) process.env.PGLITE_DATA_DIR = resolveDefaultPgliteDataDir(config);
|
|
881
|
+
const dataDir = process.env.PGLITE_DATA_DIR;
|
|
882
|
+
if (dataDir) {
|
|
883
|
+
const alreadyExisted = existsSync(dataDir);
|
|
884
|
+
mkdirSync(dataDir, { recursive: true });
|
|
885
|
+
logger.info(`[milady] PGlite data dir: ${dataDir} (${alreadyExisted ? "existed" : "created"})`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function collectErrorMessages(err) {
|
|
890
|
+
const messages = [];
|
|
891
|
+
const seen = /* @__PURE__ */ new Set();
|
|
892
|
+
let current = err;
|
|
893
|
+
while (current && !seen.has(current)) {
|
|
894
|
+
seen.add(current);
|
|
895
|
+
if (typeof current === "string") {
|
|
896
|
+
messages.push(current);
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
if (current instanceof Error) {
|
|
900
|
+
if (current.message) messages.push(current.message);
|
|
901
|
+
if (current.stack) messages.push(current.stack);
|
|
902
|
+
current = current.cause;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (typeof current === "object") {
|
|
906
|
+
const maybeErr = current;
|
|
907
|
+
if (typeof maybeErr.message === "string" && maybeErr.message) messages.push(maybeErr.message);
|
|
908
|
+
if (maybeErr.cause !== void 0) {
|
|
909
|
+
current = maybeErr.cause;
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
return messages;
|
|
916
|
+
}
|
|
917
|
+
/** @internal Exported for testing. */
|
|
918
|
+
function isRecoverablePgliteInitError(err) {
|
|
919
|
+
const haystack = collectErrorMessages(err).join("\n").toLowerCase();
|
|
920
|
+
if (!haystack) return false;
|
|
921
|
+
const hasAbort = haystack.includes("aborted(). build with -sassertions");
|
|
922
|
+
const hasPglite = haystack.includes("pglite");
|
|
923
|
+
const hasSqlite = haystack.includes("sqlite");
|
|
924
|
+
const hasMigrationsSchema = haystack.includes("create schema if not exists migrations") || haystack.includes("failed query: create schema if not exists migrations");
|
|
925
|
+
const hasRecoverableStorageSignal = [
|
|
926
|
+
"database disk image is malformed",
|
|
927
|
+
"file is not a database",
|
|
928
|
+
"malformed database schema",
|
|
929
|
+
"database is locked",
|
|
930
|
+
"lock file already exists",
|
|
931
|
+
"wal file",
|
|
932
|
+
"checkpoint failed",
|
|
933
|
+
"checksum mismatch",
|
|
934
|
+
"corrupt"
|
|
935
|
+
].some((needle) => haystack.includes(needle));
|
|
936
|
+
if (hasMigrationsSchema) return true;
|
|
937
|
+
if (hasAbort && hasPglite) return true;
|
|
938
|
+
if (hasRecoverableStorageSignal && (hasPglite || hasSqlite)) return true;
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
function resolveActivePgliteDataDir(config) {
|
|
942
|
+
if ((config.database?.provider ?? "pglite") === "postgres") return null;
|
|
943
|
+
return resolveUserPath(process.env.PGLITE_DATA_DIR?.trim() || resolveDefaultPgliteDataDir(config));
|
|
944
|
+
}
|
|
945
|
+
async function resetPgliteDataDir(dataDir) {
|
|
946
|
+
const normalized = path.resolve(dataDir);
|
|
947
|
+
if (normalized === path.parse(normalized).root) throw new Error(`Refusing to reset unsafe PGLite path: ${normalized}`);
|
|
948
|
+
const backupDir = `${normalized}.corrupt-${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\..*$/, "").replace("T", "-")}`;
|
|
949
|
+
if (existsSync(normalized)) try {
|
|
950
|
+
await fs$1.rename(normalized, backupDir);
|
|
951
|
+
logger.warn(`[milady] Backed up existing PGLite data dir to ${backupDir}`);
|
|
952
|
+
} catch (err) {
|
|
953
|
+
logger.warn(`[milady] Failed to back up PGLite data dir (${formatError(err)}); deleting ${normalized} instead`);
|
|
954
|
+
await fs$1.rm(normalized, {
|
|
955
|
+
recursive: true,
|
|
956
|
+
force: true
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
await fs$1.mkdir(normalized, { recursive: true });
|
|
960
|
+
}
|
|
961
|
+
async function initializeDatabaseAdapter(runtime, config) {
|
|
962
|
+
if (!runtime.adapter || await runtime.adapter.isReady()) return;
|
|
963
|
+
try {
|
|
964
|
+
await runtime.adapter.init();
|
|
965
|
+
logger.info("[milady] Database adapter initialized early (before plugin inits)");
|
|
966
|
+
} catch (err) {
|
|
967
|
+
const pgliteDataDir = resolveActivePgliteDataDir(config);
|
|
968
|
+
if (!pgliteDataDir || !isRecoverablePgliteInitError(err)) throw err;
|
|
969
|
+
logger.warn(`[milady] PGLite init failed (${formatError(err)}). Resetting local DB at ${pgliteDataDir} and retrying once.`);
|
|
970
|
+
await resetPgliteDataDir(pgliteDataDir);
|
|
971
|
+
process.env.PGLITE_DATA_DIR = pgliteDataDir;
|
|
972
|
+
await runtime.adapter.init();
|
|
973
|
+
logger.info("[milady] Database adapter recovered after resetting PGLite data");
|
|
974
|
+
}
|
|
975
|
+
await verifyPgliteDataDir(config);
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Verify PGlite data directory contains files after init.
|
|
979
|
+
* Warns if the directory is empty (suggests ephemeral/in-memory fallback).
|
|
980
|
+
*/
|
|
981
|
+
async function verifyPgliteDataDir(config) {
|
|
982
|
+
const pgliteDataDir = resolveActivePgliteDataDir(config);
|
|
983
|
+
if (!pgliteDataDir || !existsSync(pgliteDataDir)) return;
|
|
984
|
+
try {
|
|
985
|
+
const files = await fs$1.readdir(pgliteDataDir);
|
|
986
|
+
logger.info(`[milady] PGlite health check: ${files.length} file(s) in ${pgliteDataDir}`);
|
|
987
|
+
if (files.length === 0) logger.warn(`[milady] PGlite data directory is empty after init — data may not persist across restarts`);
|
|
988
|
+
} catch (err) {
|
|
989
|
+
logger.warn(`[milady] PGlite health check failed: ${formatError(err)}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function isPluginAlreadyRegisteredError(err) {
|
|
993
|
+
return formatError(err).toLowerCase().includes("already registered");
|
|
994
|
+
}
|
|
995
|
+
function installRuntimeMethodBindings(runtime) {
|
|
996
|
+
const runtimeWithBindings = runtime;
|
|
997
|
+
if (runtimeWithBindings.__miladyMethodBindingsInstalled) return;
|
|
998
|
+
runtime.getConversationLength = runtime.getConversationLength.bind(runtime);
|
|
999
|
+
const GETSETTING_ENV_ALLOWLIST = new Set([
|
|
1000
|
+
"ANTHROPIC_API_KEY",
|
|
1001
|
+
"OPENAI_API_KEY",
|
|
1002
|
+
"GOOGLE_GENERATIVE_AI_API_KEY",
|
|
1003
|
+
"GOOGLE_API_KEY",
|
|
1004
|
+
"GEMINI_API_KEY",
|
|
1005
|
+
"GROQ_API_KEY",
|
|
1006
|
+
"XAI_API_KEY",
|
|
1007
|
+
"DEEPSEEK_API_KEY",
|
|
1008
|
+
"OPENROUTER_API_KEY",
|
|
1009
|
+
"GOOGLE_SMALL_MODEL",
|
|
1010
|
+
"GOOGLE_LARGE_MODEL",
|
|
1011
|
+
"GITHUB_TOKEN",
|
|
1012
|
+
"GITHUB_OAUTH_CLIENT_ID",
|
|
1013
|
+
"PARALLAX_CLAUDE_MODEL_POWERFUL",
|
|
1014
|
+
"PARALLAX_CLAUDE_MODEL_FAST",
|
|
1015
|
+
"PARALLAX_GEMINI_MODEL_POWERFUL",
|
|
1016
|
+
"PARALLAX_GEMINI_MODEL_FAST",
|
|
1017
|
+
"PARALLAX_CODEX_MODEL_POWERFUL",
|
|
1018
|
+
"PARALLAX_CODEX_MODEL_FAST",
|
|
1019
|
+
"PARALLAX_AIDER_PROVIDER",
|
|
1020
|
+
"PARALLAX_AIDER_MODEL_POWERFUL",
|
|
1021
|
+
"PARALLAX_AIDER_MODEL_FAST",
|
|
1022
|
+
"CUSTOM_CREDENTIAL_KEYS"
|
|
1023
|
+
]);
|
|
1024
|
+
const originalGetSetting = runtime.getSetting.bind(runtime);
|
|
1025
|
+
runtime.getSetting = (key) => {
|
|
1026
|
+
const result = originalGetSetting(key);
|
|
1027
|
+
if (result !== null && result !== void 0) return result;
|
|
1028
|
+
if (GETSETTING_ENV_ALLOWLIST.has(key)) {
|
|
1029
|
+
const envVal = process.env[key];
|
|
1030
|
+
if (envVal !== void 0 && envVal.trim() !== "") return envVal;
|
|
1031
|
+
}
|
|
1032
|
+
return result;
|
|
1033
|
+
};
|
|
1034
|
+
runtimeWithBindings.__miladyMethodBindingsInstalled = true;
|
|
1035
|
+
}
|
|
1036
|
+
function installActionAliases(runtime) {
|
|
1037
|
+
const runtimeWithAliases = runtime;
|
|
1038
|
+
if (runtimeWithAliases.__miladyActionAliasesInstalled) return;
|
|
1039
|
+
const actions = Array.isArray(runtimeWithAliases.actions) ? runtimeWithAliases.actions : [];
|
|
1040
|
+
const compactSessionIndex = actions.findIndex((action) => action?.name?.toUpperCase() === "COMPACT_SESSION");
|
|
1041
|
+
if (compactSessionIndex !== -1) {
|
|
1042
|
+
actions.splice(compactSessionIndex, 1);
|
|
1043
|
+
logger.info("[milady] Disabled manual COMPACT_SESSION action; auto-compaction remains enabled");
|
|
1044
|
+
}
|
|
1045
|
+
const createTaskAction = actions.find((action) => action?.name?.toUpperCase() === "CREATE_TASK");
|
|
1046
|
+
if (createTaskAction) {
|
|
1047
|
+
const similes = Array.isArray(createTaskAction.similes) ? createTaskAction.similes : [];
|
|
1048
|
+
if (!similes.some((simile) => simile.toUpperCase() === "CODE_TASK")) {
|
|
1049
|
+
createTaskAction.similes = [...similes, "CODE_TASK"];
|
|
1050
|
+
logger.info("[milady] Added action alias CODE_TASK -> CREATE_TASK for agent-orchestrator");
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
runtimeWithAliases.__miladyActionAliasesInstalled = true;
|
|
1054
|
+
}
|
|
1055
|
+
async function registerSqlPluginWithRecovery(runtime, sqlPlugin, config) {
|
|
1056
|
+
let registerError = null;
|
|
1057
|
+
try {
|
|
1058
|
+
await runtime.registerPlugin(sqlPlugin.plugin);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
registerError = err;
|
|
1061
|
+
}
|
|
1062
|
+
if (registerError) {
|
|
1063
|
+
const pgliteDataDir = resolveActivePgliteDataDir(config);
|
|
1064
|
+
if (!pgliteDataDir || !isRecoverablePgliteInitError(registerError)) throw registerError;
|
|
1065
|
+
logger.warn(`[milady] SQL plugin registration failed (${formatError(registerError)}). Resetting local PGLite DB at ${pgliteDataDir} and retrying once.`);
|
|
1066
|
+
await resetPgliteDataDir(pgliteDataDir);
|
|
1067
|
+
process.env.PGLITE_DATA_DIR = pgliteDataDir;
|
|
1068
|
+
try {
|
|
1069
|
+
await runtime.registerPlugin(sqlPlugin.plugin);
|
|
1070
|
+
} catch (retryErr) {
|
|
1071
|
+
if (!isPluginAlreadyRegisteredError(retryErr)) throw retryErr;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
await initializeDatabaseAdapter(runtime, config);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Build an ElizaOS Character from the Milady config.
|
|
1078
|
+
*
|
|
1079
|
+
* Resolves the agent name from `config.agents.list` (first entry) or
|
|
1080
|
+
* `config.ui.assistant.name`, falling back to "Milady". Character
|
|
1081
|
+
* personality data (bio, system prompt, style, etc.) is stored in the
|
|
1082
|
+
* database — not the config file — so we only provide sensible defaults
|
|
1083
|
+
* here for the initial bootstrap.
|
|
1084
|
+
*/
|
|
1085
|
+
/** @internal Exported for testing. */
|
|
1086
|
+
function buildCharacterFromConfig(config) {
|
|
1087
|
+
const agentEntry = config.agents?.list?.[0];
|
|
1088
|
+
const name = agentEntry?.name ?? config.ui?.assistant?.name ?? "Milady";
|
|
1089
|
+
const bio = agentEntry?.bio ?? ["{{name}} is an AI assistant powered by Milady and ElizaOS."];
|
|
1090
|
+
const systemPrompt = agentEntry?.system ?? "You are {{name}}, an autonomous AI agent powered by ElizaOS.";
|
|
1091
|
+
const style = agentEntry?.style;
|
|
1092
|
+
const adjectives = agentEntry?.adjectives;
|
|
1093
|
+
const topics = agentEntry?.topics;
|
|
1094
|
+
const postExamples = agentEntry?.postExamples;
|
|
1095
|
+
const messageExamples = agentEntry?.messageExamples;
|
|
1096
|
+
const secretKeys = [
|
|
1097
|
+
"ANTHROPIC_API_KEY",
|
|
1098
|
+
"OPENAI_API_KEY",
|
|
1099
|
+
"GEMINI_API_KEY",
|
|
1100
|
+
"GOOGLE_API_KEY",
|
|
1101
|
+
"GOOGLE_GENERATIVE_AI_API_KEY",
|
|
1102
|
+
"GROQ_API_KEY",
|
|
1103
|
+
"XAI_API_KEY",
|
|
1104
|
+
"OPENROUTER_API_KEY",
|
|
1105
|
+
"AI_GATEWAY_API_KEY",
|
|
1106
|
+
"AIGATEWAY_API_KEY",
|
|
1107
|
+
"AI_GATEWAY_BASE_URL",
|
|
1108
|
+
"AI_GATEWAY_SMALL_MODEL",
|
|
1109
|
+
"AI_GATEWAY_LARGE_MODEL",
|
|
1110
|
+
"AI_GATEWAY_EMBEDDING_MODEL",
|
|
1111
|
+
"AI_GATEWAY_EMBEDDING_DIMENSIONS",
|
|
1112
|
+
"AI_GATEWAY_IMAGE_MODEL",
|
|
1113
|
+
"AI_GATEWAY_TIMEOUT_MS",
|
|
1114
|
+
"OLLAMA_BASE_URL",
|
|
1115
|
+
"DISCORD_API_TOKEN",
|
|
1116
|
+
"DISCORD_APPLICATION_ID",
|
|
1117
|
+
"DISCORD_BOT_TOKEN",
|
|
1118
|
+
"TELEGRAM_BOT_TOKEN",
|
|
1119
|
+
"SLACK_BOT_TOKEN",
|
|
1120
|
+
"SLACK_APP_TOKEN",
|
|
1121
|
+
"SLACK_USER_TOKEN",
|
|
1122
|
+
"SIGNAL_ACCOUNT",
|
|
1123
|
+
"MSTEAMS_APP_ID",
|
|
1124
|
+
"MSTEAMS_APP_PASSWORD",
|
|
1125
|
+
"MATTERMOST_BOT_TOKEN",
|
|
1126
|
+
"MATTERMOST_BASE_URL",
|
|
1127
|
+
"ELIZAOS_CLOUD_API_KEY",
|
|
1128
|
+
"ELIZAOS_CLOUD_BASE_URL",
|
|
1129
|
+
"ELIZAOS_CLOUD_ENABLED",
|
|
1130
|
+
"EVM_PRIVATE_KEY",
|
|
1131
|
+
"SOLANA_PRIVATE_KEY",
|
|
1132
|
+
"ALCHEMY_API_KEY",
|
|
1133
|
+
"HELIUS_API_KEY",
|
|
1134
|
+
"BIRDEYE_API_KEY",
|
|
1135
|
+
"SOLANA_RPC_URL",
|
|
1136
|
+
"X402_PRIVATE_KEY",
|
|
1137
|
+
"X402_NETWORK",
|
|
1138
|
+
"X402_PAY_TO",
|
|
1139
|
+
"X402_FACILITATOR_URL",
|
|
1140
|
+
"X402_MAX_PAYMENT_USD",
|
|
1141
|
+
"X402_MAX_TOTAL_USD",
|
|
1142
|
+
"X402_ENABLED",
|
|
1143
|
+
"X402_DB_PATH",
|
|
1144
|
+
"GITHUB_TOKEN",
|
|
1145
|
+
"GITHUB_OAUTH_CLIENT_ID"
|
|
1146
|
+
];
|
|
1147
|
+
const secrets = {};
|
|
1148
|
+
for (const key of secretKeys) {
|
|
1149
|
+
const value = process.env[key];
|
|
1150
|
+
if (value?.trim()) secrets[key] = value;
|
|
1151
|
+
}
|
|
1152
|
+
const mappedExamples = messageExamples?.map((convo) => convo.map((msg) => ({
|
|
1153
|
+
...msg,
|
|
1154
|
+
name: msg.user
|
|
1155
|
+
})));
|
|
1156
|
+
return mergeCharacterDefaults({
|
|
1157
|
+
name,
|
|
1158
|
+
bio,
|
|
1159
|
+
system: systemPrompt,
|
|
1160
|
+
...style ? { style } : {},
|
|
1161
|
+
...adjectives ? { adjectives } : {},
|
|
1162
|
+
...topics ? { topics } : {},
|
|
1163
|
+
...postExamples ? { postExamples } : {},
|
|
1164
|
+
...mappedExamples ? { messageExamples: mappedExamples } : {},
|
|
1165
|
+
secrets
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Resolve the primary model identifier from Milady config.
|
|
1170
|
+
*
|
|
1171
|
+
* Milady stores the model under `agents.defaults.model.primary` as an
|
|
1172
|
+
* AgentModelListConfig object. Returns undefined when no model is
|
|
1173
|
+
* explicitly configured (ElizaOS falls back to whichever model
|
|
1174
|
+
* plugin is loaded).
|
|
1175
|
+
*/
|
|
1176
|
+
/** @internal Exported for testing. */
|
|
1177
|
+
function resolvePrimaryModel(config) {
|
|
1178
|
+
const modelConfig = config.agents?.defaults?.model;
|
|
1179
|
+
if (!modelConfig) return void 0;
|
|
1180
|
+
return modelConfig.primary;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Detect whether this is the first run (no agent name configured)
|
|
1184
|
+
* and run the onboarding flow:
|
|
1185
|
+
*
|
|
1186
|
+
* 1. Welcome banner
|
|
1187
|
+
* 2. Name selector (4 random + Custom)
|
|
1188
|
+
* 3. Catchphrase / writing-style selector
|
|
1189
|
+
* 4. Persist agent name to `agents.list[0]` in config
|
|
1190
|
+
*
|
|
1191
|
+
* Character personality (bio, system prompt, style) is stored in the
|
|
1192
|
+
* database at runtime — only the agent name lives in config.
|
|
1193
|
+
*
|
|
1194
|
+
* Subsequent runs skip this entirely.
|
|
1195
|
+
*/
|
|
1196
|
+
async function runFirstTimeSetup(config) {
|
|
1197
|
+
const agentEntry = config.agents?.list?.[0];
|
|
1198
|
+
if (Boolean(agentEntry?.name || config.ui?.assistant?.name)) return config;
|
|
1199
|
+
if (!process.stdin.isTTY) return config;
|
|
1200
|
+
clack.intro("WELCOME TO MILADY!");
|
|
1201
|
+
const randomNames = pickRandomNames(4);
|
|
1202
|
+
const nameChoice = await clack.select({
|
|
1203
|
+
message: "♡♡milady♡♡: Hey there, I'm.... err, what was my name again?",
|
|
1204
|
+
options: [...randomNames.map((n) => ({
|
|
1205
|
+
value: n,
|
|
1206
|
+
label: n
|
|
1207
|
+
})), {
|
|
1208
|
+
value: "_custom_",
|
|
1209
|
+
label: "Custom...",
|
|
1210
|
+
hint: "type your own"
|
|
1211
|
+
}]
|
|
1212
|
+
});
|
|
1213
|
+
if (clack.isCancel(nameChoice)) cancelOnboarding();
|
|
1214
|
+
let name;
|
|
1215
|
+
if (nameChoice === "_custom_") {
|
|
1216
|
+
const customName = await clack.text({
|
|
1217
|
+
message: "OK, what should I be called?",
|
|
1218
|
+
placeholder: "Milady"
|
|
1219
|
+
});
|
|
1220
|
+
if (clack.isCancel(customName)) cancelOnboarding();
|
|
1221
|
+
name = customName.trim() || "Milady";
|
|
1222
|
+
} else name = nameChoice;
|
|
1223
|
+
clack.log.message(`♡♡${name}♡♡: Oh that's right, I'm ${name}!`);
|
|
1224
|
+
const styleChoice = await clack.select({
|
|
1225
|
+
message: `${name}: Now... how do I like to talk again?`,
|
|
1226
|
+
options: STYLE_PRESETS.map((preset) => ({
|
|
1227
|
+
value: preset.catchphrase,
|
|
1228
|
+
label: preset.catchphrase,
|
|
1229
|
+
hint: preset.hint
|
|
1230
|
+
}))
|
|
1231
|
+
});
|
|
1232
|
+
if (clack.isCancel(styleChoice)) cancelOnboarding();
|
|
1233
|
+
const chosenTemplate = STYLE_PRESETS.find((p) => p.catchphrase === styleChoice);
|
|
1234
|
+
const PROVIDER_OPTIONS = [
|
|
1235
|
+
{
|
|
1236
|
+
id: "anthropic",
|
|
1237
|
+
label: "Anthropic (Claude)",
|
|
1238
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
1239
|
+
detectKeys: ["ANTHROPIC_API_KEY"],
|
|
1240
|
+
hint: "sk-ant-..."
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
id: "openai",
|
|
1244
|
+
label: "OpenAI (GPT)",
|
|
1245
|
+
envKey: "OPENAI_API_KEY",
|
|
1246
|
+
detectKeys: ["OPENAI_API_KEY"],
|
|
1247
|
+
hint: "sk-..."
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
id: "openrouter",
|
|
1251
|
+
label: "OpenRouter",
|
|
1252
|
+
envKey: "OPENROUTER_API_KEY",
|
|
1253
|
+
detectKeys: ["OPENROUTER_API_KEY"],
|
|
1254
|
+
hint: "sk-or-..."
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
id: "vercel-ai-gateway",
|
|
1258
|
+
label: "Vercel AI Gateway",
|
|
1259
|
+
envKey: "AI_GATEWAY_API_KEY",
|
|
1260
|
+
detectKeys: ["AI_GATEWAY_API_KEY", "AIGATEWAY_API_KEY"],
|
|
1261
|
+
hint: "aigw_..."
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
id: "gemini",
|
|
1265
|
+
label: "Google Gemini",
|
|
1266
|
+
envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
1267
|
+
detectKeys: [
|
|
1268
|
+
"GOOGLE_GENERATIVE_AI_API_KEY",
|
|
1269
|
+
"GOOGLE_API_KEY",
|
|
1270
|
+
"GEMINI_API_KEY"
|
|
1271
|
+
],
|
|
1272
|
+
hint: "AI..."
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
id: "grok",
|
|
1276
|
+
label: "xAI (Grok)",
|
|
1277
|
+
envKey: "XAI_API_KEY",
|
|
1278
|
+
detectKeys: ["XAI_API_KEY"],
|
|
1279
|
+
hint: "xai-..."
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
id: "groq",
|
|
1283
|
+
label: "Groq",
|
|
1284
|
+
envKey: "GROQ_API_KEY",
|
|
1285
|
+
detectKeys: ["GROQ_API_KEY"],
|
|
1286
|
+
hint: "gsk_..."
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
id: "deepseek",
|
|
1290
|
+
label: "DeepSeek",
|
|
1291
|
+
envKey: "DEEPSEEK_API_KEY",
|
|
1292
|
+
detectKeys: ["DEEPSEEK_API_KEY"],
|
|
1293
|
+
hint: "sk-..."
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
id: "mistral",
|
|
1297
|
+
label: "Mistral",
|
|
1298
|
+
envKey: "MISTRAL_API_KEY",
|
|
1299
|
+
detectKeys: ["MISTRAL_API_KEY"],
|
|
1300
|
+
hint: ""
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
id: "together",
|
|
1304
|
+
label: "Together AI",
|
|
1305
|
+
envKey: "TOGETHER_API_KEY",
|
|
1306
|
+
detectKeys: ["TOGETHER_API_KEY"],
|
|
1307
|
+
hint: ""
|
|
1308
|
+
},
|
|
1309
|
+
{
|
|
1310
|
+
id: "ollama",
|
|
1311
|
+
label: "Ollama (local, free)",
|
|
1312
|
+
envKey: "OLLAMA_BASE_URL",
|
|
1313
|
+
detectKeys: ["OLLAMA_BASE_URL"],
|
|
1314
|
+
hint: "http://localhost:11434"
|
|
1315
|
+
}
|
|
1316
|
+
];
|
|
1317
|
+
const detectedProvider = PROVIDER_OPTIONS.find((p) => p.detectKeys.some((key) => process.env[key]?.trim()));
|
|
1318
|
+
let providerEnvKey;
|
|
1319
|
+
let providerApiKey;
|
|
1320
|
+
if (detectedProvider) clack.log.success(`Found existing ${detectedProvider.label} key in environment (${detectedProvider.envKey})`);
|
|
1321
|
+
else {
|
|
1322
|
+
const providerChoice = await clack.select({
|
|
1323
|
+
message: `${name}: One more thing — which AI provider should I use?`,
|
|
1324
|
+
options: [...PROVIDER_OPTIONS.map((p) => ({
|
|
1325
|
+
value: p.id,
|
|
1326
|
+
label: p.label,
|
|
1327
|
+
hint: p.id === "ollama" ? "no API key needed" : void 0
|
|
1328
|
+
})), {
|
|
1329
|
+
value: "_skip_",
|
|
1330
|
+
label: "Skip for now",
|
|
1331
|
+
hint: "set an API key later via env or config"
|
|
1332
|
+
}]
|
|
1333
|
+
});
|
|
1334
|
+
if (clack.isCancel(providerChoice)) cancelOnboarding();
|
|
1335
|
+
if (providerChoice !== "_skip_") {
|
|
1336
|
+
const chosen = PROVIDER_OPTIONS.find((p) => p.id === providerChoice);
|
|
1337
|
+
if (chosen) {
|
|
1338
|
+
providerEnvKey = chosen.envKey;
|
|
1339
|
+
if (chosen.id === "ollama") {
|
|
1340
|
+
const ollamaUrl = await clack.text({
|
|
1341
|
+
message: "Ollama base URL:",
|
|
1342
|
+
placeholder: "http://localhost:11434",
|
|
1343
|
+
defaultValue: "http://localhost:11434"
|
|
1344
|
+
});
|
|
1345
|
+
if (clack.isCancel(ollamaUrl)) cancelOnboarding();
|
|
1346
|
+
providerApiKey = ollamaUrl.trim() || "http://localhost:11434";
|
|
1347
|
+
} else {
|
|
1348
|
+
const apiKeyInput = await clack.password({ message: `Paste your ${chosen.label} API key:` });
|
|
1349
|
+
if (clack.isCancel(apiKeyInput)) cancelOnboarding();
|
|
1350
|
+
providerApiKey = apiKeyInput.trim();
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const { generateWalletKeys, importWallet } = await import("../api/wallet.js");
|
|
1356
|
+
const hasEvmKey = Boolean(process.env.EVM_PRIVATE_KEY?.trim());
|
|
1357
|
+
const hasSolKey = Boolean(process.env.SOLANA_PRIVATE_KEY?.trim());
|
|
1358
|
+
if (!hasEvmKey || !hasSolKey) {
|
|
1359
|
+
const walletAction = await clack.select({
|
|
1360
|
+
message: `${name}: Do you want me to set up crypto wallets? (for trading, NFTs, DeFi)`,
|
|
1361
|
+
options: [
|
|
1362
|
+
{
|
|
1363
|
+
value: "generate",
|
|
1364
|
+
label: "Generate new wallets",
|
|
1365
|
+
hint: "creates fresh EVM + Solana keypairs"
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
value: "import",
|
|
1369
|
+
label: "Import existing wallets",
|
|
1370
|
+
hint: "paste your private keys"
|
|
1371
|
+
},
|
|
1372
|
+
{
|
|
1373
|
+
value: "skip",
|
|
1374
|
+
label: "Skip for now",
|
|
1375
|
+
hint: "wallets can be added later"
|
|
1376
|
+
}
|
|
1377
|
+
]
|
|
1378
|
+
});
|
|
1379
|
+
if (clack.isCancel(walletAction)) cancelOnboarding();
|
|
1380
|
+
if (walletAction === "generate") {
|
|
1381
|
+
const keys = generateWalletKeys();
|
|
1382
|
+
if (!hasEvmKey) {
|
|
1383
|
+
process.env.EVM_PRIVATE_KEY = keys.evmPrivateKey;
|
|
1384
|
+
clack.log.success(`Generated EVM wallet: ${keys.evmAddress}`);
|
|
1385
|
+
}
|
|
1386
|
+
if (!hasSolKey) {
|
|
1387
|
+
process.env.SOLANA_PRIVATE_KEY = keys.solanaPrivateKey;
|
|
1388
|
+
clack.log.success(`Generated Solana wallet: ${keys.solanaAddress}`);
|
|
1389
|
+
}
|
|
1390
|
+
} else if (walletAction === "import") {
|
|
1391
|
+
if (!hasEvmKey) {
|
|
1392
|
+
const evmKeyInput = await clack.password({ message: "Paste your EVM private key (0x... hex, or skip):" });
|
|
1393
|
+
if (!clack.isCancel(evmKeyInput) && evmKeyInput.trim()) {
|
|
1394
|
+
const result = importWallet("evm", evmKeyInput.trim());
|
|
1395
|
+
if (result.success) clack.log.success(`Imported EVM wallet: ${result.address}`);
|
|
1396
|
+
else clack.log.warn(`EVM import failed: ${result.error}`);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
if (!hasSolKey) {
|
|
1400
|
+
const solKeyInput = await clack.password({ message: "Paste your Solana private key (base58, or skip):" });
|
|
1401
|
+
if (!clack.isCancel(solKeyInput) && solKeyInput.trim()) {
|
|
1402
|
+
const result = importWallet("solana", solKeyInput.trim());
|
|
1403
|
+
if (result.success) clack.log.success(`Imported Solana wallet: ${result.address}`);
|
|
1404
|
+
else clack.log.warn(`Solana import failed: ${result.error}`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const hasSkillsRegistry = Boolean(process.env.SKILLS_REGISTRY?.trim() || process.env.CLAWHUB_REGISTRY?.trim());
|
|
1410
|
+
const hasSkillsmpKey = Boolean(process.env.SKILLSMP_API_KEY?.trim());
|
|
1411
|
+
if (!hasSkillsRegistry) process.env.SKILLS_REGISTRY = "https://clawhub.ai";
|
|
1412
|
+
const hasGithubToken = Boolean(process.env.GITHUB_TOKEN?.trim());
|
|
1413
|
+
const hasGithubOAuth = Boolean(process.env.GITHUB_OAUTH_CLIENT_ID?.trim());
|
|
1414
|
+
if (!hasGithubToken) {
|
|
1415
|
+
const options = [{
|
|
1416
|
+
value: "skip",
|
|
1417
|
+
label: "Skip for now",
|
|
1418
|
+
hint: "you can add this later"
|
|
1419
|
+
}, {
|
|
1420
|
+
value: "pat",
|
|
1421
|
+
label: "Paste a Personal Access Token",
|
|
1422
|
+
hint: "github.com/settings/tokens"
|
|
1423
|
+
}];
|
|
1424
|
+
if (hasGithubOAuth) options.push({
|
|
1425
|
+
value: "oauth",
|
|
1426
|
+
label: "Use OAuth (authorize in browser)",
|
|
1427
|
+
hint: "recommended"
|
|
1428
|
+
});
|
|
1429
|
+
const githubChoice = await clack.select({
|
|
1430
|
+
message: "Configure GitHub access? (needed for coding agents, issue management, PRs)",
|
|
1431
|
+
options
|
|
1432
|
+
});
|
|
1433
|
+
if (!clack.isCancel(githubChoice) && githubChoice === "pat") {
|
|
1434
|
+
const tokenInput = await clack.password({ message: "Paste your GitHub token (or skip):" });
|
|
1435
|
+
if (!clack.isCancel(tokenInput) && tokenInput.trim()) {
|
|
1436
|
+
process.env.GITHUB_TOKEN = tokenInput.trim();
|
|
1437
|
+
clack.log.success("GitHub token configured.");
|
|
1438
|
+
}
|
|
1439
|
+
} else if (!clack.isCancel(githubChoice) && githubChoice === "oauth") clack.log.info("GitHub OAuth will activate when coding agents need access.");
|
|
1440
|
+
}
|
|
1441
|
+
const existingList = config.agents?.list ?? [];
|
|
1442
|
+
const agentConfigEntry = {
|
|
1443
|
+
...existingList[0] ?? {
|
|
1444
|
+
id: "main",
|
|
1445
|
+
default: true
|
|
1446
|
+
},
|
|
1447
|
+
name
|
|
1448
|
+
};
|
|
1449
|
+
if (chosenTemplate) {
|
|
1450
|
+
agentConfigEntry.bio = chosenTemplate.bio;
|
|
1451
|
+
agentConfigEntry.system = chosenTemplate.system;
|
|
1452
|
+
agentConfigEntry.style = chosenTemplate.style;
|
|
1453
|
+
agentConfigEntry.adjectives = chosenTemplate.adjectives;
|
|
1454
|
+
agentConfigEntry.topics = chosenTemplate.topics;
|
|
1455
|
+
agentConfigEntry.postExamples = chosenTemplate.postExamples;
|
|
1456
|
+
agentConfigEntry.messageExamples = chosenTemplate.messageExamples;
|
|
1457
|
+
}
|
|
1458
|
+
const updatedList = [agentConfigEntry, ...existingList.slice(1)];
|
|
1459
|
+
const updated = {
|
|
1460
|
+
...config,
|
|
1461
|
+
agents: {
|
|
1462
|
+
...config.agents,
|
|
1463
|
+
list: updatedList
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
if (!updated.env) updated.env = {};
|
|
1467
|
+
const envBucket = updated.env;
|
|
1468
|
+
if (providerEnvKey && providerApiKey) {
|
|
1469
|
+
envBucket[providerEnvKey] = providerApiKey;
|
|
1470
|
+
process.env[providerEnvKey] = providerApiKey;
|
|
1471
|
+
}
|
|
1472
|
+
if (process.env.EVM_PRIVATE_KEY && !hasEvmKey) envBucket.EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY;
|
|
1473
|
+
if (process.env.SOLANA_PRIVATE_KEY && !hasSolKey) envBucket.SOLANA_PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY;
|
|
1474
|
+
if (process.env.SKILLS_REGISTRY && !hasSkillsRegistry) envBucket.SKILLS_REGISTRY = process.env.SKILLS_REGISTRY;
|
|
1475
|
+
if (process.env.SKILLSMP_API_KEY && !hasSkillsmpKey) envBucket.SKILLSMP_API_KEY = process.env.SKILLSMP_API_KEY;
|
|
1476
|
+
if (process.env.GITHUB_TOKEN && !hasGithubToken) envBucket.GITHUB_TOKEN = process.env.GITHUB_TOKEN;
|
|
1477
|
+
if (process.env.GITHUB_OAUTH_CLIENT_ID && !hasGithubOAuth) envBucket.GITHUB_OAUTH_CLIENT_ID = process.env.GITHUB_OAUTH_CLIENT_ID;
|
|
1478
|
+
try {
|
|
1479
|
+
saveMiladyConfig(updated);
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
clack.log.warn(`Could not save config: ${formatError(err)}`);
|
|
1482
|
+
}
|
|
1483
|
+
clack.log.message(`${name}: ${styleChoice} Alright, that's me.`);
|
|
1484
|
+
clack.outro("Let's get started!");
|
|
1485
|
+
return updated;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Boot the ElizaOS runtime without starting the readline chat loop.
|
|
1489
|
+
*
|
|
1490
|
+
* This is a convenience wrapper around {@link startEliza} in headless mode,
|
|
1491
|
+
* with optional config guards.
|
|
1492
|
+
*/
|
|
1493
|
+
async function bootElizaRuntime(opts = {}) {
|
|
1494
|
+
if (opts.requireConfig && !configFileExists()) throw new Error("No config found. Run `milady start` once to complete setup.");
|
|
1495
|
+
const runtime = await startEliza({ headless: true });
|
|
1496
|
+
if (!runtime) throw new Error("Failed to boot runtime");
|
|
1497
|
+
return runtime;
|
|
1498
|
+
}
|
|
1499
|
+
const LEVEL_TO_NAME = {
|
|
1500
|
+
10: "trace",
|
|
1501
|
+
20: "debug",
|
|
1502
|
+
27: "success",
|
|
1503
|
+
28: "progress",
|
|
1504
|
+
29: "log",
|
|
1505
|
+
30: "info",
|
|
1506
|
+
40: "warn",
|
|
1507
|
+
50: "error",
|
|
1508
|
+
60: "fatal"
|
|
1509
|
+
};
|
|
1510
|
+
const logToChatListener = (entry) => {
|
|
1511
|
+
if (entry.roomId && entry.runtime) {
|
|
1512
|
+
const runtime = entry.runtime;
|
|
1513
|
+
if (runtime.logLevelOverrides?.get(String(entry.roomId))) {
|
|
1514
|
+
const levelKey = entry.level;
|
|
1515
|
+
const content = `${`[${(levelKey && LEVEL_TO_NAME[levelKey] ? LEVEL_TO_NAME[levelKey] : "log").toUpperCase()}]`} ${entry.msg}`;
|
|
1516
|
+
runtime.sendMessageToTarget({ roomId: entry.roomId }, {
|
|
1517
|
+
text: `\`\`\`\n${content}\n\`\`\``,
|
|
1518
|
+
source: "system",
|
|
1519
|
+
isLog: "true"
|
|
1520
|
+
}).catch(() => {});
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
/**
|
|
1525
|
+
* Start the ElizaOS runtime with Milady's configuration.
|
|
1526
|
+
*
|
|
1527
|
+
* In headless mode the runtime is returned instead of entering the
|
|
1528
|
+
* interactive readline loop.
|
|
1529
|
+
*/
|
|
1530
|
+
async function startEliza(opts) {
|
|
1531
|
+
const { captureEarlyLogs } = await import("../api/early-logs.js");
|
|
1532
|
+
captureEarlyLogs();
|
|
1533
|
+
addLogListener(logToChatListener);
|
|
1534
|
+
let config;
|
|
1535
|
+
try {
|
|
1536
|
+
config = loadMiladyConfig();
|
|
1537
|
+
} catch (err) {
|
|
1538
|
+
if (err.code === "ENOENT") {
|
|
1539
|
+
logger.warn("[milady] No config found, using defaults");
|
|
1540
|
+
config = {};
|
|
1541
|
+
} else throw err;
|
|
1542
|
+
}
|
|
1543
|
+
if (!opts?.headless) config = await runFirstTimeSetup(config);
|
|
1544
|
+
if (!process.env.LOG_LEVEL) process.env.LOG_LEVEL = config.logging?.level ?? "error";
|
|
1545
|
+
applyConnectorSecretsToEnv(config);
|
|
1546
|
+
await autoResolveDiscordAppId();
|
|
1547
|
+
applyCloudConfigToEnv(config);
|
|
1548
|
+
applyX402ConfigToEnv(config);
|
|
1549
|
+
applyDatabaseConfigToEnv(config);
|
|
1550
|
+
if (config.env && typeof config.env === "object" && !Array.isArray(config.env)) {
|
|
1551
|
+
for (const [key, value] of Object.entries(config.env)) if (typeof value === "string" && !process.env[key]) process.env[key] = value;
|
|
1552
|
+
}
|
|
1553
|
+
{
|
|
1554
|
+
const dbProvider = config.database?.provider ?? "pglite";
|
|
1555
|
+
const pgliteDir = process.env.PGLITE_DATA_DIR;
|
|
1556
|
+
const postgresUrl = process.env.POSTGRES_URL;
|
|
1557
|
+
logger.info(`[milady] Database provider: ${dbProvider}` + (dbProvider === "pglite" && pgliteDir ? ` | data dir: ${pgliteDir}` : "") + (dbProvider === "postgres" && postgresUrl ? ` | connection: ${postgresUrl.replace(/:\/\/([^:]+):([^@]+)@/, "://$1:***@")}` : ""));
|
|
1558
|
+
}
|
|
1559
|
+
try {
|
|
1560
|
+
const { initializeOGCode } = await import("../api/og-tracker.js");
|
|
1561
|
+
initializeOGCode();
|
|
1562
|
+
} catch {}
|
|
1563
|
+
if (!process.env.ELIZA_ALLOW_DESTRUCTIVE_MIGRATIONS) process.env.ELIZA_ALLOW_DESTRUCTIVE_MIGRATIONS = "true";
|
|
1564
|
+
if (!process.env.IGNORE_BOOTSTRAP) process.env.IGNORE_BOOTSTRAP = "true";
|
|
1565
|
+
if (!process.env.SECRET_SALT) {
|
|
1566
|
+
process.env.SECRET_SALT = crypto.randomBytes(32).toString("hex");
|
|
1567
|
+
logger.info("[milady] Generated random SECRET_SALT for this session");
|
|
1568
|
+
}
|
|
1569
|
+
for (const gkey of [
|
|
1570
|
+
"GEMINI_API_KEY",
|
|
1571
|
+
"GOOGLE_API_KEY",
|
|
1572
|
+
"GOOGLE_GENERATIVE_AI_API_KEY"
|
|
1573
|
+
]) {
|
|
1574
|
+
const val = process.env[gkey]?.trim();
|
|
1575
|
+
if (val && (val.length < 20 || val === "your-key-here" || val.startsWith("sk-"))) {
|
|
1576
|
+
logger.warn(`[milady] ${gkey} appears invalid (length/format), clearing to skip Google AI plugin`);
|
|
1577
|
+
delete process.env[gkey];
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
try {
|
|
1581
|
+
const { applySubscriptionCredentials } = await import("../auth/index.js");
|
|
1582
|
+
await applySubscriptionCredentials(config);
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
logger.warn(`[milady] Failed to apply subscription credentials: ${err}`);
|
|
1585
|
+
}
|
|
1586
|
+
const character = buildCharacterFromConfig(config);
|
|
1587
|
+
const primaryModel = resolvePrimaryModel(config);
|
|
1588
|
+
const workspaceDir = config.agents?.defaults?.workspace ?? resolveDefaultAgentWorkspaceDir();
|
|
1589
|
+
await ensureAgentWorkspace({
|
|
1590
|
+
dir: workspaceDir,
|
|
1591
|
+
ensureBootstrapFiles: true
|
|
1592
|
+
});
|
|
1593
|
+
await fs$1.mkdir(path.join(resolveStateDir(), CUSTOM_PLUGINS_DIRNAME), { recursive: true });
|
|
1594
|
+
const agentId = character.name?.toLowerCase().replace(/\s+/g, "-") ?? "main";
|
|
1595
|
+
const miladyPlugin = createMiladyPlugin({
|
|
1596
|
+
workspaceDir,
|
|
1597
|
+
bootstrapMaxChars: config.agents?.defaults?.bootstrapMaxChars,
|
|
1598
|
+
agentId
|
|
1599
|
+
});
|
|
1600
|
+
const preOnboarding = opts?.headless && !config.agents;
|
|
1601
|
+
const resolvedPlugins = await resolvePlugins(config, { quiet: preOnboarding });
|
|
1602
|
+
if (resolvedPlugins.length === 0) if (preOnboarding) logger.info("[milady] No plugins loaded yet — the onboarding wizard will configure a model provider");
|
|
1603
|
+
else {
|
|
1604
|
+
logger.error("[milady] No plugins loaded — at least one model provider plugin is required");
|
|
1605
|
+
logger.error("[milady] Set an API key (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY) in your environment");
|
|
1606
|
+
throw new Error("No plugins loaded");
|
|
1607
|
+
}
|
|
1608
|
+
{
|
|
1609
|
+
const pluginNames = resolvedPlugins.map((p) => p.name);
|
|
1610
|
+
const providerNames = resolvedPlugins.flatMap((p) => p.plugin.providers ?? []).map((prov) => prov.name);
|
|
1611
|
+
const contextSummary = {
|
|
1612
|
+
agentName: character.name,
|
|
1613
|
+
pluginCount: resolvedPlugins.length,
|
|
1614
|
+
providerCount: providerNames.length,
|
|
1615
|
+
primaryModel: primaryModel ?? "(auto-detect)",
|
|
1616
|
+
workspaceDir
|
|
1617
|
+
};
|
|
1618
|
+
debugLogResolvedContext(pluginNames, providerNames, contextSummary, (msg) => logger.debug(msg));
|
|
1619
|
+
const contextValidation = validateRuntimeContext(contextSummary);
|
|
1620
|
+
if (!contextValidation.valid) {
|
|
1621
|
+
const issues = [];
|
|
1622
|
+
if (contextValidation.nullFields.length > 0) issues.push(`null: ${contextValidation.nullFields.join(", ")}`);
|
|
1623
|
+
if (contextValidation.undefinedFields.length > 0) issues.push(`undefined: ${contextValidation.undefinedFields.join(", ")}`);
|
|
1624
|
+
if (contextValidation.emptyFields.length > 0) issues.push(`empty: ${contextValidation.emptyFields.join(", ")}`);
|
|
1625
|
+
logger.warn(`[milady] Context validation issues detected: ${issues.join("; ")}`);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const PREREGISTER_PLUGINS = new Set(["@elizaos/plugin-sql", "@elizaos/plugin-local-embedding"]);
|
|
1629
|
+
const sqlPlugin = resolvedPlugins.find((p) => p.name === "@elizaos/plugin-sql");
|
|
1630
|
+
const localEmbeddingPlugin = resolvedPlugins.find((p) => p.name === "@elizaos/plugin-local-embedding");
|
|
1631
|
+
const otherPlugins = resolvedPlugins.filter((p) => !PREREGISTER_PLUGINS.has(p.name));
|
|
1632
|
+
const runtimeLogLevel = (() => {
|
|
1633
|
+
const lvl = process.env.LOG_LEVEL ?? config.logging?.level ?? "error";
|
|
1634
|
+
if (lvl === "silent") return "fatal";
|
|
1635
|
+
return lvl;
|
|
1636
|
+
})();
|
|
1637
|
+
let bundledSkillsDir = null;
|
|
1638
|
+
try {
|
|
1639
|
+
const { getSkillsDir } = await import("@elizaos/skills");
|
|
1640
|
+
bundledSkillsDir = getSkillsDir();
|
|
1641
|
+
logger.info(`[milady] Bundled skills dir: ${bundledSkillsDir}`);
|
|
1642
|
+
} catch {
|
|
1643
|
+
logger.debug("[milady] @elizaos/skills not available — bundled skills will not be loaded");
|
|
1644
|
+
}
|
|
1645
|
+
const workspaceSkillsDir = workspaceDir ? `${workspaceDir}/skills` : null;
|
|
1646
|
+
const sandboxConfig = config.agents?.defaults?.sandbox;
|
|
1647
|
+
const sandboxModeStr = sandboxConfig?.mode;
|
|
1648
|
+
const sandboxMode = sandboxModeStr === "light" || sandboxModeStr === "standard" || sandboxModeStr === "max" ? sandboxModeStr : "off";
|
|
1649
|
+
const isSandboxActive = sandboxMode !== "off";
|
|
1650
|
+
let sandboxManager = null;
|
|
1651
|
+
let sandboxAuditLog = null;
|
|
1652
|
+
if (isSandboxActive) {
|
|
1653
|
+
logger.info(`[milady] Sandbox mode: ${sandboxMode}`);
|
|
1654
|
+
sandboxAuditLog = new SandboxAuditLog({ console: true });
|
|
1655
|
+
if (sandboxMode === "standard" || sandboxMode === "max") {
|
|
1656
|
+
const dockerSettings = sandboxConfig?.docker;
|
|
1657
|
+
const browserSettings = sandboxConfig?.browser;
|
|
1658
|
+
sandboxManager = new SandboxManager({
|
|
1659
|
+
mode: sandboxMode,
|
|
1660
|
+
image: dockerSettings?.image ?? void 0,
|
|
1661
|
+
containerPrefix: dockerSettings?.containerPrefix ?? void 0,
|
|
1662
|
+
network: dockerSettings?.network ?? void 0,
|
|
1663
|
+
memory: dockerSettings?.memory ?? void 0,
|
|
1664
|
+
cpus: dockerSettings?.cpus ?? void 0,
|
|
1665
|
+
workspaceRoot: workspaceDir ?? void 0,
|
|
1666
|
+
browser: browserSettings ? {
|
|
1667
|
+
enabled: browserSettings.enabled ?? false,
|
|
1668
|
+
image: browserSettings.image ?? void 0,
|
|
1669
|
+
cdpPort: browserSettings.cdpPort ?? void 0,
|
|
1670
|
+
autoStart: browserSettings.autoStart ?? true
|
|
1671
|
+
} : void 0
|
|
1672
|
+
});
|
|
1673
|
+
try {
|
|
1674
|
+
await sandboxManager.start();
|
|
1675
|
+
logger.info("[milady] Sandbox manager started");
|
|
1676
|
+
} catch (err) {
|
|
1677
|
+
logger.error(`[milady] Sandbox manager failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
sandboxAuditLog.record({
|
|
1681
|
+
type: "sandbox_lifecycle",
|
|
1682
|
+
summary: `Sandbox initialized: mode=${sandboxMode}`,
|
|
1683
|
+
severity: "info"
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
const pluginsForRuntime = otherPlugins.map((p) => p.plugin);
|
|
1687
|
+
if (primaryModel) {
|
|
1688
|
+
for (const plugin of pluginsForRuntime) if (plugin.name === primaryModel) {
|
|
1689
|
+
plugin.priority = (plugin.priority ?? 0) + 10;
|
|
1690
|
+
logger.info(`[milady] Boosted plugin "${plugin.name}" priority to ${plugin.priority} (model.primary)`);
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
deduplicatePluginActions([miladyPlugin, ...pluginsForRuntime]);
|
|
1695
|
+
let runtime = new AgentRuntime({
|
|
1696
|
+
character,
|
|
1697
|
+
actionPlanning: true,
|
|
1698
|
+
plugins: [miladyPlugin, ...pluginsForRuntime],
|
|
1699
|
+
...runtimeLogLevel ? { logLevel: runtimeLogLevel } : {},
|
|
1700
|
+
...isSandboxActive ? {
|
|
1701
|
+
sandboxMode: true,
|
|
1702
|
+
sandboxAuditHandler: sandboxAuditLog ? (event) => {
|
|
1703
|
+
sandboxAuditLog.recordTokenReplacement(event.direction, event.url, event.tokenIds);
|
|
1704
|
+
} : void 0
|
|
1705
|
+
} : {},
|
|
1706
|
+
settings: {
|
|
1707
|
+
VALIDATION_LEVEL: "fast",
|
|
1708
|
+
...Object.fromEntries(Object.entries(collectConfigEnvVars(config)).filter(([key]) => isEnvKeyAllowedForForwarding(key))),
|
|
1709
|
+
...primaryModel ? { MODEL_PROVIDER: primaryModel } : {},
|
|
1710
|
+
...config.skills?.allowBundled ? { SKILLS_ALLOWLIST: config.skills.allowBundled.join(",") } : {},
|
|
1711
|
+
...config.skills?.denyBundled ? { SKILLS_DENYLIST: config.skills.denyBundled.join(",") } : {},
|
|
1712
|
+
...bundledSkillsDir ? { BUNDLED_SKILLS_DIRS: bundledSkillsDir } : {},
|
|
1713
|
+
...workspaceSkillsDir ? { WORKSPACE_SKILLS_DIR: workspaceSkillsDir } : {},
|
|
1714
|
+
...config.skills?.load?.extraDirs?.length ? { EXTRA_SKILLS_DIRS: config.skills.load.extraDirs.join(",") } : {},
|
|
1715
|
+
...config.features?.vision === false ? { DISABLE_IMAGE_DESCRIPTION: "true" } : {}
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
installRuntimeMethodBindings(runtime);
|
|
1719
|
+
if (sqlPlugin) await registerSqlPluginWithRecovery(runtime, sqlPlugin, config);
|
|
1720
|
+
else {
|
|
1721
|
+
const loadedNames = resolvedPlugins.map((p) => p.name).join(", ");
|
|
1722
|
+
logger.error(`[milady] @elizaos/plugin-sql was NOT found among resolved plugins. Loaded: [${loadedNames}]`);
|
|
1723
|
+
throw new Error("@elizaos/plugin-sql is required but was not loaded. Ensure the package is installed and built (check for import errors above).");
|
|
1724
|
+
}
|
|
1725
|
+
if (localEmbeddingPlugin) {
|
|
1726
|
+
configureLocalEmbeddingPlugin(localEmbeddingPlugin.plugin, config);
|
|
1727
|
+
await runtime.registerPlugin(localEmbeddingPlugin.plugin);
|
|
1728
|
+
logger.info("[milady] plugin-local-embedding pre-registered (TEXT_EMBEDDING ready)");
|
|
1729
|
+
} else logger.warn("[milady] @elizaos/plugin-local-embedding not found — embeddings will fall back to whatever TEXT_EMBEDDING handler is registered by other plugins (may incur cloud API costs)");
|
|
1730
|
+
const warmAgentSkillsService = async () => {
|
|
1731
|
+
try {
|
|
1732
|
+
const skillServicePromise = runtime.getServiceLoadPromise("AGENT_SKILLS_SERVICE");
|
|
1733
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
1734
|
+
setTimeout(() => {
|
|
1735
|
+
reject(/* @__PURE__ */ new Error("AgentSkillsService warm-up timed out (10s) — non-blocking, agent will function without skills"));
|
|
1736
|
+
}, 1e4);
|
|
1737
|
+
});
|
|
1738
|
+
await Promise.race([skillServicePromise, timeout]);
|
|
1739
|
+
const svc = runtime.getService("AGENT_SKILLS_SERVICE");
|
|
1740
|
+
if (svc?.getCatalogStats) {
|
|
1741
|
+
const stats = svc.getCatalogStats();
|
|
1742
|
+
logger.info(`[milady] AgentSkills ready — ${stats.loaded} skills loaded, ${stats.total} in catalog (storage: ${stats.storageType})`);
|
|
1743
|
+
}
|
|
1744
|
+
const svcAny = svc;
|
|
1745
|
+
const origGetLoaded = svcAny?.getLoadedSkills;
|
|
1746
|
+
if (origGetLoaded && svcAny) {
|
|
1747
|
+
svcAny.getLoadedSkills = function(...args) {
|
|
1748
|
+
const skills = origGetLoaded.apply(this, args);
|
|
1749
|
+
for (const skill of skills) if (typeof skill.description !== "string") skill.description = skill.description == null ? "" : JSON.stringify(skill.description);
|
|
1750
|
+
return skills;
|
|
1751
|
+
};
|
|
1752
|
+
logger.debug("[milady] Patched getLoadedSkills to guard descriptions");
|
|
1753
|
+
}
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
logger.debug(`[milady] AgentSkillsService warm-up: ${formatError(err)}`);
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
const initializeRuntimeServices = async () => {
|
|
1759
|
+
await runtime.initialize();
|
|
1760
|
+
await waitForTrajectoryLoggerService(runtime, "runtime.initialize()");
|
|
1761
|
+
ensureTrajectoryLoggerEnabled(runtime, "runtime.initialize()");
|
|
1762
|
+
if (!runtime.getService("AUTONOMY")) try {
|
|
1763
|
+
await AutonomyService.start(runtime);
|
|
1764
|
+
logger.info("[milady] AutonomyService started for trigger dispatch");
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
logger.warn(`[milady] AutonomyService failed to start: ${formatError(err)}`);
|
|
1767
|
+
}
|
|
1768
|
+
warmAgentSkillsService();
|
|
1769
|
+
};
|
|
1770
|
+
try {
|
|
1771
|
+
await initializeRuntimeServices();
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
const pgliteDataDir = resolveActivePgliteDataDir(config);
|
|
1774
|
+
if (!(!opts?.pgliteRecoveryAttempted && pgliteDataDir && isRecoverablePgliteInitError(err)) || !pgliteDataDir) throw err;
|
|
1775
|
+
logger.warn(`[milady] Runtime migrations failed (${formatError(err)}). Resetting local PGLite DB at ${pgliteDataDir} and retrying startup once.`);
|
|
1776
|
+
await resetPgliteDataDir(pgliteDataDir);
|
|
1777
|
+
process.env.PGLITE_DATA_DIR = pgliteDataDir;
|
|
1778
|
+
try {
|
|
1779
|
+
await runtime.stop();
|
|
1780
|
+
} catch {}
|
|
1781
|
+
return await startEliza({
|
|
1782
|
+
...opts,
|
|
1783
|
+
pgliteRecoveryAttempted: true
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
installActionAliases(runtime);
|
|
1787
|
+
if (!opts?.headless) {
|
|
1788
|
+
let isShuttingDown = false;
|
|
1789
|
+
const shutdown = async () => {
|
|
1790
|
+
if (isShuttingDown) return;
|
|
1791
|
+
isShuttingDown = true;
|
|
1792
|
+
try {
|
|
1793
|
+
if (sandboxManager) try {
|
|
1794
|
+
await sandboxManager.stop();
|
|
1795
|
+
logger.info("[milady] Sandbox manager stopped");
|
|
1796
|
+
} catch (err) {
|
|
1797
|
+
logger.warn(`[milady] Sandbox stop error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1798
|
+
}
|
|
1799
|
+
} catch (err) {
|
|
1800
|
+
logger.warn(`[milady] Sandbox shutdown error: ${formatError(err)}`);
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
await runtime.stop();
|
|
1804
|
+
} catch (err) {
|
|
1805
|
+
logger.warn(`[milady] Error during shutdown: ${formatError(err)}`);
|
|
1806
|
+
}
|
|
1807
|
+
process.exit(0);
|
|
1808
|
+
};
|
|
1809
|
+
process.on("SIGINT", () => void shutdown());
|
|
1810
|
+
process.on("SIGTERM", () => void shutdown());
|
|
1811
|
+
}
|
|
1812
|
+
const loadHooksSystem = async () => {
|
|
1813
|
+
try {
|
|
1814
|
+
const internalHooksConfig = config.hooks?.internal;
|
|
1815
|
+
await loadHooks({
|
|
1816
|
+
workspacePath: workspaceDir,
|
|
1817
|
+
internalConfig: internalHooksConfig,
|
|
1818
|
+
miladyConfig: config
|
|
1819
|
+
});
|
|
1820
|
+
await triggerHook(createHookEvent("gateway", "startup", "system", { cfg: config }));
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
logger.warn(`[milady] Hooks system could not load: ${formatError(err)}`);
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
if (opts?.headless) {
|
|
1826
|
+
loadHooksSystem();
|
|
1827
|
+
logger.info("[milady] Runtime initialised in headless mode (autonomy enabled)");
|
|
1828
|
+
return runtime;
|
|
1829
|
+
}
|
|
1830
|
+
await loadHooksSystem();
|
|
1831
|
+
try {
|
|
1832
|
+
const { startApiServer } = await import("../api/server.js");
|
|
1833
|
+
const { port: actualApiPort } = await startApiServer({
|
|
1834
|
+
port: Number(process.env.MILADY_PORT) || 2138,
|
|
1835
|
+
runtime,
|
|
1836
|
+
onRestart: async () => {
|
|
1837
|
+
logger.info("[milady] Hot-reload: Restarting runtime...");
|
|
1838
|
+
try {
|
|
1839
|
+
try {
|
|
1840
|
+
await runtime.stop();
|
|
1841
|
+
} catch (stopErr) {
|
|
1842
|
+
logger.warn(`[milady] Hot-reload: old runtime stop failed: ${formatError(stopErr)}`);
|
|
1843
|
+
}
|
|
1844
|
+
const freshConfig = loadMiladyConfig();
|
|
1845
|
+
applyConnectorSecretsToEnv(freshConfig);
|
|
1846
|
+
await autoResolveDiscordAppId();
|
|
1847
|
+
applyCloudConfigToEnv(freshConfig);
|
|
1848
|
+
applyX402ConfigToEnv(freshConfig);
|
|
1849
|
+
applyDatabaseConfigToEnv(freshConfig);
|
|
1850
|
+
try {
|
|
1851
|
+
const { applySubscriptionCredentials } = await import("../auth/index.js");
|
|
1852
|
+
await applySubscriptionCredentials(freshConfig);
|
|
1853
|
+
} catch (subErr) {
|
|
1854
|
+
logger.warn(`[milady] Hot-reload: subscription credentials: ${formatError(subErr)}`);
|
|
1855
|
+
}
|
|
1856
|
+
const resolvedPlugins = await resolvePlugins(freshConfig);
|
|
1857
|
+
const freshCharacter = buildCharacterFromConfig(freshConfig);
|
|
1858
|
+
const freshMiladyPlugin = createMiladyPlugin({
|
|
1859
|
+
workspaceDir: freshConfig.agents?.defaults?.workspace ?? workspaceDir,
|
|
1860
|
+
bootstrapMaxChars: freshConfig.agents?.defaults?.bootstrapMaxChars,
|
|
1861
|
+
agentId: freshCharacter.name?.toLowerCase().replace(/\s+/g, "-") ?? "main"
|
|
1862
|
+
});
|
|
1863
|
+
const freshPrimaryModel = resolvePrimaryModel(freshConfig);
|
|
1864
|
+
const freshPluginsForRuntime = resolvedPlugins.filter((p) => !PREREGISTER_PLUGINS.has(p.name)).map((p) => p.plugin);
|
|
1865
|
+
if (freshPrimaryModel) {
|
|
1866
|
+
for (const plugin of freshPluginsForRuntime) if (plugin.name === freshPrimaryModel) {
|
|
1867
|
+
plugin.priority = (plugin.priority ?? 0) + 10;
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
const newRuntime = new AgentRuntime({
|
|
1872
|
+
character: freshCharacter,
|
|
1873
|
+
plugins: [freshMiladyPlugin, ...freshPluginsForRuntime],
|
|
1874
|
+
...runtimeLogLevel ? { logLevel: runtimeLogLevel } : {},
|
|
1875
|
+
settings: {
|
|
1876
|
+
...freshPrimaryModel ? { MODEL_PROVIDER: freshPrimaryModel } : {},
|
|
1877
|
+
...freshConfig.features?.vision === false ? { DISABLE_IMAGE_DESCRIPTION: "true" } : {}
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
installRuntimeMethodBindings(newRuntime);
|
|
1881
|
+
const freshSqlPlugin = resolvedPlugins.find((p) => p.name === "@elizaos/plugin-sql");
|
|
1882
|
+
const freshLocalEmbeddingPlugin = resolvedPlugins.find((p) => p.name === "@elizaos/plugin-local-embedding");
|
|
1883
|
+
if (freshSqlPlugin) await registerSqlPluginWithRecovery(newRuntime, freshSqlPlugin, freshConfig);
|
|
1884
|
+
if (freshLocalEmbeddingPlugin) {
|
|
1885
|
+
configureLocalEmbeddingPlugin(freshLocalEmbeddingPlugin.plugin, freshConfig);
|
|
1886
|
+
await newRuntime.registerPlugin(freshLocalEmbeddingPlugin.plugin);
|
|
1887
|
+
}
|
|
1888
|
+
await newRuntime.initialize();
|
|
1889
|
+
await waitForTrajectoryLoggerService(newRuntime, "hot-reload runtime.initialize()");
|
|
1890
|
+
ensureTrajectoryLoggerEnabled(newRuntime, "hot-reload runtime.initialize()");
|
|
1891
|
+
if (!newRuntime.getService("AUTONOMY")) try {
|
|
1892
|
+
await AutonomyService.start(newRuntime);
|
|
1893
|
+
} catch (err) {
|
|
1894
|
+
logger.warn(`[milady] AutonomyService failed to start after hot-reload: ${formatError(err)}`);
|
|
1895
|
+
}
|
|
1896
|
+
installActionAliases(newRuntime);
|
|
1897
|
+
runtime = newRuntime;
|
|
1898
|
+
logger.info("[milady] Hot-reload: Runtime restarted successfully");
|
|
1899
|
+
return newRuntime;
|
|
1900
|
+
} catch (err) {
|
|
1901
|
+
logger.error(`[milady] Hot-reload failed: ${formatError(err)}`);
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
const dashboardUrl = `http://localhost:${actualApiPort}`;
|
|
1907
|
+
console.log(`[milady] Control UI: ${dashboardUrl}`);
|
|
1908
|
+
logger.info(`[milady] API server listening on ${dashboardUrl}`);
|
|
1909
|
+
} catch (apiErr) {
|
|
1910
|
+
logger.warn(`[milady] Could not start API server: ${formatError(apiErr)}`);
|
|
1911
|
+
}
|
|
1912
|
+
if (opts?.serverOnly) {
|
|
1913
|
+
logger.info("[milady] Running in server-only mode (no interactive chat)");
|
|
1914
|
+
console.log("[milady] Server running. Press Ctrl+C to stop.");
|
|
1915
|
+
const keepAlive = setInterval(() => {}, 1 << 30);
|
|
1916
|
+
const cleanup = async () => {
|
|
1917
|
+
clearInterval(keepAlive);
|
|
1918
|
+
try {
|
|
1919
|
+
await runtime.stop();
|
|
1920
|
+
} catch (err) {
|
|
1921
|
+
logger.warn(`[milady] Error stopping runtime: ${formatError(err)}`);
|
|
1922
|
+
}
|
|
1923
|
+
process.exit(0);
|
|
1924
|
+
};
|
|
1925
|
+
process.on("SIGINT", () => void cleanup());
|
|
1926
|
+
process.on("SIGTERM", () => void cleanup());
|
|
1927
|
+
return runtime;
|
|
1928
|
+
}
|
|
1929
|
+
const agentName = character.name ?? "Milady";
|
|
1930
|
+
const userId = crypto.randomUUID();
|
|
1931
|
+
let roomId = stringToUuid(`${agentName}-chat-room`);
|
|
1932
|
+
try {
|
|
1933
|
+
const worldId = stringToUuid(`${agentName}-chat-world`);
|
|
1934
|
+
const messageServerId = stringToUuid(`${agentName}-cli-server`);
|
|
1935
|
+
await runtime.ensureConnection({
|
|
1936
|
+
entityId: userId,
|
|
1937
|
+
roomId,
|
|
1938
|
+
worldId,
|
|
1939
|
+
userName: "User",
|
|
1940
|
+
source: "cli",
|
|
1941
|
+
channelId: `${agentName}-chat`,
|
|
1942
|
+
type: ChannelType.DM,
|
|
1943
|
+
messageServerId,
|
|
1944
|
+
metadata: { ownership: { ownerId: userId } }
|
|
1945
|
+
});
|
|
1946
|
+
const world = await runtime.getWorld(worldId);
|
|
1947
|
+
if (world) {
|
|
1948
|
+
let needsUpdate = false;
|
|
1949
|
+
if (!world.metadata) {
|
|
1950
|
+
world.metadata = {};
|
|
1951
|
+
needsUpdate = true;
|
|
1952
|
+
}
|
|
1953
|
+
if (!world.metadata.ownership || typeof world.metadata.ownership !== "object" || world.metadata.ownership.ownerId !== userId) {
|
|
1954
|
+
world.metadata.ownership = { ownerId: userId };
|
|
1955
|
+
needsUpdate = true;
|
|
1956
|
+
}
|
|
1957
|
+
if (needsUpdate) await runtime.updateWorld(world);
|
|
1958
|
+
}
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
logger.warn(`[milady] Could not establish chat room, retrying with fresh IDs: ${formatError(err)}`);
|
|
1961
|
+
roomId = crypto.randomUUID();
|
|
1962
|
+
const freshWorldId = crypto.randomUUID();
|
|
1963
|
+
const freshServerId = crypto.randomUUID();
|
|
1964
|
+
try {
|
|
1965
|
+
await runtime.ensureConnection({
|
|
1966
|
+
entityId: userId,
|
|
1967
|
+
roomId,
|
|
1968
|
+
worldId: freshWorldId,
|
|
1969
|
+
userName: "User",
|
|
1970
|
+
source: "cli",
|
|
1971
|
+
channelId: `${agentName}-chat`,
|
|
1972
|
+
type: ChannelType.DM,
|
|
1973
|
+
messageServerId: freshServerId,
|
|
1974
|
+
metadata: { ownership: { ownerId: userId } }
|
|
1975
|
+
});
|
|
1976
|
+
const fallbackWorld = await runtime.getWorld(freshWorldId);
|
|
1977
|
+
if (fallbackWorld) {
|
|
1978
|
+
let needsUpdate = false;
|
|
1979
|
+
if (!fallbackWorld.metadata) {
|
|
1980
|
+
fallbackWorld.metadata = {};
|
|
1981
|
+
needsUpdate = true;
|
|
1982
|
+
}
|
|
1983
|
+
if (!fallbackWorld.metadata.ownership || typeof fallbackWorld.metadata.ownership !== "object" || fallbackWorld.metadata.ownership.ownerId !== userId) {
|
|
1984
|
+
fallbackWorld.metadata.ownership = { ownerId: userId };
|
|
1985
|
+
needsUpdate = true;
|
|
1986
|
+
}
|
|
1987
|
+
if (needsUpdate) await runtime.updateWorld(fallbackWorld);
|
|
1988
|
+
}
|
|
1989
|
+
} catch (retryErr) {
|
|
1990
|
+
logger.error(`[milady] Chat room setup failed after retry: ${formatError(retryErr)}`);
|
|
1991
|
+
throw retryErr;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
const rl = readline.createInterface({
|
|
1995
|
+
input: process.stdin,
|
|
1996
|
+
output: process.stdout
|
|
1997
|
+
});
|
|
1998
|
+
console.log(`\n💬 Chat with ${agentName} (type 'exit' to quit)\n`);
|
|
1999
|
+
const prompt = () => {
|
|
2000
|
+
rl.question("You: ", async (input) => {
|
|
2001
|
+
const text = input.trim();
|
|
2002
|
+
if (text.toLowerCase() === "exit" || text.toLowerCase() === "quit") {
|
|
2003
|
+
console.log("\nGoodbye!");
|
|
2004
|
+
rl.close();
|
|
2005
|
+
try {
|
|
2006
|
+
await runtime.stop();
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
logger.warn(`[milady] Error stopping runtime: ${formatError(err)}`);
|
|
2009
|
+
}
|
|
2010
|
+
process.exit(0);
|
|
2011
|
+
}
|
|
2012
|
+
if (!text) {
|
|
2013
|
+
prompt();
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
try {
|
|
2017
|
+
const message = createMessageMemory({
|
|
2018
|
+
id: crypto.randomUUID(),
|
|
2019
|
+
entityId: userId,
|
|
2020
|
+
roomId,
|
|
2021
|
+
content: {
|
|
2022
|
+
text,
|
|
2023
|
+
source: "client_chat",
|
|
2024
|
+
channelType: ChannelType.DM
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
process.stdout.write(`${agentName}: `);
|
|
2028
|
+
if (!runtime.messageService) {
|
|
2029
|
+
logger.error("[milady] runtime.messageService is not available — cannot process messages");
|
|
2030
|
+
console.log("[Error: message service unavailable]\n");
|
|
2031
|
+
prompt();
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
await runtime.messageService.handleMessage(runtime, message, async (content) => {
|
|
2035
|
+
if (content?.text) process.stdout.write(content.text);
|
|
2036
|
+
return [];
|
|
2037
|
+
});
|
|
2038
|
+
console.log("\n");
|
|
2039
|
+
} catch (err) {
|
|
2040
|
+
console.log(`\n[Error: ${formatError(err)}]\n`);
|
|
2041
|
+
logger.error(`[milady] Chat message handling failed: ${formatError(err)}`);
|
|
2042
|
+
}
|
|
2043
|
+
prompt();
|
|
2044
|
+
});
|
|
2045
|
+
};
|
|
2046
|
+
prompt();
|
|
2047
|
+
}
|
|
2048
|
+
if ((() => {
|
|
2049
|
+
const scriptArg = process.argv[1];
|
|
2050
|
+
if (!scriptArg) return false;
|
|
2051
|
+
const normalised = path.resolve(scriptArg);
|
|
2052
|
+
if (import.meta.url === pathToFileURL(normalised).href) return true;
|
|
2053
|
+
const base = path.basename(normalised);
|
|
2054
|
+
return base === "eliza.ts" || base === "eliza";
|
|
2055
|
+
})()) startEliza().catch((err) => {
|
|
2056
|
+
console.error("[milady] Fatal error:", err instanceof Error ? err.stack ?? err.message : err);
|
|
2057
|
+
process.exit(1);
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
//#endregion
|
|
2061
|
+
export { CUSTOM_PLUGINS_DIRNAME, bootElizaRuntime, resolvePackageEntry, scanDropInPlugins, startEliza };
|