milaidy 1.0.0
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/LICENSE +8 -0
- package/README.md +538 -0
- package/dist/argv-CfSowvEA.js +63 -0
- package/dist/config-B-mboG4v.js +4 -0
- package/dist/eliza-CPJjgw-e.js +1491 -0
- package/dist/eliza.js +2192 -0
- package/dist/entry.js +232 -0
- package/dist/index.js +209 -0
- package/dist/links-BFKlWqSe.js +15 -0
- package/dist/paths-D_yh1DEJ.js +69 -0
- package/dist/plugins-cli-B7kSre2c.js +134 -0
- package/dist/program-6KwWwKKh.js +510 -0
- package/dist/register.agents-CPVmSjMG.js +17 -0
- package/dist/register.browser-B2ooXxNx.js +15 -0
- package/dist/register.channels-CMYQ6K6Y.js +42 -0
- package/dist/register.cron-D91lY1_Y.js +9 -0
- package/dist/register.devices-rU5I5L_y.js +13 -0
- package/dist/register.gateway-82SLAvw3.js +22 -0
- package/dist/register.hooks-B_XTBEkt.js +9 -0
- package/dist/register.logs-BgEGcPd8.js +10 -0
- package/dist/register.models-BJt9eVgZ.js +26 -0
- package/dist/register.nodes-B5xY1s8a.js +9 -0
- package/dist/register.skills-SFQqYIhg.js +10 -0
- package/dist/register.subclis-uF_AsbWR.js +187 -0
- package/dist/run-main-XODklzS-.js +56 -0
- package/dist/theme-DBvtuGeq.js +36 -0
- package/dist/utils-C1AUpp_V.js +42 -0
- package/dist/version-Cpn3yr5D.js +26 -0
- package/dist/workspace-Co3Wul2D.js +206 -0
- package/dist/workspace-DCA6MNVK.js +350 -0
- package/docs/.i18n/README.md +31 -0
- package/docs/.i18n/glossary.zh-CN.json +210 -0
- package/docs/.i18n/zh-CN.tm.jsonl +1329 -0
- package/docs/CNAME +1 -0
- package/docs/automation/cron-jobs.md +468 -0
- package/docs/automation/cron-vs-heartbeat.md +254 -0
- package/docs/automation/gmail-pubsub.md +256 -0
- package/docs/automation/poll.md +69 -0
- package/docs/automation/webhook.md +163 -0
- package/docs/bedrock.md +176 -0
- package/docs/brave-search.md +41 -0
- package/docs/broadcast-groups.md +442 -0
- package/docs/cli/acp.md +170 -0
- package/docs/cli/agent.md +24 -0
- package/docs/cli/agents.md +75 -0
- package/docs/cli/approvals.md +50 -0
- package/docs/cli/browser.md +107 -0
- package/docs/cli/channels.md +79 -0
- package/docs/cli/config.md +50 -0
- package/docs/cli/configure.md +33 -0
- package/docs/cli/cron.md +42 -0
- package/docs/cli/dashboard.md +16 -0
- package/docs/cli/devices.md +67 -0
- package/docs/cli/directory.md +63 -0
- package/docs/cli/dns.md +23 -0
- package/docs/cli/docs.md +15 -0
- package/docs/cli/doctor.md +41 -0
- package/docs/cli/gateway.md +199 -0
- package/docs/cli/health.md +21 -0
- package/docs/cli/hooks.md +291 -0
- package/docs/cli/index.md +1029 -0
- package/docs/cli/logs.md +24 -0
- package/docs/cli/memory.md +45 -0
- package/docs/cli/message.md +239 -0
- package/docs/cli/models.md +79 -0
- package/docs/cli/node.md +112 -0
- package/docs/cli/nodes.md +73 -0
- package/docs/cli/onboard.md +29 -0
- package/docs/cli/pairing.md +21 -0
- package/docs/cli/plugins.md +62 -0
- package/docs/cli/reset.md +17 -0
- package/docs/cli/sandbox.md +152 -0
- package/docs/cli/security.md +26 -0
- package/docs/cli/sessions.md +16 -0
- package/docs/cli/setup.md +29 -0
- package/docs/cli/skills.md +26 -0
- package/docs/cli/status.md +26 -0
- package/docs/cli/system.md +60 -0
- package/docs/cli/tui.md +23 -0
- package/docs/cli/uninstall.md +17 -0
- package/docs/cli/update.md +98 -0
- package/docs/cli/voicecall.md +34 -0
- package/docs/cli/webhooks.md +25 -0
- package/docs/concepts/agent-loop.md +146 -0
- package/docs/concepts/agent-workspace.md +229 -0
- package/docs/concepts/agent.md +122 -0
- package/docs/concepts/architecture.md +129 -0
- package/docs/concepts/channel-routing.md +114 -0
- package/docs/concepts/compaction.md +61 -0
- package/docs/concepts/context.md +159 -0
- package/docs/concepts/features.md +53 -0
- package/docs/concepts/group-messages.md +84 -0
- package/docs/concepts/groups.md +373 -0
- package/docs/concepts/markdown-formatting.md +130 -0
- package/docs/concepts/memory.md +546 -0
- package/docs/concepts/messages.md +154 -0
- package/docs/concepts/model-failover.md +149 -0
- package/docs/concepts/model-providers.md +315 -0
- package/docs/concepts/models.md +208 -0
- package/docs/concepts/multi-agent.md +376 -0
- package/docs/concepts/oauth.md +145 -0
- package/docs/concepts/plugins.md +454 -0
- package/docs/concepts/presence.md +102 -0
- package/docs/concepts/queue.md +89 -0
- package/docs/concepts/retry.md +69 -0
- package/docs/concepts/secrets.md +300 -0
- package/docs/concepts/session-pruning.md +122 -0
- package/docs/concepts/session-tool.md +193 -0
- package/docs/concepts/session.md +188 -0
- package/docs/concepts/sessions.md +10 -0
- package/docs/concepts/skills.md +392 -0
- package/docs/concepts/streaming.md +135 -0
- package/docs/concepts/system-prompt.md +114 -0
- package/docs/concepts/timezone.md +91 -0
- package/docs/concepts/typebox.md +289 -0
- package/docs/concepts/typing-indicators.md +68 -0
- package/docs/concepts/usage-tracking.md +35 -0
- package/docs/custom.css +4 -0
- package/docs/date-time.md +128 -0
- package/docs/debugging.md +162 -0
- package/docs/docs.json +1599 -0
- package/docs/environment.md +81 -0
- package/docs/hooks.md +876 -0
- package/docs/index.md +179 -0
- package/docs/install/ansible.md +208 -0
- package/docs/install/bun.md +59 -0
- package/docs/install/development-channels.md +75 -0
- package/docs/install/docker.md +567 -0
- package/docs/install/index.md +185 -0
- package/docs/install/installer.md +123 -0
- package/docs/install/migrating.md +192 -0
- package/docs/install/nix.md +96 -0
- package/docs/install/node.md +78 -0
- package/docs/install/uninstall.md +128 -0
- package/docs/install/updating.md +228 -0
- package/docs/logging.md +350 -0
- package/docs/multi-agent-sandbox-tools.md +395 -0
- package/docs/network.md +54 -0
- package/docs/nodes/audio.md +114 -0
- package/docs/nodes/camera.md +156 -0
- package/docs/nodes/images.md +72 -0
- package/docs/nodes/index.md +341 -0
- package/docs/nodes/location-command.md +113 -0
- package/docs/nodes/media-understanding.md +379 -0
- package/docs/nodes/talk.md +90 -0
- package/docs/nodes/voicewake.md +65 -0
- package/docs/northflank.mdx +53 -0
- package/docs/perplexity.md +80 -0
- package/docs/platforms/android.md +129 -0
- package/docs/platforms/digitalocean.md +262 -0
- package/docs/platforms/exe-dev.md +125 -0
- package/docs/platforms/fly.md +486 -0
- package/docs/platforms/gcp.md +503 -0
- package/docs/platforms/hetzner.md +330 -0
- package/docs/platforms/index.md +53 -0
- package/docs/platforms/ios.md +106 -0
- package/docs/platforms/linux.md +94 -0
- package/docs/platforms/mac/bundled-gateway.md +73 -0
- package/docs/platforms/mac/canvas.md +125 -0
- package/docs/platforms/mac/child-process.md +69 -0
- package/docs/platforms/mac/dev-setup.md +102 -0
- package/docs/platforms/mac/health.md +34 -0
- package/docs/platforms/mac/icon.md +31 -0
- package/docs/platforms/mac/logging.md +57 -0
- package/docs/platforms/mac/menu-bar.md +81 -0
- package/docs/platforms/mac/peekaboo.md +65 -0
- package/docs/platforms/mac/permissions.md +44 -0
- package/docs/platforms/mac/release.md +85 -0
- package/docs/platforms/mac/remote.md +83 -0
- package/docs/platforms/mac/signing.md +47 -0
- package/docs/platforms/mac/skills.md +33 -0
- package/docs/platforms/mac/voice-overlay.md +60 -0
- package/docs/platforms/mac/voicewake.md +67 -0
- package/docs/platforms/mac/webchat.md +41 -0
- package/docs/platforms/mac/xpc.md +61 -0
- package/docs/platforms/macos-vm.md +281 -0
- package/docs/platforms/macos.md +203 -0
- package/docs/platforms/oracle.md +303 -0
- package/docs/platforms/raspberry-pi.md +358 -0
- package/docs/platforms/windows.md +159 -0
- package/docs/plugin.md +651 -0
- package/docs/plugins/agent-tools.md +99 -0
- package/docs/plugins/manifest.md +71 -0
- package/docs/plugins/voice-call.md +273 -0
- package/docs/plugins/zalouser.md +70 -0
- package/docs/providers/anthropic.md +152 -0
- package/docs/providers/claude-max-api-proxy.md +148 -0
- package/docs/providers/cloudflare-ai-gateway.md +71 -0
- package/docs/providers/deepgram.md +93 -0
- package/docs/providers/glm.md +33 -0
- package/docs/providers/index.md +63 -0
- package/docs/providers/minimax.md +208 -0
- package/docs/providers/models.md +51 -0
- package/docs/providers/moonshot.md +142 -0
- package/docs/providers/ollama.md +223 -0
- package/docs/providers/openai.md +62 -0
- package/docs/providers/opencode.md +36 -0
- package/docs/providers/openrouter.md +37 -0
- package/docs/providers/qwen.md +53 -0
- package/docs/providers/synthetic.md +99 -0
- package/docs/providers/venice.md +267 -0
- package/docs/providers/vercel-ai-gateway.md +50 -0
- package/docs/providers/xiaomi.md +64 -0
- package/docs/providers/zai.md +36 -0
- package/docs/railway.mdx +99 -0
- package/docs/reference/templates/AGENTS.md +9 -0
- package/docs/reference/templates/BOOTSTRAP.md +3 -0
- package/docs/reference/templates/HEARTBEAT.md +3 -0
- package/docs/reference/templates/IDENTITY.md +3 -0
- package/docs/reference/templates/TOOLS.md +3 -0
- package/docs/reference/templates/USER.md +3 -0
- package/docs/render.mdx +165 -0
- package/docs/start/docs-directory.md +63 -0
- package/docs/start/getting-started.md +212 -0
- package/docs/start/milaidy.md +247 -0
- package/docs/start/onboarding.md +258 -0
- package/docs/start/pairing.md +86 -0
- package/docs/start/quickstart.md +81 -0
- package/docs/start/setup.md +149 -0
- package/docs/start/showcase.md +416 -0
- package/docs/start/wizard.md +418 -0
- package/docs/testing.md +368 -0
- package/docs/token-use.md +112 -0
- package/docs/tools/agent-send.md +53 -0
- package/docs/tools/apply-patch.md +50 -0
- package/docs/tools/browser-linux-troubleshooting.md +139 -0
- package/docs/tools/browser-login.md +68 -0
- package/docs/tools/browser.md +576 -0
- package/docs/tools/chrome-extension.md +178 -0
- package/docs/tools/clawhub.md +257 -0
- package/docs/tools/creating-skills.md +54 -0
- package/docs/tools/elevated.md +57 -0
- package/docs/tools/exec-approvals.md +246 -0
- package/docs/tools/exec.md +179 -0
- package/docs/tools/firecrawl.md +61 -0
- package/docs/tools/index.md +508 -0
- package/docs/tools/llm-task.md +115 -0
- package/docs/tools/reactions.md +22 -0
- package/docs/tools/skills-config.md +76 -0
- package/docs/tools/skills.md +300 -0
- package/docs/tools/slash-commands.md +196 -0
- package/docs/tools/subagents.md +151 -0
- package/docs/tools/thinking.md +73 -0
- package/docs/tools/web.md +261 -0
- package/docs/tui.md +159 -0
- package/docs/vps.md +43 -0
- package/docs/web/control-ui.md +221 -0
- package/docs/web/dashboard.md +46 -0
- package/docs/web/index.md +116 -0
- package/docs/web/webchat.md +49 -0
- package/milaidy.mjs +14 -0
- package/package.json +271 -0
- package/skills/.cache/catalog.json +88519 -0
package/dist/eliza.js
ADDED
|
@@ -0,0 +1,2192 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import process$1 from "node:process";
|
|
4
|
+
import * as readline from "node:readline";
|
|
5
|
+
import { AgentRuntime, ChannelType, MemoryType, ModelType, buildAgentMainSessionKey, createCharacter, createMessageMemory, getSessionProviders, isSubagentSessionKey, logger, parseAgentSessionKey, resolveDefaultSessionStorePath, stringToUuid } from "@elizaos/core";
|
|
6
|
+
import fs, { existsSync } from "node:fs";
|
|
7
|
+
import path, { basename, extname, join, resolve } from "node:path";
|
|
8
|
+
import JSON5 from "json5";
|
|
9
|
+
import os, { homedir, platform } from "node:os";
|
|
10
|
+
import fs$1, { readFile, readdir, stat } from "node:fs/promises";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import chalk, { Chalk } from "chalk";
|
|
14
|
+
|
|
15
|
+
//#region src/version.ts
|
|
16
|
+
function readVersionFromPackageJson() {
|
|
17
|
+
try {
|
|
18
|
+
return createRequire(import.meta.url)("../package.json").version ?? null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function readVersionFromBuildInfo() {
|
|
24
|
+
try {
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
for (const candidate of ["../build-info.json", "./build-info.json"]) try {
|
|
27
|
+
const info = require(candidate);
|
|
28
|
+
if (info.version) return info.version;
|
|
29
|
+
} catch {}
|
|
30
|
+
return null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const VERSION = typeof __MILAIDY_VERSION__ === "string" && __MILAIDY_VERSION__ || process.env.MILAIDY_BUNDLED_VERSION || readVersionFromPackageJson() || readVersionFromBuildInfo() || "0.0.0";
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/config/plugin-auto-enable.ts
|
|
39
|
+
/**
|
|
40
|
+
* Channel to plugin mappings.
|
|
41
|
+
*/
|
|
42
|
+
const CHANNEL_PLUGINS = {
|
|
43
|
+
telegram: "@elizaos/plugin-telegram",
|
|
44
|
+
discord: "@elizaos/plugin-discord",
|
|
45
|
+
slack: "@elizaos/plugin-slack",
|
|
46
|
+
twitter: "@elizaos/plugin-twitter",
|
|
47
|
+
whatsapp: "@elizaos/plugin-whatsapp",
|
|
48
|
+
signal: "@elizaos/plugin-signal",
|
|
49
|
+
bluebubbles: "@elizaos/plugin-bluebubbles",
|
|
50
|
+
imessage: "@elizaos/plugin-imessage",
|
|
51
|
+
farcaster: "@elizaos/plugin-farcaster",
|
|
52
|
+
lens: "@elizaos/plugin-lens",
|
|
53
|
+
msteams: "@elizaos/plugin-msteams",
|
|
54
|
+
mattermost: "@elizaos/plugin-mattermost",
|
|
55
|
+
googlechat: "@elizaos/plugin-google-chat",
|
|
56
|
+
feishu: "@elizaos/plugin-feishu",
|
|
57
|
+
matrix: "@elizaos/plugin-matrix",
|
|
58
|
+
nostr: "@elizaos/plugin-nostr"
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Provider to plugin mappings.
|
|
62
|
+
*/
|
|
63
|
+
const PROVIDER_PLUGINS = {
|
|
64
|
+
"google-antigravity": "@elizaos/plugin-google-antigravity",
|
|
65
|
+
"google-gemini": "@elizaos/plugin-google-gemini",
|
|
66
|
+
openai: "@elizaos/plugin-openai",
|
|
67
|
+
anthropic: "@elizaos/plugin-anthropic",
|
|
68
|
+
qwen: "@elizaos/plugin-qwen",
|
|
69
|
+
minimax: "@elizaos/plugin-minimax",
|
|
70
|
+
groq: "@elizaos/plugin-groq",
|
|
71
|
+
xai: "@elizaos/plugin-xai",
|
|
72
|
+
openrouter: "@elizaos/plugin-openrouter",
|
|
73
|
+
ollama: "@elizaos/plugin-ollama",
|
|
74
|
+
deepseek: "@elizaos/plugin-deepseek",
|
|
75
|
+
together: "@elizaos/plugin-together",
|
|
76
|
+
mistral: "@elizaos/plugin-mistral",
|
|
77
|
+
cohere: "@elizaos/plugin-cohere",
|
|
78
|
+
perplexity: "@elizaos/plugin-perplexity"
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Auth provider secret key to plugin mappings.
|
|
82
|
+
* Used to auto-enable plugins when API keys are present in environment or secrets.
|
|
83
|
+
*/
|
|
84
|
+
const AUTH_PROVIDER_PLUGINS = {
|
|
85
|
+
ANTHROPIC_API_KEY: "@elizaos/plugin-anthropic",
|
|
86
|
+
CLAUDE_API_KEY: "@elizaos/plugin-anthropic",
|
|
87
|
+
OPENAI_API_KEY: "@elizaos/plugin-openai",
|
|
88
|
+
GOOGLE_API_KEY: "@elizaos/plugin-google-gemini",
|
|
89
|
+
GOOGLE_GENERATIVE_AI_API_KEY: "@elizaos/plugin-google-gemini",
|
|
90
|
+
GOOGLE_CLOUD_API_KEY: "@elizaos/plugin-google-antigravity",
|
|
91
|
+
GROQ_API_KEY: "@elizaos/plugin-groq",
|
|
92
|
+
XAI_API_KEY: "@elizaos/plugin-xai",
|
|
93
|
+
GROK_API_KEY: "@elizaos/plugin-xai",
|
|
94
|
+
OPENROUTER_API_KEY: "@elizaos/plugin-openrouter",
|
|
95
|
+
OLLAMA_BASE_URL: "@elizaos/plugin-ollama",
|
|
96
|
+
DEEPSEEK_API_KEY: "@elizaos/plugin-deepseek",
|
|
97
|
+
TOGETHER_API_KEY: "@elizaos/plugin-together",
|
|
98
|
+
MISTRAL_API_KEY: "@elizaos/plugin-mistral",
|
|
99
|
+
COHERE_API_KEY: "@elizaos/plugin-cohere",
|
|
100
|
+
PERPLEXITY_API_KEY: "@elizaos/plugin-perplexity"
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Feature to plugin mappings for optional features.
|
|
104
|
+
*/
|
|
105
|
+
const FEATURE_PLUGINS = {
|
|
106
|
+
browser: "@elizaos/plugin-browser",
|
|
107
|
+
cron: "@elizaos/plugin-cron",
|
|
108
|
+
shell: "@elizaos/plugin-shell",
|
|
109
|
+
imageGen: "@elizaos/plugin-image-generation",
|
|
110
|
+
tts: "@elizaos/plugin-tts",
|
|
111
|
+
stt: "@elizaos/plugin-stt",
|
|
112
|
+
agentSkills: "@elizaos/plugin-agent-skills",
|
|
113
|
+
directives: "@elizaos/plugin-directives",
|
|
114
|
+
commands: "@elizaos/plugin-commands",
|
|
115
|
+
diagnosticsOtel: "@elizaos/plugin-diagnostics-otel",
|
|
116
|
+
webhooks: "@elizaos/plugin-webhooks",
|
|
117
|
+
gmailWatch: "@elizaos/plugin-gmail-watch",
|
|
118
|
+
personality: "@elizaos/plugin-personality",
|
|
119
|
+
experience: "@elizaos/plugin-experience",
|
|
120
|
+
form: "@elizaos/plugin-form"
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Check if a channel is configured with credentials.
|
|
124
|
+
*/
|
|
125
|
+
function isChannelConfigured(channelName, channelConfig) {
|
|
126
|
+
if (!channelConfig || typeof channelConfig !== "object") return false;
|
|
127
|
+
const config = channelConfig;
|
|
128
|
+
if (config.botToken || config.token || config.apiKey) return true;
|
|
129
|
+
switch (channelName) {
|
|
130
|
+
case "bluebubbles": return Boolean(config.serverUrl && config.password);
|
|
131
|
+
case "imessage": return Boolean(config.cliPath);
|
|
132
|
+
case "whatsapp": return Boolean(config.authState || config.sessionPath);
|
|
133
|
+
default: return Object.keys(config).length > 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Apply plugin auto-enable based on configuration.
|
|
138
|
+
*
|
|
139
|
+
* This function analyzes the configuration and automatically enables
|
|
140
|
+
* plugins that are required based on configured channels and providers.
|
|
141
|
+
*
|
|
142
|
+
* @param params - Parameters
|
|
143
|
+
* @returns Result with updated config and changes
|
|
144
|
+
*/
|
|
145
|
+
function applyPluginAutoEnable(params) {
|
|
146
|
+
const { config, env } = params;
|
|
147
|
+
const changes = [];
|
|
148
|
+
const updatedConfig = JSON.parse(JSON.stringify(config));
|
|
149
|
+
if (updatedConfig.plugins && typeof updatedConfig.plugins === "object" && updatedConfig.plugins.enabled === false) return {
|
|
150
|
+
config: updatedConfig,
|
|
151
|
+
changes
|
|
152
|
+
};
|
|
153
|
+
updatedConfig.plugins = updatedConfig.plugins ?? {};
|
|
154
|
+
const pluginsConfig = updatedConfig.plugins;
|
|
155
|
+
pluginsConfig.allow = pluginsConfig.allow ?? [];
|
|
156
|
+
pluginsConfig.entries = pluginsConfig.entries ?? {};
|
|
157
|
+
if (updatedConfig.channels && typeof updatedConfig.channels === "object") for (const [channelName, channelConfig] of Object.entries(updatedConfig.channels)) {
|
|
158
|
+
const pluginName = CHANNEL_PLUGINS[channelName];
|
|
159
|
+
if (!pluginName) continue;
|
|
160
|
+
if (!isChannelConfigured(channelName, channelConfig)) continue;
|
|
161
|
+
const entryConfig = pluginsConfig.entries[channelName];
|
|
162
|
+
if (entryConfig && entryConfig.enabled === false) continue;
|
|
163
|
+
if (!pluginsConfig.allow.includes(pluginName) && !pluginsConfig.allow.includes(channelName)) {
|
|
164
|
+
pluginsConfig.allow.push(channelName);
|
|
165
|
+
changes.push(`Auto-enabled plugin: ${pluginName} (channel: ${channelName})`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (updatedConfig.auth && typeof updatedConfig.auth === "object" && updatedConfig.auth.profiles) {
|
|
169
|
+
const profiles = updatedConfig.auth.profiles;
|
|
170
|
+
for (const [profileKey, profile] of Object.entries(profiles)) {
|
|
171
|
+
const provider = profile.provider;
|
|
172
|
+
if (!provider) continue;
|
|
173
|
+
const pluginName = PROVIDER_PLUGINS[provider];
|
|
174
|
+
if (!pluginName) continue;
|
|
175
|
+
if (!pluginsConfig.allow.includes(pluginName) && !pluginsConfig.allow.includes(provider)) {
|
|
176
|
+
pluginsConfig.allow.push(provider);
|
|
177
|
+
changes.push(`Auto-enabled plugin: ${pluginName} (auth profile: ${profileKey})`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const [envKey, pluginName] of Object.entries(AUTH_PROVIDER_PLUGINS)) {
|
|
182
|
+
const envValue = env[envKey];
|
|
183
|
+
if (!envValue || typeof envValue !== "string" || envValue.trim() === "") continue;
|
|
184
|
+
const pluginId = pluginName.replace("@elizaos/plugin-", "");
|
|
185
|
+
const entryConfig = pluginsConfig.entries[pluginId];
|
|
186
|
+
if (entryConfig && entryConfig.enabled === false) continue;
|
|
187
|
+
if (!pluginsConfig.allow.includes(pluginName) && !pluginsConfig.allow.includes(pluginId)) {
|
|
188
|
+
pluginsConfig.allow.push(pluginId);
|
|
189
|
+
changes.push(`Auto-enabled plugin: ${pluginName} (env: ${envKey})`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (updatedConfig.features && typeof updatedConfig.features === "object") {
|
|
193
|
+
const features = updatedConfig.features;
|
|
194
|
+
for (const [featureName, featureConfig] of Object.entries(features)) {
|
|
195
|
+
const pluginName = FEATURE_PLUGINS[featureName];
|
|
196
|
+
if (!pluginName) continue;
|
|
197
|
+
if (!(featureConfig === true || featureConfig && typeof featureConfig === "object" && featureConfig.enabled !== false)) continue;
|
|
198
|
+
const pluginId = pluginName.replace("@elizaos/plugin-", "");
|
|
199
|
+
const entryConfig = pluginsConfig.entries[pluginId];
|
|
200
|
+
if (entryConfig && entryConfig.enabled === false) continue;
|
|
201
|
+
if (!pluginsConfig.allow.includes(pluginName) && !pluginsConfig.allow.includes(pluginId)) {
|
|
202
|
+
pluginsConfig.allow.push(pluginId);
|
|
203
|
+
changes.push(`Auto-enabled plugin: ${pluginName} (feature: ${featureName})`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const hooksConfig = updatedConfig.hooks;
|
|
208
|
+
if (hooksConfig && hooksConfig.enabled !== false && hooksConfig.token) {
|
|
209
|
+
const webhooksPlugin = FEATURE_PLUGINS.webhooks;
|
|
210
|
+
if (webhooksPlugin) {
|
|
211
|
+
const pluginId = webhooksPlugin.replace("@elizaos/plugin-", "");
|
|
212
|
+
if (!pluginsConfig.allow.includes(webhooksPlugin) && !pluginsConfig.allow.includes(pluginId)) {
|
|
213
|
+
pluginsConfig.allow.push(pluginId);
|
|
214
|
+
changes.push(`Auto-enabled plugin: ${webhooksPlugin} (hooks.enabled + hooks.token)`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (hooksConfig) {
|
|
219
|
+
const gmailConfig = hooksConfig.gmail ?? {};
|
|
220
|
+
if (typeof gmailConfig.account === "string" && gmailConfig.account.trim()) {
|
|
221
|
+
const gmailPlugin = FEATURE_PLUGINS.gmailWatch;
|
|
222
|
+
if (gmailPlugin) {
|
|
223
|
+
const pluginId = gmailPlugin.replace("@elizaos/plugin-", "");
|
|
224
|
+
if (!pluginsConfig.allow.includes(gmailPlugin) && !pluginsConfig.allow.includes(pluginId)) {
|
|
225
|
+
pluginsConfig.allow.push(pluginId);
|
|
226
|
+
changes.push(`Auto-enabled plugin: ${gmailPlugin} (hooks.gmail.account)`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
config: updatedConfig,
|
|
233
|
+
changes
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/config/paths.ts
|
|
239
|
+
/**
|
|
240
|
+
* Nix mode detection: When MILAIDY_NIX_MODE=1, the gateway is running under Nix.
|
|
241
|
+
* In this mode:
|
|
242
|
+
* - No auto-install flows should be attempted
|
|
243
|
+
* - Missing dependencies should produce actionable Nix-specific error messages
|
|
244
|
+
* - Config is managed externally (read-only from Nix perspective)
|
|
245
|
+
*/
|
|
246
|
+
function resolveIsNixMode(env = process.env) {
|
|
247
|
+
return env.MILAIDY_NIX_MODE === "1";
|
|
248
|
+
}
|
|
249
|
+
const isNixMode = resolveIsNixMode();
|
|
250
|
+
const STATE_DIRNAME = ".milaidy";
|
|
251
|
+
const CONFIG_FILENAME = "milaidy.json";
|
|
252
|
+
function stateDir(homedir = os.homedir) {
|
|
253
|
+
return path.join(homedir(), STATE_DIRNAME);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* State directory for mutable data (sessions, logs, caches).
|
|
257
|
+
* Can be overridden via MILAIDY_STATE_DIR.
|
|
258
|
+
* Default: ~/.milaidy
|
|
259
|
+
*/
|
|
260
|
+
function resolveStateDir(env = process.env, homedir = os.homedir) {
|
|
261
|
+
const override = env.MILAIDY_STATE_DIR?.trim();
|
|
262
|
+
if (override) return resolveUserPath$1(override);
|
|
263
|
+
return stateDir(homedir);
|
|
264
|
+
}
|
|
265
|
+
function resolveUserPath$1(input) {
|
|
266
|
+
const trimmed = input.trim();
|
|
267
|
+
if (!trimmed) return trimmed;
|
|
268
|
+
if (trimmed.startsWith("~")) {
|
|
269
|
+
const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir());
|
|
270
|
+
return path.resolve(expanded);
|
|
271
|
+
}
|
|
272
|
+
return path.resolve(trimmed);
|
|
273
|
+
}
|
|
274
|
+
const STATE_DIR = resolveStateDir();
|
|
275
|
+
/**
|
|
276
|
+
* Config file path (JSON5).
|
|
277
|
+
* Can be overridden via MILAIDY_CONFIG_PATH.
|
|
278
|
+
* Default: ~/.milaidy/milaidy.json (or $MILAIDY_STATE_DIR/milaidy.json)
|
|
279
|
+
*/
|
|
280
|
+
function resolveCanonicalConfigPath(env = process.env, stateDirPath = resolveStateDir(env, os.homedir)) {
|
|
281
|
+
const override = env.MILAIDY_CONFIG_PATH?.trim();
|
|
282
|
+
if (override) return resolveUserPath$1(override);
|
|
283
|
+
return path.join(stateDirPath, CONFIG_FILENAME);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Resolve the active config path by preferring existing config candidates
|
|
287
|
+
* before falling back to the canonical path.
|
|
288
|
+
*/
|
|
289
|
+
function resolveConfigPathCandidate(env = process.env, homedir = os.homedir) {
|
|
290
|
+
return resolveCanonicalConfigPath(env, resolveStateDir(env, homedir));
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Active config path (prefers existing config files).
|
|
294
|
+
*/
|
|
295
|
+
function resolveConfigPath$1(env = process.env, stateDirPath = resolveStateDir(env, os.homedir), _homedir = os.homedir) {
|
|
296
|
+
const override = env.MILAIDY_CONFIG_PATH?.trim();
|
|
297
|
+
if (override) return resolveUserPath$1(override);
|
|
298
|
+
return path.join(stateDirPath, CONFIG_FILENAME);
|
|
299
|
+
}
|
|
300
|
+
const CONFIG_PATH = resolveConfigPathCandidate();
|
|
301
|
+
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/config/includes.ts
|
|
304
|
+
/**
|
|
305
|
+
* Config includes: $include directive for modular configs
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```json5
|
|
309
|
+
* {
|
|
310
|
+
* "$include": "./base.json5", // single file
|
|
311
|
+
* "$include": ["./a.json5", "./b.json5"] // merge multiple
|
|
312
|
+
* }
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
const INCLUDE_KEY = "$include";
|
|
316
|
+
const MAX_INCLUDE_DEPTH = 10;
|
|
317
|
+
var ConfigIncludeError = class extends Error {
|
|
318
|
+
constructor(message, includePath, cause) {
|
|
319
|
+
super(message);
|
|
320
|
+
this.includePath = includePath;
|
|
321
|
+
this.cause = cause;
|
|
322
|
+
this.name = "ConfigIncludeError";
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
var CircularIncludeError = class extends ConfigIncludeError {
|
|
326
|
+
constructor(chain) {
|
|
327
|
+
super(`Circular include detected: ${chain.join(" -> ")}`, chain[chain.length - 1]);
|
|
328
|
+
this.chain = chain;
|
|
329
|
+
this.name = "CircularIncludeError";
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
function isPlainObject(value) {
|
|
333
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
|
|
334
|
+
}
|
|
335
|
+
/** Deep merge: arrays concatenate, objects merge recursively, primitives: source wins */
|
|
336
|
+
function deepMerge(target, source) {
|
|
337
|
+
if (Array.isArray(target) && Array.isArray(source)) return [...target, ...source];
|
|
338
|
+
if (isPlainObject(target) && isPlainObject(source)) {
|
|
339
|
+
const result = { ...target };
|
|
340
|
+
for (const key of Object.keys(source)) result[key] = key in result ? deepMerge(result[key], source[key]) : source[key];
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
return source;
|
|
344
|
+
}
|
|
345
|
+
var IncludeProcessor = class IncludeProcessor {
|
|
346
|
+
constructor(basePath, resolver) {
|
|
347
|
+
this.basePath = basePath;
|
|
348
|
+
this.resolver = resolver;
|
|
349
|
+
this.visited = /* @__PURE__ */ new Set();
|
|
350
|
+
this.depth = 0;
|
|
351
|
+
this.visited.add(path.normalize(basePath));
|
|
352
|
+
}
|
|
353
|
+
process(obj) {
|
|
354
|
+
if (Array.isArray(obj)) return obj.map((item) => this.process(item));
|
|
355
|
+
if (!isPlainObject(obj)) return obj;
|
|
356
|
+
if (!(INCLUDE_KEY in obj)) return this.processObject(obj);
|
|
357
|
+
return this.processInclude(obj);
|
|
358
|
+
}
|
|
359
|
+
processObject(obj) {
|
|
360
|
+
const result = {};
|
|
361
|
+
for (const [key, value] of Object.entries(obj)) result[key] = this.process(value);
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
processInclude(obj) {
|
|
365
|
+
const includeValue = obj[INCLUDE_KEY];
|
|
366
|
+
const otherKeys = Object.keys(obj).filter((k) => k !== INCLUDE_KEY);
|
|
367
|
+
const included = this.resolveInclude(includeValue);
|
|
368
|
+
if (otherKeys.length === 0) return included;
|
|
369
|
+
if (!isPlainObject(included)) throw new ConfigIncludeError("Sibling keys require included content to be an object", typeof includeValue === "string" ? includeValue : INCLUDE_KEY);
|
|
370
|
+
const rest = {};
|
|
371
|
+
for (const key of otherKeys) rest[key] = this.process(obj[key]);
|
|
372
|
+
return deepMerge(included, rest);
|
|
373
|
+
}
|
|
374
|
+
resolveInclude(value) {
|
|
375
|
+
if (typeof value === "string") return this.loadFile(value);
|
|
376
|
+
if (Array.isArray(value)) return value.reduce((merged, item) => {
|
|
377
|
+
if (typeof item !== "string") throw new ConfigIncludeError(`Invalid $include array item: expected string, got ${typeof item}`, String(item));
|
|
378
|
+
return deepMerge(merged, this.loadFile(item));
|
|
379
|
+
}, {});
|
|
380
|
+
throw new ConfigIncludeError(`Invalid $include value: expected string or array of strings, got ${typeof value}`, String(value));
|
|
381
|
+
}
|
|
382
|
+
loadFile(includePath) {
|
|
383
|
+
const resolvedPath = this.resolvePath(includePath);
|
|
384
|
+
this.checkCircular(resolvedPath);
|
|
385
|
+
this.checkDepth(includePath);
|
|
386
|
+
const raw = this.readFile(includePath, resolvedPath);
|
|
387
|
+
const parsed = this.parseFile(includePath, resolvedPath, raw);
|
|
388
|
+
return this.processNested(resolvedPath, parsed);
|
|
389
|
+
}
|
|
390
|
+
resolvePath(includePath) {
|
|
391
|
+
const resolved = path.isAbsolute(includePath) ? includePath : path.resolve(path.dirname(this.basePath), includePath);
|
|
392
|
+
return path.normalize(resolved);
|
|
393
|
+
}
|
|
394
|
+
checkCircular(resolvedPath) {
|
|
395
|
+
if (this.visited.has(resolvedPath)) throw new CircularIncludeError([...this.visited, resolvedPath]);
|
|
396
|
+
}
|
|
397
|
+
checkDepth(includePath) {
|
|
398
|
+
if (this.depth >= MAX_INCLUDE_DEPTH) throw new ConfigIncludeError(`Maximum include depth (${MAX_INCLUDE_DEPTH}) exceeded at: ${includePath}`, includePath);
|
|
399
|
+
}
|
|
400
|
+
readFile(includePath, resolvedPath) {
|
|
401
|
+
try {
|
|
402
|
+
return this.resolver.readFile(resolvedPath);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
throw new ConfigIncludeError(`Failed to read include file: ${includePath} (resolved: ${resolvedPath})`, includePath, err instanceof Error ? err : void 0);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
parseFile(includePath, resolvedPath, raw) {
|
|
408
|
+
try {
|
|
409
|
+
return this.resolver.parseJson(raw);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
throw new ConfigIncludeError(`Failed to parse include file: ${includePath} (resolved: ${resolvedPath})`, includePath, err instanceof Error ? err : void 0);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
processNested(resolvedPath, parsed) {
|
|
415
|
+
const nested = new IncludeProcessor(resolvedPath, this.resolver);
|
|
416
|
+
nested.visited = new Set([...this.visited, resolvedPath]);
|
|
417
|
+
nested.depth = this.depth + 1;
|
|
418
|
+
return nested.process(parsed);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
const defaultResolver = {
|
|
422
|
+
readFile: (p) => fs.readFileSync(p, "utf-8"),
|
|
423
|
+
parseJson: (raw) => JSON5.parse(raw)
|
|
424
|
+
};
|
|
425
|
+
/**
|
|
426
|
+
* Resolves all $include directives in a parsed config object.
|
|
427
|
+
*/
|
|
428
|
+
function resolveConfigIncludes(obj, configPath, resolver = defaultResolver) {
|
|
429
|
+
return new IncludeProcessor(configPath, resolver).process(obj);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/config/env-vars.ts
|
|
434
|
+
function collectConfigEnvVars(cfg) {
|
|
435
|
+
const envConfig = cfg?.env;
|
|
436
|
+
if (!envConfig) return {};
|
|
437
|
+
const entries = {};
|
|
438
|
+
if (envConfig.vars) for (const [key, value] of Object.entries(envConfig.vars)) {
|
|
439
|
+
if (!value) continue;
|
|
440
|
+
entries[key] = value;
|
|
441
|
+
}
|
|
442
|
+
for (const [key, value] of Object.entries(envConfig)) {
|
|
443
|
+
if (key === "shellEnv" || key === "vars") continue;
|
|
444
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
445
|
+
entries[key] = value;
|
|
446
|
+
}
|
|
447
|
+
return entries;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/config/config.ts
|
|
452
|
+
/**
|
|
453
|
+
* Config loading and types.
|
|
454
|
+
*
|
|
455
|
+
* Reads ~/.milaidy/milaidy.json (or $MILAIDY_CONFIG_PATH), resolves $include
|
|
456
|
+
* directives, and applies inline env vars.
|
|
457
|
+
*
|
|
458
|
+
* @module config/config
|
|
459
|
+
*/
|
|
460
|
+
/**
|
|
461
|
+
* Load the Milaidy config from disk.
|
|
462
|
+
*
|
|
463
|
+
* 1. Resolves the config path (~/.milaidy/milaidy.json or MILAIDY_CONFIG_PATH).
|
|
464
|
+
* 2. Parses the file as JSON5.
|
|
465
|
+
* 3. Resolves any $include directives.
|
|
466
|
+
* 4. Applies inline env vars (config.env.vars) to process.env.
|
|
467
|
+
*
|
|
468
|
+
* Returns an empty MilaidyConfig when the file is missing.
|
|
469
|
+
*/
|
|
470
|
+
async function loadMilaidyConfig() {
|
|
471
|
+
const configPath = resolveConfigPath$1();
|
|
472
|
+
let raw;
|
|
473
|
+
try {
|
|
474
|
+
raw = fs.readFileSync(configPath, "utf-8");
|
|
475
|
+
} catch {
|
|
476
|
+
return {};
|
|
477
|
+
}
|
|
478
|
+
const resolved = resolveConfigIncludes(JSON5.parse(raw), configPath);
|
|
479
|
+
const envVars = collectConfigEnvVars(resolved);
|
|
480
|
+
for (const [key, value] of Object.entries(envVars)) if (!process.env[key]) process.env[key] = value;
|
|
481
|
+
return resolved;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Save the Milaidy config to disk.
|
|
485
|
+
*
|
|
486
|
+
* Writes the config as pretty-printed JSON to the resolved config path.
|
|
487
|
+
* Creates the parent directory if it doesn't exist.
|
|
488
|
+
*/
|
|
489
|
+
function saveMilaidyConfig(config) {
|
|
490
|
+
const configPath = resolveConfigPath$1();
|
|
491
|
+
const dir = path.dirname(configPath);
|
|
492
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, {
|
|
493
|
+
recursive: true,
|
|
494
|
+
mode: 448
|
|
495
|
+
});
|
|
496
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/hooks/registry.ts
|
|
501
|
+
/**
|
|
502
|
+
* Hook Registry ā event registration and dispatch.
|
|
503
|
+
*
|
|
504
|
+
* Maintains a registry of hook handlers keyed by event strings.
|
|
505
|
+
* Handlers are dispatched sequentially; errors are isolated.
|
|
506
|
+
*
|
|
507
|
+
* @module hooks/registry
|
|
508
|
+
*/
|
|
509
|
+
/** Internal registry: event key -> handler list. */
|
|
510
|
+
const registry = /* @__PURE__ */ new Map();
|
|
511
|
+
/**
|
|
512
|
+
* Register a handler for an event key.
|
|
513
|
+
*
|
|
514
|
+
* Event keys can be:
|
|
515
|
+
* - General: "command" (matches all command events)
|
|
516
|
+
* - Specific: "command:new" (matches only /new)
|
|
517
|
+
*/
|
|
518
|
+
function registerHook(eventKey, handler) {
|
|
519
|
+
const handlers = registry.get(eventKey) ?? [];
|
|
520
|
+
handlers.push(handler);
|
|
521
|
+
registry.set(eventKey, handlers);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Clear all registered hooks (useful for testing).
|
|
525
|
+
*/
|
|
526
|
+
function clearHooks() {
|
|
527
|
+
registry.clear();
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Trigger a hook event. Dispatches to all matching handlers.
|
|
531
|
+
*
|
|
532
|
+
* Matching order:
|
|
533
|
+
* 1. Specific key: "command:new"
|
|
534
|
+
* 2. General key: "command"
|
|
535
|
+
*
|
|
536
|
+
* Handlers run sequentially. Errors are caught and logged
|
|
537
|
+
* but don't prevent other handlers from running.
|
|
538
|
+
*/
|
|
539
|
+
async function triggerHook(event) {
|
|
540
|
+
const specificKey = `${event.type}:${event.action}`;
|
|
541
|
+
const generalKey = event.type;
|
|
542
|
+
const handlers = [];
|
|
543
|
+
const specificHandlers = registry.get(specificKey);
|
|
544
|
+
if (specificHandlers) for (const handler of specificHandlers) handlers.push({
|
|
545
|
+
key: specificKey,
|
|
546
|
+
handler
|
|
547
|
+
});
|
|
548
|
+
const generalHandlers = registry.get(generalKey);
|
|
549
|
+
if (generalHandlers) for (const handler of generalHandlers) handlers.push({
|
|
550
|
+
key: generalKey,
|
|
551
|
+
handler
|
|
552
|
+
});
|
|
553
|
+
if (handlers.length === 0) return;
|
|
554
|
+
for (const { key, handler } of handlers) try {
|
|
555
|
+
await handler(event);
|
|
556
|
+
} catch (err) {
|
|
557
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
558
|
+
logger.error(`[hooks] Handler error for "${key}": ${msg}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Create a hook event with sensible defaults.
|
|
563
|
+
*/
|
|
564
|
+
function createHookEvent(type, action, sessionKey, context = {}) {
|
|
565
|
+
return {
|
|
566
|
+
type,
|
|
567
|
+
action,
|
|
568
|
+
sessionKey,
|
|
569
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
570
|
+
messages: [],
|
|
571
|
+
context
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/hooks/discovery.ts
|
|
577
|
+
/**
|
|
578
|
+
* Hook Discovery ā scan directories for hooks.
|
|
579
|
+
*
|
|
580
|
+
* Discovers hooks from three locations (in order of precedence):
|
|
581
|
+
* 1. Workspace hooks: <workspace>/hooks/ (highest)
|
|
582
|
+
* 2. Managed hooks: ~/.milaidy/hooks/
|
|
583
|
+
* 3. Bundled hooks: <milaidy>/dist/hooks/bundled/ (lowest)
|
|
584
|
+
*
|
|
585
|
+
* Each hook is a directory containing HOOK.md + handler.ts/js.
|
|
586
|
+
*
|
|
587
|
+
* @module hooks/discovery
|
|
588
|
+
*/
|
|
589
|
+
const HOOK_MD = "HOOK.md";
|
|
590
|
+
const HANDLER_NAMES = [
|
|
591
|
+
"handler.ts",
|
|
592
|
+
"handler.js",
|
|
593
|
+
"index.ts",
|
|
594
|
+
"index.js"
|
|
595
|
+
];
|
|
596
|
+
/**
|
|
597
|
+
* Parse YAML-like frontmatter from HOOK.md content.
|
|
598
|
+
* Supports name, description, homepage, and metadata (JSON).
|
|
599
|
+
*/
|
|
600
|
+
function parseFrontmatter(content) {
|
|
601
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
602
|
+
if (!fmMatch) return null;
|
|
603
|
+
const fmBlock = fmMatch[1];
|
|
604
|
+
const result = {
|
|
605
|
+
name: "",
|
|
606
|
+
description: ""
|
|
607
|
+
};
|
|
608
|
+
for (const line of fmBlock.split("\n")) {
|
|
609
|
+
const kvMatch = line.match(/^(\w+):\s*(.+)/);
|
|
610
|
+
if (!kvMatch) continue;
|
|
611
|
+
const [, key, rawValue] = kvMatch;
|
|
612
|
+
const value = rawValue.replace(/^["']|["']$/g, "").trim();
|
|
613
|
+
switch (key) {
|
|
614
|
+
case "name":
|
|
615
|
+
result.name = value;
|
|
616
|
+
break;
|
|
617
|
+
case "description":
|
|
618
|
+
result.description = value;
|
|
619
|
+
break;
|
|
620
|
+
case "homepage":
|
|
621
|
+
result.homepage = value;
|
|
622
|
+
break;
|
|
623
|
+
case "metadata":
|
|
624
|
+
try {
|
|
625
|
+
const metaStart = fmBlock.indexOf("metadata:");
|
|
626
|
+
if (metaStart !== -1) {
|
|
627
|
+
const jsonMatch = fmBlock.slice(metaStart + 9).trim().match(/\{[\s\S]*\}/);
|
|
628
|
+
if (jsonMatch) result.metadata = JSON.parse(jsonMatch[0]);
|
|
629
|
+
}
|
|
630
|
+
} catch {
|
|
631
|
+
try {
|
|
632
|
+
result.metadata = JSON.parse(value);
|
|
633
|
+
} catch {
|
|
634
|
+
logger.warn(`[hooks] Failed to parse metadata in HOOK.md`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return result.name ? result : null;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Extract MilaidyHookMetadata from parsed frontmatter.
|
|
644
|
+
*/
|
|
645
|
+
function extractMetadata(frontmatter) {
|
|
646
|
+
const milaidy = frontmatter.metadata?.milaidy;
|
|
647
|
+
if (!milaidy) return void 0;
|
|
648
|
+
return {
|
|
649
|
+
always: milaidy.always,
|
|
650
|
+
hookKey: milaidy.hookKey,
|
|
651
|
+
emoji: milaidy.emoji,
|
|
652
|
+
homepage: milaidy.homepage ?? frontmatter.homepage,
|
|
653
|
+
events: Array.isArray(milaidy.events) ? milaidy.events : [],
|
|
654
|
+
export: milaidy.export,
|
|
655
|
+
os: milaidy.os,
|
|
656
|
+
requires: milaidy.requires,
|
|
657
|
+
install: milaidy.install
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Check if a path exists and is a directory.
|
|
662
|
+
*/
|
|
663
|
+
async function isDirectory(path) {
|
|
664
|
+
try {
|
|
665
|
+
return (await stat(path)).isDirectory();
|
|
666
|
+
} catch {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Check if a file exists.
|
|
672
|
+
*/
|
|
673
|
+
async function fileExists(path) {
|
|
674
|
+
try {
|
|
675
|
+
return (await stat(path)).isFile();
|
|
676
|
+
} catch {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Find the handler file in a hook directory.
|
|
682
|
+
*/
|
|
683
|
+
async function findHandlerPath(dir) {
|
|
684
|
+
for (const name of HANDLER_NAMES) {
|
|
685
|
+
const p = join(dir, name);
|
|
686
|
+
if (await fileExists(p)) return p;
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Load a single hook from a directory.
|
|
692
|
+
*/
|
|
693
|
+
async function loadHookFromDir(dir, source, pluginId) {
|
|
694
|
+
const hookMdPath = join(dir, HOOK_MD);
|
|
695
|
+
if (!await fileExists(hookMdPath)) return null;
|
|
696
|
+
const handlerPath = await findHandlerPath(dir);
|
|
697
|
+
if (!handlerPath) {
|
|
698
|
+
logger.warn(`[hooks] Hook at ${dir} has HOOK.md but no handler`);
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const frontmatter = parseFrontmatter(await readFile(hookMdPath, "utf-8"));
|
|
703
|
+
if (!frontmatter) {
|
|
704
|
+
logger.warn(`[hooks] Invalid frontmatter in ${hookMdPath}`);
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
const metadata = extractMetadata(frontmatter);
|
|
708
|
+
return {
|
|
709
|
+
hook: {
|
|
710
|
+
name: frontmatter.name,
|
|
711
|
+
description: frontmatter.description,
|
|
712
|
+
source,
|
|
713
|
+
pluginId,
|
|
714
|
+
filePath: hookMdPath,
|
|
715
|
+
baseDir: dir,
|
|
716
|
+
handlerPath
|
|
717
|
+
},
|
|
718
|
+
frontmatter,
|
|
719
|
+
metadata
|
|
720
|
+
};
|
|
721
|
+
} catch (err) {
|
|
722
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
723
|
+
logger.warn(`[hooks] Error loading hook from ${dir}: ${msg}`);
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Scan a directory for hook subdirectories.
|
|
729
|
+
*/
|
|
730
|
+
async function scanHooksDir(dir, source) {
|
|
731
|
+
if (!await isDirectory(dir)) return [];
|
|
732
|
+
const entries = [];
|
|
733
|
+
try {
|
|
734
|
+
const items = await readdir(dir);
|
|
735
|
+
for (const item of items) {
|
|
736
|
+
const itemPath = join(dir, item);
|
|
737
|
+
if (!await isDirectory(itemPath)) continue;
|
|
738
|
+
const entry = await loadHookFromDir(itemPath, source);
|
|
739
|
+
if (entry) entries.push(entry);
|
|
740
|
+
}
|
|
741
|
+
} catch (err) {
|
|
742
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
743
|
+
logger.warn(`[hooks] Error scanning ${dir}: ${msg}`);
|
|
744
|
+
}
|
|
745
|
+
return entries;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Discover all hooks from configured directories.
|
|
749
|
+
*
|
|
750
|
+
* Precedence (later wins on name conflicts):
|
|
751
|
+
* 1. Extra dirs (lowest)
|
|
752
|
+
* 2. Bundled hooks
|
|
753
|
+
* 3. Managed hooks (~/.milaidy/hooks/)
|
|
754
|
+
* 4. Workspace hooks (<workspace>/hooks/) (highest)
|
|
755
|
+
*/
|
|
756
|
+
async function discoverHooks(options = {}) {
|
|
757
|
+
const seen = /* @__PURE__ */ new Map();
|
|
758
|
+
if (options.extraDirs) for (const dir of options.extraDirs) {
|
|
759
|
+
const entries = await scanHooksDir(resolve(dir.replace(/^~/, homedir())), "milaidy-managed");
|
|
760
|
+
for (const entry of entries) seen.set(entry.hook.name, entry);
|
|
761
|
+
}
|
|
762
|
+
if (options.bundledDir) {
|
|
763
|
+
const entries = await scanHooksDir(options.bundledDir, "milaidy-bundled");
|
|
764
|
+
for (const entry of entries) seen.set(entry.hook.name, entry);
|
|
765
|
+
}
|
|
766
|
+
const managedEntries = await scanHooksDir(join(homedir(), ".milaidy", "hooks"), "milaidy-managed");
|
|
767
|
+
for (const entry of managedEntries) seen.set(entry.hook.name, entry);
|
|
768
|
+
if (options.workspacePath) {
|
|
769
|
+
const wsEntries = await scanHooksDir(join(options.workspacePath.replace(/^~/, homedir()), "hooks"), "milaidy-workspace");
|
|
770
|
+
for (const entry of wsEntries) seen.set(entry.hook.name, entry);
|
|
771
|
+
}
|
|
772
|
+
const all = Array.from(seen.values());
|
|
773
|
+
logger.info(`[hooks] Discovered ${all.length} hooks`);
|
|
774
|
+
return all;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/hooks/eligibility.ts
|
|
779
|
+
/**
|
|
780
|
+
* Hook Eligibility ā determine if a hook should be loaded.
|
|
781
|
+
*
|
|
782
|
+
* Checks OS, binary, environment, and config requirements.
|
|
783
|
+
*
|
|
784
|
+
* @module hooks/eligibility
|
|
785
|
+
*/
|
|
786
|
+
/**
|
|
787
|
+
* Check if a binary exists on PATH.
|
|
788
|
+
*/
|
|
789
|
+
function binaryExists(name) {
|
|
790
|
+
const pathDirs = (process.env.PATH ?? "").split(":");
|
|
791
|
+
for (const dir of pathDirs) if (existsSync(`${dir}/${name}`)) return true;
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Resolve a dot-separated config path to a value.
|
|
796
|
+
*/
|
|
797
|
+
function resolveConfigPath(config, pathStr) {
|
|
798
|
+
const parts = pathStr.split(".");
|
|
799
|
+
let current = config;
|
|
800
|
+
for (const part of parts) {
|
|
801
|
+
if (current === null || current === void 0 || typeof current !== "object") return;
|
|
802
|
+
current = current[part];
|
|
803
|
+
}
|
|
804
|
+
return current;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Check if a config path is truthy.
|
|
808
|
+
*/
|
|
809
|
+
function isConfigPathTruthy(config, pathStr) {
|
|
810
|
+
const value = resolveConfigPath(config, pathStr);
|
|
811
|
+
return value !== void 0 && value !== null && value !== false && value !== "" && value !== 0;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Check if a hook meets all eligibility requirements.
|
|
815
|
+
*/
|
|
816
|
+
function checkEligibility(metadata, hookConfig, milaidyConfig = {}) {
|
|
817
|
+
const missing = [];
|
|
818
|
+
if (!metadata) return {
|
|
819
|
+
eligible: true,
|
|
820
|
+
missing: []
|
|
821
|
+
};
|
|
822
|
+
if (hookConfig?.enabled === false) return {
|
|
823
|
+
eligible: false,
|
|
824
|
+
missing: ["Disabled in config"]
|
|
825
|
+
};
|
|
826
|
+
if (metadata.os && metadata.os.length > 0) {
|
|
827
|
+
if (!metadata.os.includes(platform())) missing.push(`OS: requires ${metadata.os.join("|")}, current: ${platform()}`);
|
|
828
|
+
}
|
|
829
|
+
if (metadata.always) return {
|
|
830
|
+
eligible: missing.length === 0,
|
|
831
|
+
missing
|
|
832
|
+
};
|
|
833
|
+
if (metadata.requires?.bins) {
|
|
834
|
+
for (const bin of metadata.requires.bins) if (!binaryExists(bin)) missing.push(`Binary missing: ${bin}`);
|
|
835
|
+
}
|
|
836
|
+
if (metadata.requires?.anyBins && metadata.requires.anyBins.length > 0) {
|
|
837
|
+
if (!metadata.requires.anyBins.some(binaryExists)) missing.push(`None of: ${metadata.requires.anyBins.join(", ")}`);
|
|
838
|
+
}
|
|
839
|
+
if (metadata.requires?.env) for (const envVar of metadata.requires.env) {
|
|
840
|
+
const hasInProcess = Boolean(process.env[envVar]);
|
|
841
|
+
const hasInHookConfig = Boolean(hookConfig?.env?.[envVar]);
|
|
842
|
+
if (!hasInProcess && !hasInHookConfig) missing.push(`Env missing: ${envVar}`);
|
|
843
|
+
}
|
|
844
|
+
if (metadata.requires?.config) {
|
|
845
|
+
for (const configPath of metadata.requires.config) if (!isConfigPathTruthy(milaidyConfig, configPath)) missing.push(`Config missing: ${configPath}`);
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
eligible: missing.length === 0,
|
|
849
|
+
missing
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Resolve per-hook config from the internal hooks config.
|
|
854
|
+
*/
|
|
855
|
+
function resolveHookConfig(internalConfig, hookKey) {
|
|
856
|
+
return internalConfig?.entries?.[hookKey];
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region src/hooks/loader.ts
|
|
861
|
+
/**
|
|
862
|
+
* Hook Loader ā load and register hooks into the event system.
|
|
863
|
+
*
|
|
864
|
+
* Orchestrates discovery -> eligibility -> loading -> registration.
|
|
865
|
+
*
|
|
866
|
+
* @module hooks/loader
|
|
867
|
+
*/
|
|
868
|
+
/**
|
|
869
|
+
* Dynamically import a hook handler module.
|
|
870
|
+
* Uses cache-busting query parameter for dev mode hot reload.
|
|
871
|
+
*/
|
|
872
|
+
async function loadHandlerModule(handlerPath, exportName = "default") {
|
|
873
|
+
try {
|
|
874
|
+
const handler = (await import(`${pathToFileURL(handlerPath).href}?t=${Date.now()}`))[exportName];
|
|
875
|
+
if (typeof handler !== "function") {
|
|
876
|
+
logger.warn(`[hooks] Handler at ${handlerPath} does not export a function as "${exportName}"`);
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
return handler;
|
|
880
|
+
} catch (err) {
|
|
881
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
882
|
+
logger.error(`[hooks] Failed to load handler ${handlerPath}: ${msg}`);
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Discover, filter, load, and register all hooks.
|
|
888
|
+
*
|
|
889
|
+
* This is the main entry point called during gateway startup.
|
|
890
|
+
*/
|
|
891
|
+
async function loadHooks(options = {}) {
|
|
892
|
+
const { internalConfig, milaidyConfig = {} } = options;
|
|
893
|
+
if (internalConfig?.enabled === false) {
|
|
894
|
+
logger.info("[hooks] Internal hooks disabled");
|
|
895
|
+
return {
|
|
896
|
+
discovered: 0,
|
|
897
|
+
eligible: 0,
|
|
898
|
+
registered: 0,
|
|
899
|
+
skipped: [],
|
|
900
|
+
failed: []
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
clearHooks();
|
|
904
|
+
const entries = await discoverHooks({
|
|
905
|
+
workspacePath: options.workspacePath,
|
|
906
|
+
bundledDir: options.bundledDir,
|
|
907
|
+
extraDirs: [...options.extraDirs ?? [], ...internalConfig?.load?.extraDirs ?? []]
|
|
908
|
+
});
|
|
909
|
+
const result = {
|
|
910
|
+
discovered: entries.length,
|
|
911
|
+
eligible: 0,
|
|
912
|
+
registered: 0,
|
|
913
|
+
skipped: [],
|
|
914
|
+
failed: []
|
|
915
|
+
};
|
|
916
|
+
for (const entry of entries) {
|
|
917
|
+
const hookConfig = resolveHookConfig(internalConfig, entry.metadata?.hookKey ?? entry.hook.name);
|
|
918
|
+
const eligibility = checkEligibility(entry.metadata, hookConfig, milaidyConfig);
|
|
919
|
+
if (!eligibility.eligible) {
|
|
920
|
+
result.skipped.push(`${entry.hook.name}: ${eligibility.missing.join(", ")}`);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
result.eligible++;
|
|
924
|
+
if (hookConfig?.enabled === false) {
|
|
925
|
+
result.skipped.push(`${entry.hook.name}: disabled in config`);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const exportName = entry.metadata?.export ?? "default";
|
|
929
|
+
const handler = await loadHandlerModule(entry.hook.handlerPath, exportName);
|
|
930
|
+
if (!handler) {
|
|
931
|
+
result.failed.push(entry.hook.name);
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
const events = entry.metadata?.events ?? [];
|
|
935
|
+
if (events.length === 0) {
|
|
936
|
+
logger.warn(`[hooks] Hook "${entry.hook.name}" has no events configured`);
|
|
937
|
+
result.skipped.push(`${entry.hook.name}: no events`);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
for (const eventKey of events) registerHook(eventKey, handler);
|
|
941
|
+
const emoji = entry.metadata?.emoji ?? "š";
|
|
942
|
+
logger.info(`[hooks] ${emoji} Registered: ${entry.hook.name} -> ${events.join(", ")}`);
|
|
943
|
+
result.registered++;
|
|
944
|
+
}
|
|
945
|
+
if (internalConfig?.handlers) for (const legacyHandler of internalConfig.handlers) try {
|
|
946
|
+
const handler = await loadHandlerModule(legacyHandler.module, legacyHandler.export ?? "default");
|
|
947
|
+
if (handler) {
|
|
948
|
+
registerHook(legacyHandler.event, handler);
|
|
949
|
+
logger.info(`[hooks] Registered legacy handler: ${legacyHandler.event} -> ${legacyHandler.module}`);
|
|
950
|
+
result.registered++;
|
|
951
|
+
}
|
|
952
|
+
} catch (err) {
|
|
953
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
954
|
+
logger.warn(`[hooks] Failed to load legacy handler: ${msg}`);
|
|
955
|
+
result.failed.push(legacyHandler.module);
|
|
956
|
+
}
|
|
957
|
+
logger.info(`[hooks] Load complete: ${result.registered}/${result.discovered} registered, ${result.skipped.length} skipped, ${result.failed.length} failed`);
|
|
958
|
+
return result;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
//#endregion
|
|
962
|
+
//#region src/process/exec.ts
|
|
963
|
+
/**
|
|
964
|
+
* Runs a command with an optional timeout.
|
|
965
|
+
* Returns { code, stdout, stderr }.
|
|
966
|
+
* Rejects if the process cannot be spawned or the timeout fires.
|
|
967
|
+
*/
|
|
968
|
+
function runCommandWithTimeout(argv, opts = {}) {
|
|
969
|
+
const [cmd, ...args] = argv;
|
|
970
|
+
if (!cmd) return Promise.reject(/* @__PURE__ */ new Error("runCommandWithTimeout: empty argv"));
|
|
971
|
+
return new Promise((resolve, reject) => {
|
|
972
|
+
const child = spawn(cmd, args, {
|
|
973
|
+
cwd: opts.cwd,
|
|
974
|
+
env: opts.env ?? process.env,
|
|
975
|
+
stdio: [
|
|
976
|
+
"ignore",
|
|
977
|
+
"pipe",
|
|
978
|
+
"pipe"
|
|
979
|
+
]
|
|
980
|
+
});
|
|
981
|
+
const stdoutChunks = [];
|
|
982
|
+
const stderrChunks = [];
|
|
983
|
+
child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
984
|
+
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
985
|
+
let timedOut = false;
|
|
986
|
+
let timer;
|
|
987
|
+
if (opts.timeoutMs && opts.timeoutMs > 0) timer = setTimeout(() => {
|
|
988
|
+
timedOut = true;
|
|
989
|
+
child.kill("SIGKILL");
|
|
990
|
+
}, opts.timeoutMs);
|
|
991
|
+
child.on("error", (err) => {
|
|
992
|
+
if (timer) clearTimeout(timer);
|
|
993
|
+
reject(err);
|
|
994
|
+
});
|
|
995
|
+
child.on("close", (exitCode) => {
|
|
996
|
+
if (timer) clearTimeout(timer);
|
|
997
|
+
if (timedOut) {
|
|
998
|
+
reject(/* @__PURE__ */ new Error(`Command timed out after ${opts.timeoutMs}ms: ${argv.join(" ")}`));
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
resolve({
|
|
1002
|
+
code: exitCode ?? 1,
|
|
1003
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
1004
|
+
stderr: Buffer.concat(stderrChunks).toString("utf-8")
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/logging.ts
|
|
1012
|
+
const ALLOWED_LOG_LEVELS = [
|
|
1013
|
+
"trace",
|
|
1014
|
+
"debug",
|
|
1015
|
+
"info",
|
|
1016
|
+
"warn",
|
|
1017
|
+
"error",
|
|
1018
|
+
"fatal",
|
|
1019
|
+
"silent"
|
|
1020
|
+
];
|
|
1021
|
+
function normalizeLogLevel(level, fallback = "info") {
|
|
1022
|
+
if (!level) return fallback;
|
|
1023
|
+
const normalized = level.toLowerCase();
|
|
1024
|
+
return ALLOWED_LOG_LEVELS.includes(normalized) ? normalized : fallback;
|
|
1025
|
+
}
|
|
1026
|
+
let consoleSettings = {
|
|
1027
|
+
level: normalizeLogLevel(process.env.LOG_LEVEL, "info"),
|
|
1028
|
+
style: process.env.LOG_JSON_FORMAT === "true" ? "json" : "pretty"
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
//#endregion
|
|
1032
|
+
//#region src/terminal/palette.ts
|
|
1033
|
+
const CLI_PALETTE = {
|
|
1034
|
+
accent: "#FF5A2D",
|
|
1035
|
+
accentBright: "#FF7A3D",
|
|
1036
|
+
accentDim: "#D14A22",
|
|
1037
|
+
info: "#FF8A5B",
|
|
1038
|
+
success: "#2FBF71",
|
|
1039
|
+
warn: "#FFB020",
|
|
1040
|
+
error: "#E23D2D",
|
|
1041
|
+
muted: "#8B7F77"
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
//#endregion
|
|
1045
|
+
//#region src/terminal/theme.ts
|
|
1046
|
+
const hasForceColor = typeof process.env.FORCE_COLOR === "string" && process.env.FORCE_COLOR.trim().length > 0 && process.env.FORCE_COLOR.trim() !== "0";
|
|
1047
|
+
const baseChalk = process.env.NO_COLOR && !hasForceColor ? new Chalk({ level: 0 }) : chalk;
|
|
1048
|
+
const hex = (value) => baseChalk.hex(value);
|
|
1049
|
+
const theme = {
|
|
1050
|
+
accent: hex(CLI_PALETTE.accent),
|
|
1051
|
+
accentBright: hex(CLI_PALETTE.accentBright),
|
|
1052
|
+
accentDim: hex(CLI_PALETTE.accentDim),
|
|
1053
|
+
info: hex(CLI_PALETTE.info),
|
|
1054
|
+
success: hex(CLI_PALETTE.success),
|
|
1055
|
+
warn: hex(CLI_PALETTE.warn),
|
|
1056
|
+
error: hex(CLI_PALETTE.error),
|
|
1057
|
+
muted: hex(CLI_PALETTE.muted),
|
|
1058
|
+
heading: baseChalk.bold.hex(CLI_PALETTE.accent),
|
|
1059
|
+
command: hex(CLI_PALETTE.accentBright),
|
|
1060
|
+
option: hex(CLI_PALETTE.warn)
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
//#endregion
|
|
1064
|
+
//#region src/globals.ts
|
|
1065
|
+
const success = theme.success;
|
|
1066
|
+
const warn = theme.warn;
|
|
1067
|
+
const info = theme.info;
|
|
1068
|
+
const danger = theme.error;
|
|
1069
|
+
|
|
1070
|
+
//#endregion
|
|
1071
|
+
//#region src/utils.ts
|
|
1072
|
+
function resolveUserPath(input) {
|
|
1073
|
+
const trimmed = input.trim();
|
|
1074
|
+
if (!trimmed) return trimmed;
|
|
1075
|
+
if (trimmed.startsWith("~")) {
|
|
1076
|
+
const expanded = trimmed.replace(/^~(?=$|[\\/])/, os.homedir());
|
|
1077
|
+
return path.resolve(expanded);
|
|
1078
|
+
}
|
|
1079
|
+
return path.resolve(trimmed);
|
|
1080
|
+
}
|
|
1081
|
+
function resolveConfigDir(env = process.env, homedir = os.homedir) {
|
|
1082
|
+
const override = env.MILAIDY_STATE_DIR?.trim();
|
|
1083
|
+
if (override) return resolveUserPath(override);
|
|
1084
|
+
return path.join(homedir(), ".milaidy");
|
|
1085
|
+
}
|
|
1086
|
+
const CONFIG_DIR = resolveConfigDir();
|
|
1087
|
+
|
|
1088
|
+
//#endregion
|
|
1089
|
+
//#region src/infra/milaidy-root.ts
|
|
1090
|
+
const CORE_PACKAGE_NAMES = new Set(["milaidy"]);
|
|
1091
|
+
async function readPackageName(dir) {
|
|
1092
|
+
try {
|
|
1093
|
+
const raw = await fs$1.readFile(path.join(dir, "package.json"), "utf-8");
|
|
1094
|
+
const parsed = JSON.parse(raw);
|
|
1095
|
+
return typeof parsed.name === "string" ? parsed.name : null;
|
|
1096
|
+
} catch {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async function findPackageRoot(startDir, maxDepth = 12) {
|
|
1101
|
+
let current = path.resolve(startDir);
|
|
1102
|
+
for (let i = 0; i < maxDepth; i += 1) {
|
|
1103
|
+
const name = await readPackageName(current);
|
|
1104
|
+
if (name && CORE_PACKAGE_NAMES.has(name)) return current;
|
|
1105
|
+
const parent = path.dirname(current);
|
|
1106
|
+
if (parent === current) break;
|
|
1107
|
+
current = parent;
|
|
1108
|
+
}
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
function candidateDirsFromArgv1(argv1) {
|
|
1112
|
+
const normalized = path.resolve(argv1);
|
|
1113
|
+
const candidates = [path.dirname(normalized)];
|
|
1114
|
+
const parts = normalized.split(path.sep);
|
|
1115
|
+
const binIndex = parts.lastIndexOf(".bin");
|
|
1116
|
+
if (binIndex > 0 && parts[binIndex - 1] === "node_modules") {
|
|
1117
|
+
const binName = path.basename(normalized);
|
|
1118
|
+
const nodeModulesDir = parts.slice(0, binIndex).join(path.sep);
|
|
1119
|
+
candidates.push(path.join(nodeModulesDir, binName));
|
|
1120
|
+
}
|
|
1121
|
+
return candidates;
|
|
1122
|
+
}
|
|
1123
|
+
async function resolveMilaidyPackageRoot(opts) {
|
|
1124
|
+
const candidates = [];
|
|
1125
|
+
if (opts.moduleUrl) candidates.push(path.dirname(fileURLToPath(opts.moduleUrl)));
|
|
1126
|
+
if (opts.argv1) candidates.push(...candidateDirsFromArgv1(opts.argv1));
|
|
1127
|
+
if (opts.cwd) candidates.push(opts.cwd);
|
|
1128
|
+
for (const candidate of candidates) {
|
|
1129
|
+
const found = await findPackageRoot(candidate);
|
|
1130
|
+
if (found) return found;
|
|
1131
|
+
}
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
//#endregion
|
|
1136
|
+
//#region src/agents/workspace-templates.ts
|
|
1137
|
+
const FALLBACK_TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../docs/reference/templates");
|
|
1138
|
+
let cachedTemplateDir;
|
|
1139
|
+
let resolvingTemplateDir;
|
|
1140
|
+
async function pathExists(candidate) {
|
|
1141
|
+
try {
|
|
1142
|
+
await fs$1.access(candidate);
|
|
1143
|
+
return true;
|
|
1144
|
+
} catch {
|
|
1145
|
+
return false;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
async function resolveWorkspaceTemplateDir(opts) {
|
|
1149
|
+
if (cachedTemplateDir) return cachedTemplateDir;
|
|
1150
|
+
if (resolvingTemplateDir) return resolvingTemplateDir;
|
|
1151
|
+
resolvingTemplateDir = (async () => {
|
|
1152
|
+
const moduleUrl = opts?.moduleUrl ?? import.meta.url;
|
|
1153
|
+
const argv1 = opts?.argv1 ?? process.argv[1];
|
|
1154
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
1155
|
+
const packageRoot = await resolveMilaidyPackageRoot({
|
|
1156
|
+
moduleUrl,
|
|
1157
|
+
argv1,
|
|
1158
|
+
cwd
|
|
1159
|
+
});
|
|
1160
|
+
const candidates = [
|
|
1161
|
+
packageRoot ? path.join(packageRoot, "docs", "reference", "templates") : null,
|
|
1162
|
+
cwd ? path.resolve(cwd, "docs", "reference", "templates") : null,
|
|
1163
|
+
FALLBACK_TEMPLATE_DIR
|
|
1164
|
+
].filter(Boolean);
|
|
1165
|
+
for (const candidate of candidates) if (await pathExists(candidate)) {
|
|
1166
|
+
cachedTemplateDir = candidate;
|
|
1167
|
+
return candidate;
|
|
1168
|
+
}
|
|
1169
|
+
cachedTemplateDir = candidates[0] ?? FALLBACK_TEMPLATE_DIR;
|
|
1170
|
+
return cachedTemplateDir;
|
|
1171
|
+
})();
|
|
1172
|
+
try {
|
|
1173
|
+
return await resolvingTemplateDir;
|
|
1174
|
+
} finally {
|
|
1175
|
+
resolvingTemplateDir = void 0;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
//#endregion
|
|
1180
|
+
//#region src/agents/workspace.ts
|
|
1181
|
+
function resolveDefaultAgentWorkspaceDir(env = process.env, homedir = os.homedir) {
|
|
1182
|
+
const profile = env.MILAIDY_PROFILE?.trim();
|
|
1183
|
+
if (profile && profile.toLowerCase() !== "default") return path.join(homedir(), ".milaidy", `workspace-${profile}`);
|
|
1184
|
+
return path.join(homedir(), ".milaidy", "workspace");
|
|
1185
|
+
}
|
|
1186
|
+
const DEFAULT_AGENT_WORKSPACE_DIR = resolveDefaultAgentWorkspaceDir();
|
|
1187
|
+
const DEFAULT_AGENTS_FILENAME = "AGENTS.md";
|
|
1188
|
+
const DEFAULT_TOOLS_FILENAME = "TOOLS.md";
|
|
1189
|
+
const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
|
|
1190
|
+
const DEFAULT_USER_FILENAME = "USER.md";
|
|
1191
|
+
const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md";
|
|
1192
|
+
const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
|
|
1193
|
+
const DEFAULT_MEMORY_FILENAME = "MEMORY.md";
|
|
1194
|
+
const DEFAULT_MEMORY_ALT_FILENAME = "memory.md";
|
|
1195
|
+
function stripFrontMatter(content) {
|
|
1196
|
+
if (!content.startsWith("---")) return content;
|
|
1197
|
+
const endIndex = content.indexOf("\n---", 3);
|
|
1198
|
+
if (endIndex === -1) return content;
|
|
1199
|
+
const start = endIndex + 4;
|
|
1200
|
+
let trimmed = content.slice(start);
|
|
1201
|
+
trimmed = trimmed.replace(/^\s+/, "");
|
|
1202
|
+
return trimmed;
|
|
1203
|
+
}
|
|
1204
|
+
async function loadTemplate(name) {
|
|
1205
|
+
const templateDir = await resolveWorkspaceTemplateDir();
|
|
1206
|
+
const templatePath = path.join(templateDir, name);
|
|
1207
|
+
try {
|
|
1208
|
+
return stripFrontMatter(await fs$1.readFile(templatePath, "utf-8"));
|
|
1209
|
+
} catch {
|
|
1210
|
+
throw new Error(`Missing workspace template: ${name} (${templatePath}). Ensure docs/reference/templates are packaged.`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
async function writeFileIfMissing(filePath, content) {
|
|
1214
|
+
try {
|
|
1215
|
+
await fs$1.writeFile(filePath, content, {
|
|
1216
|
+
encoding: "utf-8",
|
|
1217
|
+
flag: "wx"
|
|
1218
|
+
});
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
if (err.code !== "EEXIST") throw err;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
async function hasGitRepo(dir) {
|
|
1224
|
+
try {
|
|
1225
|
+
await fs$1.stat(path.join(dir, ".git"));
|
|
1226
|
+
return true;
|
|
1227
|
+
} catch {
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
async function isGitAvailable() {
|
|
1232
|
+
try {
|
|
1233
|
+
return (await runCommandWithTimeout(["git", "--version"], { timeoutMs: 2e3 })).code === 0;
|
|
1234
|
+
} catch {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async function ensureGitRepo(dir, isBrandNewWorkspace) {
|
|
1239
|
+
if (!isBrandNewWorkspace) return;
|
|
1240
|
+
if (await hasGitRepo(dir)) return;
|
|
1241
|
+
if (!await isGitAvailable()) return;
|
|
1242
|
+
try {
|
|
1243
|
+
await runCommandWithTimeout(["git", "init"], {
|
|
1244
|
+
cwd: dir,
|
|
1245
|
+
timeoutMs: 1e4
|
|
1246
|
+
});
|
|
1247
|
+
} catch {}
|
|
1248
|
+
}
|
|
1249
|
+
async function ensureAgentWorkspace(params) {
|
|
1250
|
+
const dir = resolveUserPath(params?.dir?.trim() ? params.dir.trim() : DEFAULT_AGENT_WORKSPACE_DIR);
|
|
1251
|
+
await fs$1.mkdir(dir, { recursive: true });
|
|
1252
|
+
if (!params?.ensureBootstrapFiles) return { dir };
|
|
1253
|
+
const agentsPath = path.join(dir, DEFAULT_AGENTS_FILENAME);
|
|
1254
|
+
const toolsPath = path.join(dir, DEFAULT_TOOLS_FILENAME);
|
|
1255
|
+
const identityPath = path.join(dir, DEFAULT_IDENTITY_FILENAME);
|
|
1256
|
+
const userPath = path.join(dir, DEFAULT_USER_FILENAME);
|
|
1257
|
+
const heartbeatPath = path.join(dir, DEFAULT_HEARTBEAT_FILENAME);
|
|
1258
|
+
const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME);
|
|
1259
|
+
const isBrandNewWorkspace = await (async () => {
|
|
1260
|
+
const paths = [
|
|
1261
|
+
agentsPath,
|
|
1262
|
+
toolsPath,
|
|
1263
|
+
identityPath,
|
|
1264
|
+
userPath,
|
|
1265
|
+
heartbeatPath
|
|
1266
|
+
];
|
|
1267
|
+
return (await Promise.all(paths.map(async (p) => {
|
|
1268
|
+
try {
|
|
1269
|
+
await fs$1.access(p);
|
|
1270
|
+
return true;
|
|
1271
|
+
} catch {
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
}))).every((v) => !v);
|
|
1275
|
+
})();
|
|
1276
|
+
const agentsTemplate = await loadTemplate(DEFAULT_AGENTS_FILENAME);
|
|
1277
|
+
const toolsTemplate = await loadTemplate(DEFAULT_TOOLS_FILENAME);
|
|
1278
|
+
const identityTemplate = await loadTemplate(DEFAULT_IDENTITY_FILENAME);
|
|
1279
|
+
const userTemplate = await loadTemplate(DEFAULT_USER_FILENAME);
|
|
1280
|
+
const heartbeatTemplate = await loadTemplate(DEFAULT_HEARTBEAT_FILENAME);
|
|
1281
|
+
const bootstrapTemplate = await loadTemplate(DEFAULT_BOOTSTRAP_FILENAME);
|
|
1282
|
+
await writeFileIfMissing(agentsPath, agentsTemplate);
|
|
1283
|
+
await writeFileIfMissing(toolsPath, toolsTemplate);
|
|
1284
|
+
await writeFileIfMissing(identityPath, identityTemplate);
|
|
1285
|
+
await writeFileIfMissing(userPath, userTemplate);
|
|
1286
|
+
await writeFileIfMissing(heartbeatPath, heartbeatTemplate);
|
|
1287
|
+
if (isBrandNewWorkspace) await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
|
|
1288
|
+
await ensureGitRepo(dir, isBrandNewWorkspace);
|
|
1289
|
+
return {
|
|
1290
|
+
dir,
|
|
1291
|
+
agentsPath,
|
|
1292
|
+
toolsPath,
|
|
1293
|
+
identityPath,
|
|
1294
|
+
userPath,
|
|
1295
|
+
heartbeatPath,
|
|
1296
|
+
bootstrapPath
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
async function resolveMemoryBootstrapEntries(resolvedDir) {
|
|
1300
|
+
const candidates = [DEFAULT_MEMORY_FILENAME, DEFAULT_MEMORY_ALT_FILENAME];
|
|
1301
|
+
const entries = [];
|
|
1302
|
+
for (const name of candidates) {
|
|
1303
|
+
const filePath = path.join(resolvedDir, name);
|
|
1304
|
+
try {
|
|
1305
|
+
await fs$1.access(filePath);
|
|
1306
|
+
entries.push({
|
|
1307
|
+
name,
|
|
1308
|
+
filePath
|
|
1309
|
+
});
|
|
1310
|
+
} catch {}
|
|
1311
|
+
}
|
|
1312
|
+
if (entries.length <= 1) return entries;
|
|
1313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1314
|
+
const deduped = [];
|
|
1315
|
+
for (const entry of entries) {
|
|
1316
|
+
let key = entry.filePath;
|
|
1317
|
+
try {
|
|
1318
|
+
key = await fs$1.realpath(entry.filePath);
|
|
1319
|
+
} catch {}
|
|
1320
|
+
if (seen.has(key)) continue;
|
|
1321
|
+
seen.add(key);
|
|
1322
|
+
deduped.push(entry);
|
|
1323
|
+
}
|
|
1324
|
+
return deduped;
|
|
1325
|
+
}
|
|
1326
|
+
async function loadWorkspaceBootstrapFiles(dir) {
|
|
1327
|
+
const resolvedDir = resolveUserPath(dir);
|
|
1328
|
+
const entries = [
|
|
1329
|
+
{
|
|
1330
|
+
name: DEFAULT_AGENTS_FILENAME,
|
|
1331
|
+
filePath: path.join(resolvedDir, DEFAULT_AGENTS_FILENAME)
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
name: DEFAULT_TOOLS_FILENAME,
|
|
1335
|
+
filePath: path.join(resolvedDir, DEFAULT_TOOLS_FILENAME)
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
name: DEFAULT_IDENTITY_FILENAME,
|
|
1339
|
+
filePath: path.join(resolvedDir, DEFAULT_IDENTITY_FILENAME)
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
name: DEFAULT_USER_FILENAME,
|
|
1343
|
+
filePath: path.join(resolvedDir, DEFAULT_USER_FILENAME)
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
name: DEFAULT_HEARTBEAT_FILENAME,
|
|
1347
|
+
filePath: path.join(resolvedDir, DEFAULT_HEARTBEAT_FILENAME)
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
name: DEFAULT_BOOTSTRAP_FILENAME,
|
|
1351
|
+
filePath: path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME)
|
|
1352
|
+
}
|
|
1353
|
+
];
|
|
1354
|
+
entries.push(...await resolveMemoryBootstrapEntries(resolvedDir));
|
|
1355
|
+
const result = [];
|
|
1356
|
+
for (const entry of entries) try {
|
|
1357
|
+
const content = await fs$1.readFile(entry.filePath, "utf-8");
|
|
1358
|
+
result.push({
|
|
1359
|
+
name: entry.name,
|
|
1360
|
+
path: entry.filePath,
|
|
1361
|
+
content,
|
|
1362
|
+
missing: false
|
|
1363
|
+
});
|
|
1364
|
+
} catch {
|
|
1365
|
+
result.push({
|
|
1366
|
+
name: entry.name,
|
|
1367
|
+
path: entry.filePath,
|
|
1368
|
+
missing: true
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
return result;
|
|
1372
|
+
}
|
|
1373
|
+
const SUBAGENT_BOOTSTRAP_ALLOWLIST = new Set([DEFAULT_AGENTS_FILENAME, DEFAULT_TOOLS_FILENAME]);
|
|
1374
|
+
function filterBootstrapFilesForSession(files, sessionKey) {
|
|
1375
|
+
if (!sessionKey || !isSubagentSessionKey(sessionKey)) return files;
|
|
1376
|
+
return files.filter((file) => SUBAGENT_BOOTSTRAP_ALLOWLIST.has(file.name));
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
//#endregion
|
|
1380
|
+
//#region src/agents/workspace-provider.ts
|
|
1381
|
+
const DEFAULT_MAX_CHARS = 2e4;
|
|
1382
|
+
const CACHE_TTL_MS = 6e4;
|
|
1383
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1384
|
+
async function getFiles(dir) {
|
|
1385
|
+
const entry = cache.get(dir);
|
|
1386
|
+
if (entry && Date.now() - entry.at < CACHE_TTL_MS) return entry.files;
|
|
1387
|
+
const files = await loadWorkspaceBootstrapFiles(dir);
|
|
1388
|
+
cache.set(dir, {
|
|
1389
|
+
files,
|
|
1390
|
+
at: Date.now()
|
|
1391
|
+
});
|
|
1392
|
+
return files;
|
|
1393
|
+
}
|
|
1394
|
+
function truncate(content, max) {
|
|
1395
|
+
if (content.length <= max) return content;
|
|
1396
|
+
return `${content.slice(0, max)}\n\n[... truncated at ${max.toLocaleString()} chars]`;
|
|
1397
|
+
}
|
|
1398
|
+
function buildContext(files, maxChars) {
|
|
1399
|
+
const sections = [];
|
|
1400
|
+
for (const f of files) {
|
|
1401
|
+
if (f.missing || !f.content?.trim()) continue;
|
|
1402
|
+
const text = truncate(f.content.trim(), maxChars);
|
|
1403
|
+
const tag = text.length > f.content.trim().length ? " [TRUNCATED]" : "";
|
|
1404
|
+
sections.push(`### ${f.name}${tag}\n\n${text}`);
|
|
1405
|
+
}
|
|
1406
|
+
if (sections.length === 0) return "";
|
|
1407
|
+
return `## Project Context (Workspace)\n\n${sections.join("\n\n---\n\n")}`;
|
|
1408
|
+
}
|
|
1409
|
+
function createWorkspaceProvider(options) {
|
|
1410
|
+
const dir = options?.workspaceDir ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
|
1411
|
+
const maxChars = options?.maxCharsPerFile ?? DEFAULT_MAX_CHARS;
|
|
1412
|
+
return {
|
|
1413
|
+
name: "workspaceContext",
|
|
1414
|
+
description: "Workspace bootstrap files (AGENTS.md, TOOLS.md, IDENTITY.md, etc.)",
|
|
1415
|
+
position: 10,
|
|
1416
|
+
async get(_runtime, message, _state) {
|
|
1417
|
+
try {
|
|
1418
|
+
const allFiles = await getFiles(dir);
|
|
1419
|
+
const sessionKey = message.metadata?.sessionKey;
|
|
1420
|
+
return {
|
|
1421
|
+
text: buildContext(filterBootstrapFilesForSession(allFiles, sessionKey), maxChars),
|
|
1422
|
+
data: { workspaceDir: dir }
|
|
1423
|
+
};
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
return {
|
|
1426
|
+
text: `[Workspace context unavailable: ${err instanceof Error ? err.message : err}]`,
|
|
1427
|
+
data: {}
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
//#endregion
|
|
1435
|
+
//#region src/agents/session-bridge.ts
|
|
1436
|
+
/**
|
|
1437
|
+
* Resolve an Milaidy session key from an ElizaOS room.
|
|
1438
|
+
*
|
|
1439
|
+
* DMs -> agent:{agentId}:main
|
|
1440
|
+
* Groups -> agent:{agentId}:{channel}:group:{groupId}
|
|
1441
|
+
* Channels -> agent:{agentId}:{channel}:channel:{channelId}
|
|
1442
|
+
* Threads append :thread:{threadId}
|
|
1443
|
+
*/
|
|
1444
|
+
function resolveSessionKeyFromRoom(agentId, room, meta) {
|
|
1445
|
+
const channel = meta?.channel ?? room.source ?? "unknown";
|
|
1446
|
+
if (room.type === ChannelType.DM || room.type === ChannelType.SELF) return buildAgentMainSessionKey({
|
|
1447
|
+
agentId,
|
|
1448
|
+
mainKey: "main"
|
|
1449
|
+
});
|
|
1450
|
+
const id = meta?.groupId ?? room.channelId ?? room.id;
|
|
1451
|
+
const base = `agent:${agentId}:${channel}:${room.type === ChannelType.GROUP ? "group" : "channel"}:${id}`;
|
|
1452
|
+
return meta?.threadId ? `${base}:thread:${meta.threadId}` : base;
|
|
1453
|
+
}
|
|
1454
|
+
function createSessionKeyProvider(options) {
|
|
1455
|
+
const agentId = options?.defaultAgentId ?? "main";
|
|
1456
|
+
return {
|
|
1457
|
+
name: "milaidySessionKey",
|
|
1458
|
+
description: "Milaidy session key (DM/group/thread isolation)",
|
|
1459
|
+
dynamic: true,
|
|
1460
|
+
position: 5,
|
|
1461
|
+
async get(runtime, message, _state) {
|
|
1462
|
+
const meta = message.metadata ?? {};
|
|
1463
|
+
const existing = meta.sessionKey;
|
|
1464
|
+
if (existing) {
|
|
1465
|
+
const parsed = parseAgentSessionKey(existing);
|
|
1466
|
+
return {
|
|
1467
|
+
text: `Session: ${existing}`,
|
|
1468
|
+
values: {
|
|
1469
|
+
sessionKey: existing,
|
|
1470
|
+
agentId: parsed?.agentId ?? agentId
|
|
1471
|
+
},
|
|
1472
|
+
data: { sessionKey: existing }
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
const room = await runtime.getRoom(message.roomId);
|
|
1476
|
+
if (!room) {
|
|
1477
|
+
const key = buildAgentMainSessionKey({
|
|
1478
|
+
agentId,
|
|
1479
|
+
mainKey: "main"
|
|
1480
|
+
});
|
|
1481
|
+
return {
|
|
1482
|
+
text: `Session: ${key}`,
|
|
1483
|
+
values: { sessionKey: key },
|
|
1484
|
+
data: { sessionKey: key }
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
const key = resolveSessionKeyFromRoom(agentId, room, {
|
|
1488
|
+
threadId: meta.threadId,
|
|
1489
|
+
groupId: meta.groupId,
|
|
1490
|
+
channel: meta.channel ?? room.source
|
|
1491
|
+
});
|
|
1492
|
+
return {
|
|
1493
|
+
text: `Session: ${key}`,
|
|
1494
|
+
values: {
|
|
1495
|
+
sessionKey: key,
|
|
1496
|
+
isGroup: room.type === ChannelType.GROUP
|
|
1497
|
+
},
|
|
1498
|
+
data: { sessionKey: key }
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
//#endregion
|
|
1505
|
+
//#region src/agents/compaction-action.ts
|
|
1506
|
+
/**
|
|
1507
|
+
* Compaction: summarize conversation history, store summary, set compaction point.
|
|
1508
|
+
*
|
|
1509
|
+
* After compaction, recentMessages provider only loads messages after lastCompactionAt.
|
|
1510
|
+
* The summary becomes the effective "first message" in the room.
|
|
1511
|
+
*/
|
|
1512
|
+
function buildPrompt(messages, instructions) {
|
|
1513
|
+
let prompt = "Summarize this conversation for context preservation. Focus on decisions, facts learned, open questions, action items, and key context needed to continue.\n\nConversation:\n" + messages;
|
|
1514
|
+
if (instructions) prompt += `\n\nAdditional instructions: ${instructions}`;
|
|
1515
|
+
prompt += "\n\nSummary:";
|
|
1516
|
+
return prompt;
|
|
1517
|
+
}
|
|
1518
|
+
async function summarize(runtime, roomId, instructions) {
|
|
1519
|
+
const messages = await runtime.getMemories({
|
|
1520
|
+
tableName: "messages",
|
|
1521
|
+
roomId,
|
|
1522
|
+
count: 200
|
|
1523
|
+
});
|
|
1524
|
+
if (!messages?.length) return "No conversation history to compact.";
|
|
1525
|
+
const formatted = messages.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0)).map((m) => {
|
|
1526
|
+
return `${m.entityId === runtime.agentId ? "Assistant" : "User"}: ${m.content?.text ?? ""}`;
|
|
1527
|
+
}).join("\n");
|
|
1528
|
+
return runtime.useModel(ModelType.TEXT_LARGE, { prompt: buildPrompt(formatted, instructions) });
|
|
1529
|
+
}
|
|
1530
|
+
const compactAction = {
|
|
1531
|
+
name: "COMPACT_SESSION",
|
|
1532
|
+
similes: [
|
|
1533
|
+
"COMPACT",
|
|
1534
|
+
"COMPRESS",
|
|
1535
|
+
"SUMMARIZE_SESSION"
|
|
1536
|
+
],
|
|
1537
|
+
description: "Summarize conversation history and set a compaction point.",
|
|
1538
|
+
validate: async () => true,
|
|
1539
|
+
handler: async (runtime, message, _state, _options, callback) => {
|
|
1540
|
+
const { roomId } = message;
|
|
1541
|
+
const instructions = (message.content?.text ?? "").replace(/^\/?compact\s*/i, "").trim() || void 0;
|
|
1542
|
+
try {
|
|
1543
|
+
const summary = await summarize(runtime, roomId, instructions);
|
|
1544
|
+
const now = Date.now();
|
|
1545
|
+
await runtime.createMemory({
|
|
1546
|
+
id: crypto.randomUUID(),
|
|
1547
|
+
entityId: runtime.agentId,
|
|
1548
|
+
roomId,
|
|
1549
|
+
content: {
|
|
1550
|
+
text: `[Compaction Summary]\n\n${summary}`,
|
|
1551
|
+
source: "compaction"
|
|
1552
|
+
},
|
|
1553
|
+
createdAt: now,
|
|
1554
|
+
metadata: { type: MemoryType.CUSTOM }
|
|
1555
|
+
}, "messages");
|
|
1556
|
+
const room = await runtime.getRoom(roomId);
|
|
1557
|
+
if (room) {
|
|
1558
|
+
const prev = Array.isArray(room.metadata?.compactionHistory) ? room.metadata.compactionHistory : [];
|
|
1559
|
+
const entry = {
|
|
1560
|
+
timestamp: now,
|
|
1561
|
+
triggeredBy: message.entityId
|
|
1562
|
+
};
|
|
1563
|
+
const compactionHistory = [...prev, entry].slice(-10);
|
|
1564
|
+
await runtime.updateRoom({
|
|
1565
|
+
...room,
|
|
1566
|
+
metadata: {
|
|
1567
|
+
...room.metadata,
|
|
1568
|
+
lastCompactionAt: now,
|
|
1569
|
+
compactionHistory
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
if (callback) await callback({ text: "Session compacted." });
|
|
1574
|
+
return {
|
|
1575
|
+
success: true,
|
|
1576
|
+
data: { compactedAt: now }
|
|
1577
|
+
};
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1580
|
+
if (callback) await callback({ text: `Compaction failed: ${msg}` });
|
|
1581
|
+
return {
|
|
1582
|
+
success: false,
|
|
1583
|
+
error: msg
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1587
|
+
examples: [[{
|
|
1588
|
+
name: "{{name1}}",
|
|
1589
|
+
content: { text: "/compact" }
|
|
1590
|
+
}, {
|
|
1591
|
+
name: "{{agentName}}",
|
|
1592
|
+
content: {
|
|
1593
|
+
text: "Session compacted.",
|
|
1594
|
+
actions: ["COMPACT_SESSION"]
|
|
1595
|
+
}
|
|
1596
|
+
}], [{
|
|
1597
|
+
name: "{{name1}}",
|
|
1598
|
+
content: { text: "/compact Focus on decisions" }
|
|
1599
|
+
}, {
|
|
1600
|
+
name: "{{agentName}}",
|
|
1601
|
+
content: {
|
|
1602
|
+
text: "Session compacted.",
|
|
1603
|
+
actions: ["COMPACT_SESSION"]
|
|
1604
|
+
}
|
|
1605
|
+
}]]
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
//#endregion
|
|
1609
|
+
//#region src/agents/tools/memory-tools.ts
|
|
1610
|
+
/**
|
|
1611
|
+
* memory_search and memory_get actions for workspace memory files.
|
|
1612
|
+
*/
|
|
1613
|
+
const SUPPORTED_EXT = new Set([
|
|
1614
|
+
".md",
|
|
1615
|
+
".txt",
|
|
1616
|
+
".json"
|
|
1617
|
+
]);
|
|
1618
|
+
function resolveMemoryDir(workspacePath) {
|
|
1619
|
+
return join(workspacePath?.replace(/^~/, homedir()) ?? join(homedir(), ".milaidy", "workspace"), "memory");
|
|
1620
|
+
}
|
|
1621
|
+
function score(content, query) {
|
|
1622
|
+
const lower = content.toLowerCase();
|
|
1623
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1624
|
+
if (terms.length === 0) return 0;
|
|
1625
|
+
let matched = 0;
|
|
1626
|
+
for (const t of terms) if (lower.includes(t)) matched++;
|
|
1627
|
+
return matched / terms.length;
|
|
1628
|
+
}
|
|
1629
|
+
const memorySearchAction = {
|
|
1630
|
+
name: "memory_search",
|
|
1631
|
+
description: "Search workspace memory files for relevant context.",
|
|
1632
|
+
similes: ["MEMORY_SEARCH", "SEARCH_MEMORY"],
|
|
1633
|
+
examples: [],
|
|
1634
|
+
validate: async (_runtime, message) => {
|
|
1635
|
+
const text = message.content?.text;
|
|
1636
|
+
return typeof text === "string" && text.length > 0;
|
|
1637
|
+
},
|
|
1638
|
+
handler: async (_runtime, message, _state, options, callback) => {
|
|
1639
|
+
const params = options ?? {};
|
|
1640
|
+
const query = params.query ?? message.content?.text ?? "";
|
|
1641
|
+
const limit = Math.min(Number(params.limit ?? 10), 50);
|
|
1642
|
+
const memDir = resolveMemoryDir(params.workspacePath);
|
|
1643
|
+
let files;
|
|
1644
|
+
try {
|
|
1645
|
+
files = await readdir(memDir);
|
|
1646
|
+
} catch {
|
|
1647
|
+
const msg = `Memory directory not found: ${memDir}`;
|
|
1648
|
+
if (callback) await callback({ text: msg });
|
|
1649
|
+
return {
|
|
1650
|
+
success: false,
|
|
1651
|
+
error: msg
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
const results = [];
|
|
1655
|
+
for (const file of files) {
|
|
1656
|
+
if (!SUPPORTED_EXT.has(extname(file))) continue;
|
|
1657
|
+
const filePath = join(memDir, file);
|
|
1658
|
+
let content;
|
|
1659
|
+
try {
|
|
1660
|
+
content = await readFile(filePath, "utf-8");
|
|
1661
|
+
} catch {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
const relevance = score(content, query);
|
|
1665
|
+
if (relevance > 0) {
|
|
1666
|
+
const snippet = content.slice(0, 200).replace(/\n/g, " ").trim();
|
|
1667
|
+
results.push({
|
|
1668
|
+
name: basename(file, extname(file)),
|
|
1669
|
+
path: filePath,
|
|
1670
|
+
relevance,
|
|
1671
|
+
snippet: snippet.length < content.length ? `${snippet}...` : snippet
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
results.sort((a, b) => b.relevance - a.relevance);
|
|
1676
|
+
const top = results.slice(0, limit);
|
|
1677
|
+
if (top.length === 0) {
|
|
1678
|
+
const msg = `No memory entries found matching "${query}"`;
|
|
1679
|
+
if (callback) await callback({ text: msg });
|
|
1680
|
+
return {
|
|
1681
|
+
success: true,
|
|
1682
|
+
text: msg
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
const output = top.map((r, i) => `${i + 1}. **${r.name}** (${(r.relevance * 100).toFixed(0)}%)\n ${r.snippet}`).join("\n\n");
|
|
1686
|
+
const text = `Found ${top.length} memory entries:\n\n${output}`;
|
|
1687
|
+
if (callback) await callback({ text });
|
|
1688
|
+
return {
|
|
1689
|
+
success: true,
|
|
1690
|
+
text,
|
|
1691
|
+
data: { results: top }
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
const memoryGetAction = {
|
|
1696
|
+
name: "memory_get",
|
|
1697
|
+
description: "Retrieve a specific memory file by name.",
|
|
1698
|
+
similes: [
|
|
1699
|
+
"MEMORY_GET",
|
|
1700
|
+
"GET_MEMORY",
|
|
1701
|
+
"READ_MEMORY"
|
|
1702
|
+
],
|
|
1703
|
+
examples: [],
|
|
1704
|
+
validate: async (_runtime, message) => {
|
|
1705
|
+
const text = message.content?.text;
|
|
1706
|
+
return typeof text === "string" && text.length > 0;
|
|
1707
|
+
},
|
|
1708
|
+
handler: async (_runtime, message, _state, options, callback) => {
|
|
1709
|
+
const params = options ?? {};
|
|
1710
|
+
const name = params.name ?? message.content?.text ?? "";
|
|
1711
|
+
const memDir = resolveMemoryDir(params.workspacePath);
|
|
1712
|
+
const candidates = [
|
|
1713
|
+
name,
|
|
1714
|
+
`${name}.md`,
|
|
1715
|
+
`${name}.txt`,
|
|
1716
|
+
`${name}.json`
|
|
1717
|
+
];
|
|
1718
|
+
for (const candidate of candidates) {
|
|
1719
|
+
const filePath = candidate.startsWith("/") ? candidate : join(memDir, candidate);
|
|
1720
|
+
let content;
|
|
1721
|
+
try {
|
|
1722
|
+
content = await readFile(filePath, "utf-8");
|
|
1723
|
+
} catch {
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
const text = `# Memory: ${basename(filePath)}\n\n${content}`;
|
|
1727
|
+
if (callback) await callback({ text });
|
|
1728
|
+
return {
|
|
1729
|
+
success: true,
|
|
1730
|
+
text,
|
|
1731
|
+
data: {
|
|
1732
|
+
name: basename(filePath),
|
|
1733
|
+
content,
|
|
1734
|
+
path: filePath
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
let hint = "";
|
|
1739
|
+
try {
|
|
1740
|
+
const available = (await readdir(memDir)).filter((f) => SUPPORTED_EXT.has(extname(f)));
|
|
1741
|
+
if (available.length > 0) hint = `\n\nAvailable: ${available.slice(0, 10).join(", ")}`;
|
|
1742
|
+
} catch {}
|
|
1743
|
+
const msg = `Memory not found: "${name}"${hint}`;
|
|
1744
|
+
if (callback) await callback({ text: msg });
|
|
1745
|
+
return {
|
|
1746
|
+
success: false,
|
|
1747
|
+
error: msg,
|
|
1748
|
+
text: msg
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
//#endregion
|
|
1754
|
+
//#region src/milaidy-plugin.ts
|
|
1755
|
+
function createMilaidyPlugin(config) {
|
|
1756
|
+
const workspaceDir = config?.workspaceDir ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
|
1757
|
+
const agentId = config?.agentId ?? "main";
|
|
1758
|
+
const sessionStorePath = config?.sessionStorePath ?? resolveDefaultSessionStorePath(agentId);
|
|
1759
|
+
return {
|
|
1760
|
+
name: "milaidy",
|
|
1761
|
+
description: "Milaidy workspace context, session keys, and compaction",
|
|
1762
|
+
providers: [
|
|
1763
|
+
createWorkspaceProvider({
|
|
1764
|
+
workspaceDir,
|
|
1765
|
+
maxCharsPerFile: config?.bootstrapMaxChars
|
|
1766
|
+
}),
|
|
1767
|
+
createSessionKeyProvider({ defaultAgentId: agentId }),
|
|
1768
|
+
...getSessionProviders({ storePath: sessionStorePath })
|
|
1769
|
+
],
|
|
1770
|
+
actions: [
|
|
1771
|
+
compactAction,
|
|
1772
|
+
memorySearchAction,
|
|
1773
|
+
memoryGetAction
|
|
1774
|
+
],
|
|
1775
|
+
events: { MESSAGE_RECEIVED: [async (payload) => {
|
|
1776
|
+
const { runtime, message } = payload;
|
|
1777
|
+
if (!message || !runtime) return;
|
|
1778
|
+
const meta = message.metadata ?? {};
|
|
1779
|
+
if (meta.sessionKey) return;
|
|
1780
|
+
const room = await runtime.getRoom(message.roomId);
|
|
1781
|
+
if (!room) return;
|
|
1782
|
+
const key = resolveSessionKeyFromRoom(agentId, room, {
|
|
1783
|
+
threadId: meta.threadId,
|
|
1784
|
+
groupId: meta.groupId,
|
|
1785
|
+
channel: meta.channel ?? room.source
|
|
1786
|
+
});
|
|
1787
|
+
message.metadata.sessionKey = key;
|
|
1788
|
+
}] }
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
const milaidyPlugin = createMilaidyPlugin();
|
|
1792
|
+
|
|
1793
|
+
//#endregion
|
|
1794
|
+
//#region src/eliza.ts
|
|
1795
|
+
/**
|
|
1796
|
+
* ElizaOS runtime entry point for Milaidy.
|
|
1797
|
+
*
|
|
1798
|
+
* Starts the ElizaOS agent runtime with Milaidy's plugin configuration.
|
|
1799
|
+
* Can be run directly via: node --import tsx src/eliza.ts
|
|
1800
|
+
* Or via the CLI: milaidy start
|
|
1801
|
+
*
|
|
1802
|
+
* @module eliza
|
|
1803
|
+
*/
|
|
1804
|
+
/**
|
|
1805
|
+
* Maps Milaidy channel config fields to the environment variable names
|
|
1806
|
+
* that ElizaOS plugins expect.
|
|
1807
|
+
*
|
|
1808
|
+
* Milaidy stores channel credentials under `config.channels.<name>.<field>`,
|
|
1809
|
+
* while ElizaOS plugins read them from process.env.
|
|
1810
|
+
*/
|
|
1811
|
+
const CHANNEL_ENV_MAP = {
|
|
1812
|
+
discord: { token: "DISCORD_BOT_TOKEN" },
|
|
1813
|
+
telegram: { botToken: "TELEGRAM_BOT_TOKEN" },
|
|
1814
|
+
slack: {
|
|
1815
|
+
botToken: "SLACK_BOT_TOKEN",
|
|
1816
|
+
appToken: "SLACK_APP_TOKEN",
|
|
1817
|
+
userToken: "SLACK_USER_TOKEN"
|
|
1818
|
+
},
|
|
1819
|
+
signal: { account: "SIGNAL_ACCOUNT" },
|
|
1820
|
+
msteams: {
|
|
1821
|
+
appId: "MSTEAMS_APP_ID",
|
|
1822
|
+
appPassword: "MSTEAMS_APP_PASSWORD"
|
|
1823
|
+
},
|
|
1824
|
+
mattermost: {
|
|
1825
|
+
botToken: "MATTERMOST_BOT_TOKEN",
|
|
1826
|
+
baseUrl: "MATTERMOST_BASE_URL"
|
|
1827
|
+
},
|
|
1828
|
+
googlechat: { serviceAccountKey: "GOOGLE_CHAT_SERVICE_ACCOUNT_KEY" }
|
|
1829
|
+
};
|
|
1830
|
+
/** Core plugins that should always be loaded. */
|
|
1831
|
+
const CORE_PLUGINS = [
|
|
1832
|
+
"@elizaos/plugin-sql",
|
|
1833
|
+
"@elizaos/plugin-agent-skills",
|
|
1834
|
+
"@elizaos/plugin-directives",
|
|
1835
|
+
"@elizaos/plugin-commands",
|
|
1836
|
+
"@elizaos/plugin-shell",
|
|
1837
|
+
"@elizaos/plugin-personality",
|
|
1838
|
+
"@elizaos/plugin-experience",
|
|
1839
|
+
"@elizaos/plugin-form"
|
|
1840
|
+
];
|
|
1841
|
+
/** Maps Milaidy channel names to ElizaOS plugin package names. */
|
|
1842
|
+
const CHANNEL_PLUGIN_MAP = {
|
|
1843
|
+
discord: "@elizaos/plugin-discord",
|
|
1844
|
+
telegram: "@elizaos/plugin-telegram",
|
|
1845
|
+
slack: "@elizaos/plugin-slack",
|
|
1846
|
+
whatsapp: "@elizaos/plugin-whatsapp",
|
|
1847
|
+
signal: "@elizaos/plugin-signal",
|
|
1848
|
+
imessage: "@elizaos/plugin-imessage",
|
|
1849
|
+
bluebubbles: "@elizaos/plugin-bluebubbles",
|
|
1850
|
+
msteams: "@elizaos/plugin-msteams",
|
|
1851
|
+
mattermost: "@elizaos/plugin-mattermost",
|
|
1852
|
+
googlechat: "@elizaos/plugin-google-chat"
|
|
1853
|
+
};
|
|
1854
|
+
/** Maps environment variable names to model-provider plugin packages. */
|
|
1855
|
+
const PROVIDER_PLUGIN_MAP = {
|
|
1856
|
+
ANTHROPIC_API_KEY: "@elizaos/plugin-anthropic",
|
|
1857
|
+
OPENAI_API_KEY: "@elizaos/plugin-openai",
|
|
1858
|
+
GOOGLE_API_KEY: "@elizaos/plugin-google-genai",
|
|
1859
|
+
GOOGLE_GENERATIVE_AI_API_KEY: "@elizaos/plugin-google-genai",
|
|
1860
|
+
GROQ_API_KEY: "@elizaos/plugin-groq",
|
|
1861
|
+
XAI_API_KEY: "@elizaos/plugin-xai",
|
|
1862
|
+
OPENROUTER_API_KEY: "@elizaos/plugin-openrouter",
|
|
1863
|
+
OLLAMA_BASE_URL: "@elizaos/plugin-ollama"
|
|
1864
|
+
};
|
|
1865
|
+
/** Optional feature plugins keyed by feature name. */
|
|
1866
|
+
const OPTIONAL_PLUGIN_MAP = {};
|
|
1867
|
+
function looksLikePlugin(value) {
|
|
1868
|
+
if (!value || typeof value !== "object") return false;
|
|
1869
|
+
const obj = value;
|
|
1870
|
+
return typeof obj.name === "string" && typeof obj.description === "string";
|
|
1871
|
+
}
|
|
1872
|
+
function extractPlugin(mod) {
|
|
1873
|
+
if (looksLikePlugin(mod.default)) return mod.default;
|
|
1874
|
+
if (looksLikePlugin(mod.plugin)) return mod.plugin;
|
|
1875
|
+
if (looksLikePlugin(mod)) return mod;
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Collect the set of plugin package names that should be loaded
|
|
1880
|
+
* based on config, environment variables, and feature flags.
|
|
1881
|
+
*/
|
|
1882
|
+
function collectPluginNames(config) {
|
|
1883
|
+
const pluginsToLoad = new Set(CORE_PLUGINS);
|
|
1884
|
+
const channels = config.channels ?? {};
|
|
1885
|
+
for (const [channelName, channelConfig] of Object.entries(channels)) if (channelConfig && typeof channelConfig === "object") {
|
|
1886
|
+
const pluginName = CHANNEL_PLUGIN_MAP[channelName];
|
|
1887
|
+
if (pluginName) pluginsToLoad.add(pluginName);
|
|
1888
|
+
}
|
|
1889
|
+
for (const [envKey, pluginName] of Object.entries(PROVIDER_PLUGIN_MAP)) if (process$1.env[envKey]) pluginsToLoad.add(pluginName);
|
|
1890
|
+
const pluginsConfig = config.plugins;
|
|
1891
|
+
if (pluginsConfig?.entries) {
|
|
1892
|
+
for (const [key, entry] of Object.entries(pluginsConfig.entries)) if (entry && typeof entry === "object" && entry.enabled !== false) {
|
|
1893
|
+
const pluginName = OPTIONAL_PLUGIN_MAP[key];
|
|
1894
|
+
if (pluginName) pluginsToLoad.add(pluginName);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
const features = config.features;
|
|
1898
|
+
if (features && typeof features === "object") {
|
|
1899
|
+
for (const [featureName, featureValue] of Object.entries(features)) if (featureValue === true || typeof featureValue === "object" && featureValue !== null && featureValue.enabled !== false) {
|
|
1900
|
+
const pluginName = OPTIONAL_PLUGIN_MAP[featureName];
|
|
1901
|
+
if (pluginName) pluginsToLoad.add(pluginName);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return pluginsToLoad;
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Resolve Milaidy plugins from config and auto-enable logic.
|
|
1908
|
+
* Returns an array of ElizaOS Plugin instances ready for AgentRuntime.
|
|
1909
|
+
*/
|
|
1910
|
+
async function resolvePlugins(config) {
|
|
1911
|
+
const plugins = [];
|
|
1912
|
+
const autoEnableResult = applyPluginAutoEnable({
|
|
1913
|
+
config,
|
|
1914
|
+
env: process$1.env
|
|
1915
|
+
});
|
|
1916
|
+
for (const change of autoEnableResult.changes) logger.info(`[milaidy] ${change}`);
|
|
1917
|
+
const pluginsToLoad = collectPluginNames(config);
|
|
1918
|
+
for (const pluginName of pluginsToLoad) try {
|
|
1919
|
+
const pluginInstance = extractPlugin(await import(pluginName));
|
|
1920
|
+
if (pluginInstance) {
|
|
1921
|
+
plugins.push({
|
|
1922
|
+
name: pluginName,
|
|
1923
|
+
plugin: pluginInstance
|
|
1924
|
+
});
|
|
1925
|
+
logger.info(`[milaidy] Loaded plugin: ${pluginName}`);
|
|
1926
|
+
} else logger.warn(`[milaidy] Plugin ${pluginName} did not export a valid Plugin object`);
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1929
|
+
logger.warn(`[milaidy] Could not load plugin ${pluginName}: ${msg}`);
|
|
1930
|
+
}
|
|
1931
|
+
return plugins;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Propagate channel credentials from Milaidy config into process.env so
|
|
1935
|
+
* that ElizaOS plugins can find them.
|
|
1936
|
+
*/
|
|
1937
|
+
function applyChannelSecretsToEnv(config) {
|
|
1938
|
+
const channels = config.channels ?? {};
|
|
1939
|
+
for (const [channelName, channelConfig] of Object.entries(channels)) {
|
|
1940
|
+
if (!channelConfig || typeof channelConfig !== "object") continue;
|
|
1941
|
+
const envMap = CHANNEL_ENV_MAP[channelName];
|
|
1942
|
+
if (!envMap) continue;
|
|
1943
|
+
const configObj = channelConfig;
|
|
1944
|
+
for (const [configField, envKey] of Object.entries(envMap)) {
|
|
1945
|
+
const value = configObj[configField];
|
|
1946
|
+
if (typeof value === "string" && value.trim() && !process$1.env[envKey]) process$1.env[envKey] = value;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Build an ElizaOS Character from the Milaidy config.
|
|
1952
|
+
*
|
|
1953
|
+
* Merges the deprecated `config.agent` object and the newer
|
|
1954
|
+
* `config.agents.defaults` into a single Character, collecting
|
|
1955
|
+
* secrets from environment variables along the way.
|
|
1956
|
+
*/
|
|
1957
|
+
function buildCharacterFromConfig(config) {
|
|
1958
|
+
const legacyAgent = config.agent;
|
|
1959
|
+
const name = legacyAgent?.name ?? config.ui?.assistant?.name ?? "Milaidy";
|
|
1960
|
+
const bio = legacyAgent?.bio ?? "An AI assistant powered by Milaidy and ElizaOS.";
|
|
1961
|
+
const systemPrompt = legacyAgent?.system_prompt;
|
|
1962
|
+
const secretKeys = [
|
|
1963
|
+
"ANTHROPIC_API_KEY",
|
|
1964
|
+
"OPENAI_API_KEY",
|
|
1965
|
+
"GOOGLE_API_KEY",
|
|
1966
|
+
"GOOGLE_GENERATIVE_AI_API_KEY",
|
|
1967
|
+
"GROQ_API_KEY",
|
|
1968
|
+
"XAI_API_KEY",
|
|
1969
|
+
"OPENROUTER_API_KEY",
|
|
1970
|
+
"OLLAMA_BASE_URL",
|
|
1971
|
+
"DISCORD_BOT_TOKEN",
|
|
1972
|
+
"TELEGRAM_BOT_TOKEN",
|
|
1973
|
+
"SLACK_BOT_TOKEN",
|
|
1974
|
+
"SLACK_APP_TOKEN",
|
|
1975
|
+
"SLACK_USER_TOKEN",
|
|
1976
|
+
"SIGNAL_ACCOUNT",
|
|
1977
|
+
"MSTEAMS_APP_ID",
|
|
1978
|
+
"MSTEAMS_APP_PASSWORD",
|
|
1979
|
+
"MATTERMOST_BOT_TOKEN",
|
|
1980
|
+
"MATTERMOST_BASE_URL"
|
|
1981
|
+
];
|
|
1982
|
+
const secrets = {};
|
|
1983
|
+
for (const key of secretKeys) {
|
|
1984
|
+
const value = process$1.env[key];
|
|
1985
|
+
if (value && value.trim()) secrets[key] = value;
|
|
1986
|
+
}
|
|
1987
|
+
return createCharacter({
|
|
1988
|
+
name,
|
|
1989
|
+
bio,
|
|
1990
|
+
system: systemPrompt,
|
|
1991
|
+
secrets
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Resolve the primary model identifier from Milaidy config.
|
|
1996
|
+
*
|
|
1997
|
+
* Milaidy stores the model under `agents.defaults.model.primary` as an
|
|
1998
|
+
* AgentModelListConfig object. Returns undefined when no model is
|
|
1999
|
+
* explicitly configured (ElizaOS falls back to whichever model
|
|
2000
|
+
* plugin is loaded).
|
|
2001
|
+
*/
|
|
2002
|
+
function resolvePrimaryModel(config) {
|
|
2003
|
+
const modelConfig = config.agents?.defaults?.model;
|
|
2004
|
+
if (!modelConfig) return void 0;
|
|
2005
|
+
return modelConfig.primary;
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Detect whether this is the first run (no agent name configured)
|
|
2009
|
+
* and prompt the user to pick a name for their agent.
|
|
2010
|
+
*
|
|
2011
|
+
* Saves the chosen name, a default bio, and a basic system prompt
|
|
2012
|
+
* back to the Milaidy config so subsequent runs skip this step.
|
|
2013
|
+
*/
|
|
2014
|
+
async function runFirstTimeSetup(config) {
|
|
2015
|
+
if (Boolean(config.agent?.name || config.ui?.assistant?.name)) return config;
|
|
2016
|
+
if (!process$1.stdin.isTTY) return config;
|
|
2017
|
+
const rl = readline.createInterface({
|
|
2018
|
+
input: process$1.stdin,
|
|
2019
|
+
output: process$1.stdout
|
|
2020
|
+
});
|
|
2021
|
+
const ask = (question) => new Promise((resolve) => {
|
|
2022
|
+
rl.question(question, (answer) => resolve(answer));
|
|
2023
|
+
});
|
|
2024
|
+
console.log("");
|
|
2025
|
+
const answer = await ask(" What should your agent be called? (Milaidy) ");
|
|
2026
|
+
rl.close();
|
|
2027
|
+
const name = answer.trim() || "Milaidy";
|
|
2028
|
+
const updated = {
|
|
2029
|
+
...config,
|
|
2030
|
+
agent: {
|
|
2031
|
+
...config.agent,
|
|
2032
|
+
name,
|
|
2033
|
+
bio: `An autonomous agent`,
|
|
2034
|
+
system_prompt: `You are ${name}, an autonomous AI agent powered by ElizaOS. You are helpful, concise, and proactive.`
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
saveMilaidyConfig(updated);
|
|
2038
|
+
console.log(` Agent "${name}" created.\n`);
|
|
2039
|
+
return updated;
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Start the ElizaOS runtime with Milaidy's configuration.
|
|
2043
|
+
*/
|
|
2044
|
+
async function startEliza() {
|
|
2045
|
+
logger.info(`Milaidy v${VERSION} ā starting ElizaOS runtime`);
|
|
2046
|
+
let config;
|
|
2047
|
+
try {
|
|
2048
|
+
config = await loadMilaidyConfig();
|
|
2049
|
+
} catch {
|
|
2050
|
+
logger.warn("[milaidy] No config found, using defaults");
|
|
2051
|
+
config = {};
|
|
2052
|
+
}
|
|
2053
|
+
config = await runFirstTimeSetup(config);
|
|
2054
|
+
applyChannelSecretsToEnv(config);
|
|
2055
|
+
const character = buildCharacterFromConfig(config);
|
|
2056
|
+
logger.info(`[milaidy] Agent character: ${character.name ?? "Milaidy"}`);
|
|
2057
|
+
const primaryModel = resolvePrimaryModel(config);
|
|
2058
|
+
if (primaryModel) logger.info(`[milaidy] Primary model: ${primaryModel}`);
|
|
2059
|
+
const workspaceDir = config.agents?.defaults?.workspace ?? resolveDefaultAgentWorkspaceDir();
|
|
2060
|
+
logger.info(`[milaidy] Agent workspace: ${workspaceDir}`);
|
|
2061
|
+
await ensureAgentWorkspace({
|
|
2062
|
+
dir: workspaceDir,
|
|
2063
|
+
ensureBootstrapFiles: true
|
|
2064
|
+
});
|
|
2065
|
+
const agentId = character.name?.toLowerCase().replace(/\s+/g, "-") ?? "main";
|
|
2066
|
+
const milaidyPlugin = createMilaidyPlugin({
|
|
2067
|
+
workspaceDir,
|
|
2068
|
+
bootstrapMaxChars: config.agents?.defaults?.bootstrapMaxChars,
|
|
2069
|
+
agentId
|
|
2070
|
+
});
|
|
2071
|
+
const resolvedPlugins = await resolvePlugins(config);
|
|
2072
|
+
logger.info(`[milaidy] Resolved ${resolvedPlugins.length} plugins`);
|
|
2073
|
+
logger.info(`[milaidy] Plugins: ${resolvedPlugins.map((p) => p.name.replace("@elizaos/", "")).join(", ")}`);
|
|
2074
|
+
if (resolvedPlugins.length === 0) {
|
|
2075
|
+
logger.error("[milaidy] No plugins loaded ā at least one model provider plugin is required");
|
|
2076
|
+
logger.error("[milaidy] Set an API key (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY) in your environment");
|
|
2077
|
+
throw new Error("No plugins loaded");
|
|
2078
|
+
}
|
|
2079
|
+
const sqlPlugin = resolvedPlugins.find((p) => p.name === "@elizaos/plugin-sql");
|
|
2080
|
+
const runtime = new AgentRuntime({
|
|
2081
|
+
character,
|
|
2082
|
+
plugins: [milaidyPlugin, ...resolvedPlugins.filter((p) => p.name !== "@elizaos/plugin-sql").map((p) => p.plugin)],
|
|
2083
|
+
settings: { ...primaryModel ? { MODEL_PROVIDER: primaryModel } : {} }
|
|
2084
|
+
});
|
|
2085
|
+
if (sqlPlugin) await runtime.registerPlugin(sqlPlugin.plugin);
|
|
2086
|
+
logger.info("[milaidy] Initializing AgentRuntime...");
|
|
2087
|
+
await runtime.initialize();
|
|
2088
|
+
logger.info("[milaidy] AgentRuntime initialized successfully");
|
|
2089
|
+
let isShuttingDown = false;
|
|
2090
|
+
const shutdown = async () => {
|
|
2091
|
+
if (isShuttingDown) return;
|
|
2092
|
+
isShuttingDown = true;
|
|
2093
|
+
logger.info("[milaidy] Shutting down...");
|
|
2094
|
+
try {
|
|
2095
|
+
await runtime.stop();
|
|
2096
|
+
} catch (err) {
|
|
2097
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2098
|
+
logger.warn(`[milaidy] Error during shutdown: ${msg}`);
|
|
2099
|
+
}
|
|
2100
|
+
process$1.exit(0);
|
|
2101
|
+
};
|
|
2102
|
+
process$1.on("SIGINT", () => void shutdown());
|
|
2103
|
+
process$1.on("SIGTERM", () => void shutdown());
|
|
2104
|
+
try {
|
|
2105
|
+
const internalHooksConfig = config.hooks?.internal;
|
|
2106
|
+
const hooksResult = await loadHooks({
|
|
2107
|
+
workspacePath: workspaceDir,
|
|
2108
|
+
internalConfig: internalHooksConfig,
|
|
2109
|
+
milaidyConfig: config
|
|
2110
|
+
});
|
|
2111
|
+
if (hooksResult.registered > 0) logger.info(`[milaidy] Hooks: ${hooksResult.registered} registered`);
|
|
2112
|
+
await triggerHook(createHookEvent("gateway", "startup", "system", { cfg: config }));
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2115
|
+
logger.warn(`[milaidy] Hooks system could not load: ${msg}`);
|
|
2116
|
+
}
|
|
2117
|
+
logger.info("[milaidy] Runtime is ready.");
|
|
2118
|
+
const agentName = character.name ?? "Milaidy";
|
|
2119
|
+
const userId = crypto.randomUUID();
|
|
2120
|
+
const roomId = stringToUuid(`${agentName}-chat-room`);
|
|
2121
|
+
const worldId = stringToUuid(`${agentName}-chat-world`);
|
|
2122
|
+
try {
|
|
2123
|
+
await runtime.ensureConnection({
|
|
2124
|
+
entityId: userId,
|
|
2125
|
+
roomId,
|
|
2126
|
+
worldId,
|
|
2127
|
+
userName: "User",
|
|
2128
|
+
source: "cli",
|
|
2129
|
+
channelId: `${agentName}-chat`,
|
|
2130
|
+
type: ChannelType.DM
|
|
2131
|
+
});
|
|
2132
|
+
} catch (err) {
|
|
2133
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2134
|
+
logger.warn(`[milaidy] Could not establish chat room, retrying with fresh IDs: ${msg}`);
|
|
2135
|
+
const freshRoomId = crypto.randomUUID();
|
|
2136
|
+
const freshWorldId = crypto.randomUUID();
|
|
2137
|
+
await runtime.ensureConnection({
|
|
2138
|
+
entityId: userId,
|
|
2139
|
+
roomId: freshRoomId,
|
|
2140
|
+
worldId: freshWorldId,
|
|
2141
|
+
userName: "User",
|
|
2142
|
+
source: "cli",
|
|
2143
|
+
channelId: `${agentName}-chat`,
|
|
2144
|
+
type: ChannelType.DM
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
const rl = readline.createInterface({
|
|
2148
|
+
input: process$1.stdin,
|
|
2149
|
+
output: process$1.stdout
|
|
2150
|
+
});
|
|
2151
|
+
console.log(`\nš¬ Chat with ${agentName} (type 'exit' to quit)\n`);
|
|
2152
|
+
const prompt = () => {
|
|
2153
|
+
rl.question("You: ", async (input) => {
|
|
2154
|
+
const text = input.trim();
|
|
2155
|
+
if (text.toLowerCase() === "exit" || text.toLowerCase() === "quit") {
|
|
2156
|
+
console.log("\nGoodbye!");
|
|
2157
|
+
rl.close();
|
|
2158
|
+
await runtime.stop();
|
|
2159
|
+
process$1.exit(0);
|
|
2160
|
+
}
|
|
2161
|
+
if (!text) {
|
|
2162
|
+
prompt();
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
const message = createMessageMemory({
|
|
2166
|
+
id: crypto.randomUUID(),
|
|
2167
|
+
entityId: userId,
|
|
2168
|
+
roomId,
|
|
2169
|
+
content: {
|
|
2170
|
+
text,
|
|
2171
|
+
source: "client_chat",
|
|
2172
|
+
channelType: ChannelType.DM
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
process$1.stdout.write(`${agentName}: `);
|
|
2176
|
+
await runtime?.messageService?.handleMessage(runtime, message, async (content) => {
|
|
2177
|
+
if (content?.text) process$1.stdout.write(content.text);
|
|
2178
|
+
return [];
|
|
2179
|
+
});
|
|
2180
|
+
console.log("\n");
|
|
2181
|
+
prompt();
|
|
2182
|
+
});
|
|
2183
|
+
};
|
|
2184
|
+
prompt();
|
|
2185
|
+
}
|
|
2186
|
+
if (import.meta.url === `file://${process$1.argv[1]}` || process$1.argv[1]?.endsWith("/eliza.ts") || process$1.argv[1]?.endsWith("/eliza.js")) startEliza().catch((err) => {
|
|
2187
|
+
console.error("[milaidy] Fatal error:", err instanceof Error ? err.stack ?? err.message : err);
|
|
2188
|
+
process$1.exit(1);
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
//#endregion
|
|
2192
|
+
export { startEliza };
|