@johpaz/hive-sdk 0.0.15 → 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 (94) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +179 -57
  3. package/docs/API-AGENTS.md +9 -9
  4. package/docs/API-CONTEXT-COMPILER.md +13 -13
  5. package/docs/API-DAG-SCHEDULER.md +8 -8
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +150 -93
  7. package/docs/API-WORKERS-EVENTS.md +206 -59
  8. package/docs/INDEX.md +99 -50
  9. package/docs/README.md +117 -24
  10. package/docs/TEMPLATE-HIVE-APP.md +360 -0
  11. package/package.json +13 -6
  12. package/packages/cli/bin/hive +2 -0
  13. package/packages/cli/src/commands/add-skill.ts +42 -0
  14. package/packages/cli/src/commands/add-tool.ts +45 -0
  15. package/packages/cli/src/commands/add-worker.ts +49 -0
  16. package/packages/cli/src/commands/create-app-utils.ts +32 -0
  17. package/packages/cli/src/commands/create-app.test.ts +151 -0
  18. package/packages/cli/src/commands/create-app.ts +35 -0
  19. package/packages/cli/src/index.ts +21 -5
  20. package/packages/cli/templates/hive-app/.env.example +17 -0
  21. package/packages/cli/templates/hive-app/docker-compose.yml +20 -0
  22. package/packages/cli/templates/hive-app/hive.config.ts +19 -0
  23. package/packages/cli/templates/hive-app/package.json +16 -0
  24. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +9 -0
  25. package/packages/cli/templates/hive-app/src/main.ts +56 -0
  26. package/packages/core/src/auth/auth.ts +108 -0
  27. package/packages/core/src/auth/index.ts +1 -0
  28. package/packages/core/src/canvas/canvas.test.ts +32 -0
  29. package/packages/core/src/canvas/emitter.ts +1 -1
  30. package/packages/core/src/canvas/index.ts +3 -6
  31. package/packages/core/src/channels/base.ts +154 -0
  32. package/packages/core/src/channels/channels.test.ts +18 -0
  33. package/packages/core/src/channels/discord.ts +273 -0
  34. package/packages/core/src/channels/index.ts +7 -0
  35. package/packages/core/src/channels/manager.ts +450 -0
  36. package/packages/core/src/channels/slack.ts +323 -0
  37. package/packages/core/src/channels/telegram.ts +612 -0
  38. package/packages/core/src/channels/webchat.ts +139 -0
  39. package/packages/core/src/channels/whatsapp.ts +548 -0
  40. package/packages/core/src/events/agent-bus.ts +460 -0
  41. package/packages/core/src/events/event-bus.ts +169 -0
  42. package/packages/core/src/gateway/channel-notify.ts +32 -7
  43. package/packages/core/src/gateway/gateway.test.ts +38 -0
  44. package/packages/core/src/gateway/index.ts +2 -1
  45. package/packages/core/src/gateway/server.ts +139 -0
  46. package/packages/core/src/heartbeat/index.ts +157 -0
  47. package/packages/core/src/index.ts +44 -0
  48. package/packages/core/src/multimodal/index.ts +2 -2
  49. package/packages/core/src/multimodal/vision-service.ts +283 -0
  50. package/packages/core/src/plugins/api.ts +128 -0
  51. package/packages/core/src/plugins/index.ts +2 -0
  52. package/packages/core/src/plugins/loader.ts +365 -0
  53. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  54. package/packages/core/src/scheduler/CronScheduler.ts +699 -0
  55. package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
  56. package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
  57. package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
  58. package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
  59. package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
  60. package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
  61. package/packages/core/src/scheduler/dag/errors.ts +37 -0
  62. package/packages/core/src/scheduler/dag/index.ts +26 -0
  63. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
  64. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
  65. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
  66. package/packages/core/src/scheduler/index.ts +22 -0
  67. package/packages/core/src/scheduler/integration.ts +237 -0
  68. package/packages/core/src/scheduler/scheduler.test.ts +19 -0
  69. package/packages/core/src/scheduler/types.ts +164 -0
  70. package/packages/core/src/security/google-chat.ts +269 -0
  71. package/packages/core/src/security/index.ts +192 -4
  72. package/packages/core/src/security/rate-limit.ts +270 -0
  73. package/packages/core/src/security/signal.ts +321 -0
  74. package/packages/core/src/storage/crypto.ts +198 -66
  75. package/packages/core/src/storage/storage.test.ts +37 -0
  76. package/packages/core/src/swarm/swarm.test.ts +24 -0
  77. package/packages/core/src/tool-runtime/index.ts +522 -0
  78. package/packages/core/src/tool-runtime/tool-runtime.test.ts +91 -0
  79. package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
  80. package/packages/core/src/voice/index.ts +5 -18
  81. package/packages/core/src/workers/WorkerPool.ts +167 -0
  82. package/packages/core/src/workers/agent.worker.ts +68 -0
  83. package/packages/core/src/workers/createWorker.ts +144 -0
  84. package/packages/core/src/workers/index.ts +5 -0
  85. package/packages/core/src/workers/workers.test.ts +48 -0
  86. package/test/setup-db.ts +2 -2
  87. package/tsconfig.json +2 -1
  88. package/.github/CODEOWNERS +0 -9
  89. package/.github/workflows/publish.yml +0 -89
  90. package/.github/workflows/version-bump.yml +0 -102
  91. package/bun.lock +0 -543
  92. package/bunfig.toml +0 -7
  93. package/packages/core/src/agent/providers.ts +0 -1
  94. package/packages/core/src/gateway/channel-notify.test.ts +0 -14
