@johpaz/hive 1.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/CONTRIBUTING.md +44 -0
- package/README.md +310 -0
- package/package.json +96 -0
- package/packages/cli/package.json +28 -0
- package/packages/cli/src/commands/agent-run.ts +168 -0
- package/packages/cli/src/commands/agents.ts +398 -0
- package/packages/cli/src/commands/chat.ts +142 -0
- package/packages/cli/src/commands/config.ts +50 -0
- package/packages/cli/src/commands/cron.ts +161 -0
- package/packages/cli/src/commands/dev.ts +95 -0
- package/packages/cli/src/commands/doctor.ts +133 -0
- package/packages/cli/src/commands/gateway.ts +443 -0
- package/packages/cli/src/commands/logs.ts +57 -0
- package/packages/cli/src/commands/mcp.ts +175 -0
- package/packages/cli/src/commands/message.ts +77 -0
- package/packages/cli/src/commands/onboard.ts +1868 -0
- package/packages/cli/src/commands/security.ts +144 -0
- package/packages/cli/src/commands/service.ts +50 -0
- package/packages/cli/src/commands/sessions.ts +116 -0
- package/packages/cli/src/commands/skills.ts +187 -0
- package/packages/cli/src/commands/update.ts +25 -0
- package/packages/cli/src/index.ts +185 -0
- package/packages/cli/src/utils/token.ts +6 -0
- package/packages/code-bridge/README.md +78 -0
- package/packages/code-bridge/package.json +18 -0
- package/packages/code-bridge/src/index.ts +95 -0
- package/packages/code-bridge/src/process-manager.ts +212 -0
- package/packages/code-bridge/src/schemas.ts +133 -0
- package/packages/core/package.json +46 -0
- package/packages/core/src/agent/agent-loop.ts +369 -0
- package/packages/core/src/agent/compaction.ts +140 -0
- package/packages/core/src/agent/context-compiler.ts +378 -0
- package/packages/core/src/agent/context-guard.ts +91 -0
- package/packages/core/src/agent/context.ts +138 -0
- package/packages/core/src/agent/conversation-store.ts +198 -0
- package/packages/core/src/agent/curator.ts +158 -0
- package/packages/core/src/agent/hooks.ts +166 -0
- package/packages/core/src/agent/index.ts +116 -0
- package/packages/core/src/agent/llm-client.ts +503 -0
- package/packages/core/src/agent/native-tools.ts +505 -0
- package/packages/core/src/agent/prompt-builder.ts +532 -0
- package/packages/core/src/agent/providers/index.ts +167 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/reflector.ts +170 -0
- package/packages/core/src/agent/service.ts +64 -0
- package/packages/core/src/agent/stuck-loop.ts +133 -0
- package/packages/core/src/agent/supervisor.ts +39 -0
- package/packages/core/src/agent/tracer.ts +102 -0
- package/packages/core/src/agent/workspace.ts +110 -0
- package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
- package/packages/core/src/canvas/canvas-manager.ts +319 -0
- package/packages/core/src/canvas/canvas-tools.ts +420 -0
- package/packages/core/src/canvas/emitter.ts +115 -0
- package/packages/core/src/canvas/index.ts +2 -0
- package/packages/core/src/channels/base.ts +138 -0
- package/packages/core/src/channels/discord.ts +260 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +383 -0
- package/packages/core/src/channels/slack.ts +287 -0
- package/packages/core/src/channels/telegram.ts +502 -0
- package/packages/core/src/channels/webchat.ts +128 -0
- package/packages/core/src/channels/whatsapp.ts +375 -0
- package/packages/core/src/config/index.ts +12 -0
- package/packages/core/src/config/loader.ts +529 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/index.ts +5 -0
- package/packages/core/src/gateway/initializer.ts +290 -0
- package/packages/core/src/gateway/lane-queue.ts +169 -0
- package/packages/core/src/gateway/resolver.ts +108 -0
- package/packages/core/src/gateway/router.ts +124 -0
- package/packages/core/src/gateway/server.ts +3317 -0
- package/packages/core/src/gateway/session.ts +95 -0
- package/packages/core/src/gateway/slash-commands.ts +192 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +19 -0
- package/packages/core/src/integrations/catalog.ts +286 -0
- package/packages/core/src/integrations/env.ts +64 -0
- package/packages/core/src/integrations/index.ts +2 -0
- package/packages/core/src/memory/index.ts +1 -0
- package/packages/core/src/memory/notes.ts +68 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/pairing.ts +250 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/db-context.ts +333 -0
- package/packages/core/src/storage/onboarding.ts +1087 -0
- package/packages/core/src/storage/schema.ts +541 -0
- package/packages/core/src/storage/seed.ts +571 -0
- package/packages/core/src/storage/sqlite.ts +387 -0
- package/packages/core/src/storage/usage.ts +212 -0
- package/packages/core/src/tools/bridge-events.ts +74 -0
- package/packages/core/src/tools/browser.ts +275 -0
- package/packages/core/src/tools/codebridge.ts +421 -0
- package/packages/core/src/tools/coordinator-tools.ts +179 -0
- package/packages/core/src/tools/cron.ts +611 -0
- package/packages/core/src/tools/exec.ts +140 -0
- package/packages/core/src/tools/fs.ts +364 -0
- package/packages/core/src/tools/index.ts +12 -0
- package/packages/core/src/tools/memory.ts +176 -0
- package/packages/core/src/tools/notify.ts +113 -0
- package/packages/core/src/tools/project-management.ts +376 -0
- package/packages/core/src/tools/project.ts +375 -0
- package/packages/core/src/tools/read.ts +158 -0
- package/packages/core/src/tools/web.ts +436 -0
- package/packages/core/src/tools/workspace.ts +171 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +4 -0
- package/packages/core/src/utils/logger.ts +388 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/voice/index.ts +583 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/mcp/package.json +26 -0
- package/packages/mcp/src/config.ts +13 -0
- package/packages/mcp/src/index.ts +1 -0
- package/packages/mcp/src/logger.ts +42 -0
- package/packages/mcp/src/manager.ts +434 -0
- package/packages/mcp/src/transports/index.ts +67 -0
- package/packages/mcp/src/transports/sse.ts +241 -0
- package/packages/mcp/src/transports/websocket.ts +159 -0
- package/packages/skills/package.json +21 -0
- package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
- package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
- package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
- package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
- package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
- package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
- package/packages/skills/src/bundled/memory/SKILL.md +42 -0
- package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
- package/packages/skills/src/bundled/shell/SKILL.md +43 -0
- package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
- package/packages/skills/src/bundled/voice/SKILL.md +25 -0
- package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +282 -0
- package/packages/tools/package.json +43 -0
- package/packages/tools/src/browser/browser.test.ts +111 -0
- package/packages/tools/src/browser/index.ts +272 -0
- package/packages/tools/src/canvas/index.ts +220 -0
- package/packages/tools/src/cron/cron.test.ts +164 -0
- package/packages/tools/src/cron/index.ts +304 -0
- package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
- package/packages/tools/src/filesystem/index.ts +379 -0
- package/packages/tools/src/git/index.ts +239 -0
- package/packages/tools/src/index.ts +4 -0
- package/packages/tools/src/shell/detect-env.ts +70 -0
- package/packages/tools/tsconfig.json +9 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, existsSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
import { eventBus } from "../events/event-bus.ts";
|
|
5
|
+
import { stateStore } from "../state/store.ts";
|
|
6
|
+
import type {
|
|
7
|
+
HivePlugin,
|
|
8
|
+
PluginManifest,
|
|
9
|
+
PluginState,
|
|
10
|
+
PluginContext,
|
|
11
|
+
ToolDefinition,
|
|
12
|
+
ChannelDefinition,
|
|
13
|
+
CLICommand,
|
|
14
|
+
PluginConstructor,
|
|
15
|
+
} from "./api.ts";
|
|
16
|
+
|
|
17
|
+
export interface PluginLoaderOptions {
|
|
18
|
+
pluginDir: string;
|
|
19
|
+
enableSandbox?: boolean;
|
|
20
|
+
autoActivate?: boolean;
|
|
21
|
+
pluginConfig?: Record<string, Record<string, unknown>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class PluginLoader {
|
|
25
|
+
private plugins: Map<string, HivePlugin> = new Map();
|
|
26
|
+
private pluginStates: Map<string, PluginState> = new Map();
|
|
27
|
+
private manifests: Map<string, PluginManifest> = new Map();
|
|
28
|
+
private tools: Map<string, ToolDefinition> = new Map();
|
|
29
|
+
private channels: Map<string, ChannelDefinition> = new Map();
|
|
30
|
+
private commands: Map<string, CLICommand> = new Map();
|
|
31
|
+
private log = logger.child("plugins");
|
|
32
|
+
|
|
33
|
+
constructor(private options: PluginLoaderOptions) {
|
|
34
|
+
if (!existsSync(options.pluginDir)) {
|
|
35
|
+
mkdirSync(options.pluginDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async discover(): Promise<string[]> {
|
|
40
|
+
const pluginDir = this.options.pluginDir;
|
|
41
|
+
const discovered: string[] = [];
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const entries = readdirSync(pluginDir, { withFileTypes: true });
|
|
45
|
+
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
const manifestPath = path.join(pluginDir, entry.name, "manifest.json");
|
|
49
|
+
if (existsSync(manifestPath)) {
|
|
50
|
+
discovered.push(entry.name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
this.log.error("Failed to discover plugins", { error: (error as Error).message });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return discovered;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async load(pluginName: string): Promise<void> {
|
|
62
|
+
if (this.plugins.has(pluginName)) {
|
|
63
|
+
this.log.warn(`Plugin ${pluginName} already loaded`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const pluginPath = path.join(this.options.pluginDir, pluginName);
|
|
68
|
+
const manifestPath = path.join(pluginPath, "manifest.json");
|
|
69
|
+
|
|
70
|
+
this.updateState(pluginName, "activating");
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
this.log.debug(`Reading plugin manifest: ${manifestPath}`);
|
|
74
|
+
const manifestContent = await Bun.file(manifestPath).text();
|
|
75
|
+
const manifest: PluginManifest = JSON.parse(manifestContent);
|
|
76
|
+
this.manifests.set(pluginName, manifest);
|
|
77
|
+
|
|
78
|
+
if (manifest.enabled === false) {
|
|
79
|
+
this.updateState(pluginName, "inactive", "Disabled in manifest");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const mainFile = manifest.main ?? "index.js";
|
|
84
|
+
const mainPath = path.join(pluginPath, mainFile);
|
|
85
|
+
|
|
86
|
+
if (!(await Bun.file(mainPath).exists())) {
|
|
87
|
+
throw new Error(`Main file not found: ${mainPath}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const PluginClass = await this.loadPluginClass(mainPath);
|
|
91
|
+
const plugin = new PluginClass();
|
|
92
|
+
const context = this.createContext(pluginName);
|
|
93
|
+
|
|
94
|
+
await plugin.activate(context);
|
|
95
|
+
|
|
96
|
+
this.plugins.set(pluginName, plugin);
|
|
97
|
+
this.updateState(pluginName, "active");
|
|
98
|
+
|
|
99
|
+
eventBus.emit("gateway:started", {
|
|
100
|
+
host: "plugin",
|
|
101
|
+
port: 0,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.log.info(`Plugin ${pluginName} loaded`, {
|
|
105
|
+
name: plugin.name,
|
|
106
|
+
version: plugin.version,
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const errorMsg = (error as Error).message;
|
|
110
|
+
this.updateState(pluginName, "error", errorMsg);
|
|
111
|
+
this.log.error(`Failed to load plugin ${pluginName}`, { error: errorMsg });
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async loadPluginClass(mainPath: string): Promise<PluginConstructor> {
|
|
117
|
+
if (this.options.enableSandbox) {
|
|
118
|
+
return this.loadSandboxed(mainPath);
|
|
119
|
+
}
|
|
120
|
+
return this.loadDirect(mainPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async loadDirect(mainPath: string): Promise<PluginConstructor> {
|
|
124
|
+
const module = await import(mainPath);
|
|
125
|
+
const PluginClass = module.default ?? module[Object.keys(module)[0]!];
|
|
126
|
+
|
|
127
|
+
if (typeof PluginClass !== "function") {
|
|
128
|
+
throw new Error("Plugin must export a class as default");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return PluginClass;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async loadSandboxed(mainPath: string): Promise<PluginConstructor> {
|
|
135
|
+
this.log.debug(`Reading plugin source for sandboxed execution: ${mainPath}`);
|
|
136
|
+
const code = await Bun.file(mainPath).text();
|
|
137
|
+
|
|
138
|
+
const safeGlobals = this.createSafeGlobals();
|
|
139
|
+
const wrappedCode = `
|
|
140
|
+
(function(module, exports, __dirname, __filename) {
|
|
141
|
+
${code}
|
|
142
|
+
})
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
const module = { exports: {} };
|
|
146
|
+
const exports = {};
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const fn = eval(wrappedCode);
|
|
150
|
+
fn(module, exports, path.dirname(mainPath), mainPath);
|
|
151
|
+
|
|
152
|
+
const PluginClass = (module.exports as any).default ?? (exports as any).default;
|
|
153
|
+
|
|
154
|
+
if (typeof PluginClass !== "function") {
|
|
155
|
+
throw new Error("Plugin must export a class as default");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return PluginClass;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw new Error(`Sandboxed plugin execution failed: ${(error as Error).message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private createSafeGlobals(): Record<string, unknown> {
|
|
165
|
+
return {
|
|
166
|
+
console: {
|
|
167
|
+
log: (...args: unknown[]) => this.log.debug(String(args[0])),
|
|
168
|
+
error: (...args: unknown[]) => this.log.error(String(args[0])),
|
|
169
|
+
warn: (...args: unknown[]) => this.log.warn(String(args[0])),
|
|
170
|
+
info: (...args: unknown[]) => this.log.info(String(args[0])),
|
|
171
|
+
},
|
|
172
|
+
setTimeout: () => 0,
|
|
173
|
+
clearTimeout: () => { },
|
|
174
|
+
setInterval: () => 0,
|
|
175
|
+
clearInterval: () => { },
|
|
176
|
+
Buffer: {
|
|
177
|
+
from: () => Buffer.from,
|
|
178
|
+
isBuffer: () => false,
|
|
179
|
+
},
|
|
180
|
+
URL: URL,
|
|
181
|
+
URLSearchParams: URLSearchParams,
|
|
182
|
+
JSON: JSON,
|
|
183
|
+
Object: Object,
|
|
184
|
+
Array: Array,
|
|
185
|
+
String: String,
|
|
186
|
+
Number: Number,
|
|
187
|
+
Boolean: Boolean,
|
|
188
|
+
Date: Date,
|
|
189
|
+
Error: Error,
|
|
190
|
+
Promise: Promise,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async unload(pluginName: string): Promise<void> {
|
|
195
|
+
const plugin = this.plugins.get(pluginName);
|
|
196
|
+
|
|
197
|
+
if (!plugin) {
|
|
198
|
+
this.log.warn(`Plugin ${pluginName} not loaded`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.updateState(pluginName, "deactivating");
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await plugin.deactivate();
|
|
206
|
+
|
|
207
|
+
for (const [toolName, tool] of this.tools) {
|
|
208
|
+
if (toolName.startsWith(`${pluginName}:`)) {
|
|
209
|
+
this.tools.delete(toolName);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const [channelName, channel] of this.channels) {
|
|
214
|
+
if (channelName.startsWith(`${pluginName}:`)) {
|
|
215
|
+
await channel.stop();
|
|
216
|
+
this.channels.delete(channelName);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const [cmdName] of this.commands) {
|
|
221
|
+
if (cmdName.startsWith(`${pluginName}:`)) {
|
|
222
|
+
this.commands.delete(cmdName);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.plugins.delete(pluginName);
|
|
227
|
+
this.updateState(pluginName, "inactive");
|
|
228
|
+
|
|
229
|
+
this.log.info(`Plugin ${pluginName} unloaded`);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const errorMsg = (error as Error).message;
|
|
232
|
+
this.updateState(pluginName, "error", errorMsg);
|
|
233
|
+
this.log.error(`Failed to unload plugin ${pluginName}`, { error: errorMsg });
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async reload(pluginName: string): Promise<void> {
|
|
239
|
+
this.log.info(`Reloading plugin ${pluginName}`);
|
|
240
|
+
await this.unload(pluginName);
|
|
241
|
+
await this.load(pluginName);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private createContext(pluginName: string): PluginContext {
|
|
245
|
+
const pluginLogger = logger.child(pluginName);
|
|
246
|
+
const pluginConfig = this.options.pluginConfig?.[pluginName] ?? {};
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
pluginName,
|
|
250
|
+
logger: pluginLogger,
|
|
251
|
+
config: pluginConfig,
|
|
252
|
+
registerTool: (tool: ToolDefinition) => {
|
|
253
|
+
const fullName = `${pluginName}:${tool.name}`;
|
|
254
|
+
this.tools.set(fullName, tool);
|
|
255
|
+
this.log.debug(`Tool registered: ${fullName}`);
|
|
256
|
+
},
|
|
257
|
+
unregisterTool: (name: string) => {
|
|
258
|
+
const fullName = `${pluginName}:${name}`;
|
|
259
|
+
this.tools.delete(fullName);
|
|
260
|
+
},
|
|
261
|
+
registerChannel: (channel: ChannelDefinition) => {
|
|
262
|
+
const fullName = `${pluginName}:${channel.name}`;
|
|
263
|
+
this.channels.set(fullName, channel);
|
|
264
|
+
this.log.debug(`Channel registered: ${fullName}`);
|
|
265
|
+
},
|
|
266
|
+
unregisterChannel: (name: string) => {
|
|
267
|
+
const fullName = `${pluginName}:${name}`;
|
|
268
|
+
this.channels.delete(fullName);
|
|
269
|
+
},
|
|
270
|
+
registerCommand: (command: CLICommand) => {
|
|
271
|
+
const fullName = `${pluginName}:${command.name}`;
|
|
272
|
+
this.commands.set(fullName, command);
|
|
273
|
+
this.log.debug(`Command registered: ${fullName}`);
|
|
274
|
+
},
|
|
275
|
+
unregisterCommand: (name: string) => {
|
|
276
|
+
const fullName = `${pluginName}:${name}`;
|
|
277
|
+
this.commands.delete(fullName);
|
|
278
|
+
},
|
|
279
|
+
events: {
|
|
280
|
+
emit: (event, data) => eventBus.emit(event as any, data as any),
|
|
281
|
+
on: (event, handler) => eventBus.on(event as any, handler as any),
|
|
282
|
+
once: (event, handler) => eventBus.once(event as any, handler as any),
|
|
283
|
+
},
|
|
284
|
+
state: {
|
|
285
|
+
get: () => stateStore.getState(),
|
|
286
|
+
subscribe: (listener) => stateStore.subscribe(listener),
|
|
287
|
+
},
|
|
288
|
+
getTool: (name: string) => this.tools.get(name) ?? this.tools.get(`${pluginName}:${name}`),
|
|
289
|
+
getTools: () => Array.from(this.tools.values()),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private updateState(name: string, status: PluginState["status"], error?: string): void {
|
|
294
|
+
const existing = this.pluginStates.get(name);
|
|
295
|
+
const manifest = this.manifests.get(name);
|
|
296
|
+
|
|
297
|
+
this.pluginStates.set(name, {
|
|
298
|
+
name,
|
|
299
|
+
status,
|
|
300
|
+
version: manifest?.version ?? existing?.version ?? "unknown",
|
|
301
|
+
enabled: manifest?.enabled ?? true,
|
|
302
|
+
error,
|
|
303
|
+
loadedAt: existing?.loadedAt ?? Date.now(),
|
|
304
|
+
activatedAt: status === "active" ? Date.now() : existing?.activatedAt,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getPlugin(name: string): HivePlugin | undefined {
|
|
309
|
+
return this.plugins.get(name);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
getPluginState(name: string): PluginState | undefined {
|
|
313
|
+
return this.pluginStates.get(name);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
getAllPluginStates(): PluginState[] {
|
|
317
|
+
return Array.from(this.pluginStates.values());
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
getTool(name: string): ToolDefinition | undefined {
|
|
321
|
+
return this.tools.get(name);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
getTools(): ToolDefinition[] {
|
|
325
|
+
return Array.from(this.tools.values());
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
getChannel(name: string): ChannelDefinition | undefined {
|
|
329
|
+
return this.channels.get(name);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
getChannels(): ChannelDefinition[] {
|
|
333
|
+
return Array.from(this.channels.values());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
getCommand(name: string): CLICommand | undefined {
|
|
337
|
+
return this.commands.get(name);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
getCommands(): CLICommand[] {
|
|
341
|
+
return Array.from(this.commands.values());
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async loadAll(): Promise<void> {
|
|
345
|
+
const discovered = await this.discover();
|
|
346
|
+
|
|
347
|
+
for (const pluginName of discovered) {
|
|
348
|
+
try {
|
|
349
|
+
await this.load(pluginName);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
this.log.error(`Failed to load ${pluginName}`, { error: (error as Error).message });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async unloadAll(): Promise<void> {
|
|
357
|
+
for (const pluginName of this.plugins.keys()) {
|
|
358
|
+
try {
|
|
359
|
+
await this.unload(pluginName);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
this.log.error(`Failed to unload ${pluginName}`, { error: (error as Error).message });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.ts";
|
|
2
|
+
|
|
3
|
+
type CircuitState = "closed" | "open" | "half-open";
|
|
4
|
+
|
|
5
|
+
export interface CircuitBreakerOptions {
|
|
6
|
+
failureThreshold: number;
|
|
7
|
+
successThreshold: number;
|
|
8
|
+
resetTimeout: number;
|
|
9
|
+
halfOpenMaxCalls: number;
|
|
10
|
+
onStateChange?: (from: CircuitState, to: CircuitState) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const defaultOptions: CircuitBreakerOptions = {
|
|
14
|
+
failureThreshold: 5,
|
|
15
|
+
successThreshold: 3,
|
|
16
|
+
resetTimeout: 30000,
|
|
17
|
+
halfOpenMaxCalls: 1,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface CircuitBreakerStats {
|
|
21
|
+
state: CircuitState;
|
|
22
|
+
failures: number;
|
|
23
|
+
successes: number;
|
|
24
|
+
lastFailureTime?: number;
|
|
25
|
+
lastSuccessTime?: number;
|
|
26
|
+
totalCalls: number;
|
|
27
|
+
totalFailures: number;
|
|
28
|
+
totalSuccesses: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class CircuitBreaker {
|
|
32
|
+
private state: CircuitState = "closed";
|
|
33
|
+
private failures = 0;
|
|
34
|
+
private successes = 0;
|
|
35
|
+
private lastFailureTime?: number;
|
|
36
|
+
private lastSuccessTime?: number;
|
|
37
|
+
private halfOpenCalls = 0;
|
|
38
|
+
private totalCalls = 0;
|
|
39
|
+
private totalFailures = 0;
|
|
40
|
+
private totalSuccesses = 0;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
private readonly name: string,
|
|
44
|
+
private readonly options: CircuitBreakerOptions = defaultOptions
|
|
45
|
+
) { }
|
|
46
|
+
|
|
47
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
48
|
+
if (this.state === "open") {
|
|
49
|
+
const timeSinceLastFailure = Date.now() - (this.lastFailureTime ?? 0);
|
|
50
|
+
|
|
51
|
+
if (timeSinceLastFailure > this.options.resetTimeout) {
|
|
52
|
+
this.transitionTo("half-open");
|
|
53
|
+
} else {
|
|
54
|
+
const waitTime = this.options.resetTimeout - timeSinceLastFailure;
|
|
55
|
+
throw new CircuitBreakerOpenError(
|
|
56
|
+
`Circuit breaker '${this.name}' is OPEN. Retry in ${Math.ceil(waitTime / 1000)}s`,
|
|
57
|
+
this.name,
|
|
58
|
+
waitTime
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (this.state === "half-open") {
|
|
64
|
+
if (this.halfOpenCalls >= this.options.halfOpenMaxCalls) {
|
|
65
|
+
throw new CircuitBreakerOpenError(
|
|
66
|
+
`Circuit breaker '${this.name}' is HALF-OPEN with pending calls`,
|
|
67
|
+
this.name,
|
|
68
|
+
0
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
this.halfOpenCalls++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.totalCalls++;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const result = await fn();
|
|
78
|
+
this.onSuccess();
|
|
79
|
+
return result;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
this.onFailure();
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private onSuccess(): void {
|
|
87
|
+
this.lastSuccessTime = Date.now();
|
|
88
|
+
this.totalSuccesses++;
|
|
89
|
+
this.failures = 0;
|
|
90
|
+
|
|
91
|
+
if (this.state === "half-open") {
|
|
92
|
+
this.successes++;
|
|
93
|
+
this.halfOpenCalls--;
|
|
94
|
+
|
|
95
|
+
if (this.successes >= this.options.successThreshold) {
|
|
96
|
+
this.transitionTo("closed");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private onFailure(): void {
|
|
102
|
+
this.lastFailureTime = Date.now();
|
|
103
|
+
this.totalFailures++;
|
|
104
|
+
this.failures++;
|
|
105
|
+
|
|
106
|
+
if (this.state === "half-open") {
|
|
107
|
+
this.halfOpenCalls--;
|
|
108
|
+
this.transitionTo("open");
|
|
109
|
+
} else if (this.state === "closed") {
|
|
110
|
+
if (this.failures >= this.options.failureThreshold) {
|
|
111
|
+
this.transitionTo("open");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private transitionTo(newState: CircuitState): void {
|
|
117
|
+
const oldState = this.state;
|
|
118
|
+
this.state = newState;
|
|
119
|
+
|
|
120
|
+
if (newState === "closed") {
|
|
121
|
+
this.failures = 0;
|
|
122
|
+
this.successes = 0;
|
|
123
|
+
this.halfOpenCalls = 0;
|
|
124
|
+
} else if (newState === "open") {
|
|
125
|
+
this.successes = 0;
|
|
126
|
+
this.halfOpenCalls = 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.options.onStateChange?.(oldState, newState);
|
|
130
|
+
logger.info(`[CircuitBreaker] ${this.name}: ${oldState} → ${newState}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getState(): CircuitState {
|
|
134
|
+
return this.state;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
isOpen(): boolean {
|
|
138
|
+
return this.state === "open";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
isClosed(): boolean {
|
|
142
|
+
return this.state === "closed";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
isHalfOpen(): boolean {
|
|
146
|
+
return this.state === "half-open";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getStats(): CircuitBreakerStats {
|
|
150
|
+
return {
|
|
151
|
+
state: this.state,
|
|
152
|
+
failures: this.failures,
|
|
153
|
+
successes: this.successes,
|
|
154
|
+
lastFailureTime: this.lastFailureTime,
|
|
155
|
+
lastSuccessTime: this.lastSuccessTime,
|
|
156
|
+
totalCalls: this.totalCalls,
|
|
157
|
+
totalFailures: this.totalFailures,
|
|
158
|
+
totalSuccesses: this.totalSuccesses,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
reset(): void {
|
|
163
|
+
this.transitionTo("closed");
|
|
164
|
+
this.failures = 0;
|
|
165
|
+
this.successes = 0;
|
|
166
|
+
this.lastFailureTime = undefined;
|
|
167
|
+
this.lastSuccessTime = undefined;
|
|
168
|
+
this.halfOpenCalls = 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
forceOpen(): void {
|
|
172
|
+
this.transitionTo("open");
|
|
173
|
+
this.lastFailureTime = Date.now();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class CircuitBreakerOpenError extends Error {
|
|
178
|
+
constructor(
|
|
179
|
+
message: string,
|
|
180
|
+
public readonly circuitName: string,
|
|
181
|
+
public readonly retryAfterMs: number
|
|
182
|
+
) {
|
|
183
|
+
super(message);
|
|
184
|
+
this.name = "CircuitBreakerOpenError";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export class CircuitBreakerRegistry {
|
|
189
|
+
private breakers: Map<string, CircuitBreaker> = new Map();
|
|
190
|
+
|
|
191
|
+
getOrCreate(name: string, options?: Partial<CircuitBreakerOptions>): CircuitBreaker {
|
|
192
|
+
let breaker = this.breakers.get(name);
|
|
193
|
+
|
|
194
|
+
if (!breaker) {
|
|
195
|
+
breaker = new CircuitBreaker(name, { ...defaultOptions, ...options });
|
|
196
|
+
this.breakers.set(name, breaker);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return breaker;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
get(name: string): CircuitBreaker | undefined {
|
|
203
|
+
return this.breakers.get(name);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getAll(): Map<string, CircuitBreaker> {
|
|
207
|
+
return new Map(this.breakers);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getAllStats(): Record<string, CircuitBreakerStats> {
|
|
211
|
+
const stats: Record<string, CircuitBreakerStats> = {};
|
|
212
|
+
for (const [name, breaker] of this.breakers) {
|
|
213
|
+
stats[name] = breaker.getStats();
|
|
214
|
+
}
|
|
215
|
+
return stats;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
resetAll(): void {
|
|
219
|
+
for (const breaker of this.breakers.values()) {
|
|
220
|
+
breaker.reset();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const circuitBreakerRegistry = new CircuitBreakerRegistry();
|