@onmars/lunar-core 0.1.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 +21 -0
- package/README.md +13 -0
- package/package.json +32 -0
- package/src/__tests__/clear-command.test.ts +214 -0
- package/src/__tests__/command-handler.test.ts +169 -0
- package/src/__tests__/compact-command.test.ts +80 -0
- package/src/__tests__/config-command.test.ts +240 -0
- package/src/__tests__/config-loader.test.ts +1512 -0
- package/src/__tests__/config.test.ts +429 -0
- package/src/__tests__/cron-command.test.ts +418 -0
- package/src/__tests__/cron-parser.test.ts +259 -0
- package/src/__tests__/daemon.test.ts +346 -0
- package/src/__tests__/dedup.test.ts +404 -0
- package/src/__tests__/e2e-sanitization.ts +168 -0
- package/src/__tests__/e2e-skill-loader.test.ts +176 -0
- package/src/__tests__/fixtures/AGENTS.md +4 -0
- package/src/__tests__/fixtures/IDENTITY.md +2 -0
- package/src/__tests__/fixtures/SOUL.md +3 -0
- package/src/__tests__/fixtures/moons/athena/IDENTITY.md +2 -0
- package/src/__tests__/fixtures/moons/athena/SOUL.md +3 -0
- package/src/__tests__/fixtures/moons/hermes/SOUL.md +3 -0
- package/src/__tests__/fixtures/skills/brain/SKILL.md +6 -0
- package/src/__tests__/fixtures/skills/empty/SKILL.md +3 -0
- package/src/__tests__/fixtures/skills/multiline/SKILL.md +7 -0
- package/src/__tests__/fixtures/skills/no-desc/SKILL.md +5 -0
- package/src/__tests__/fixtures/skills/notion/SKILL.md +6 -0
- package/src/__tests__/fixtures/skills/quoted/SKILL.md +6 -0
- package/src/__tests__/hook-runner.test.ts +1689 -0
- package/src/__tests__/input-sanitization.test.ts +367 -0
- package/src/__tests__/logger.test.ts +163 -0
- package/src/__tests__/memory-orchestrator.test.ts +552 -0
- package/src/__tests__/model-catalog.test.ts +215 -0
- package/src/__tests__/model-command.test.ts +185 -0
- package/src/__tests__/moon-loader.test.ts +398 -0
- package/src/__tests__/ping-command.test.ts +85 -0
- package/src/__tests__/plugin.test.ts +258 -0
- package/src/__tests__/remind-command.test.ts +368 -0
- package/src/__tests__/reset-command.test.ts +92 -0
- package/src/__tests__/router.test.ts +1246 -0
- package/src/__tests__/scheduler.test.ts +469 -0
- package/src/__tests__/security.test.ts +214 -0
- package/src/__tests__/session-meta.test.ts +101 -0
- package/src/__tests__/session-tracker.test.ts +389 -0
- package/src/__tests__/session.test.ts +241 -0
- package/src/__tests__/skill-loader.test.ts +153 -0
- package/src/__tests__/status-command.test.ts +153 -0
- package/src/__tests__/stop-command.test.ts +60 -0
- package/src/__tests__/think-command.test.ts +146 -0
- package/src/__tests__/usage-api.test.ts +222 -0
- package/src/__tests__/usage-command-api-fail.test.ts +48 -0
- package/src/__tests__/usage-command-no-oauth.test.ts +48 -0
- package/src/__tests__/usage-command.test.ts +173 -0
- package/src/__tests__/whoami-command.test.ts +124 -0
- package/src/index.ts +122 -0
- package/src/lib/command-handler.ts +135 -0
- package/src/lib/commands/clear.ts +69 -0
- package/src/lib/commands/compact.ts +14 -0
- package/src/lib/commands/config-show.ts +49 -0
- package/src/lib/commands/cron.ts +118 -0
- package/src/lib/commands/help.ts +26 -0
- package/src/lib/commands/model.ts +71 -0
- package/src/lib/commands/ping.ts +24 -0
- package/src/lib/commands/remind.ts +75 -0
- package/src/lib/commands/status.ts +118 -0
- package/src/lib/commands/stop.ts +18 -0
- package/src/lib/commands/think.ts +42 -0
- package/src/lib/commands/usage.ts +56 -0
- package/src/lib/commands/whoami.ts +23 -0
- package/src/lib/config-loader.ts +1449 -0
- package/src/lib/config.ts +202 -0
- package/src/lib/cron-parser.ts +388 -0
- package/src/lib/daemon.ts +216 -0
- package/src/lib/dedup.ts +414 -0
- package/src/lib/hook-runner.ts +1270 -0
- package/src/lib/logger.ts +55 -0
- package/src/lib/memory-orchestrator.ts +415 -0
- package/src/lib/model-catalog.ts +240 -0
- package/src/lib/moon-loader.ts +291 -0
- package/src/lib/plugin.ts +148 -0
- package/src/lib/router.ts +1135 -0
- package/src/lib/scheduler.ts +422 -0
- package/src/lib/security.ts +259 -0
- package/src/lib/session-tracker.ts +222 -0
- package/src/lib/session.ts +158 -0
- package/src/lib/skill-loader.ts +166 -0
- package/src/lib/usage-api.ts +145 -0
- package/src/types/agent.ts +86 -0
- package/src/types/channel.ts +93 -0
- package/src/types/index.ts +32 -0
- package/src/types/memory.ts +92 -0
- package/src/types/moon.ts +56 -0
- package/src/types/voice.ts +74 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { basename, resolve } from 'node:path'
|
|
3
|
+
import type { ChannelPersona, Moon } from '../types/moon'
|
|
4
|
+
import { log } from './logger'
|
|
5
|
+
import type { SkillLoader } from './skill-loader'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MoonLoader — Discovers, loads, and manages Moon personalities.
|
|
9
|
+
*
|
|
10
|
+
* Conventions:
|
|
11
|
+
* - Default moon: workspace root (SOUL.md, AGENTS.md, IDENTITY.md)
|
|
12
|
+
* - Additional moons: `moons/<name>/` subdirectories
|
|
13
|
+
* - Minimum requirement: SOUL.md must exist
|
|
14
|
+
* - `moons` config section only needed for custom paths
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const loader = new MoonLoader()
|
|
18
|
+
* await loader.loadAll('~/.lunar')
|
|
19
|
+
* const moon = loader.resolve(channelPersona)
|
|
20
|
+
* const prompt = loader.buildSystemPrompt(moon, channelPersona)
|
|
21
|
+
*/
|
|
22
|
+
export class MoonLoader {
|
|
23
|
+
private moons = new Map<string, Moon>()
|
|
24
|
+
private defaultMoonName = ''
|
|
25
|
+
private skillManifest = ''
|
|
26
|
+
private skillLoader?: SkillLoader
|
|
27
|
+
|
|
28
|
+
/** Get number of loaded moons */
|
|
29
|
+
get size(): number {
|
|
30
|
+
return this.moons.size
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Set the skills manifest to inject into system prompts */
|
|
34
|
+
setSkillManifest(manifest: string): void {
|
|
35
|
+
this.skillManifest = manifest
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Set the full SkillLoader for per-channel skill filtering */
|
|
39
|
+
setSkillLoader(loader: SkillLoader): void {
|
|
40
|
+
this.skillLoader = loader
|
|
41
|
+
this.skillManifest = loader.getManifest() // backward compat fallback
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load all moons from workspace.
|
|
46
|
+
*
|
|
47
|
+
* 1. Root workspace → default moon
|
|
48
|
+
* 2. `moons/` subdirectories → additional moons (auto-discovered)
|
|
49
|
+
* 3. `moonsConfig` overrides → custom paths for specific moons
|
|
50
|
+
*
|
|
51
|
+
* @param workspacePath — root workspace directory
|
|
52
|
+
* @param moonsConfig — optional name→path mapping for custom moon locations
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* Moon config entry for loadAll.
|
|
56
|
+
* Includes path + optional model/context1m overrides.
|
|
57
|
+
*/
|
|
58
|
+
async loadAll(
|
|
59
|
+
workspacePath: string,
|
|
60
|
+
moonsConfig?: Record<string, string | { path: string; model?: string; context1m?: boolean }>,
|
|
61
|
+
defaultMoonOverrides?: { model?: string; context1m?: boolean },
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
this.moons.clear()
|
|
64
|
+
this.defaultMoonName = ''
|
|
65
|
+
|
|
66
|
+
// 1. Load root moon (default)
|
|
67
|
+
const rootSoul = resolve(workspacePath, 'SOUL.md')
|
|
68
|
+
if (existsSync(rootSoul)) {
|
|
69
|
+
const rootMoon = await this.loadFromPath(workspacePath, undefined, defaultMoonOverrides)
|
|
70
|
+
this.moons.set(rootMoon.name, rootMoon)
|
|
71
|
+
this.defaultMoonName = rootMoon.name
|
|
72
|
+
log.info(
|
|
73
|
+
{ name: rootMoon.name, path: workspacePath, model: rootMoon.model ?? '(default)' },
|
|
74
|
+
'Default moon loaded',
|
|
75
|
+
)
|
|
76
|
+
} else {
|
|
77
|
+
log.warn({ path: workspacePath }, 'No SOUL.md in workspace root — no default moon')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2. Auto-discover moons in moons/ directory
|
|
81
|
+
const moonsDir = resolve(workspacePath, 'moons')
|
|
82
|
+
if (existsSync(moonsDir) && statSync(moonsDir).isDirectory()) {
|
|
83
|
+
for (const entry of readdirSync(moonsDir)) {
|
|
84
|
+
const moonPath = resolve(moonsDir, entry)
|
|
85
|
+
if (!statSync(moonPath).isDirectory()) continue
|
|
86
|
+
if (!existsSync(resolve(moonPath, 'SOUL.md'))) continue
|
|
87
|
+
|
|
88
|
+
// Skip if already loaded via moonsConfig (config takes precedence)
|
|
89
|
+
if (moonsConfig?.[entry]) continue
|
|
90
|
+
|
|
91
|
+
const moon = await this.loadFromPath(moonPath, entry)
|
|
92
|
+
this.moons.set(entry, moon)
|
|
93
|
+
log.info({ name: entry, path: moonPath }, 'Moon auto-discovered')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. Load explicitly configured moons (override auto-discovered)
|
|
98
|
+
if (moonsConfig) {
|
|
99
|
+
for (const [name, value] of Object.entries(moonsConfig)) {
|
|
100
|
+
const moonPath = typeof value === 'string' ? value : value.path
|
|
101
|
+
const overrides =
|
|
102
|
+
typeof value === 'object' ? { model: value.model, context1m: value.context1m } : undefined
|
|
103
|
+
const fullPath = resolve(workspacePath, moonPath)
|
|
104
|
+
|
|
105
|
+
if (!existsSync(resolve(fullPath, 'SOUL.md'))) {
|
|
106
|
+
log.warn({ name, path: fullPath }, 'Configured moon has no SOUL.md — skipping')
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const moon = await this.loadFromPath(fullPath, name, overrides)
|
|
111
|
+
this.moons.set(name, moon)
|
|
112
|
+
log.info(
|
|
113
|
+
{ name, path: fullPath, model: moon.model ?? '(default)' },
|
|
114
|
+
'Moon loaded from config',
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
log.info(
|
|
120
|
+
{ total: this.moons.size, default: this.defaultMoonName, names: [...this.moons.keys()] },
|
|
121
|
+
'All moons loaded',
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Load a single moon from a directory path.
|
|
127
|
+
* Backward-compatible: can be used standalone for single-moon setups.
|
|
128
|
+
*/
|
|
129
|
+
async load(workspacePath: string): Promise<Moon> {
|
|
130
|
+
const moon = await this.loadFromPath(workspacePath)
|
|
131
|
+
this.moons.set(moon.name, moon)
|
|
132
|
+
if (!this.defaultMoonName) this.defaultMoonName = moon.name
|
|
133
|
+
return moon
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Get a loaded moon by name */
|
|
137
|
+
get(name: string): Moon | undefined {
|
|
138
|
+
return this.moons.get(name)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Get the default (root) moon */
|
|
142
|
+
getDefault(): Moon | undefined {
|
|
143
|
+
return this.defaultMoonName ? this.moons.get(this.defaultMoonName) : undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** List all loaded moon names */
|
|
147
|
+
list(): string[] {
|
|
148
|
+
return [...this.moons.keys()]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolve which moon should handle a channel.
|
|
153
|
+
*
|
|
154
|
+
* Priority:
|
|
155
|
+
* 1. ChannelPersona.moon → look up by name
|
|
156
|
+
* 2. No moon specified → use default (root) moon
|
|
157
|
+
* 3. Not found → undefined (caller should handle gracefully)
|
|
158
|
+
*/
|
|
159
|
+
resolve(channel?: ChannelPersona): Moon | undefined {
|
|
160
|
+
if (!channel?.moon) return this.getDefault()
|
|
161
|
+
|
|
162
|
+
const moon = this.moons.get(channel.moon)
|
|
163
|
+
if (!moon) {
|
|
164
|
+
log.warn(
|
|
165
|
+
{ requested: channel.moon, channel: channel.name, available: this.list() },
|
|
166
|
+
'Moon not found for channel — falling back to default',
|
|
167
|
+
)
|
|
168
|
+
return this.getDefault()
|
|
169
|
+
}
|
|
170
|
+
return moon
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Build the complete system prompt for a channel.
|
|
175
|
+
*
|
|
176
|
+
* Structure:
|
|
177
|
+
* # Moon: <name>
|
|
178
|
+
* ## SOUL.md — personality core
|
|
179
|
+
* ## AGENTS.md — operational rules
|
|
180
|
+
* ## IDENTITY.md — identity metadata
|
|
181
|
+
* ## Workspace — available files
|
|
182
|
+
* ## Channel: <name> — per-channel overlay (if present)
|
|
183
|
+
*/
|
|
184
|
+
buildSystemPrompt(moon: Moon, channel?: ChannelPersona): string {
|
|
185
|
+
const sections: string[] = []
|
|
186
|
+
|
|
187
|
+
// Header
|
|
188
|
+
sections.push(`# Moon: ${moon.name}`)
|
|
189
|
+
|
|
190
|
+
// Soul (personality core — always first)
|
|
191
|
+
if (moon.soul.trim()) {
|
|
192
|
+
sections.push(`## SOUL.md\n${moon.soul.trim()}`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Agents (operational rules)
|
|
196
|
+
if (moon.agents.trim()) {
|
|
197
|
+
sections.push(`## AGENTS.md\n${moon.agents.trim()}`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Identity
|
|
201
|
+
if (moon.identity.trim()) {
|
|
202
|
+
sections.push(`## IDENTITY.md\n${moon.identity.trim()}`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Workspace reference
|
|
206
|
+
const workspaceLines = [
|
|
207
|
+
'## Workspace',
|
|
208
|
+
`Your workspace is: ${moon.path}`,
|
|
209
|
+
'',
|
|
210
|
+
'Files available on-demand (read with tools when needed):',
|
|
211
|
+
'- `USER.md` — Information about the user',
|
|
212
|
+
'- `MEMORY.md` — Persistent context and decisions',
|
|
213
|
+
'- `memory/` — Session notes and daily logs',
|
|
214
|
+
]
|
|
215
|
+
sections.push(workspaceLines.join('\n'))
|
|
216
|
+
|
|
217
|
+
// Skills manifest — per-channel filter if SkillLoader available, else global manifest
|
|
218
|
+
const manifest = this.skillLoader
|
|
219
|
+
? this.skillLoader.getManifest(channel?.skills)
|
|
220
|
+
: this.skillManifest
|
|
221
|
+
if (manifest) {
|
|
222
|
+
sections.push(manifest.trim())
|
|
223
|
+
}
|
|
224
|
+
// Channel persona overlay
|
|
225
|
+
if (channel) {
|
|
226
|
+
const channelLines = [`## Channel: #${channel.name}`]
|
|
227
|
+
if (channel.tone) channelLines.push(`- **Tone:** ${channel.tone}`)
|
|
228
|
+
if (channel.focus) channelLines.push(`- **Focus:** ${channel.focus}`)
|
|
229
|
+
if (channel.instructions) {
|
|
230
|
+
channelLines.push('')
|
|
231
|
+
channelLines.push(channel.instructions)
|
|
232
|
+
}
|
|
233
|
+
sections.push(channelLines.join('\n'))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return sections.join('\n\n---\n\n')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Find a ChannelPersona by Discord channel ID from a list of channel configs.
|
|
241
|
+
* Returns undefined if no matching channel is configured (open routing).
|
|
242
|
+
*/
|
|
243
|
+
findChannel(discordChannelId: string, channels: ChannelPersona[]): ChannelPersona | undefined {
|
|
244
|
+
return channels.find((c) => c.id === discordChannelId)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --- Private ---
|
|
248
|
+
|
|
249
|
+
private async loadFromPath(
|
|
250
|
+
moonPath: string,
|
|
251
|
+
nameOverride?: string,
|
|
252
|
+
overrides?: { model?: string; context1m?: boolean },
|
|
253
|
+
): Promise<Moon> {
|
|
254
|
+
const soulPath = resolve(moonPath, 'SOUL.md')
|
|
255
|
+
const agentsPath = resolve(moonPath, 'AGENTS.md')
|
|
256
|
+
const identityPath = resolve(moonPath, 'IDENTITY.md')
|
|
257
|
+
|
|
258
|
+
if (!existsSync(soulPath)) {
|
|
259
|
+
throw new Error(`Missing required SOUL.md at ${soulPath}`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const soul = await Bun.file(soulPath).text()
|
|
263
|
+
const agents = existsSync(agentsPath) ? await Bun.file(agentsPath).text() : ''
|
|
264
|
+
const identity = existsSync(identityPath) ? await Bun.file(identityPath).text() : ''
|
|
265
|
+
|
|
266
|
+
if (!soul.trim()) {
|
|
267
|
+
log.warn({ path: soulPath }, 'SOUL.md exists but is empty — moon will have no personality')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Display name: prefer IDENTITY.md, fall back to nameOverride or dir name
|
|
271
|
+
const name = this.parseName(identity, moonPath) || (nameOverride ?? basename(moonPath))
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
name,
|
|
275
|
+
soul,
|
|
276
|
+
agents,
|
|
277
|
+
identity,
|
|
278
|
+
path: moonPath,
|
|
279
|
+
model: overrides?.model,
|
|
280
|
+
context1m: overrides?.context1m,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private parseName(identity: string, moonPath: string): string {
|
|
285
|
+
const nameMatch = identity.match(/\*\*Name:\*\*\s*(.+)/i)
|
|
286
|
+
if (nameMatch?.[1]) {
|
|
287
|
+
return nameMatch[1].trim()
|
|
288
|
+
}
|
|
289
|
+
return basename(moonPath)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Agent } from '../types/agent'
|
|
2
|
+
import type { Channel } from '../types/channel'
|
|
3
|
+
import type { MemoryProvider } from '../types/memory'
|
|
4
|
+
import type { STTProvider, TTSProvider } from '../types/voice'
|
|
5
|
+
import { log } from './logger'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Plugin registry — the central hub that connects all adapters.
|
|
9
|
+
*
|
|
10
|
+
* Register channels, agents, memory providers, and voice providers here.
|
|
11
|
+
* The router uses the registry to dispatch messages.
|
|
12
|
+
*/
|
|
13
|
+
export class PluginRegistry {
|
|
14
|
+
private channels = new Map<string, Channel>()
|
|
15
|
+
private agents = new Map<string, Agent>()
|
|
16
|
+
private memoryProviders = new Map<string, MemoryProvider>()
|
|
17
|
+
private ttsProviders = new Map<string, TTSProvider>()
|
|
18
|
+
private sttProviders = new Map<string, STTProvider>()
|
|
19
|
+
|
|
20
|
+
private defaultAgentId?: string
|
|
21
|
+
|
|
22
|
+
// --- Registration ---
|
|
23
|
+
|
|
24
|
+
registerChannel(channel: Channel): void {
|
|
25
|
+
if (this.channels.has(channel.id)) {
|
|
26
|
+
throw new Error(`Channel '${channel.id}' already registered`)
|
|
27
|
+
}
|
|
28
|
+
this.channels.set(channel.id, channel)
|
|
29
|
+
log.info({ channel: channel.id }, 'Channel registered')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
registerAgent(agent: Agent, isDefault = false): void {
|
|
33
|
+
if (this.agents.has(agent.id)) {
|
|
34
|
+
throw new Error(`Agent '${agent.id}' already registered`)
|
|
35
|
+
}
|
|
36
|
+
this.agents.set(agent.id, agent)
|
|
37
|
+
if (isDefault || !this.defaultAgentId) {
|
|
38
|
+
this.defaultAgentId = agent.id
|
|
39
|
+
}
|
|
40
|
+
log.info({ agent: agent.id, default: isDefault }, 'Agent registered')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
registerMemory(provider: MemoryProvider): void {
|
|
44
|
+
this.memoryProviders.set(provider.id, provider)
|
|
45
|
+
log.info({ memory: provider.id }, 'Memory provider registered')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
registerTTS(provider: TTSProvider): void {
|
|
49
|
+
this.ttsProviders.set(provider.id, provider)
|
|
50
|
+
log.info({ tts: provider.id }, 'TTS provider registered')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
registerSTT(provider: STTProvider): void {
|
|
54
|
+
this.sttProviders.set(provider.id, provider)
|
|
55
|
+
log.info({ stt: provider.id }, 'STT provider registered')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Retrieval ---
|
|
59
|
+
|
|
60
|
+
getChannel(id: string): Channel | undefined {
|
|
61
|
+
return this.channels.get(id)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getAgent(id?: string): Agent | undefined {
|
|
65
|
+
if (id) return this.agents.get(id)
|
|
66
|
+
if (this.defaultAgentId) return this.agents.get(this.defaultAgentId)
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getMemory(id: string): MemoryProvider | undefined {
|
|
71
|
+
return this.memoryProviders.get(id)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getTTS(id?: string): TTSProvider | undefined {
|
|
75
|
+
if (id) return this.ttsProviders.get(id)
|
|
76
|
+
// Return first available
|
|
77
|
+
const [first] = this.ttsProviders.values()
|
|
78
|
+
return first
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getSTT(id?: string): STTProvider | undefined {
|
|
82
|
+
if (id) return this.sttProviders.get(id)
|
|
83
|
+
const [first] = this.sttProviders.values()
|
|
84
|
+
return first
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Lifecycle ---
|
|
88
|
+
|
|
89
|
+
/** Initialize all registered plugins */
|
|
90
|
+
async initAll(): Promise<void> {
|
|
91
|
+
const inits: Promise<void>[] = []
|
|
92
|
+
|
|
93
|
+
for (const agent of this.agents.values()) {
|
|
94
|
+
inits.push(
|
|
95
|
+
agent.init().catch((e) => log.error({ agent: agent.id, err: e }, 'Agent init failed')),
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
for (const channel of this.channels.values()) {
|
|
99
|
+
inits.push(
|
|
100
|
+
channel
|
|
101
|
+
.connect()
|
|
102
|
+
.catch((e) => log.error({ channel: channel.id, err: e }, 'Channel connect failed')),
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
for (const mem of this.memoryProviders.values()) {
|
|
106
|
+
inits.push(
|
|
107
|
+
mem.init().catch((e) => log.error({ memory: mem.id, err: e }, 'Memory init failed')),
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
for (const tts of this.ttsProviders.values()) {
|
|
111
|
+
inits.push(tts.init().catch((e) => log.error({ tts: tts.id, err: e }, 'TTS init failed')))
|
|
112
|
+
}
|
|
113
|
+
for (const stt of this.sttProviders.values()) {
|
|
114
|
+
inits.push(stt.init().catch((e) => log.error({ stt: stt.id, err: e }, 'STT init failed')))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await Promise.allSettled(inits)
|
|
118
|
+
log.info('All plugins initialized')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Gracefully destroy all plugins */
|
|
122
|
+
async destroyAll(): Promise<void> {
|
|
123
|
+
const destroys: Promise<void>[] = []
|
|
124
|
+
|
|
125
|
+
for (const channel of this.channels.values()) destroys.push(channel.disconnect())
|
|
126
|
+
for (const agent of this.agents.values()) destroys.push(agent.destroy())
|
|
127
|
+
for (const mem of this.memoryProviders.values()) destroys.push(mem.destroy())
|
|
128
|
+
for (const tts of this.ttsProviders.values()) destroys.push(tts.destroy())
|
|
129
|
+
for (const stt of this.sttProviders.values()) destroys.push(stt.destroy())
|
|
130
|
+
|
|
131
|
+
await Promise.allSettled(destroys)
|
|
132
|
+
log.info('All plugins destroyed')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Inspection ---
|
|
136
|
+
|
|
137
|
+
listChannels(): string[] {
|
|
138
|
+
return [...this.channels.keys()]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
listAgents(): string[] {
|
|
142
|
+
return [...this.agents.keys()]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
listMemory(): string[] {
|
|
146
|
+
return [...this.memoryProviders.keys()]
|
|
147
|
+
}
|
|
148
|
+
}
|