@@ -0,0 +1,49 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import * as process from "node:process";
4
+
5
+ function toPascalCase(str: string): string {
6
+ return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase()).replace(/^(.)/, (_, char) => char.toUpperCase());
7
+ }
8
+
9
+ async function runAddWorker() {
10
+ const workerName = process.argv[3];
11
+
12
+ if (!workerName) {
13
+ console.error("Usage: hive add-worker <name>");
14
+ process.exit(1);
15
+ }
16
+
17
+ const workersDir = join(process.cwd(), "src", "workers");
18
+ const filePath = join(workersDir, `${workerName}.worker.ts`);
19
+
20
+ if (existsSync(filePath)) {
21
+ console.error(`Worker '${workerName}' already exists.`);
22
+ process.exit(1);
23
+ }
24
+
25
+ mkdirSync(workersDir, { recursive: true });
26
+
27
+ const className = toPascalCase(workerName) + "Worker";
28
+
29
+ const content = `import { createWorker } from "@johpaz/hive-sdk";
30
+
31
+ export const ${workerName}Worker = createWorker({
32
+ name: "${workerName}",
33
+ systemPrompt: \`
34
+ You are the ${className} specialist.
35
+ You handle tasks related to ${workerName} with precision and expertise.
36
+ Always provide clear, actionable results.
37
+ \`,
38
+ });
39
+
40
+ // Example usage:
41
+ // const result = await ${workerName}Worker.run("Your task here");
42
+ // console.log(result);
43
+ `;
44
+
45
+ writeFileSync(filePath, content);
46
+ console.log(`Created worker: ${filePath}`);
47
+ }
48
+
49
+ runAddWorker();
@@ -0,0 +1,32 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+
4
+ const TEMPLATE_DIR = join(import.meta.dir, "..", "..", "templates", "hive-app");
5
+
6
+ export function copyTemplate(dest: string, replacements: Record<string, string>) {
7
+ if (!existsSync(TEMPLATE_DIR)) {
8
+ throw new Error("Template not found: " + TEMPLATE_DIR);
9
+ }
10
+
11
+ copyDir(TEMPLATE_DIR, dest, replacements);
12
+ }
13
+
14
+ function copyDir(src: string, dest: string, replacements: Record<string, string>) {
15
+ for (const entry of readdirSync(src)) {
16
+ const srcPath = join(src, entry);
17
+ const destPath = join(dest, entry);
18
+ const stat = statSync(srcPath);
19
+
20
+ if (stat.isDirectory()) {
21
+ mkdirSync(destPath, { recursive: true });
22
+ copyDir(srcPath, destPath, replacements);
23
+ } else {
24
+ let content = readFileSync(srcPath, "utf-8");
25
+ for (const [key, value] of Object.entries(replacements)) {
26
+ content = content.replaceAll(key, value);
27
+ }
28
+ mkdirSync(dirname(destPath), { recursive: true });
29
+ writeFileSync(destPath, content);
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,151 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from "bun:test";
2
+ import { existsSync, rmSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import * as process from "node:process";
5
+
6
+ const TEST_DIR = join(process.cwd(), "test-create-app-output");
7
+
8
+ describe("hive create-app", () => {
9
+ beforeEach(() => {
10
+ // Clean up before each test
11
+ if (existsSync(TEST_DIR)) {
12
+ rmSync(TEST_DIR, { recursive: true });
13
+ }
14
+ });
15
+
16
+ afterEach(() => {
17
+ // Clean up after each test
18
+ if (existsSync(TEST_DIR)) {
19
+ rmSync(TEST_DIR, { recursive: true });
20
+ }
21
+ });
22
+
23
+ it("generates a hive-app template with correct structure", async () => {
24
+ const { copyTemplate } = await import("./create-app-utils.ts");
25
+
26
+ copyTemplate(TEST_DIR, { "{{APP_NAME}}": "my-test-app" });
27
+
28
+ // Check all expected files exist
29
+ expect(existsSync(join(TEST_DIR, "package.json"))).toBe(true);
30
+ expect(existsSync(join(TEST_DIR, "hive.config.ts"))).toBe(true);
31
+ expect(existsSync(join(TEST_DIR, "docker-compose.yml"))).toBe(true);
32
+ expect(existsSync(join(TEST_DIR, ".env.example"))).toBe(true);
33
+ expect(existsSync(join(TEST_DIR, ".gitignore"))).toBe(true);
34
+ expect(existsSync(join(TEST_DIR, "src", "main.ts"))).toBe(true);
35
+ expect(existsSync(join(TEST_DIR, "src", "agents", "coordinator.ts"))).toBe(true);
36
+ });
37
+
38
+ it("replaces {{APP_NAME}} placeholder in all files", async () => {
39
+ const { copyTemplate } = await import("./create-app-utils.ts");
40
+
41
+ copyTemplate(TEST_DIR, { "{{APP_NAME}}": "my-awesome-app" });
42
+
43
+ const packageJson = readFileSync(join(TEST_DIR, "package.json"), "utf-8");
44
+ expect(packageJson).toContain('"name": "my-awesome-app"');
45
+
46
+ const config = readFileSync(join(TEST_DIR, "hive.config.ts"), "utf-8");
47
+ expect(config).toContain('name: "my-awesome-app"');
48
+
49
+ const main = readFileSync(join(TEST_DIR, "src", "main.ts"), "utf-8");
50
+ expect(main).toContain("Starting my-awesome-app...");
51
+ expect(main).toContain("my-awesome-app is running at");
52
+ });
53
+
54
+ it("generates valid package.json", async () => {
55
+ const { copyTemplate } = await import("./create-app-utils.ts");
56
+
57
+ copyTemplate(TEST_DIR, { "{{APP_NAME}}": "test-app" });
58
+
59
+ const packageJson = JSON.parse(readFileSync(join(TEST_DIR, "package.json"), "utf-8"));
60
+
61
+ expect(packageJson.name).toBe("test-app");
62
+ expect(packageJson.version).toBe("0.1.0");
63
+ expect(packageJson.type).toBe("module");
64
+ expect(packageJson.scripts.dev).toBe("bun run src/main.ts");
65
+ expect(packageJson.scripts.build).toBeDefined();
66
+ expect(packageJson.dependencies["@johpaz/hive-sdk"]).toBe("latest");
67
+ });
68
+
69
+ it("generates hive.config.ts with correct defaults", async () => {
70
+ const { copyTemplate } = await import("./create-app-utils.ts");
71
+
72
+ copyTemplate(TEST_DIR, { "{{APP_NAME}}": "config-test" });
73
+
74
+ const config = readFileSync(join(TEST_DIR, "hive.config.ts"), "utf-8");
75
+
76
+ expect(config).toContain('host: process.env.HIVE_HOST ?? "127.0.0.1"');
77
+ expect(config).toContain('port: Number(process.env.HIVE_PORT ?? 18790)');
78
+ expect(config).toContain("webchat: { enabled: true }");
79
+ expect(config).toContain("telegram: { enabled: false }");
80
+ expect(config).toContain("discord: { enabled: false }");
81
+ expect(config).toContain("whatsapp: { enabled: false }");
82
+ expect(config).toContain("slack: { enabled: false }");
83
+ expect(config).toContain('path: process.env.HIVE_DATA_DIR ?? "./data/hive.db"');
84
+ });
85
+
86
+ it("generates main.ts with all required imports", async () => {
87
+ const { copyTemplate } = await import("./create-app-utils.ts");
88
+
89
+ copyTemplate(TEST_DIR, { "{{APP_NAME}}": "main-test" });
90
+
91
+ const main = readFileSync(join(TEST_DIR, "src", "main.ts"), "utf-8");
92
+
93
+ expect(main).toContain('from "@johpaz/hive-sdk"');
94
+ expect(main).toContain("createAgent");
95
+ expect(main).toContain("startGateway");
96
+ expect(main).toContain("initializeDatabase");
97
+ expect(main).toContain("ChannelManager");
98
+ expect(main).toContain("logger");
99
+ expect(main).toContain('import config from "../hive.config.ts"');
100
+ expect(main).toContain("await initializeDatabase()");
101
+ expect(main).toContain('await startGateway({');
102
+ expect(main).toContain('process.on("SIGINT"');
103
+ });
104
+
105
+ it("generates coordinator agent with correct config", async () => {
106
+ const { copyTemplate } = await import("./create-app-utils.ts");
107
+
108
+ copyTemplate(TEST_DIR, { "{{APP_NAME}}": "agent-test" });
109
+
110
+ const agent = readFileSync(join(TEST_DIR, "src", "agents", "coordinator.ts"), "utf-8");
111
+
112
+ expect(agent).toContain('import { createAgent } from "@johpaz/hive-sdk"');
113
+ expect(agent).toContain('name: "coordinator"');
114
+ expect(agent).toContain('provider: "openai"');
115
+ expect(agent).toContain('model: "gpt-4o-mini"');
116
+ });
117
+
118
+ it("generates docker-compose.yml with correct ports", async () => {
119
+ const { copyTemplate } = await import("./create-app-utils.ts");
120
+
121
+ copyTemplate(TEST_DIR, { "{{APP_NAME}}": "docker-test" });
122
+
123
+ const docker = readFileSync(join(TEST_DIR, "docker-compose.yml"), "utf-8");
124
+
125
+ expect(docker).toContain('image: oven/bun:latest');
126
+ expect(docker).toContain('"${HIVE_PORT:-18790}:18790"');
127
+ expect(docker).toContain('HIVE_HOST=0.0.0.0');
128
+ expect(docker).toContain('HIVE_PORT=18790');
129
+ expect(docker).toContain('command: ["bun", "run", "src/main.ts"]');
130
+ expect(docker).toContain('restart: unless-stopped');
131
+ });
132
+
133
+ it("generates .env.example with all required variables", async () => {
134
+ const { copyTemplate } = await import("./create-app-utils.ts");
135
+
136
+ copyTemplate(TEST_DIR, { "{{APP_NAME}}": "env-test" });
137
+
138
+ const env = readFileSync(join(TEST_DIR, ".env.example"), "utf-8");
139
+
140
+ expect(env).toContain("HIVE_HOST=");
141
+ expect(env).toContain("HIVE_PORT=");
142
+ expect(env).toContain("HIVE_DATA_DIR=");
143
+ expect(env).toContain("OPENAI_API_KEY=");
144
+ expect(env).toContain("ANTHROPIC_API_KEY=");
145
+ expect(env).toContain("GOOGLE_API_KEY=");
146
+ expect(env).toContain("TELEGRAM_BOT_TOKEN=");
147
+ expect(env).toContain("DISCORD_BOT_TOKEN=");
148
+ expect(env).toContain("SLACK_BOT_TOKEN=");
149
+ expect(env).toContain("LOG_LEVEL=");
150
+ });
151
+ });
@@ -0,0 +1,35 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import * as process from "node:process";
4
+ import { copyTemplate } from "./create-app-utils.ts";
5
+
6
+ async function runCreateApp() {
7
+ const appName = process.argv[3];
8
+
9
+ if (!appName) {
10
+ console.error("Usage: hive create-app <name>");
11
+ process.exit(1);
12
+ }
13
+
14
+ const targetDir = join(process.cwd(), appName);
15
+
16
+ if (existsSync(targetDir)) {
17
+ console.error(`Directory '${appName}' already exists.`);
18
+ process.exit(1);
19
+ }
20
+
21
+ console.log(`Creating Hive app '${appName}'...`);
22
+
23
+ copyTemplate(targetDir, {
24
+ "{{APP_NAME}}": appName,
25
+ });
26
+
27
+ console.log(`\n✅ Created ${appName} in ${targetDir}`);
28
+ console.log("\nNext steps:");
29
+ console.log(` cd ${appName}`);
30
+ console.log(" bun install");
31
+ console.log(" cp .env.example .env");
32
+ console.log(" bun run dev");
33
+ }
34
+
35
+ runCreateApp();
@@ -7,6 +7,18 @@ switch (command) {
7
7
  case "init":
8
8
  await import("./commands/init.ts");
9
9
  break;
10
+ case "create-app":
11
+ await import("./commands/create-app.ts");
12
+ break;
13
+ case "add-tool":
14
+ await import("./commands/add-tool.ts");
15
+ break;
16
+ case "add-skill":
17
+ await import("./commands/add-skill.ts");
18
+ break;
19
+ case "add-worker":
20
+ await import("./commands/add-worker.ts");
21
+ break;
10
22
  case "run":
11
23
  await import("./commands/run.ts");
12
24
  break;
@@ -32,12 +44,16 @@ function printHelp() {
32
44
  Usage: hive <command> [options]
33
45
 
34
46
  Commands:
35
- init Initialize a new Hive project
36
- run Run the agent
37
- test Test tools or skills
38
- trace View trace execution logs
47
+ init <name> Initialize a new Hive agent project
48
+ create-app <name> Create a full Hive harness application
49
+ add-tool <name> Add a new tool to the current project
50
+ add-skill <name> Add a new skill to the current project
51
+ add-worker <name> Add a new Bun Worker to the current project
52
+ run Run the agent
53
+ test Test tools or skills
54
+ trace View trace execution logs
39
55
 
40
56
  Options:
41
- --help, -h Show this help message
57
+ --help, -h Show this help message
42
58
  `);
43
59
  }
@@ -0,0 +1,17 @@
1
+ # Hive Harness Configuration
2
+ HIVE_HOST=127.0.0.1
3
+ HIVE_PORT=18790
4
+ HIVE_DATA_DIR=./data
5
+
6
+ # LLM Providers
7
+ OPENAI_API_KEY=sk-...
8
+ ANTHROPIC_API_KEY=sk-ant-...
9
+ GOOGLE_API_KEY=...
10
+
11
+ # Channels (enable as needed)
12
+ TELEGRAM_BOT_TOKEN=
13
+ DISCORD_BOT_TOKEN=
14
+ SLACK_BOT_TOKEN=
15
+
16
+ # Logging
17
+ LOG_LEVEL=info
@@ -0,0 +1,20 @@
1
+ services:
2
+ app:
3
+ image: oven/bun:latest
4
+ working_dir: /app
5
+ volumes:
6
+ - .:/app
7
+ - hive-data:/app/data
8
+ ports:
9
+ - "${HIVE_PORT:-18790}:18790"
10
+ environment:
11
+ - HIVE_HOST=0.0.0.0
12
+ - HIVE_PORT=18790
13
+ - HIVE_DATA_DIR=/app/data
14
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
15
+ - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
16
+ command: ["bun", "run", "src/main.ts"]
17
+ restart: unless-stopped
18
+
19
+ volumes:
20
+ hive-data:
@@ -0,0 +1,19 @@
1
+ import type { Config } from "@johpaz/hive-sdk";
2
+
3
+ export default {
4
+ name: "{{APP_NAME}}",
5
+ gateway: {
6
+ host: process.env.HIVE_HOST ?? "127.0.0.1",
7
+ port: Number(process.env.HIVE_PORT ?? 18790),
8
+ },
9
+ channels: {
10
+ webchat: { enabled: true },
11
+ telegram: { enabled: false },
12
+ discord: { enabled: false },
13
+ whatsapp: { enabled: false },
14
+ slack: { enabled: false },
15
+ },
16
+ database: {
17
+ path: process.env.HIVE_DATA_DIR ?? "./data/hive.db",
18
+ },
19
+ } satisfies Config;
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "{{APP_NAME}}",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "bun run src/main.ts",
7
+ "start": "bun run src/main.ts",
8
+ "build": "bun build src/main.ts --outdir dist --target bun"
9
+ },
10
+ "dependencies": {
11
+ "@johpaz/hive-sdk": "latest"
12
+ },
13
+ "devDependencies": {
14
+ "@types/bun": "latest"
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ import { createAgent } from "@johpaz/hive-sdk";
2
+
3
+ export const coordinatorAgent = await createAgent({
4
+ name: "coordinator",
5
+ provider: "openai",
6
+ model: "gpt-4o-mini",
7
+ systemPrompt:
8
+ "You are the coordinator agent. You orchestrate tasks, answer questions, and delegate to specialized agents when needed.",
9
+ });
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import {
4
+ createAgent,
5
+ startGateway,
6
+ initializeDatabase,
7
+ ChannelManager,
8
+ logger,
9
+ loadConfig,
10
+ } from "@johpaz/hive-sdk";
11
+ import config from "../hive.config.ts";
12
+
13
+ const log = logger.child("app");
14
+
15
+ async function main() {
16
+ log.info(`Starting {{APP_NAME}}...`);
17
+
18
+ // Initialize database
19
+ await initializeDatabase();
20
+
21
+ // Create the main agent
22
+ const agent = await createAgent({
23
+ name: "coordinator",
24
+ provider: "openai",
25
+ model: "gpt-4o-mini",
26
+ systemPrompt:
27
+ "You are a helpful AI assistant running in a Hive harness. You can use tools, manage tasks, and communicate across channels.",
28
+ });
29
+
30
+ log.info(`Agent ready: ${agent.name}`);
31
+
32
+ // Initialize channels
33
+ const channelManager = new ChannelManager();
34
+ // TODO: configure channels from hive.config.ts
35
+
36
+ // Start the gateway
37
+ const gateway = await startGateway({
38
+ host: config.gateway?.host,
39
+ port: config.gateway?.port,
40
+ agentId: "coordinator",
41
+ });
42
+
43
+ log.info(`{{APP_NAME}} is running at http://${gateway.hostname}:${gateway.port}`);
44
+
45
+ // Graceful shutdown
46
+ process.on("SIGINT", async () => {
47
+ log.info("Shutting down...");
48
+ gateway.stop(true);
49
+ process.exit(0);
50
+ });
51
+ }
52
+
53
+ main().catch((err) => {
54
+ log.error("Fatal error:", err);
55
+ process.exit(1);
56
+ });
@@ -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,32 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { emitCanvas, subscribeCanvas, unsubscribeCanvas } from "./emitter.ts";
3
+
4
+ describe("canvas emitter", () => {
5
+ it("subscribes and unsubscribes websocket-like objects", () => {
6
+ let received: any = null;
7
+ const ws = {
8
+ send: (data: string) => { received = JSON.parse(data); },
9
+ };
10
+
11
+ subscribeCanvas(ws);
12
+ emitCanvas("canvas:render", { component: { id: "test", type: "card" } });
13
+ unsubscribeCanvas(ws);
14
+
15
+ expect(received).toBeDefined();
16
+ expect(received.type).toBe("canvas:render");
17
+ });
18
+
19
+ it("stops receiving after unsubscribe", () => {
20
+ let count = 0;
21
+ const ws = {
22
+ send: () => { count++; },
23
+ };
24
+
25
+ subscribeCanvas(ws);
26
+ emitCanvas("canvas:render", { component: { id: "a", type: "card" } });
27
+ unsubscribeCanvas(ws);
28
+ emitCanvas("canvas:render", { component: { id: "b", type: "card" } });
29
+
30
+ expect(count).toBe(1);
31
+ });
32
+ });
@@ -1,4 +1,4 @@
1
- import { getDb } from "../storage/SQLiteStorage.ts"
1
+ import { getDb } from "../storage/SQLiteStorage"
2
2
 
3
3
  export interface CanvasEvent {
4
4
  type: CanvasEventType
@@ -1,6 +1,3 @@
1
- export { createCanvasRenderTool, createCanvasAskTool, createCanvasClearTool, createCanvasTools, createCanvasCardTool, createCanvasProgressTool, createCanvasListTool, createCanvasConfirmTool } from "./canvas-tools.ts";
2
- export { createA2UISurfaceTool, createA2UIUpdateComponentsTool, createA2UIUpdateDataModelTool, createA2UIDeleteSurfaceTool } from "./a2ui-tools.ts";
3
- export type { CanvasEvent, CanvasEventType } from "./emitter.ts";
4
- export { subscribeCanvas, unsubscribeCanvas, emitCanvas, getCanvasSnapshot, removeCanvasComponent } from "./emitter.ts";
5
- export type { WebSocketLike, CanvasComponent, CanvasMessage, InteractionEvent } from "./CanvasManager.ts";
6
- export { WebSocketState, CanvasManager, canvasManager } from "./CanvasManager.ts";
1
+ export * from "./CanvasManager.ts";
2
+ export * from "./canvas-tools.ts";
3
+ export * from "./a2ui-tools.ts";