@johpaz/hive-sdk 0.0.14 → 0.0.16

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 (257) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +280 -0
  3. package/docs/API-AGENTS.md +316 -0
  4. package/docs/API-CONTEXT-COMPILER.md +252 -0
  5. package/docs/API-DAG-SCHEDULER.md +273 -0
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +350 -0
  7. package/docs/API-WORKERS-EVENTS.md +299 -0
  8. package/docs/INDEX.md +190 -0
  9. package/docs/README.md +161 -0
  10. package/docs/TEMPLATE-HIVE-APP.md +360 -0
  11. package/package.json +60 -104
  12. package/packages/cli/bin/hive +2 -0
  13. package/packages/cli/package.json +17 -0
  14. package/packages/cli/src/commands/add-skill.ts +42 -0
  15. package/packages/cli/src/commands/add-tool.ts +45 -0
  16. package/packages/cli/src/commands/add-worker.ts +49 -0
  17. package/packages/cli/src/commands/create-app-utils.ts +32 -0
  18. package/packages/cli/src/commands/create-app.test.ts +151 -0
  19. package/packages/cli/src/commands/create-app.ts +35 -0
  20. package/packages/cli/src/commands/init.ts +56 -0
  21. package/packages/cli/src/commands/run.ts +45 -0
  22. package/packages/cli/src/commands/test.ts +42 -0
  23. package/packages/cli/src/commands/trace.ts +55 -0
  24. package/packages/cli/src/index.ts +59 -0
  25. package/packages/cli/templates/hive-app/.env.example +17 -0
  26. package/packages/cli/templates/hive-app/docker-compose.yml +20 -0
  27. package/packages/cli/templates/hive-app/hive.config.ts +19 -0
  28. package/packages/cli/templates/hive-app/package.json +16 -0
  29. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +9 -0
  30. package/packages/cli/templates/hive-app/src/main.ts +56 -0
  31. package/packages/core/package.json +58 -0
  32. package/packages/core/src/ace/Curator.ts +158 -0
  33. package/packages/core/src/ace/Reflector.ts +200 -0
  34. package/packages/core/src/ace/Tracer.ts +100 -0
  35. package/packages/core/src/ace/index.ts +4 -0
  36. package/packages/core/src/agent/AgentRunner.ts +699 -0
  37. package/packages/core/src/agent/Compaction.ts +221 -0
  38. package/packages/core/src/agent/ContextCompiler.ts +567 -0
  39. package/packages/core/src/agent/ContextGuard.ts +91 -0
  40. package/packages/core/src/agent/ConversationStore.ts +244 -0
  41. package/packages/core/src/agent/Hooks.ts +166 -0
  42. package/packages/core/src/agent/NativeTools.ts +31 -0
  43. package/packages/core/src/agent/PromptBuilder.ts +169 -0
  44. package/packages/core/src/agent/Service.ts +267 -0
  45. package/packages/core/src/agent/StuckLoop.ts +133 -0
  46. package/packages/core/src/agent/index.ts +12 -0
  47. package/packages/core/src/agent/providers/LLMClient.ts +149 -0
  48. package/packages/core/src/agent/providers/anthropic.ts +212 -0
  49. package/packages/core/src/agent/providers/gemini.ts +215 -0
  50. package/packages/core/src/agent/providers/index.ts +199 -0
  51. package/packages/core/src/agent/providers/interface.ts +195 -0
  52. package/packages/core/src/agent/providers/ollama.ts +175 -0
  53. package/packages/core/src/agent/providers/openai-compat.ts +231 -0
  54. package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
  55. package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
  56. package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
  57. package/packages/core/src/agent/selectors/index.ts +6 -0
  58. package/packages/core/src/api/createAgent.test.ts +48 -0
  59. package/packages/core/src/api/createAgent.ts +122 -0
  60. package/packages/core/src/api/index.ts +2 -0
  61. package/packages/core/src/auth/auth.ts +108 -0
  62. package/packages/core/src/auth/index.ts +1 -0
  63. package/packages/core/src/canvas/CanvasManager.ts +390 -0
  64. package/packages/core/src/canvas/a2ui-tools.ts +255 -0
  65. package/packages/core/src/canvas/canvas-tools.ts +448 -0
  66. package/packages/core/src/canvas/canvas.test.ts +32 -0
  67. package/packages/core/src/canvas/emitter.ts +149 -0
  68. package/packages/core/src/canvas/index.ts +3 -0
  69. package/packages/core/src/channels/base.ts +154 -0
  70. package/packages/core/src/channels/channels.test.ts +18 -0
  71. package/packages/core/src/channels/discord.ts +273 -0
  72. package/packages/core/src/channels/index.ts +7 -0
  73. package/packages/core/src/channels/manager.ts +450 -0
  74. package/packages/core/src/channels/slack.ts +323 -0
  75. package/packages/core/src/channels/telegram.ts +612 -0
  76. package/packages/core/src/channels/webchat.ts +139 -0
  77. package/packages/core/src/channels/whatsapp.ts +548 -0
  78. package/packages/core/src/config/index.ts +2 -0
  79. package/packages/core/src/config/loader.ts +554 -0
  80. package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
  81. package/packages/core/src/ethics/EthicsGuard.ts +66 -0
  82. package/packages/core/src/ethics/index.ts +2 -0
  83. package/packages/core/src/events/agent-bus.ts +460 -0
  84. package/packages/core/src/events/event-bus.ts +169 -0
  85. package/packages/core/src/gateway/channel-notify.ts +37 -0
  86. package/packages/core/src/gateway/gateway.test.ts +38 -0
  87. package/packages/core/src/gateway/index.ts +2 -0
  88. package/packages/core/src/gateway/server.ts +139 -0
  89. package/packages/core/src/heartbeat/index.ts +157 -0
  90. package/packages/core/src/index.ts +81 -0
  91. package/packages/core/src/mcp/MCPClient.ts +439 -0
  92. package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
  93. package/packages/core/src/mcp/config.ts +13 -0
  94. package/packages/core/src/mcp/hot-reload.ts +147 -0
  95. package/packages/core/src/mcp/index.ts +11 -0
  96. package/packages/core/src/mcp/logger.ts +42 -0
  97. package/packages/core/src/mcp/singleton.ts +21 -0
  98. package/packages/core/src/mcp/transports/index.ts +67 -0
  99. package/packages/core/src/mcp/transports/sse.ts +241 -0
  100. package/packages/core/src/mcp/transports/websocket.ts +159 -0
  101. package/packages/core/src/memory/Scratchpad.test.ts +47 -0
  102. package/packages/core/src/memory/Scratchpad.ts +37 -0
  103. package/packages/core/src/memory/Storage.ts +6 -0
  104. package/packages/core/src/memory/index.ts +2 -0
  105. package/packages/core/src/multimodal/VisionService.ts +293 -0
  106. package/packages/core/src/multimodal/index.ts +2 -0
  107. package/packages/core/src/multimodal/types.ts +28 -0
  108. package/packages/core/src/multimodal/vision-service.ts +283 -0
  109. package/packages/core/src/plugins/api.ts +128 -0
  110. package/packages/core/src/plugins/index.ts +2 -0
  111. package/packages/core/src/plugins/loader.ts +365 -0
  112. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  113. package/packages/core/src/scheduler/CronScheduler.ts +699 -0
  114. package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
  115. package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
  116. package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
  117. package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
  118. package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
  119. package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
  120. package/packages/core/src/scheduler/dag/errors.ts +37 -0
  121. package/packages/core/src/scheduler/dag/index.ts +26 -0
  122. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
  123. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
  124. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
  125. package/packages/core/src/scheduler/index.ts +22 -0
  126. package/packages/core/src/scheduler/integration.ts +237 -0
  127. package/packages/core/src/scheduler/scheduler.test.ts +19 -0
  128. package/packages/core/src/scheduler/types.ts +164 -0
  129. package/packages/core/src/security/Pairing.ts +250 -0
  130. package/packages/core/src/security/RateLimit.ts +270 -0
  131. package/packages/core/src/security/google-chat.ts +269 -0
  132. package/packages/core/src/security/index.ts +192 -0
  133. package/packages/core/src/security/rate-limit.ts +270 -0
  134. package/packages/core/src/security/signal.ts +321 -0
  135. package/packages/core/src/skills/SkillLoader.ts +388 -0
  136. package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
  137. package/packages/core/src/skills/defineSkill.ts +18 -0
  138. package/packages/core/src/skills/index.ts +4 -0
  139. package/packages/core/src/state/index.ts +2 -0
  140. package/packages/core/src/state/store.ts +312 -0
  141. package/packages/core/src/storage/SQLiteStorage.ts +407 -0
  142. package/packages/core/src/storage/crypto.ts +233 -0
  143. package/packages/core/src/storage/index.ts +10 -0
  144. package/packages/core/src/storage/onboarding.ts +1603 -0
  145. package/packages/core/src/storage/schema.ts +689 -0
  146. package/packages/core/src/storage/seed.ts +740 -0
  147. package/packages/core/src/storage/storage.test.ts +37 -0
  148. package/packages/core/src/storage/usage.ts +374 -0
  149. package/packages/core/src/swarm/AgentBus.ts +460 -0
  150. package/packages/core/src/swarm/AgentExecutor.ts +53 -0
  151. package/packages/core/src/swarm/Coordinator.ts +251 -0
  152. package/packages/core/src/swarm/EventBridge.ts +122 -0
  153. package/packages/core/src/swarm/EventBus.ts +169 -0
  154. package/packages/core/src/swarm/TaskGraph.ts +192 -0
  155. package/packages/core/src/swarm/TaskNode.ts +97 -0
  156. package/packages/core/src/swarm/TaskResult.ts +22 -0
  157. package/packages/core/src/swarm/WorkerPool.ts +236 -0
  158. package/packages/core/src/swarm/errors.ts +37 -0
  159. package/packages/core/src/swarm/index.ts +30 -0
  160. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
  161. package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
  162. package/packages/core/src/swarm/presets/index.ts +4 -0
  163. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
  164. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
  165. package/packages/core/src/swarm/strategies/index.ts +3 -0
  166. package/packages/core/src/swarm/swarm.test.ts +24 -0
  167. package/packages/core/src/swarm/types.ts +164 -0
  168. package/packages/core/src/tool-runtime/index.ts +522 -0
  169. package/packages/core/src/tool-runtime/tool-runtime.test.ts +91 -0
  170. package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
  171. package/packages/core/src/tools/ToolExecutor.ts +58 -0
  172. package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
  173. package/packages/core/src/tools/ToolRegistry.ts +61 -0
  174. package/packages/core/src/tools/agents/get-available-models.ts +118 -0
  175. package/packages/core/src/tools/agents/index.ts +715 -0
  176. package/packages/core/src/tools/bridge-events.ts +26 -0
  177. package/packages/core/src/tools/canvas/index.ts +375 -0
  178. package/packages/core/src/tools/cli/index.ts +142 -0
  179. package/packages/core/src/tools/codebridge/index.ts +342 -0
  180. package/packages/core/src/tools/core/index.ts +476 -0
  181. package/packages/core/src/tools/cron/index.ts +626 -0
  182. package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
  183. package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
  184. package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
  185. package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
  186. package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
  187. package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
  188. package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
  189. package/packages/core/src/tools/filesystem/index.ts +34 -0
  190. package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
  191. package/packages/core/src/tools/index.ts +231 -0
  192. package/packages/core/src/tools/meeting/index.ts +363 -0
  193. package/packages/core/src/tools/office/index.ts +47 -0
  194. package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
  195. package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
  196. package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
  197. package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
  198. package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
  199. package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
  200. package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
  201. package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
  202. package/packages/core/src/tools/projects/index.ts +37 -0
  203. package/packages/core/src/tools/projects/project-create.ts +94 -0
  204. package/packages/core/src/tools/projects/project-done.ts +66 -0
  205. package/packages/core/src/tools/projects/project-fail.ts +66 -0
  206. package/packages/core/src/tools/projects/project-list.ts +96 -0
  207. package/packages/core/src/tools/projects/project-update.ts +72 -0
  208. package/packages/core/src/tools/projects/task-create.ts +68 -0
  209. package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
  210. package/packages/core/src/tools/projects/task-update.ts +93 -0
  211. package/packages/core/src/tools/types.ts +39 -0
  212. package/packages/core/src/tools/voice/index.ts +104 -0
  213. package/packages/core/src/tools/web/browser-click.ts +78 -0
  214. package/packages/core/src/tools/web/browser-extract.ts +139 -0
  215. package/packages/core/src/tools/web/browser-navigate.ts +106 -0
  216. package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
  217. package/packages/core/src/tools/web/browser-script.ts +88 -0
  218. package/packages/core/src/tools/web/browser-service.ts +554 -0
  219. package/packages/core/src/tools/web/browser-type.ts +101 -0
  220. package/packages/core/src/tools/web/browser-wait.ts +136 -0
  221. package/packages/core/src/tools/web/index.ts +41 -0
  222. package/packages/core/src/tools/web/web-fetch.ts +78 -0
  223. package/packages/core/src/tools/web/web-search.ts +123 -0
  224. package/packages/core/src/utils/benchmark.ts +80 -0
  225. package/packages/core/src/utils/crypto.ts +73 -0
  226. package/packages/core/src/utils/date.ts +42 -0
  227. package/packages/core/src/utils/index.ts +10 -0
  228. package/packages/core/src/utils/logger.ts +389 -0
  229. package/packages/core/src/utils/retry.ts +70 -0
  230. package/packages/core/src/utils/toon.ts +253 -0
  231. package/packages/core/src/voice/index.ts +643 -0
  232. package/packages/core/src/workers/WorkerPool.ts +167 -0
  233. package/packages/core/src/workers/agent.worker.ts +68 -0
  234. package/packages/core/src/workers/createWorker.ts +144 -0
  235. package/packages/core/src/workers/index.ts +5 -0
  236. package/packages/core/src/workers/workers.test.ts +48 -0
  237. package/test/setup-db.ts +216 -0
  238. package/tsconfig.json +40 -0
  239. package/src/agents.ts +0 -1
  240. package/src/canvas.ts +0 -1
  241. package/src/channels.ts +0 -1
  242. package/src/config.ts +0 -1
  243. package/src/events.ts +0 -1
  244. package/src/gateway.ts +0 -1
  245. package/src/index.ts +0 -304
  246. package/src/mcp.ts +0 -1
  247. package/src/multimodal.ts +0 -1
  248. package/src/scheduler.ts +0 -1
  249. package/src/security.ts +0 -1
  250. package/src/skills.ts +0 -1
  251. package/src/state.ts +0 -1
  252. package/src/storage.ts +0 -1
  253. package/src/tools.ts +0 -1
  254. package/src/tts.ts +0 -1
  255. package/src/types.ts +0 -82
  256. package/src/utils.ts +0 -1
  257. package/src/voice.ts +0 -1
