@johpaz/hive-core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +43 -0
  2. package/src/agent/compaction.ts +161 -0
  3. package/src/agent/context-guard.ts +91 -0
  4. package/src/agent/context.ts +148 -0
  5. package/src/agent/ethics.ts +102 -0
  6. package/src/agent/hooks.ts +166 -0
  7. package/src/agent/index.ts +67 -0
  8. package/src/agent/providers/index.ts +278 -0
  9. package/src/agent/providers.ts +1 -0
  10. package/src/agent/soul.ts +89 -0
  11. package/src/agent/stuck-loop.ts +133 -0
  12. package/src/agent/user.ts +86 -0
  13. package/src/channels/base.ts +91 -0
  14. package/src/channels/discord.ts +185 -0
  15. package/src/channels/index.ts +7 -0
  16. package/src/channels/manager.ts +204 -0
  17. package/src/channels/slack.ts +209 -0
  18. package/src/channels/telegram.ts +177 -0
  19. package/src/channels/webchat.ts +83 -0
  20. package/src/channels/whatsapp.ts +305 -0
  21. package/src/config/index.ts +1 -0
  22. package/src/config/loader.ts +508 -0
  23. package/src/gateway/index.ts +5 -0
  24. package/src/gateway/lane-queue.ts +169 -0
  25. package/src/gateway/router.ts +124 -0
  26. package/src/gateway/server.ts +347 -0
  27. package/src/gateway/session.ts +131 -0
  28. package/src/gateway/slash-commands.ts +176 -0
  29. package/src/heartbeat/index.ts +157 -0
  30. package/src/index.ts +21 -0
  31. package/src/memory/index.ts +1 -0
  32. package/src/memory/notes.ts +170 -0
  33. package/src/multi-agent/bindings.ts +171 -0
  34. package/src/multi-agent/index.ts +4 -0
  35. package/src/multi-agent/manager.ts +182 -0
  36. package/src/multi-agent/sandbox.ts +130 -0
  37. package/src/multi-agent/subagents.ts +302 -0
  38. package/src/security/index.ts +187 -0
  39. package/src/tools/cron.ts +156 -0
  40. package/src/tools/exec.ts +105 -0
  41. package/src/tools/index.ts +6 -0
  42. package/src/tools/memory.ts +176 -0
  43. package/src/tools/notify.ts +53 -0
  44. package/src/tools/read.ts +154 -0
  45. package/src/tools/registry.ts +115 -0
  46. package/src/tools/web.ts +186 -0
  47. package/src/utils/crypto.ts +73 -0
  48. package/src/utils/index.ts +3 -0
  49. package/src/utils/logger.ts +254 -0
  50. package/src/utils/retry.ts +70 -0
