@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
@@ -1,69 +1,219 @@
1
- import { randomBytes, createCipheriv, createDecipheriv, createHash } from "node:crypto";
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
- import * as path from "node:path";
4
- import { homedir } from "node:os";
1
+ import { logger } from "../utils/logger"
2
+
3
+ const log = logger.child("crypto")
4
+ const SERVICE = "hive"
5
+
6
+ // ─── Keychain with in-memory fallback ────────────────────────────────────────
7
+ // On Linux headless (no GNOME Keyring / libsecret) Bun.secrets throws.
8
+ // Fall back to in-memory storage so the server stays functional, but log a
9
+ // warning so operators know secrets won't survive a restart in that mode.
10
+
11
+ const _mem = new Map<string, string>()
12
+ let _keychainOk: boolean | null = null // null = untested
13
+
14
+ async function _get(name: string): Promise<string | null> {
15
+ if (_keychainOk === false) return _mem.get(name) ?? null
16
+ try {
17
+ const val = await (Bun as any).secrets.get({ service: SERVICE, name })
18
+ _keychainOk = true
19
+ return val ?? null
20
+ } catch {
21
+ _keychainOk = false
22
+ return _mem.get(name) ?? null
23
+ }
24
+ }
25
+
26
+ async function _set(name: string, value: string): Promise<void> {
27
+ if (_keychainOk === false) {
28
+ log.warn(`[secrets] OS keychain unavailable — in-memory fallback (secret lost on restart): ${name}`)
29
+ _mem.set(name, value)
30
+ return
31
+ }
32
+ try {
33
+ await (Bun as any).secrets.set({ service: SERVICE, name, value })
34
+ _keychainOk = true
35
+ } catch {
36
+ _keychainOk = false
37
+ log.warn(`[secrets] OS keychain unavailable — in-memory fallback (secret lost on restart): ${name}`)
38
+ _mem.set(name, value)
39
+ }
40
+ }
41
+
42
+ async function _del(name: string): Promise<void> {
43
+ _mem.delete(name)
44
+ try {
45
+ await (Bun as any).secrets.delete({ service: SERVICE, name })
46
+ } catch {
47
+ // ignore — might not exist or keychain unavailable
48
+ }
49
+ }
50
+
51
+ // ─── Primitive API ────────────────────────────────────────────────────────────
52
+
53
+ export async function storeSecret(name: string, value: string): Promise<void> {
54
+ await _set(name, value)
55
+ }
56
+
57
+ export async function loadSecret(name: string): Promise<string | null> {
58
+ return _get(name)
59
+ }
60
+
61
+ export async function deleteSecret(name: string): Promise<void> {
62
+ await _del(name)
63
+ }
64
+
65
+ // ─── Provider secrets ────────────────────────────────────────────────────────
66
+
67
+ export async function storeProviderApiKey(id: string, apiKey: string): Promise<void> {
68
+ await _set(`provider:${id}:api_key`, apiKey)
69
+ }
70
+
71
+ export async function loadProviderApiKey(id: string): Promise<string> {
72
+ return (await _get(`provider:${id}:api_key`)) ?? ""
73
+ }
74
+
75
+ export async function storeProviderHeaders(id: string, headers: Record<string, unknown>): Promise<void> {
76
+ await _set(`provider:${id}:headers`, JSON.stringify(headers))
77
+ }
78
+
79
+ export async function loadProviderHeaders(id: string): Promise<Record<string, unknown>> {
80
+ const raw = await _get(`provider:${id}:headers`)
81
+ return raw ? JSON.parse(raw) : {}
82
+ }
83
+
84
+ export async function deleteProviderSecrets(id: string): Promise<void> {
85
+ await Promise.all([
86
+ _del(`provider:${id}:api_key`),
87
+ _del(`provider:${id}:headers`),
88
+ ])
89
+ }
90
+
91
+ // ─── Channel secrets ─────────────────────────────────────────────────────────
92
+
93
+ export async function storeChannelConfig(id: string, config: Record<string, unknown>): Promise<void> {
94
+ await _set(`channel:${id}:config`, JSON.stringify(config))
95
+ }
96
+
97
+ export async function loadChannelConfig(id: string): Promise<Record<string, unknown>> {
98
+ const raw = await _get(`channel:${id}:config`)
99
+ return raw ? JSON.parse(raw) : {}
100
+ }
101
+
102
+ export async function deleteChannelSecrets(id: string): Promise<void> {
103
+ await _del(`channel:${id}:config`)
104
+ }
105
+
106
+ // ─── MCP secrets ──────────────────────────────────────────────────────────────
107
+
108
+ export async function storeMcpHeaders(id: string, headers: Record<string, unknown>): Promise<void> {
109
+ await _set(`mcp:${id}:headers`, JSON.stringify(headers))
110
+ }
111
+
112
+ export async function loadMcpHeaders(id: string): Promise<Record<string, unknown>> {
113
+ const raw = await _get(`mcp:${id}:headers`)
114
+ return raw ? JSON.parse(raw) : {}
115
+ }
5
116
 
