@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,290 @@
|
|
|
1
|
+
import type { Config } from "../config/loader";
|
|
2
|
+
import { logger } from "../utils/logger";
|
|
3
|
+
import { getDb, initializeDatabase } from "../storage/sqlite";
|
|
4
|
+
import { loadContextToStore } from "../storage/bun-sqlite-store";
|
|
5
|
+
import { Agent } from "../agent/index";
|
|
6
|
+
import { buildSupervisorGraph } from "../agent/supervisor";
|
|
7
|
+
import { AgentRunner } from "../agent/providers/index";
|
|
8
|
+
import { ChannelManager } from "../channels/manager";
|
|
9
|
+
import { mkdirSync } from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
|
|
12
|
+
const log = logger.child("gateway:init");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Verifica que exista al menos un usuario en la base de datos
|
|
16
|
+
*/
|
|
17
|
+
export async function verifyDatabaseUsers(): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
// Initialize database if not already done
|
|
20
|
+
initializeDatabase();
|
|
21
|
+
|
|
22
|
+
const db = getDb();
|
|
23
|
+
const userCount = db.query("SELECT COUNT(*) as count FROM users").get() as { count: number };
|
|
24
|
+
|
|
25
|
+
if (userCount.count === 0) {
|
|
26
|
+
const error = new Error("No users found in the database. A valid user is required to start the Hive Gateway.");
|
|
27
|
+
log.error(error.message);
|
|
28
|
+
log.error("Please run the onboarding process or manually insert a user.");
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
log.info(`Database verified: ${userCount.count} user(s) found`);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
log.error(`Database verification failed: ${(error as Error).message}`);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Escribe el archivo PID del proceso
|
|
41
|
+
*/
|
|
42
|
+
export async function writePidFile(pidFile: string): Promise<void> {
|
|
43
|
+
try {
|
|
44
|
+
const dir = path.dirname(pidFile);
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
await Bun.write(pidFile, process.pid.toString());
|
|
47
|
+
log.info(`PID file written: ${pidFile}`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
log.warn(`Could not write PID file: ${(error as Error).message}`);
|
|
50
|
+
// No throw - PID file is not critical
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Carga la configuración del agente desde la base de datos
|
|
56
|
+
* @returns Provider y modelo configurados
|
|
57
|
+
*/
|
|
58
|
+
export async function loadAgentConfigFromDB(
|
|
59
|
+
config: Config
|
|
60
|
+
): Promise<{ provider: string; model: string }> {
|
|
61
|
+
const defaultProvider = "gemini";
|
|
62
|
+
const defaultModel = "gemini-2.5-flash";
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const db = getDb();
|
|
66
|
+
|
|
67
|
+
// Obtener configuración del agente coordinador
|
|
68
|
+
const agentConfig = db.query(`
|
|
69
|
+
SELECT provider_id, model_id FROM agents
|
|
70
|
+
WHERE id = ? OR is_coordinator = 1
|
|
71
|
+
ORDER BY (CASE WHEN id = ? THEN 1 ELSE 0 END) DESC
|
|
72
|
+
LIMIT 1
|
|
73
|
+
`).get(process.env.HIVE_AGENT_ID || "", process.env.HIVE_AGENT_ID || "") as
|
|
74
|
+
{ provider_id: string | null; model_id: string | null } | undefined;
|
|
75
|
+
|
|
76
|
+
let provider = agentConfig?.provider_id || defaultProvider;
|
|
77
|
+
let model = agentConfig?.model_id || defaultModel;
|
|
78
|
+
|
|
79
|
+
// Cargar API keys de los providers desde la DB
|
|
80
|
+
const providers = db.query(`
|
|
81
|
+
SELECT id, name, api_key_encrypted, api_key_iv, base_url
|
|
82
|
+
FROM providers
|
|
83
|
+
WHERE active = 1 AND api_key_encrypted IS NOT NULL
|
|
84
|
+
`).all() as Array<{
|
|
85
|
+
id: string;
|
|
86
|
+
name: string;
|
|
87
|
+
api_key_encrypted: string;
|
|
88
|
+
api_key_iv: string;
|
|
89
|
+
base_url: string | null
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
if (providers.length > 0) {
|
|
93
|
+
config.models = config.models || {};
|
|
94
|
+
config.models.providers = config.models.providers || {};
|
|
95
|
+
|
|
96
|
+
const { decryptApiKey } = await import("../storage/crypto");
|
|
97
|
+
|
|
98
|
+
for (const p of providers) {
|
|
99
|
+
const apiKey = await decryptApiKey(p.api_key_encrypted, p.api_key_iv);
|
|
100
|
+
|
|
101
|
+
config.models.providers[p.name] = {
|
|
102
|
+
apiKey,
|
|
103
|
+
baseUrl: p.base_url || undefined,
|
|
104
|
+
defaultModel: model,
|
|
105
|
+
availableModels: [],
|
|
106
|
+
maxRetries: 3,
|
|
107
|
+
timeoutMs: 30000,
|
|
108
|
+
} as any;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
log.info(`Loaded ${providers.length} provider(s) from DB with API keys`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
log.info(`Agent config loaded from DB: ${provider}/${model}`);
|
|
115
|
+
return { provider, model };
|
|
116
|
+
|
|
117
|
+
} catch (error) {
|
|
118
|
+
log.debug(`Could not read agent config from DB, using defaults: ${defaultProvider}/${defaultModel}`);
|
|
119
|
+
return { provider: defaultProvider, model: defaultModel };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Inicializa el contexto desde la base de datos (Skills, Ethics, MCP, Tools)
|
|
125
|
+
*/
|
|
126
|
+
export async function initializeContextStore(): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
const db = getDb();
|
|
129
|
+
await loadContextToStore(db);
|
|
130
|
+
log.info("Context store initialized (Skills, Ethics, MCP, Tools)");
|
|
131
|
+
} catch (error) {
|
|
132
|
+
log.error(`Failed to initialize context store: ${(error as Error).message}`);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Carga las éticas activas desde la base de datos
|
|
139
|
+
*/
|
|
140
|
+
export async function loadEthicsToAgent(agent: Agent): Promise<void> {
|
|
141
|
+
try {
|
|
142
|
+
const db = getDb();
|
|
143
|
+
const ethics = db.query("SELECT content FROM ethics WHERE active = 1 LIMIT 1").get() as
|
|
144
|
+
{ content: string } | undefined;
|
|
145
|
+
|
|
146
|
+
if (ethics) {
|
|
147
|
+
agent.setEthics(ethics.content);
|
|
148
|
+
log.info("Active ethics loaded from DB");
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log.error(`Error loading ethics from DB: ${(error as Error).message}`);
|
|
152
|
+
// No throw - ethics are not critical
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Inicializa el grafo del supervisor (LangGraph)
|
|
158
|
+
*/
|
|
159
|
+
export async function initializeSupervisorGraph(mcpManager?: any): Promise<void> {
|
|
160
|
+
try {
|
|
161
|
+
await buildSupervisorGraph({ mcpManager });
|
|
162
|
+
log.info("Supervisor graph initialized");
|
|
163
|
+
} catch (error) {
|
|
164
|
+
log.warn(`Supervisor graph initialization failed: ${(error as Error).message}`);
|
|
165
|
+
// No throw - supervisor graph can be rebuilt later
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Inicializa el agente con su configuración
|
|
171
|
+
*/
|
|
172
|
+
export async function initializeAgent(
|
|
173
|
+
config: Config,
|
|
174
|
+
workspacePath: string
|
|
175
|
+
): Promise<Agent> {
|
|
176
|
+
try {
|
|
177
|
+
const agentId = process.env.HIVE_AGENT_ID || "main";
|
|
178
|
+
const agent = new Agent({
|
|
179
|
+
agentId,
|
|
180
|
+
config,
|
|
181
|
+
workspacePath,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await agent.initialize();
|
|
185
|
+
log.info(`Agent initialized: ${agent.agentId}`);
|
|
186
|
+
|
|
187
|
+
return agent;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
log.error(`Failed to initialize agent: ${(error as Error).message}`);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Inicializa el runner de LLM
|
|
196
|
+
*/
|
|
197
|
+
export async function initializeLLMRunner(
|
|
198
|
+
config: Config,
|
|
199
|
+
provider: string,
|
|
200
|
+
model: string
|
|
201
|
+
): Promise<AgentRunner> {
|
|
202
|
+
try {
|
|
203
|
+
const runner = new AgentRunner(config);
|
|
204
|
+
log.info(`LLM runner initialized: ${provider}/${model}`);
|
|
205
|
+
return runner;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
log.error(`Failed to initialize LLM runner: ${(error as Error).message}`);
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Inicializa el manager de canales
|
|
214
|
+
*/
|
|
215
|
+
export async function initializeChannelManager(
|
|
216
|
+
config: Config
|
|
217
|
+
): Promise<ChannelManager> {
|
|
218
|
+
try {
|
|
219
|
+
const channelManager = new ChannelManager(config);
|
|
220
|
+
await channelManager.initialize();
|
|
221
|
+
await channelManager.startAll();
|
|
222
|
+
log.info("Channel manager initialized and started");
|
|
223
|
+
return channelManager;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
log.error(`Failed to initialize channel manager: ${(error as Error).message}`);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Función principal de inicialización que orquesta todos los módulos
|
|
232
|
+
*/
|
|
233
|
+
export interface GatewayInitializationResult {
|
|
234
|
+
agent: Agent;
|
|
235
|
+
runner: AgentRunner;
|
|
236
|
+
channelManager: ChannelManager;
|
|
237
|
+
provider: string;
|
|
238
|
+
model: string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function initializeGateway(
|
|
242
|
+
config: Config,
|
|
243
|
+
pidFile: string
|
|
244
|
+
): Promise<GatewayInitializationResult> {
|
|
245
|
+
const errors: Array<{ module: string; error: Error }> = [];
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
// 1. Verificar base de datos (crítico)
|
|
249
|
+
await verifyDatabaseUsers();
|
|
250
|
+
|
|
251
|
+
// 2. Escribir archivo PID (no crítico)
|
|
252
|
+
await writePidFile(pidFile);
|
|
253
|
+
|
|
254
|
+
// 3. Cargar configuración del agente desde DB
|
|
255
|
+
const { provider, model } = await loadAgentConfigFromDB(config);
|
|
256
|
+
|
|
257
|
+
// 4. Inicializar contexto (crítico)
|
|
258
|
+
await initializeContextStore();
|
|
259
|
+
|
|
260
|
+
// 5. Obtener workspace path
|
|
261
|
+
const agentList = config.agents?.list ?? [];
|
|
262
|
+
const defaultAgent = agentList.find((a) => a.default) ?? agentList[0];
|
|
263
|
+
const workspacePath = path.join(
|
|
264
|
+
process.env.HOME || "",
|
|
265
|
+
defaultAgent?.workspace?.replace("~/", "") || ".hive/workspace"
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// 6. Inicializar agente (crítico)
|
|
269
|
+
const agent = await initializeAgent(config, workspacePath);
|
|
270
|
+
|
|
271
|
+
// 7. Cargar éticas (no crítico)
|
|
272
|
+
await loadEthicsToAgent(agent);
|
|
273
|
+
|
|
274
|
+
// 8. Inicializar grafo del supervisor (no crítico)
|
|
275
|
+
const mcpManager = agent.getMCPManager();
|
|
276
|
+
await initializeSupervisorGraph(mcpManager);
|
|
277
|
+
|
|
278
|
+
// 9. Inicializar LLM runner (crítico)
|
|
279
|
+
const runner = await initializeLLMRunner(config, provider, model);
|
|
280
|
+
|
|
281
|
+
// 10. Inicializar channel manager (crítico)
|
|
282
|
+
const channelManager = await initializeChannelManager(config);
|
|
283
|
+
|
|
284
|
+
return { agent, runner, channelManager, provider, model };
|
|
285
|
+
|
|
286
|
+
} catch (error) {
|
|
287
|
+
log.error(`Gateway initialization failed: ${(error as Error).message}`);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
export type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
|
|
2
|
+
|
|
3
|
+
export interface Task {
|
|
4
|
+
id: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
status: TaskStatus;
|
|
7
|
+
priority: number;
|
|
8
|
+
createdAt: Date;
|
|
9
|
+
startedAt?: Date;
|
|
10
|
+
completedAt?: Date;
|
|
11
|
+
error?: string;
|
|
12
|
+
abortController: AbortController;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LaneQueueOptions {
|
|
16
|
+
maxConcurrency?: number;
|
|
17
|
+
taskTimeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type TaskHandler<T> = (task: Task, signal: AbortSignal) => Promise<T>;
|
|
21
|
+
|
|
22
|
+
export class LaneQueue {
|
|
23
|
+
private queues: Map<string, Task[]> = new Map();
|
|
24
|
+
private running: Map<string, Task> = new Map();
|
|
25
|
+
private handlers: Map<string, TaskHandler<unknown>> = new Map();
|
|
26
|
+
private taskIdCounter = 0;
|
|
27
|
+
private options: Required<LaneQueueOptions>;
|
|
28
|
+
|
|
29
|
+
constructor(options: LaneQueueOptions = {}) {
|
|
30
|
+
this.options = {
|
|
31
|
+
maxConcurrency: options.maxConcurrency ?? 1,
|
|
32
|
+
taskTimeoutMs: options.taskTimeoutMs ?? 300000,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private generateTaskId(): string {
|
|
37
|
+
return `task-${Date.now()}-${++this.taskIdCounter}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private getQueue(sessionId: string): Task[] {
|
|
41
|
+
let queue = this.queues.get(sessionId);
|
|
42
|
+
if (!queue) {
|
|
43
|
+
queue = [];
|
|
44
|
+
this.queues.set(sessionId, queue);
|
|
45
|
+
}
|
|
46
|
+
return queue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
enqueue<T>(
|
|
50
|
+
sessionId: string,
|
|
51
|
+
handler: TaskHandler<T>,
|
|
52
|
+
priority = 0
|
|
53
|
+
): Task {
|
|
54
|
+
const task: Task = {
|
|
55
|
+
id: this.generateTaskId(),
|
|
56
|
+
sessionId,
|
|
57
|
+
status: "pending",
|
|
58
|
+
priority,
|
|
59
|
+
createdAt: new Date(),
|
|
60
|
+
abortController: new AbortController(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.handlers.set(task.id, handler as TaskHandler<unknown>);
|
|
64
|
+
|
|
65
|
+
const queue = this.getQueue(sessionId);
|
|
66
|
+
queue.push(task);
|
|
67
|
+
queue.sort((a, b) => b.priority - a.priority);
|
|
68
|
+
|
|
69
|
+
this.processQueue(sessionId);
|
|
70
|
+
|
|
71
|
+
return task;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async processQueue(sessionId: string): Promise<void> {
|
|
75
|
+
const running = this.running.get(sessionId);
|
|
76
|
+
if (running) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const queue = this.getQueue(sessionId);
|
|
81
|
+
if (queue.length === 0) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const task = queue.shift();
|
|
86
|
+
if (!task) return;
|
|
87
|
+
|
|
88
|
+
task.status = "running";
|
|
89
|
+
task.startedAt = new Date();
|
|
90
|
+
this.running.set(sessionId, task);
|
|
91
|
+
|
|
92
|
+
const handler = this.handlers.get(task.id);
|
|
93
|
+
|
|
94
|
+
const timeoutId = setTimeout(() => {
|
|
95
|
+
task.abortController.abort();
|
|
96
|
+
}, this.options.taskTimeoutMs);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (handler) {
|
|
100
|
+
await handler(task, task.abortController.signal);
|
|
101
|
+
}
|
|
102
|
+
task.status = "completed";
|
|
103
|
+
task.completedAt = new Date();
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if ((error as Error).name === "AbortError") {
|
|
106
|
+
task.status = "cancelled";
|
|
107
|
+
} else {
|
|
108
|
+
task.status = "failed";
|
|
109
|
+
task.error = (error as Error).message;
|
|
110
|
+
}
|
|
111
|
+
task.completedAt = new Date();
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timeoutId);
|
|
114
|
+
this.running.delete(sessionId);
|
|
115
|
+
this.handlers.delete(task.id);
|
|
116
|
+
|
|
117
|
+
if (queue.length > 0) {
|
|
118
|
+
this.processQueue(sessionId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cancel(sessionId: string): boolean {
|
|
124
|
+
const task = this.running.get(sessionId);
|
|
125
|
+
if (task) {
|
|
126
|
+
task.abortController.abort();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const queue = this.getQueue(sessionId);
|
|
131
|
+
const index = queue.findIndex((t) => t.status === "pending");
|
|
132
|
+
if (index >= 0) {
|
|
133
|
+
const cancelled = queue.splice(index, 1)[0];
|
|
134
|
+
if (cancelled) {
|
|
135
|
+
cancelled.status = "cancelled";
|
|
136
|
+
cancelled.completedAt = new Date();
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getStatus(sessionId: string): {
|
|
145
|
+
queueLength: number;
|
|
146
|
+
running?: Task;
|
|
147
|
+
} {
|
|
148
|
+
const queue = this.getQueue(sessionId);
|
|
149
|
+
const running = this.running.get(sessionId);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
queueLength: queue.length,
|
|
153
|
+
running,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
isProcessing(sessionId: string): boolean {
|
|
158
|
+
return this.running.has(sessionId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
prune(sessionId: string): void {
|
|
162
|
+
const queue = this.getQueue(sessionId);
|
|
163
|
+
if (queue.length === 0 && !this.running.has(sessionId)) {
|
|
164
|
+
this.queues.delete(sessionId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const laneQueue = new LaneQueue();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { getDb } from "../storage/sqlite"
|
|
2
|
+
|
|
3
|
+
export interface ResolveContextResult {
|
|
4
|
+
userId: string
|
|
5
|
+
agentId: string
|
|
6
|
+
isNewUser: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ResolveContextOptions {
|
|
10
|
+
channel: string
|
|
11
|
+
channelUserId: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveContext(options: ResolveContextOptions): ResolveContextResult {
|
|
15
|
+
const { channel, channelUserId } = options
|
|
16
|
+
const db = getDb()
|
|
17
|
+
|
|
18
|
+
const identity = db
|
|
19
|
+
.query<any, [string, string]>(
|
|
20
|
+
"SELECT user_id FROM user_identities WHERE channel = ? AND channel_user_id = ?"
|
|
21
|
+
)
|
|
22
|
+
.get(channel, channelUserId)
|
|
23
|
+
|
|
24
|
+
let userId: string
|
|
25
|
+
let isNewUser = false
|
|
26
|
+
|
|
27
|
+
if (identity) {
|
|
28
|
+
userId = identity.user_id
|
|
29
|
+
} else {
|
|
30
|
+
// Sistema mono-usuario: reutilizar el usuario del onboarding
|
|
31
|
+
const existingUser = db
|
|
32
|
+
.query<any, []>("SELECT id FROM users ORDER BY created_at ASC LIMIT 1")
|
|
33
|
+
.get() as { id: string } | undefined
|
|
34
|
+
|
|
35
|
+
if (!existingUser) {
|
|
36
|
+
throw new Error("No user found in database. Please run the onboarding process first.")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
userId = existingUser.id
|
|
40
|
+
|
|
41
|
+
// Vincular este canal al usuario existente (auto-link en el primer mensaje)
|
|
42
|
+
// INSERT OR REPLACE: si ya existe una fila (user_id, channel), actualiza channel_user_id
|
|
43
|
+
// con el valor real del canal (e.g. chat ID numérico de Telegram).
|
|
44
|
+
db.query(
|
|
45
|
+
"INSERT OR REPLACE INTO user_identities (user_id, channel, channel_user_id, linked_at) VALUES (?, ?, ?, ?)"
|
|
46
|
+
).run(userId, channel, channelUserId, Math.floor(Date.now() / 1000))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const coordinatorAgent = db
|
|
50
|
+
.query<any, []>("SELECT id FROM agents WHERE is_coordinator = 1 LIMIT 1")
|
|
51
|
+
.get()
|
|
52
|
+
|
|
53
|
+
const agentId = coordinatorAgent?.id || "bee"
|
|
54
|
+
|
|
55
|
+
return { userId, agentId, isNewUser }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getDefaultAgentId(): string {
|
|
59
|
+
const db = getDb()
|
|
60
|
+
const coordinatorAgent = db
|
|
61
|
+
.query<any, []>("SELECT id FROM agents WHERE is_coordinator = 1 LIMIT 1")
|
|
62
|
+
.get()
|
|
63
|
+
|
|
64
|
+
return coordinatorAgent?.id || "bee"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getUserById(userId: string): any {
|
|
68
|
+
const db = getDb()
|
|
69
|
+
return db.query<any, [string]>("SELECT * FROM users WHERE id = ?").get(userId)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function updateUserProfile(userId: string, updates: {
|
|
73
|
+
name?: string
|
|
74
|
+
language?: string
|
|
75
|
+
timezone?: string
|
|
76
|
+
occupation?: string
|
|
77
|
+
notes?: string
|
|
78
|
+
}): void {
|
|
79
|
+
const db = getDb()
|
|
80
|
+
const setClauses: string[] = []
|
|
81
|
+
const values: any[] = []
|
|
82
|
+
|
|
83
|
+
if (updates.name !== undefined) {
|
|
84
|
+
setClauses.push("name = ?")
|
|
85
|
+
values.push(updates.name)
|
|
86
|
+
}
|
|
87
|
+
if (updates.language !== undefined) {
|
|
88
|
+
setClauses.push("language = ?")
|
|
89
|
+
values.push(updates.language)
|
|
90
|
+
}
|
|
91
|
+
if (updates.timezone !== undefined) {
|
|
92
|
+
setClauses.push("timezone = ?")
|
|
93
|
+
values.push(updates.timezone)
|
|
94
|
+
}
|
|
95
|
+
if (updates.occupation !== undefined) {
|
|
96
|
+
setClauses.push("occupation = ?")
|
|
97
|
+
values.push(updates.occupation)
|
|
98
|
+
}
|
|
99
|
+
if (updates.notes !== undefined) {
|
|
100
|
+
setClauses.push("notes = ?")
|
|
101
|
+
values.push(updates.notes)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (setClauses.length > 0) {
|
|
105
|
+
values.push(userId)
|
|
106
|
+
db.query(`UPDATE users SET ${setClauses.join(", ")} WHERE id = ?`).run(...values)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Config, Binding } from "../config/loader.ts";
|
|
2
|
+
|
|
3
|
+
export interface RoutingContext {
|
|
4
|
+
channel: string;
|
|
5
|
+
accountId?: string;
|
|
6
|
+
peerKind?: "direct" | "group";
|
|
7
|
+
peerId?: string;
|
|
8
|
+
guildId?: string;
|
|
9
|
+
teamId?: string;
|
|
10
|
+
roles?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function matchBinding(binding: Binding, ctx: RoutingContext): number {
|
|
14
|
+
const match = binding.match;
|
|
15
|
+
let score = 0;
|
|
16
|
+
|
|
17
|
+
if (match.peer?.id && match.peer?.kind) {
|
|
18
|
+
if (ctx.peerId === match.peer.id && ctx.peerKind === match.peer.kind) {
|
|
19
|
+
score += 1000;
|
|
20
|
+
} else {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
} else if (match.peer?.id) {
|
|
24
|
+
if (ctx.peerId === match.peer.id) {
|
|
25
|
+
score += 900;
|
|
26
|
+
} else {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
} else if (match.peer?.kind) {
|
|
30
|
+
if (ctx.peerKind === match.peer.kind) {
|
|
31
|
+
score += 50;
|
|
32
|
+
} else {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (match.guildId && match.roles && match.roles.length > 0) {
|
|
38
|
+
if (ctx.guildId === match.guildId && ctx.roles?.some((r) => match.roles?.includes(r))) {
|
|
39
|
+
score += 800;
|
|
40
|
+
} else {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
} else if (match.guildId) {
|
|
44
|
+
if (ctx.guildId === match.guildId) {
|
|
45
|
+
score += 200;
|
|
46
|
+
} else {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (match.teamId) {
|
|
52
|
+
if (ctx.teamId === match.teamId) {
|
|
53
|
+
score += 300;
|
|
54
|
+
} else {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (match.accountId) {
|
|
60
|
+
if (ctx.accountId === match.accountId) {
|
|
61
|
+
score += 400;
|
|
62
|
+
} else {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (match.channel) {
|
|
68
|
+
if (ctx.channel === match.channel) {
|
|
69
|
+
score += 100;
|
|
70
|
+
} else {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return score || 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveAgent(
|
|
79
|
+
config: Config,
|
|
80
|
+
ctx: RoutingContext
|
|
81
|
+
): string {
|
|
82
|
+
const bindings = config.bindings ?? [];
|
|
83
|
+
|
|
84
|
+
if (bindings.length === 0) {
|
|
85
|
+
return config.agent?.defaultAgentId ?? "main";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let bestMatch: { agentId: string; score: number } | null = null;
|
|
89
|
+
|
|
90
|
+
for (const binding of bindings) {
|
|
91
|
+
const score = matchBinding(binding, ctx);
|
|
92
|
+
if (score > 0 && (!bestMatch || score > bestMatch.score)) {
|
|
93
|
+
bestMatch = { agentId: binding.agentId, score };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (bestMatch) {
|
|
98
|
+
return bestMatch.agentId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return config.agent?.defaultAgentId ?? "main";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class Router {
|
|
105
|
+
constructor(private config: Config) {}
|
|
106
|
+
|
|
107
|
+
route(ctx: RoutingContext): string {
|
|
108
|
+
return resolveAgent(this.config, ctx);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getAgentWorkspace(agentId: string): string {
|
|
112
|
+
const agents = this.config.agents?.list ?? [];
|
|
113
|
+
const agent = agents.find((a) => a.id === agentId);
|
|
114
|
+
|
|
115
|
+
if (agent?.workspace) {
|
|
116
|
+
return agent.workspace.replace(/^~/, process.env.HOME ?? "");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const baseDir = this.config.agent?.baseDir?.replace(/^~/, process.env.HOME ?? "")
|
|
120
|
+
?? `${process.env.HOME}/.hive/agents`;
|
|
121
|
+
|
|
122
|
+
return `${baseDir}/${agentId}/workspace`;
|
|
123
|
+
}
|
|
124
|
+
}
|