@@ -0,0 +1,176 @@
1
+ import type { ServerWebSocket } from "bun";
2
+ import { sessionManager } from "./session.ts";
3
+ import { laneQueue } from "./lane-queue.ts";
4
+ import { logger } from "../utils/logger.ts";
5
+
6
+ export interface InboundMessage {
7
+ type: "message" | "command" | "ping";
8
+ sessionId: string;
9
+ content?: string;
10
+ command?: string;
11
+ args?: string[];
12
+ metadata?: Record<string, unknown>;
13
+ }
14
+
15
+ export interface OutboundMessage {
16
+ type: "message" | "stream" | "status" | "error" | "pong" | "command_result";
17
+ sessionId: string;
18
+ content?: string;
19
+ chunk?: string;
20
+ isLast?: boolean;
21
+ status?: {
22
+ state: string;
23
+ model?: string;
24
+ tokens?: number;
25
+ };
26
+ error?: string;
27
+ result?: unknown;
28
+ }
29
+
30
+ export interface SlashCommand {
31
+ name: string;
32
+ description: string;
33
+ handler: (sessionId: string, args: string[], ws: ServerWebSocket<unknown>) => Promise<unknown>;
34
+ }
35
+
36
+ const slashCommands = new Map<string, SlashCommand>();
37
+
38
+ export function registerSlashCommand(command: SlashCommand): void {
39
+ slashCommands.set(command.name, command);
40
+ }
41
+
42
+ export function isSlashCommand(content: string): boolean {
43
+ return content.startsWith("/") && content.length > 1;
44
+ }
45
+
46
+ export function parseSlashCommand(content: string): { name: string; args: string[] } | null {
47
+ if (!isSlashCommand(content)) return null;
48
+
49
+ const parts = content.slice(1).split(/\s+/);
50
+ const name = parts[0]?.toLowerCase();
51
+ if (!name) return null;
52
+
53
+ return {
54
+ name,
55
+ args: parts.slice(1),
56
+ };
57
+ }
58
+
59
+ export async function executeSlashCommand(
60
+ sessionId: string,
61
+ content: string,
62
+ ws: ServerWebSocket<unknown>
63
+ ): Promise<OutboundMessage> {
64
+ const parsed = parseSlashCommand(content);
65
+ if (!parsed) {
66
+ return {
67
+ type: "error",
68
+ sessionId,
69
+ error: "Invalid slash command format",
70
+ };
71
+ }
72
+
73
+ const command = slashCommands.get(parsed.name);
74
+ if (!command) {
75
+ return {
76
+ type: "error",
77
+ sessionId,
78
+ error: `Unknown command: /${parsed.name}`,
79
+ };
80
+ }
81
+
82
+ logger.info(`Executing slash command: /${parsed.name}`, { sessionId, args: parsed.args });
83
+
84
+ try {
85
+ const result = await command.handler(sessionId, parsed.args, ws);
86
+ return {
87
+ type: "command_result",
88
+ sessionId,
89
+ result,
90
+ };
91
+ } catch (error) {
92
+ logger.error(`Slash command failed: /${parsed.name}`, { error: (error as Error).message });
93
+ return {
94
+ type: "error",
95
+ sessionId,
96
+ error: (error as Error).message,
97
+ };
98
+ }
99
+ }
100
+
101
+ registerSlashCommand({
102
+ name: "stop",
103
+ description: "Stop the current task",
104
+ handler: async (sessionId) => {
105
+ const cancelled = laneQueue.cancel(sessionId);
106
+ return {
107
+ success: cancelled,
108
+ message: cancelled ? "Task stopped" : "No task running",
109
+ };
110
+ },
111
+ });
112
+
113
+ registerSlashCommand({
114
+ name: "status",
115
+ description: "Show session status",
116
+ handler: async (sessionId) => {
117
+ const session = sessionManager.get(sessionId);
118
+ const queueStatus = laneQueue.getStatus(sessionId);
119
+
120
+ return {
121
+ sessionId,
122
+ createdAt: session?.createdAt,
123
+ messageCount: session?.messageCount,
124
+ queueLength: queueStatus.queueLength,
125
+ isProcessing: queueStatus.running !== undefined,
126
+ };
127
+ },
128
+ });
129
+
130
+ registerSlashCommand({
131
+ name: "new",
132
+ description: "Start a new session",
133
+ handler: async (sessionId) => {
134
+ sessionManager.delete(sessionId);
135
+ return { success: true, message: "Session reset" };
136
+ },
137
+ });
138
+
139
+ registerSlashCommand({
140
+ name: "compact",
141
+ description: "Force context compaction",
142
+ handler: async (sessionId) => {
143
+ logger.info(`Compaction requested for session: ${sessionId}`);
144
+ return { success: true, message: "Compaction triggered" };
145
+ },
146
+ });
147
+
148
+ registerSlashCommand({
149
+ name: "reset",
150
+ description: "Reset the current context",
151
+ handler: async (sessionId) => {
152
+ logger.info(`Context reset requested for session: ${sessionId}`);
153
+ return { success: true, message: "Context reset" };
154
+ },
155
+ });
156
+
157
+ registerSlashCommand({
158
+ name: "model",
159
+ description: "Switch model for this session",
160
+ handler: async (_sessionId, args) => {
161
+ const modelName = args[0];
162
+ if (!modelName) {
163
+ return { success: false, message: "Usage: /model <model-name>" };
164
+ }
165
+ return { success: true, message: `Model switched to: ${modelName}` };
166
+ },
167
+ });
168
+
169
+ registerSlashCommand({
170
+ name: "help",
171
+ description: "Show available commands",
172
+ handler: async () => {
173
+ const commands = Array.from(slashCommands.values()).map((c) => `/${c.name} - ${c.description}`);
174
+ return { commands };
175
+ },
176
+ });
@@ -0,0 +1,157 @@
1
+ import type { Config } from "../config/loader.ts";
2
+ import { logger } from "../utils/logger.ts";
3
+
4
+ export interface HealthStatus {
5
+ status: "healthy" | "degraded" | "unhealthy";
6
+ checks: Record<string, {
7
+ status: "ok" | "warning" | "error";
8
+ message?: string;
9
+ latency?: number;
10
+ }>;
11
+ uptime: number;
12
+ lastCheck: Date;
13
+ }
14
+
15
+ export interface HeartbeatOptions {
16
+ intervalMs?: number;
17
+ onHealthChange?: (status: HealthStatus) => void;
18
+ }
19
+
20
+ type HealthCheck = () => Promise<{
21
+ status: "ok" | "warning" | "error";
22
+ message?: string;
23
+ latency?: number;
24
+ }>;
25
+
26
+ export class Heartbeat {
27
+ private intervalMs: number;
28
+ private checks: Map<string, HealthCheck> = new Map();
29
+ private intervalId: Timer | null = null;
30
+ private startTime: Date;
31
+ private lastStatus: HealthStatus | null = null;
32
+ private onHealthChange?: (status: HealthStatus) => void;
33
+ private log = logger.child("heartbeat");
34
+
35
+ constructor(_config: Config, options: HeartbeatOptions = {}) {
36
+ this.intervalMs = options.intervalMs ?? 30000;
37
+ this.onHealthChange = options.onHealthChange;
38
+ this.startTime = new Date();
39
+ }
40
+
41
+ registerCheck(name: string, check: HealthCheck): void {
42
+ this.checks.set(name, check);
43
+ this.log.debug(`Registered health check: ${name}`);
44
+ }
45
+
46
+ removeCheck(name: string): boolean {
47
+ return this.checks.delete(name);
48
+ }
49
+
50
+ async runChecks(): Promise<HealthStatus> {
51
+ const checks: HealthStatus["checks"] = {};
52
+ let overallStatus: "healthy" | "degraded" | "unhealthy" = "healthy";
53
+
54
+ for (const [name, check] of this.checks) {
55
+ try {
56
+ const start = Date.now();
57
+ const result = await check();
58
+ const latency = Date.now() - start;
59
+
60
+ checks[name] = {
61
+ status: result.status,
62
+ message: result.message,
63
+ latency: result.latency ?? latency,
64
+ };
65
+
66
+ if (result.status === "warning" && overallStatus === "healthy") {
67
+ overallStatus = "degraded";
68
+ } else if (result.status === "error") {
69
+ overallStatus = "unhealthy";
70
+ }
71
+ } catch (error) {
72
+ checks[name] = {
73
+ status: "error",
74
+ message: (error as Error).message,
75
+ };
76
+ overallStatus = "unhealthy";
77
+ }
78
+ }
79
+
80
+ const status: HealthStatus = {
81
+ status: overallStatus,
82
+ checks,
83
+ uptime: Date.now() - this.startTime.getTime(),
84
+ lastCheck: new Date(),
85
+ };
86
+
87
+ const prevStatus = this.lastStatus?.status;
88
+ if (prevStatus && prevStatus !== overallStatus) {
89
+ this.log.info(`Health status changed: ${prevStatus} -> ${overallStatus}`);
90
+ this.onHealthChange?.(status);
91
+ }
92
+
93
+ this.lastStatus = status;
94
+ return status;
95
+ }
96
+
97
+ start(): void {
98
+ if (this.intervalId) {
99
+ this.log.warn("Heartbeat already running");
100
+ return;
101
+ }
102
+
103
+ this.runChecks();
104
+
105
+ this.intervalId = setInterval(async () => {
106
+ await this.runChecks();
107
+ }, this.intervalMs);
108
+
109
+ this.log.info(`Heartbeat started (interval: ${this.intervalMs}ms)`);
110
+ }
111
+
112
+ stop(): void {
113
+ if (this.intervalId) {
114
+ clearInterval(this.intervalId);
115
+ this.intervalId = null;
116
+ this.log.info("Heartbeat stopped");
117
+ }
118
+ }
119
+
120
+ getStatus(): HealthStatus | null {
121
+ return this.lastStatus;
122
+ }
123
+
124
+ isRunning(): boolean {
125
+ return this.intervalId !== null;
126
+ }
127
+ }
128
+
129
+ export function createHeartbeat(config: Config, options?: HeartbeatOptions): Heartbeat {
130
+ const heartbeat = new Heartbeat(config, options);
131
+
132
+ heartbeat.registerCheck("memory", async () => {
133
+ const memUsage = process.memoryUsage();
134
+ const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
135
+ const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
136
+ const ratio = memUsage.heapUsed / memUsage.heapTotal;
137
+
138
+ if (ratio > 0.9) {
139
+ return {
140
+ status: "error",
141
+ message: `Memory critically high: ${heapUsedMB}/${heapTotalMB}MB`,
142
+ };
143
+ } else if (ratio > 0.75) {
144
+ return {
145
+ status: "warning",
146
+ message: `Memory usage high: ${heapUsedMB}/${heapTotalMB}MB`,
147
+ };
148
+ }
149
+
150
+ return {
151
+ status: "ok",
152
+ message: `${heapUsedMB}/${heapTotalMB}MB used`,
153
+ };
154
+ });
155
+
156
+ return heartbeat;
157
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ export * from "./gateway/index.ts";
2
+ export * from "./gateway/server.ts";
3
+ export * from "./agent/index.ts";
4
+ export * from "./agent/ethics.ts";
5
+ export * from "./agent/context.ts";
6
+ export * from "./agent/soul.ts";
7
+ export * from "./agent/user.ts";
8
+ export * from "./channels/index.ts";
9
+ export * from "./channels/manager.ts";
10
+ export * from "./config/loader.ts";
11
+ export * from "./utils/logger.ts";
12
+ export * from "./utils/crypto.ts";
13
+ export * from "./utils/retry.ts";
14
+ export * from "./security/index.ts";
15
+ export * from "./heartbeat/index.ts";
16
+ export * from "./tools/registry.ts";
17
+ export * from "./multi-agent/manager.ts";
18
+ export * from "./multi-agent/bindings.ts";
19
+ export * from "./multi-agent/sandbox.ts";
20
+ export * from "./multi-agent/subagents.ts";
21
+ export * from "./memory/notes.ts";
@@ -0,0 +1 @@
1
+ export * from "./notes.ts";
@@ -0,0 +1,170 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { Config } from "../config/loader.ts";
4
+ import { logger } from "../utils/logger.ts";
5
+
6
+ export interface Note {
7
+ title: string;
8
+ content: string;
9
+ createdAt: Date;
10
+ updatedAt: Date;
11
+ }
12
+
13
+ export class MemoryStore {
14
+ private notesDir: string;
15
+ private log = logger.child("memory");
16
+
17
+ constructor(config: Config) {
18
+ this.notesDir = this.expandPath(
19
+ config.memory?.notesDir ?? "~/.hive/agents/main/workspace/memory"
20
+ );
21
+ this.ensureDir();
22
+ }
23
+
24
+ private expandPath(p: string): string {
25
+ if (p.startsWith("~")) {
26
+ return path.join(process.env.HOME ?? "", p.slice(1));
27
+ }
28
+ return p;
29
+ }
30
+
31
+ private ensureDir(): void {
32
+ if (!fs.existsSync(this.notesDir)) {
33
+ fs.mkdirSync(this.notesDir, { recursive: true });
34
+ }
35
+ }
36
+
37
+ private getNotePath(title: string): string {
38
+ const safeTitle = title.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
39
+ return path.join(this.notesDir, `${safeTitle}.md`);
40
+ }
41
+
42
+ write(title: string, content: string): Note {
43
+ const notePath = this.getNotePath(title);
44
+ const now = new Date();
45
+
46
+ let note: Note;
47
+
48
+ if (fs.existsSync(notePath)) {
49
+ const existing = fs.readFileSync(notePath, "utf-8");
50
+ const lines = existing.split("\n");
51
+ const createdAtLine = lines.find((l) => l.startsWith("created:"));
52
+ const createdAt = createdAtLine
53
+ ? new Date(createdAtLine.replace("created:", "").trim())
54
+ : now;
55
+
56
+ note = {
57
+ title,
58
+ content,
59
+ createdAt,
60
+ updatedAt: now,
61
+ };
62
+ } else {
63
+ note = {
64
+ title,
65
+ content,
66
+ createdAt: now,
67
+ updatedAt: now,
68
+ };
69
+ }
70
+
71
+ const frontmatter = `---
72
+ title: ${title}
73
+ created: ${note.createdAt.toISOString()}
74
+ updated: ${note.updatedAt.toISOString()}
75
+ ---
76
+
77
+ ${content}`;
78
+
79
+ fs.writeFileSync(notePath, frontmatter, "utf-8");
80
+ this.log.debug(`Wrote note: ${title}`);
81
+
82
+ return note;
83
+ }
84
+
85
+ read(title: string): Note | null {
86
+ const notePath = this.getNotePath(title);
87
+
88
+ if (!fs.existsSync(notePath)) {
89
+ return null;
90
+ }
91
+
92
+ const content = fs.readFileSync(notePath, "utf-8");
93
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
94
+
95
+ if (!match) {
96
+ return {
97
+ title,
98
+ content,
99
+ createdAt: new Date(),
100
+ updatedAt: new Date(),
101
+ };
102
+ }
103
+
104
+ const frontmatter = match[1]!;
105
+ const body = match[2]!.trim();
106
+
107
+ const titleMatch = frontmatter.match(/title:\s*(.+)/);
108
+ const createdMatch = frontmatter.match(/created:\s*(.+)/);
109
+ const updatedMatch = frontmatter.match(/updated:\s*(.+)/);
110
+
111
+ return {
112
+ title: titleMatch?.[1]?.trim() ?? title,
113
+ content: body,
114
+ createdAt: createdMatch ? new Date(createdMatch[1]!) : new Date(),
115
+ updatedAt: updatedMatch ? new Date(updatedMatch[1]!) : new Date(),
116
+ };
117
+ }
118
+
119
+ list(): Array<{ title: string; path: string }> {
120
+ if (!fs.existsSync(this.notesDir)) {
121
+ return [];
122
+ }
123
+
124
+ const files = fs.readdirSync(this.notesDir).filter((f) => f.endsWith(".md"));
125
+
126
+ return files.map((file) => {
127
+ const filePath = path.join(this.notesDir, file);
128
+ const content = fs.readFileSync(filePath, "utf-8");
129
+ const titleMatch = content.match(/title:\s*(.+)/);
130
+
131
+ return {
132
+ title: titleMatch?.[1]?.trim() ?? file.replace(".md", ""),
133
+ path: filePath,
134
+ };
135
+ });
136
+ }
137
+
138
+ delete(title: string): boolean {
139
+ const notePath = this.getNotePath(title);
140
+
141
+ if (fs.existsSync(notePath)) {
142
+ fs.unlinkSync(notePath);
143
+ this.log.debug(`Deleted note: ${title}`);
144
+ return true;
145
+ }
146
+
147
+ return false;
148
+ }
149
+
150
+ search(query: string): Note[] {
151
+ const results: Note[] = [];
152
+ const lowerQuery = query.toLowerCase();
153
+
154
+ for (const { title } of this.list()) {
155
+ const note = this.read(title);
156
+ if (note && (
157
+ note.title.toLowerCase().includes(lowerQuery) ||
158
+ note.content.toLowerCase().includes(lowerQuery)
159
+ )) {
160
+ results.push(note);
161
+ }
162
+ }
163
+
164
+ return results;
165
+ }
166
+ }
167
+
168
+ export function createMemoryStore(config: Config): MemoryStore {
169
+ return new MemoryStore(config);
170
+ }
@@ -0,0 +1,171 @@
1
+ import type { Config, Binding } from "../config/loader.ts";
2
+ import { logger } from "../utils/logger.ts";
3
+
4
+ export interface RoutingContext {
5
+ channel: string;
6
+ accountId?: string;
7
+ peerId?: string;
8
+ peerKind?: "direct" | "group";
9
+ guildId?: string;
10
+ teamId?: string;
11
+ roles?: string[];
12
+ }
13
+
14
+ export class BindingRouter {
15
+ private config: Config;
16
+ private bindings: Binding[];
17
+ private log = logger.child("bindings");
18
+
19
+ constructor(config: Config) {
20
+ this.config = config;
21
+ this.bindings = config.bindings ?? [];
22
+ }
23
+
24
+ resolve(ctx: RoutingContext): string {
25
+ if (this.bindings.length === 0) {
26
+ return this.getDefaultAgentId();
27
+ }
28
+
29
+ const scoredBindings: Array<{ binding: Binding; score: number }> = [];
30
+
31
+ for (const binding of this.bindings) {
32
+ const score = this.calculateScore(binding, ctx);
33
+ if (score > 0) {
34
+ scoredBindings.push({ binding, score });
35
+ }
36
+ }
37
+
38
+ if (scoredBindings.length === 0) {
39
+ return this.getDefaultAgentId();
40
+ }
41
+
42
+ scoredBindings.sort((a, b) => b.score - a.score);
43
+
44
+ const best = scoredBindings[0];
45
+ this.log.debug(`Routing to agent: ${best?.binding.agentId}`, {
46
+ score: best?.score,
47
+ channel: ctx.channel
48
+ });
49
+
50
+ return best?.binding.agentId ?? this.getDefaultAgentId();
51
+ }
52
+
53
+ private calculateScore(binding: Binding, ctx: RoutingContext): number {
54
+ const match = binding.match;
55
+ let score = 0;
56
+
57
+ if (match.peer?.id && match.peer?.kind) {
58
+ if (ctx.peerId === match.peer.id && ctx.peerKind === match.peer.kind) {
59
+ score += 1000;
60
+ } else {
61
+ return 0;
62
+ }
63
+ } else if (match.peer?.id) {
64
+ if (ctx.peerId === match.peer.id) {
65
+ score += 900;
66
+ } else {
67
+ return 0;
68
+ }
69
+ } else if (match.peer?.kind) {
70
+ if (ctx.peerKind === match.peer.kind) {
71
+ score += 100;
72
+ } else {
73
+ return 0;
74
+ }
75
+ }
76
+
77
+ if (match.guildId) {
78
+ if (ctx.guildId === match.guildId) {
79
+ if (match.roles && match.roles.length > 0) {
80
+ const hasRole = ctx.roles?.some((r) => match.roles?.includes(r));
81
+ if (hasRole) {
82
+ score += 800;
83
+ } else {
84
+ return 0;
85
+ }
86
+ } else {
87
+ score += 200;
88
+ }
89
+ } else {
90
+ return 0;
91
+ }
92
+ }
93
+
94
+ if (match.teamId) {
95
+ if (ctx.teamId === match.teamId) {
96
+ score += 300;
97
+ } else {
98
+ return 0;
99
+ }
100
+ }
101
+
102
+ if (match.accountId) {
103
+ if (ctx.accountId === match.accountId) {
104
+ score += 400;
105
+ } else {
106
+ return 0;
107
+ }
108
+ }
109
+
110
+ if (match.channel) {
111
+ if (ctx.channel === match.channel) {
112
+ score += 50;
113
+ } else {
114
+ return 0;
115
+ }
116
+ }
117
+
118
+ return score || 1;
119
+ }
120
+
121
+ private getDefaultAgentId(): string {
122
+ const agents = this.config.agents?.list ?? [];
123
+
124
+ for (const agent of agents) {
125
+ if (agent.default) {
126
+ return agent.id;
127
+ }
128
+ }
129
+
130
+ return "main";
131
+ }
132
+
133
+ addBinding(binding: Binding): void {
134
+ this.bindings.push(binding);
135
+ this.log.info(`Added binding: ${binding.agentId}`, { match: binding.match });
136
+ }
137
+
138
+ removeBinding(agentId: string, match?: Partial<Binding["match"]>): number {
139
+ const initialLength = this.bindings.length;
140
+
141
+ this.bindings = this.bindings.filter((b) => {
142
+ if (b.agentId !== agentId) return true;
143
+ if (!match) return false;
144
+
145
+ for (const [key, value] of Object.entries(match)) {
146
+ if (JSON.stringify((b.match as any)[key]) !== JSON.stringify(value)) {
147
+ return true;
148
+ }
149
+ }
150
+ return false;
151
+ });
152
+
153
+ const removed = initialLength - this.bindings.length;
154
+ if (removed > 0) {
155
+ this.log.info(`Removed ${removed} binding(s) for agent: ${agentId}`);
156
+ }
157
+ return removed;
158
+ }
159
+
160
+ listBindings(): Binding[] {
161
+ return [...this.bindings];
162
+ }
163
+
164
+ getBindingsForAgent(agentId: string): Binding[] {
165
+ return this.bindings.filter((b) => b.agentId === agentId);
166
+ }
167
+ }
168
+
169
+ export function createBindingRouter(config: Config): BindingRouter {
170
+ return new BindingRouter(config);
171
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./manager.ts";
2
+ export * from "./bindings.ts";
3
+ export * from "./sandbox.ts";
4
+ export * from "./subagents.ts";