@@ -0,0 +1,122 @@
1
+ import type { ToolDefinition } from "../tools/ToolRegistry.ts";
2
+ import type { SkillDefinition } from "../skills/defineSkill.ts";
3
+ import { logger } from "../utils/logger.ts";
4
+
5
+ const log = logger.child("api");
6
+
7
+ export interface AgentConfig {
8
+ name: string;
9
+ model?: string;
10
+ provider?: "openai" | "anthropic" | "gemini" | "ollama";
11
+ systemPrompt?: string;
12
+ tools?: ToolDefinition[];
13
+ skills?: SkillDefinition[];
14
+ mcpServers?: Record<string, { command?: string; url?: string; args?: string[]; env?: Record<string, string> }>;
15
+ maxIterations?: number;
16
+ workspace?: string;
17
+ }
18
+
19
+ export interface Agent {
20
+ readonly name: string;
21
+ readonly config: AgentConfig;
22
+ chat(message: string, opts?: { threadId?: string; channel?: string }): AsyncGenerator<AgentEvent>;
23
+ run(task: string, opts?: { threadId?: string; channel?: string }): Promise<string>;
24
+ }
25
+
26
+ export type AgentEvent =
27
+ | { type: "text"; content: string }
28
+ | { type: "tool_call"; name: string; args: Record<string, unknown> }
29
+ | { type: "tool_result"; name: string; result: unknown }
30
+ | { type: "done"; response: string };
31
+
32
+ export async function createAgent(config: AgentConfig): Promise<Agent> {
33
+ const { initializeDatabase } = await import("../storage/SQLiteStorage.ts");
34
+ const { createAllTools } = await import("../tools/index.ts");
35
+ const { loadConfig } = await import("../config/loader.ts");
36
+
37
+ await initializeDatabase();
38
+
39
+ const coreConfig = await loadConfig();
40
+ const allBuiltInTools = createAllTools(coreConfig);
41
+
42
+ const customTools = (config.tools ?? []).map(t => ({
43
+ name: t.name,
44
+ description: t.description,
45
+ execute: t.execute,
46
+ }));
47
+
48
+ const mergedTools = [...allBuiltInTools, ...customTools];
49
+
50
+ let mcpManager = null;
51
+ if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
52
+ const { MCPClientManager } = await import("../mcp/index.ts");
53
+ const mcpConfig = {
54
+ servers: Object.fromEntries(
55
+ Object.entries(config.mcpServers).map(([name, serverConfig]) => [
56
+ name,
57
+ {
58
+ transport: (serverConfig.command ? "stdio" : "sse") as "stdio" | "sse",
59
+ command: serverConfig.command,
60
+ url: serverConfig.url,
61
+ args: serverConfig.args ?? [],
62
+ env: serverConfig.env ?? {},
63
+ enabled: true,
64
+ },
65
+ ])
66
+ ),
67
+ };
68
+ mcpManager = new MCPClientManager(mcpConfig);
69
+ await mcpManager.initialize();
70
+ }
71
+
72
+ const loop = await (async () => {
73
+ const { buildAgentLoop } = await import("../agent/AgentRunner.ts");
74
+ const agentLoop = buildAgentLoop({ mcpManager });
75
+ return agentLoop;
76
+ })();
77
+
78
+ return {
79
+ name: config.name,
80
+ config,
81
+ async *chat(message, opts) {
82
+ const threadId = opts?.threadId ?? crypto.randomUUID();
83
+ const iter = loop.stream(
84
+ { messages: [{ role: "user", content: message }] },
85
+ {
86
+ configurable: {
87
+ thread_id: threadId,
88
+ channel: opts?.channel ?? "cli",
89
+ system_prompt: config.systemPrompt,
90
+ },
91
+ }
92
+ );
93
+ for await (const chunk of iter) {
94
+ if (chunk.agent?.messages) {
95
+ for (const msg of chunk.agent.messages) {
96
+ if (msg.content && typeof msg.content === "string") {
97
+ yield { type: "text" as const, content: msg.content };
98
+ }
99
+ if (msg.tool_calls) {
100
+ for (const tc of msg.tool_calls) {
101
+ yield { type: "tool_call" as const, name: tc.function?.name ?? tc.name ?? "unknown", args: typeof tc.function?.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function?.arguments ?? {} };
102
+ }
103
+ }
104
+ }
105
+ }
106
+ if (chunk.tools?.messages) {
107
+ for (const msg of chunk.tools.messages) {
108
+ yield { type: "tool_result" as const, name: msg.name ?? msg.tool_name ?? "unknown", result: msg.content };
109
+ }
110
+ }
111
+ }
112
+ yield { type: "done" as const, response: "" };
113
+ },
114
+ async run(task, opts) {
115
+ let response = "";
116
+ for await (const event of this.chat(task, opts)) {
117
+ if (event.type === "text") response += event.content;
118
+ }
119
+ return response;
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,2 @@
1
+ export { createAgent } from "./createAgent.ts";
2
+ export type { AgentConfig, Agent, AgentEvent } from "./createAgent.ts";
@@ -0,0 +1,108 @@
1
+ import jwt from "jsonwebtoken";
2
+ import { hashString } from "../utils/crypto.ts";
3
+ import { getDb } from "../storage/SQLiteStorage.ts";
4
+
5
+ const JWT_SECRET = process.env.JWT_SECRET || "hive-default-jwt-secret-change-in-production";
6
+ const ACCESS_TOKEN_EXPIRY = "15m";
7
+ const REFRESH_TOKEN_EXPIRY = "7d";
8
+ const REFRESH_TOKEN_EXPIRY_SECONDS = 7 * 24 * 60 * 60;
9
+
10
+ interface AuthTokens {
11
+ accessToken: string;
12
+ refreshToken: string;
13
+ expiresIn: number;
14
+ tokenType: "Bearer";
15
+ }
16
+
17
+ interface JwtPayload {
18
+ userId: string;
19
+ type: "access" | "refresh";
20
+ }
21
+
22
+ export async function generateTokens(userId: string): Promise<AuthTokens> {
23
+ const accessToken = jwt.sign({ userId, type: "access" } satisfies JwtPayload, JWT_SECRET, {
24
+ expiresIn: ACCESS_TOKEN_EXPIRY,
25
+ });
26
+
27
+ const refreshToken = jwt.sign({ userId, type: "refresh" } satisfies JwtPayload, JWT_SECRET, {
28
+ expiresIn: REFRESH_TOKEN_EXPIRY,
29
+ });
30
+
31
+ const refreshTokenHash = hashString(refreshToken);
32
+ const expiresAt = Math.floor(Date.now() / 1000) + REFRESH_TOKEN_EXPIRY_SECONDS;
33
+
34
+ const db = getDb();
35
+ db.run(
36
+ `INSERT INTO refresh_tokens (user_id, token_hash, expires_at, revoked)
37
+ VALUES (?, ?, ?, 0)`,
38
+ [userId, refreshTokenHash, expiresAt]
39
+ );
40
+
41
+ return {
42
+ accessToken,
43
+ refreshToken,
44
+ expiresIn: 15 * 60,
45
+ tokenType: "Bearer",
46
+ };
47
+ }
48
+
49
+ export async function refreshAccessToken(refreshToken: string): Promise<AuthTokens> {
50
+ let payload: JwtPayload;
51
+ try {
52
+ payload = jwt.verify(refreshToken, JWT_SECRET) as JwtPayload;
53
+ } catch {
54
+ throw new Error("Invalid or expired refresh token");
55
+ }
56
+
57
+ if (payload.type !== "refresh") {
58
+ throw new Error("Invalid token type");
59
+ }
60
+
61
+ const refreshTokenHash = hashString(refreshToken);
62
+ const db = getDb();
63
+ const tokenRow = db
64
+ .query(
65
+ `SELECT user_id, expires_at, revoked FROM refresh_tokens WHERE token_hash = ?`
66
+ )
67
+ .get(refreshTokenHash) as { user_id: string; expires_at: number; revoked: number } | undefined;
68
+
69
+ if (!tokenRow) {
70
+ throw new Error("Refresh token not found");
71
+ }
72
+
73
+ if (tokenRow.revoked === 1) {
74
+ throw new Error("Refresh token has been revoked");
75
+ }
76
+
77
+ if (tokenRow.expires_at < Math.floor(Date.now() / 1000)) {
78
+ db.run(`DELETE FROM refresh_tokens WHERE token_hash = ?`, [refreshTokenHash]);
79
+ throw new Error("Refresh token has expired");
80
+ }
81
+
82
+ db.run(`DELETE FROM refresh_tokens WHERE token_hash = ?`, [refreshTokenHash]);
83
+
84
+ return generateTokens(payload.userId);
85
+ }
86
+
87
+ export async function validateAccessToken(token: string): Promise<{ userId: string } | null> {
88
+ try {
89
+ const payload = jwt.verify(token, JWT_SECRET) as JwtPayload;
90
+ if (payload.type !== "access") {
91
+ return null;
92
+ }
93
+ return { userId: payload.userId };
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ export async function revokeRefreshToken(refreshToken: string): Promise<void> {
100
+ const refreshTokenHash = hashString(refreshToken);
101
+ const db = getDb();
102
+ db.run(`UPDATE refresh_tokens SET revoked = 1 WHERE token_hash = ?`, [refreshTokenHash]);
103
+ }
104
+
105
+ export async function revokeAllUserTokens(userId: string): Promise<void> {
106
+ const db = getDb();
107
+ db.run(`UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ?`, [userId]);
108
+ }
@@ -0,0 +1 @@
1
+ export * from "./auth.ts";
@@ -0,0 +1,390 @@
1
+ import { EventEmitter } from "events";
2
+ import { logger } from "../utils/logger.ts";
3
+ import { eventBus } from "../swarm/EventBus.ts";
4
+
5
+ export interface WebSocketLike {
6
+ readyState: number;
7
+ send(data: string | ArrayBuffer | Uint8Array): number | boolean;
8
+ close(code?: number, reason?: string): void;
9
+ }
10
+
11
+ export const WebSocketState = {
12
+ CONNECTING: 0,
13
+ OPEN: 1,
14
+ CLOSING: 2,
15
+ CLOSED: 3,
16
+ };
17
+
18
+ export interface CanvasComponent {
19
+ id: string;
20
+ type: "button" | "form" | "chart" | "table" | "markdown" | "text" | "image" | "card" | "progress" | "list" | "confirm";
21
+ props: Record<string, unknown>;
22
+ span?: "full" | "half";
23
+ }
24
+
25
+ export interface CanvasMessage {
26
+ type: "canvas:render" | "canvas:update" | "canvas:clear" | "canvas:interact" | "canvas:connected" | "canvas:snapshot";
27
+ sessionId: string;
28
+ componentId?: string;
29
+ component?: CanvasComponent;
30
+ action?: string;
31
+ data?: unknown;
32
+ }
33
+
34
+ export interface InteractionEvent {
35
+ sessionId: string;
36
+ componentId: string;
37
+ action: string;
38
+ data: unknown;
39
+ }
40
+
41
+ interface PendingInteraction {
42
+ resolve: (data: unknown) => void;
43
+ reject: (error: Error) => void;
44
+ timeoutId: ReturnType<typeof setTimeout>;
45
+ }
46
+
47
+ interface A2UISurfaceCache {
48
+ createData: Record<string, unknown>;
49
+ components?: unknown[];
50
+ dataModel?: Record<string, unknown>;
51
+ }
52
+
53
+ export class CanvasManager extends EventEmitter {
54
+ private sessions: Map<string, WebSocketLike> = new Map();
55
+ private pendingInteractions: Map<string, PendingInteraction> = new Map();
56
+ private componentCache: Map<string, CanvasComponent[]> = new Map();
57
+ private a2uiCache: Map<string, A2UISurfaceCache> = new Map();
58
+ private log = logger.child("canvas");
59
+ private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
60
+
61
+ constructor() {
62
+ super();
63
+ this.startHeartbeat();
64
+ }
65
+
66
+ private startHeartbeat(): void {
67
+ if (this.heartbeatInterval) return;
68
+
69
+ // Enviar ping a todas las sesiones cada 30 segundos
70
+ this.heartbeatInterval = setInterval(() => {
71
+ for (const [sessionId, ws] of this.sessions) {
72
+ if (ws.readyState === WebSocketState.OPEN) {
73
+ try {
74
+ ws.send(JSON.stringify({ type: "canvas:ping", sessionId }));
75
+ this.log.debug(`Heartbeat sent to ${sessionId}`);
76
+ } catch (e) {
77
+ this.log.error(`Failed to send heartbeat to ${sessionId}`);
78
+ }
79
+ }
80
+ }
81
+ }, 30000);
82
+ }
83
+
84
+ private stopHeartbeat(): void {
85
+ if (this.heartbeatInterval) {
86
+ clearInterval(this.heartbeatInterval);
87
+ this.heartbeatInterval = null;
88
+ }
89
+ }
90
+
91
+ registerSession(sessionId: string, ws: WebSocketLike): void {
92
+ this.sessions.set(sessionId, ws);
93
+ this.log.info(`Canvas session registered: ${sessionId}`);
94
+
95
+ eventBus.emit("tool:completed" as any, {
96
+ toolName: "canvas:session:register",
97
+ result: { sessionId },
98
+ duration: 0,
99
+ success: true,
100
+ });
101
+
102
+ // Notify the client that the session is registered
103
+ ws.send(JSON.stringify({ type: "canvas:connected", sessionId }));
104
+
105
+ // Replay any cached A2UI surfaces so late-connecting clients get current state
106
+ for (const [surfaceId, cache] of this.a2uiCache) {
107
+ try {
108
+ ws.send(JSON.stringify({ type: "a2ui:createSurface", data: cache.createData }));
109
+ if (cache.components && cache.components.length > 0) {
110
+ ws.send(JSON.stringify({ type: "a2ui:updateComponents", data: { surfaceId, components: cache.components } }));
111
+ }
112
+ if (cache.dataModel && Object.keys(cache.dataModel).length > 0) {
113
+ ws.send(JSON.stringify({ type: "a2ui:updateDataModel", data: { surfaceId, path: undefined, value: cache.dataModel } }));
114
+ }
115
+ this.log.debug(`Replayed A2UI surface '${surfaceId}' to session ${sessionId}`);
116
+ } catch (e) {
117
+ this.log.warn(`Failed to replay A2UI surface '${surfaceId}' to ${sessionId}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ unregisterSession(sessionId: string): void {
123
+ this.sessions.delete(sessionId);
124
+ this.cleanupPendingInteractions(sessionId);
125
+ this.log.info(`Canvas session disconnected: ${sessionId}`);
126
+ }
127
+
128
+ handleMessage(sessionId: string, data: unknown): void {
129
+ try {
130
+ const msg = JSON.parse(data as string);
131
+
132
+ this.log.debug(`Received message from session ${sessionId}: ${msg.type}`);
133
+
134
+ // === HANDSHAKE INICIAL ===
135
+ if (msg.type === "canvas:handshake") {
136
+ this.log.debug(`Handshake received from ${sessionId}`, msg);
137
+ const ws = this.sessions.get(sessionId);
138
+
139
+ // Confirmar handshake
140
+ if (ws && ws.readyState === WebSocketState.OPEN) {
141
+ ws.send(JSON.stringify({
142
+ type: "canvas:handshake:ack",
143
+ sessionId,
144
+ serverSessionId: sessionId,
145
+ timestamp: Date.now()
146
+ }));
147
+ }
148
+ return;
149
+ }
150
+
151
+ // === HEARTBEAT: PING DEL CLIENTE ===
152
+ if (msg.type === "canvas:ping") {
153
+ const ws = this.sessions.get(sessionId);
154
+ if (ws && ws.readyState === WebSocketState.OPEN) {
155
+ ws.send(JSON.stringify({ type: "canvas:pong", sessionId }));
156
+ }
157
+ return;
158
+ }
159
+
160
+ // === PONG DEL CLIENTE (opcional) ===
161
+ if (msg.type === "canvas:pong") {
162
+ // El cliente respondió al ping del servidor - conexión viva
163
+ this.log.debug(`Pong received from ${sessionId}`);
164
+ return;
165
+ }
166
+
167
+ if (msg.type === "canvas:interact") {
168
+ const { componentId, action, data: interactionData } = msg.payload || msg;
169
+
170
+ if (componentId) {
171
+ this.resolveInteraction(sessionId, componentId, interactionData);
172
+ }
173
+
174
+ this.emit("interaction", {
175
+ sessionId,
176
+ componentId,
177
+ action,
178
+ data: interactionData,
179
+ } as InteractionEvent);
180
+ } else if (msg.type === "canvas:get_cached") {
181
+ const components = this.getSessionComponents(sessionId);
182
+ const ws = this.sessions.get(sessionId);
183
+
184
+ this.log.info(`Sending ${components.length} cached components to session ${sessionId}`);
185
+
186
+ if (ws && ws.readyState === WebSocketState.OPEN) {
187
+ for (const component of components) {
188
+ ws.send(JSON.stringify({
189
+ type: "canvas:render",
190
+ sessionId,
191
+ component
192
+ }));
193
+ }
194
+ }
195
+ }
196
+ } catch (error) {
197
+ this.log.error(`Invalid canvas message: ${(error as Error).message}`);
198
+ }
199
+ }
200
+
201
+ private resolveInteraction(sessionId: string, componentId: string, data: unknown): void {
202
+ const key = `${sessionId}:${componentId}`;
203
+ const pending = this.pendingInteractions.get(key);
204
+
205
+ if (pending) {
206
+ clearTimeout(pending.timeoutId);
207
+ this.pendingInteractions.delete(key);
208
+
209
+ // Remove from cache so it doesn't reappear in the next snapshot
210
+ const cached = this.componentCache.get(sessionId);
211
+ if (cached) {
212
+ this.componentCache.set(sessionId, cached.filter(c => c.id !== componentId));
213
+ }
214
+
215
+ pending.resolve(data);
216
+ }
217
+ }
218
+
219
+ async render(sessionId: string, component: CanvasComponent): Promise<void> {
220
+ // Always cache the component for later retrieval
221
+ const cached = this.componentCache.get(sessionId) || [];
222
+ const existingIdx = cached.findIndex(c => c.id === component.id);
223
+ if (existingIdx >= 0) {
224
+ cached[existingIdx] = component;
225
+ } else {
226
+ cached.push(component);
227
+ }
228
+ this.componentCache.set(sessionId, cached);
229
+
230
+ const ws = this.sessions.get(sessionId);
231
+
232
+ if (!ws || ws.readyState !== WebSocketState.OPEN) {
233
+ // Session not connected, but we cached the component
234
+ this.log.debug(`Session ${sessionId} NOT connected. Available: ${this.getConnectedSessions().join(", ")}`);
235
+ return;
236
+ }
237
+
238
+ const message: CanvasMessage = {
239
+ type: "canvas:render",
240
+ sessionId,
241
+ component,
242
+ };
243
+
244
+ this.log.debug(`Sending render to session ${sessionId}: ${component.id}`);
245
+ ws.send(JSON.stringify(message));
246
+ this.log.debug(`Rendered component ${component.id} to session ${sessionId}`);
247
+ }
248
+
249
+ async update(sessionId: string, component: CanvasComponent): Promise<void> {
250
+ const ws = this.sessions.get(sessionId);
251
+
252
+ if (!ws || ws.readyState !== WebSocketState.OPEN) {
253
+ throw new Error(`Session not connected: ${sessionId}`);
254
+ }
255
+
256
+ const message: CanvasMessage = {
257
+ type: "canvas:update",
258
+ sessionId,
259
+ component,
260
+ };
261
+
262
+ ws.send(JSON.stringify(message));
263
+ this.log.debug(`Updated component ${component.id} in session ${sessionId}`);
264
+ }
265
+
266
+ async clear(sessionId: string): Promise<void> {
267
+ const ws = this.sessions.get(sessionId);
268
+
269
+ if (!ws || ws.readyState !== WebSocketState.OPEN) {
270
+ throw new Error(`Session not connected: ${sessionId}`);
271
+ }
272
+
273
+ const message: CanvasMessage = {
274
+ type: "canvas:clear",
275
+ sessionId,
276
+ };
277
+
278
+ ws.send(JSON.stringify(message));
279
+ this.log.debug(`Cleared canvas for session ${sessionId}`);
280
+ }
281
+
282
+ async sendA2UIMessage(sessionId: string, messageType: string, data: Record<string, unknown>): Promise<void> {
283
+ // Update A2UI cache so late-connecting clients can receive current state
284
+ const surfaceId = data.surfaceId as string | undefined;
285
+ if (surfaceId) {
286
+ if (messageType === "a2ui:createSurface") {
287
+ this.a2uiCache.set(surfaceId, { createData: data });
288
+ } else if (messageType === "a2ui:updateComponents") {
289
+ const cached = this.a2uiCache.get(surfaceId);
290
+ if (cached) cached.components = data.components as unknown[];
291
+ } else if (messageType === "a2ui:updateDataModel") {
292
+ const cached = this.a2uiCache.get(surfaceId);
293
+ if (cached) {
294
+ const path = data.path as string | undefined;
295
+ const value = data.value as Record<string, unknown>;
296
+ if (!path || path === "/") {
297
+ cached.dataModel = value;
298
+ } else {
299
+ cached.dataModel = cached.dataModel ?? {};
300
+ // Store the full model snapshot when possible; partial paths accumulate
301
+ const key = path.replace(/^\//, "").split("/")[0];
302
+ if (key) cached.dataModel[key] = value;
303
+ }
304
+ }
305
+ } else if (messageType === "a2ui:deleteSurface") {
306
+ this.a2uiCache.delete(surfaceId);
307
+ }
308
+ }
309
+
310
+ const ws = this.sessions.get(sessionId);
311
+
312
+ if (!ws || ws.readyState !== WebSocketState.OPEN) {
313
+ const connected = this.getConnectedSessions();
314
+ this.log.warn(`Session ${sessionId} NOT connected for A2UI message. Cached for replay. Available: ${connected.join(", ")}`);
315
+ return;
316
+ }
317
+
318
+ ws.send(JSON.stringify({ type: messageType, data }));
319
+ this.log.debug(`Sent A2UI message '${messageType}' to session ${sessionId}`);
320
+ }
321
+
322
+ async waitForInteraction(
323
+ sessionId: string,
324
+ componentId: string,
325
+ timeout = 300000
326
+ ): Promise<unknown> {
327
+ const key = `${sessionId}:${componentId}`;
328
+
329
+ return new Promise((resolve, reject) => {
330
+ const timeoutId = setTimeout(() => {
331
+ this.pendingInteractions.delete(key);
332
+ reject(new Error(`Interaction timeout for ${componentId}`));
333
+ }, timeout);
334
+
335
+ this.pendingInteractions.set(key, { resolve, reject, timeoutId });
336
+ });
337
+ }
338
+
339
+ handleInteraction(sessionId: string, componentId: string, data: unknown): void {
340
+ this.resolveInteraction(sessionId, componentId, data);
341
+ }
342
+
343
+ getSessionComponents(sessionId: string): CanvasComponent[] {
344
+ return this.componentCache.get(sessionId) || [];
345
+ }
346
+
347
+ isSessionConnected(sessionId: string): boolean {
348
+ const ws = this.sessions.get(sessionId);
349
+ return ws !== undefined && ws.readyState === WebSocketState.OPEN;
350
+ }
351
+
352
+ getConnectedSessions(): string[] {
353
+ return Array.from(this.sessions.entries())
354
+ .filter(([_, ws]) => ws.readyState === WebSocketState.OPEN)
355
+ .map(([id]) => id);
356
+ }
357
+
358
+ getStats(): { totalSessions: number; activeSessions: number; pendingInteractions: number } {
359
+ return {
360
+ totalSessions: this.sessions.size,
361
+ activeSessions: this.getConnectedSessions().length,
362
+ pendingInteractions: this.pendingInteractions.size,
363
+ };
364
+ }
365
+
366
+ private cleanupPendingInteractions(sessionId: string): void {
367
+ for (const [key, pending] of this.pendingInteractions) {
368
+ if (key.startsWith(`${sessionId}:`)) {
369
+ clearTimeout(pending.timeoutId);
370
+ pending.reject(new Error(`Session disconnected: ${sessionId}`));
371
+ this.pendingInteractions.delete(key);
372
+ }
373
+ }
374
+ }
375
+
376
+ clearAll(): void {
377
+ this.stopHeartbeat();
378
+
379
+ for (const pending of this.pendingInteractions.values()) {
380
+ clearTimeout(pending.timeoutId);
381
+ pending.reject(new Error("Canvas manager cleared"));
382
+ }
383
+
384
+ this.pendingInteractions.clear();
385
+ this.sessions.clear();
386
+ this.log.info("Canvas manager cleared");
387
+ }
388
+ }
389
+
390
+ export const canvasManager = new CanvasManager();