@johpaz/hive-sdk 0.0.15 → 0.0.17
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/CHANGELOG.md +35 -1
- package/README.md +179 -57
- package/docs/API-AGENTS.md +9 -9
- package/docs/API-CONTEXT-COMPILER.md +13 -13
- package/docs/API-DAG-SCHEDULER.md +8 -8
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +150 -93
- package/docs/API-WORKERS-EVENTS.md +206 -59
- package/docs/INDEX.md +99 -50
- package/docs/README.md +117 -24
- package/docs/TEMPLATE-HIVE-APP.md +360 -0
- package/package.json +13 -6
- package/packages/cli/bin/hives +2 -0
- package/packages/cli/src/commands/add-skill.ts +42 -0
- package/packages/cli/src/commands/add-tool.ts +45 -0
- package/packages/cli/src/commands/add-worker.ts +49 -0
- package/packages/cli/src/commands/create-app-utils.ts +32 -0
- package/packages/cli/src/commands/create-app.test.ts +151 -0
- package/packages/cli/src/commands/create-app.ts +35 -0
- package/packages/cli/src/index.ts +22 -6
- package/packages/cli/templates/hive-app/.env.example +17 -0
- package/packages/cli/templates/hive-app/docker-compose.yml +20 -0
- package/packages/cli/templates/hive-app/hive.config.ts +19 -0
- package/packages/cli/templates/hive-app/package.json +16 -0
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +9 -0
- package/packages/cli/templates/hive-app/src/main.ts +56 -0
- package/packages/core/src/auth/auth.ts +108 -0
- package/packages/core/src/auth/index.ts +1 -0
- package/packages/core/src/canvas/canvas.test.ts +32 -0
- package/packages/core/src/canvas/emitter.ts +1 -1
- package/packages/core/src/canvas/index.ts +3 -6
- package/packages/core/src/channels/base.ts +154 -0
- package/packages/core/src/channels/channels.test.ts +18 -0
- package/packages/core/src/channels/discord.ts +273 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +450 -0
- package/packages/core/src/channels/slack.ts +323 -0
- package/packages/core/src/channels/telegram.ts +612 -0
- package/packages/core/src/channels/webchat.ts +139 -0
- package/packages/core/src/channels/whatsapp.ts +548 -0
- package/packages/core/src/events/agent-bus.ts +460 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/channel-notify.ts +32 -7
- package/packages/core/src/gateway/gateway.test.ts +38 -0
- package/packages/core/src/gateway/index.ts +2 -1
- package/packages/core/src/gateway/server.ts +139 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +44 -0
- package/packages/core/src/multimodal/index.ts +2 -2
- package/packages/core/src/multimodal/vision-service.ts +283 -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/scheduler/CronScheduler.ts +699 -0
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
- package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
- package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
- package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
- package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
- package/packages/core/src/scheduler/dag/errors.ts +37 -0
- package/packages/core/src/scheduler/dag/index.ts +26 -0
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/scheduler/index.ts +22 -0
- package/packages/core/src/scheduler/integration.ts +237 -0
- package/packages/core/src/scheduler/scheduler.test.ts +19 -0
- package/packages/core/src/scheduler/types.ts +164 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -4
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/storage/crypto.ts +198 -66
- package/packages/core/src/storage/storage.test.ts +37 -0
- package/packages/core/src/swarm/swarm.test.ts +24 -0
- package/packages/core/src/tool-runtime/index.ts +522 -0
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +91 -0
- package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
- package/packages/core/src/voice/index.ts +5 -18
- package/packages/core/src/workers/WorkerPool.ts +167 -0
- package/packages/core/src/workers/agent.worker.ts +68 -0
- package/packages/core/src/workers/createWorker.ts +144 -0
- package/packages/core/src/workers/index.ts +5 -0
- package/packages/core/src/workers/workers.test.ts +48 -0
- package/test/setup-db.ts +2 -2
- package/tsconfig.json +2 -1
- package/.github/CODEOWNERS +0 -9
- package/.github/workflows/publish.yml +0 -89
- package/.github/workflows/version-bump.yml +0 -102
- package/bun.lock +0 -543
- package/bunfig.toml +0 -7
- package/packages/core/src/agent/providers.ts +0 -1
- 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;
|
|
@@ -29,15 +41,19 @@ switch (command) {
|
|
|
29
41
|
|
|
30
42
|
function printHelp() {
|
|
31
43
|
console.log(`
|
|
32
|
-
Usage:
|
|
44
|
+
Usage: hives <command> [options]
|
|
33
45
|
|
|
34
46
|
Commands:
|
|
35
|
-
init Initialize a new Hive project
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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,6 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
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";
|