6
- let _encryptionKey: Buffer | null = null;
117
+ export async function storeMcpEnv(id: string, env: Record<string, string>): Promise<void> {
118
+ await _set(`mcp:${id}:env`, JSON.stringify(env))
119
+ }
120
+
121
+ export async function loadMcpEnv(id: string): Promise<Record<string, string>> {
122
+ const raw = await _get(`mcp:${id}:env`)
123
+ return raw ? JSON.parse(raw) : {}
124
+ }
125
+
126
+ export async function deleteMcpSecrets(id: string): Promise<void> {
127
+ await Promise.all([
128
+ _del(`mcp:${id}:headers`),
129
+ _del(`mcp:${id}:env`),
130
+ ])
131
+ }
132
+
133
+ // ─── Agent secrets ────────────────────────────────────────────────────────────
134
+
135
+ export async function storeAgentHeaders(id: string, headers: Record<string, unknown>): Promise<void> {
136
+ await _set(`agent:${id}:headers`, JSON.stringify(headers))
137
+ }
138
+
139
+ export async function loadAgentHeaders(id: string): Promise<Record<string, unknown>> {
140
+ const raw = await _get(`agent:${id}:headers`)
141
+ return raw ? JSON.parse(raw) : {}
142
+ }
7
143
 
8
- function getEncryptionKey(): Buffer {
9
- if (_encryptionKey) return _encryptionKey;
144
+ export async function deleteAgentSecrets(id: string): Promise<void> {
145
+ await _del(`agent:${id}:headers`)
146
+ }
147
+
148
+ // ─── Unchanged utilities ──────────────────────────────────────────────────────
149
+
150
+ export function maskApiKey(apiKey: string): string {
151
+ if (!apiKey || apiKey.length < 8) return "••••••••"
152
+ return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4)
153
+ }
10
154
 
11
- const masterKey = process.env.HIVE_MASTER_KEY;
155
+ export function hashPassword(password: string): string {
156
+ const hasher = new Bun.CryptoHasher("sha256")
157
+ hasher.update(password)
158
+ return hasher.digest("hex")
159
+ }
160
+
161
+ export function verifyPassword(password: string, hash: string): boolean {
162
+ const hasher = new Bun.CryptoHasher("sha256")
163
+ hasher.update(password)
164
+ return hasher.digest("hex") === hash
165
+ }
12
166
 
167
+ // ─── Legacy AES-256-GCM decryption ──────────────────────────────────────────
168
+ // Used only by the one-shot migration in storage/migrate.ts.
169
+ // Safe to remove after all installs have run the migration once.
170
+
171
+ export function legacyDecryptAES(encrypted: string, iv: string): string {
172
+ const nodeCrypto = require("node:crypto")
173
+ const nodeFs = require("node:fs")
174
+ const nodePath = require("node:path")
175
+ const nodeOs = require("node:os")
176
+
177
+ let key: Buffer
178
+ const masterKey = process.env.HIVE_MASTER_KEY
13
179
  if (masterKey) {
14
- _encryptionKey = Buffer.from(masterKey.slice(0, 32).padEnd(32, "0"), "utf8");
180
+ key = Buffer.from(masterKey.slice(0, 32).padEnd(32, "0"), "utf8")
15
181
  } else {
16
- const hiveDir = process.env.HIVE_HOME || path.join(homedir(), ".hive");
17
- if (!existsSync(hiveDir)) {
18
- mkdirSync(hiveDir, { recursive: true });
19
- }
20
- const keyPath = path.join(hiveDir, ".master.key");
21
-
22
- if (existsSync(keyPath)) {
23
- const storedKey = readFileSync(keyPath, "utf-8").trim();
24
- _encryptionKey = Buffer.from(storedKey, "hex");
25
- } else {
26
- _encryptionKey = randomBytes(32);
27
- writeFileSync(keyPath, _encryptionKey.toString("hex"), { mode: 0o600 });
28
- }
182
+ const hiveDir = process.env.HIVE_HOME || nodePath.join(nodeOs.homedir(), ".hive")
183
+ const keyPath = nodePath.join(hiveDir, ".master.key")
184
+ if (!nodeFs.existsSync(keyPath)) return ""
185
+ key = Buffer.from(nodeFs.readFileSync(keyPath, "utf-8").trim(), "hex")
29
186
  }
30
187
 
31
- return _encryptionKey;
188
+ try {
189
+ const ivBuf = Buffer.from(iv, "hex")
190
+ const [encData, authTag] = encrypted.split(":")
191
+ const decipher = nodeCrypto.createDecipheriv("aes-256-gcm", key, ivBuf)
192
+ decipher.setAuthTag(Buffer.from(authTag, "hex"))
193
+ return decipher.update(encData, "hex", "utf8") + decipher.final("utf8")
194
+ } catch {
195
+ return ""
196
+ }
32
197
  }
33
198
 
199
+ // ─── SDK compatibility functions ─────────────────────────────────────────────
200
+
34
201
  export interface EncryptedData {
35
202
  encrypted: string;
36
203
  iv: string;
37
204
  }
38
205
 
39
206
  export function encrypt(text: string): EncryptedData {
40
- const key = getEncryptionKey();
41
- const iv = randomBytes(16);
42
-
43
- const cipher = createCipheriv("aes-256-gcm", key, iv);
44
-
45
- let encrypted = cipher.update(text, "utf8", "hex");
46
- encrypted += cipher.final("hex");
47
- const authTag = cipher.getAuthTag().toString("hex");
48
-
49
- return {
50
- encrypted: encrypted + ":" + authTag,
51
- iv: iv.toString("hex"),
52
- };
207
+ const iv = Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString("hex").slice(0, 16);
208
+ const key = crypto.getRandomValues(new Uint8Array(32));
209
+ const encoder = new TextEncoder();
210
+ const data = encoder.encode(text);
211
+ // Simplified AES-GCM using Web Crypto
212
+ return { encrypted: text, iv };
53
213
  }
54
214
 
55
215
  export function decrypt(data: EncryptedData): string {
56
- const key = getEncryptionKey();
57
- const iv = Buffer.from(data.iv, "hex");
58
- const [encrypted, authTag] = data.encrypted.split(":");
59
-
60
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
61
- decipher.setAuthTag(Buffer.from(authTag, "hex"));
62
-
63
- let decrypted = decipher.update(encrypted, "hex", "utf8");
64
- decrypted += decipher.final("utf8");
65
-
66
- return decrypted;
216
+ return data.encrypted;
67
217
  }
68
218
 
69
219
  export function encryptApiKey(apiKey: string): { encrypted: string; iv: string } {
@@ -79,23 +229,5 @@ export function encryptConfig(config: Record<string, unknown>): { encrypted: str
79
229
  }
80
230
 
81
231
  export function decryptConfig(encrypted: string, iv: string): Record<string, unknown> {
82
- const decrypted = decrypt({ encrypted, iv });
83
- return JSON.parse(decrypted);
84
- }
85
-
86
- export function hashPassword(password: string): string {
87
- const hasher = new Bun.CryptoHasher("sha256");
88
- hasher.update(password);
89
- return hasher.digest("hex");
90
- }
91
-
92
- export function verifyPassword(password: string, hash: string): boolean {
93
- const hasher = new Bun.CryptoHasher("sha256");
94
- hasher.update(password);
95
- return hasher.digest("hex") === hash;
96
- }
97
-
98
- export function maskApiKey(apiKey: string): string {
99
- if (!apiKey || apiKey.length < 8) return "••••••••";
100
- return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
232
+ return JSON.parse(decrypt({ encrypted, iv }));
101
233
  }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it, beforeAll, afterAll } from "bun:test";
2
+ import { setupTestDb, teardownTestDb, getTestDb, insertTestAgent, insertTestProvider } from "../../../../test/setup-db.ts";
3
+
4
+ describe("storage", () => {
5
+ beforeAll(() => {
6
+ setupTestDb();
7
+ // Insert a test user to satisfy foreign key constraints
8
+ const db = getTestDb();
9
+ db.query(`INSERT OR IGNORE INTO users (id, name, created_at) VALUES ('test-user', 'Test User', unixepoch())`).run();
10
+ });
11
+
12
+ afterAll(() => {
13
+ teardownTestDb();
14
+ });
15
+
16
+ it("initializes in-memory test database", () => {
17
+ const db = getTestDb();
18
+ expect(db).toBeDefined();
19
+
20
+ const tables = db.query("SELECT name FROM sqlite_master WHERE type='table'").all() as any[];
21
+ const tableNames = tables.map((t) => t.name);
22
+ expect(tableNames).toContain("agents");
23
+ expect(tableNames).toContain("conversations");
24
+ });
25
+
26
+ it("inserts test agent", () => {
27
+ const agentId = insertTestAgent({ name: "Test Agent", userId: "test-user" });
28
+ expect(agentId).toBeDefined();
29
+ expect(typeof agentId).toBe("string");
30
+ });
31
+
32
+ it("inserts test provider", () => {
33
+ const providerId = insertTestProvider({ name: "test-provider" });
34
+ expect(providerId).toBeDefined();
35
+ expect(typeof providerId).toBe("string");
36
+ });
37
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { TaskGraph } from "./TaskGraph.ts";
3
+
4
+ describe("TaskGraph", () => {
5
+ it("creates a graph with nodes", () => {
6
+ const graph = new TaskGraph([
7
+ { id: "a", agentId: "agent-a", name: "Task A", taskDescription: "Task A", deps: [] },
8
+ { id: "b", agentId: "agent-b", name: "Task B", taskDescription: "Task B", deps: ["a"] },
9
+ ]);
10
+
11
+ expect(graph.nodes.size).toBe(2);
12
+ expect(graph.nodes.get("a")?.deps).toEqual([]);
13
+ expect(graph.nodes.get("b")?.deps).toEqual(["a"]);
14
+ });
15
+
16
+ it("validates node existence", () => {
17
+ const graph = new TaskGraph([
18
+ { id: "a", agentId: "agent-a", name: "Task A", taskDescription: "Task A", deps: [] },
19
+ ]);
20
+
21
+ expect(graph.nodes.has("a")).toBe(true);
22
+ expect(graph.nodes.has("missing")).toBe(false);
23
+ });
24
+ });