@johpaz/hive-sdk 0.1.4 → 0.1.6
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 +129 -0
- package/README.md +78 -23
- package/bun.lock +55 -29
- package/bunfig.toml +4 -2
- package/docs/API-AGENTS.md +78 -27
- package/docs/API-CONTEXT-COMPILER.md +31 -34
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
- package/docs/HIVE-HARNESS.md +1 -1
- package/docs/INDEX.md +4 -4
- package/docs/TEMPLATE-HIVE-APP.md +10 -10
- package/package.json +17 -12
- package/packages/cli/package.json +2 -2
- package/packages/cli/src/commands/create-app.test.ts +36 -7
- package/packages/cli/src/commands/init.ts +3 -3
- package/packages/cli/src/commands/run.ts +1 -1
- package/packages/cli/src/commands/test.ts +37 -25
- package/packages/cli/src/commands/trace.ts +30 -28
- package/packages/cli/templates/hive-app/.env.example +10 -2
- package/packages/cli/templates/hive-app/README.md +103 -0
- package/packages/cli/templates/hive-app/hive.config.ts +9 -3
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
- package/packages/cli/templates/hive-app/src/main.ts +12 -19
- package/packages/core/package.json +13 -12
- package/packages/core/src/agent/acceptance-checks.ts +172 -0
- package/packages/core/src/agent/agent-catalog.ts +348 -0
- package/packages/core/src/agent/agent-loop.ts +1373 -0
- package/packages/core/src/agent/capability-search.ts +186 -0
- package/packages/core/src/agent/catalog-selector.ts +103 -0
- package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
- package/packages/core/src/agent/context-compiler.ts +689 -0
- package/packages/core/src/agent/conversation-store.ts +381 -0
- package/packages/core/src/agent/curator.ts +276 -0
- package/packages/core/src/agent/delegation-runtime.ts +241 -0
- package/packages/core/src/agent/goal-runner.ts +323 -0
- package/packages/core/src/agent/index.ts +17 -12
- package/packages/core/src/agent/llm-client.ts +266 -0
- package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
- package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
- package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
- package/packages/core/src/agent/llm-providers/groq.ts +5 -0
- package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
- package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
- package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
- package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
- package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
- package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
- package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
- package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
- package/packages/core/src/agent/llm-providers/openai.ts +5 -0
- package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
- package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
- package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
- package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
- package/packages/core/src/agent/minimal-loadout.ts +47 -0
- package/packages/core/src/agent/playbook-selector.ts +119 -0
- package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
- package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
- package/packages/core/src/agent/providers/index.ts +35 -16
- package/packages/core/src/agent/reflector.ts +320 -0
- package/packages/core/src/agent/routing-intent.ts +22 -0
- package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
- package/packages/core/src/{harness → agent}/run-store.ts +142 -81
- package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
- package/packages/core/src/agent/skill-selector.ts +374 -0
- package/packages/core/src/agent/stuck-loop.ts +209 -0
- package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
- package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
- package/packages/core/src/api/createAgent.test.ts +139 -27
- package/packages/core/src/api/createAgent.ts +232 -44
- package/packages/core/src/artifacts/store.ts +162 -0
- package/packages/core/src/canvas/canvas-manager.ts +161 -0
- package/packages/core/src/canvas/canvas.test.ts +8 -4
- package/packages/core/src/canvas/emitter.ts +131 -80
- package/packages/core/src/canvas/index.ts +1 -3
- package/packages/core/src/channels/base.ts +9 -1
- package/packages/core/src/channels/discord.ts +5 -4
- package/packages/core/src/channels/manager.ts +122 -30
- package/packages/core/src/channels/slack.ts +5 -4
- package/packages/core/src/channels/telegram.ts +36 -6
- package/packages/core/src/channels/webchat.ts +11 -10
- package/packages/core/src/channels/whatsapp.ts +23 -7
- package/packages/core/src/config/index.ts +13 -2
- package/packages/core/src/config/loader.ts +76 -29
- package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
- package/packages/core/src/ethics/EthicsGuard.ts +51 -47
- package/packages/core/src/events/agent-bus.ts +44 -68
- package/packages/core/src/events/channel-narration.ts +150 -0
- package/packages/core/src/events/narration.ts +82 -0
- package/packages/core/src/events/tool-narration.ts +62 -0
- package/packages/core/src/gateway/delegation-groups.ts +258 -0
- package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
- package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
- package/packages/core/src/gateway/lane-queue.ts +173 -0
- package/packages/core/src/gateway/notification-inbox.ts +57 -0
- package/packages/core/src/gateway/server.ts +1 -1
- package/packages/core/src/harness/index.ts +46 -27
- package/packages/core/src/index.ts +33 -27
- package/packages/core/src/mcp/hot-reload.ts +32 -23
- package/packages/core/src/mcp/index.ts +6 -3
- package/packages/core/src/mcp/singleton.ts +1 -4
- package/packages/core/src/mcp/tool-sync.ts +138 -0
- package/packages/core/src/memory/Scratchpad.test.ts +39 -20
- package/packages/core/src/memory/Scratchpad.ts +27 -34
- package/packages/core/src/multimodal/vision-service.ts +44 -38
- package/packages/core/src/resilience/retry.ts +95 -0
- package/packages/core/src/scheduler/CronScheduler.ts +334 -287
- package/packages/core/src/scheduler/index.ts +9 -7
- package/packages/core/src/scheduler/integration.ts +46 -26
- package/packages/core/src/scheduler/scheduler.test.ts +9 -13
- package/packages/core/src/scheduler/types.ts +7 -2
- package/packages/core/src/security/Pairing.ts +1 -1
- package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
- package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
- package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
- package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
- package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
- package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
- package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
- package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
- package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
- package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
- package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
- package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
- package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
- package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
- package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
- package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
- package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
- package/packages/core/src/skills/skills.test.ts +52 -11
- package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
- package/packages/core/src/storage/bootstrap.ts +151 -0
- package/packages/core/src/storage/causal-events.ts +84 -0
- package/packages/core/src/storage/collections.ts +680 -0
- package/packages/core/src/storage/crypto.ts +205 -74
- package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
- package/packages/core/src/storage/hivedb.ts +61 -0
- package/packages/core/src/storage/index.ts +111 -18
- package/packages/core/src/storage/model-id.ts +53 -0
- package/packages/core/src/storage/onboarding.ts +540 -972
- package/packages/core/src/storage/reconcile.ts +238 -0
- package/packages/core/src/storage/seed.ts +572 -406
- package/packages/core/src/storage/usage.ts +285 -225
- package/packages/core/src/storage/user-email.ts +11 -0
- package/packages/core/src/swarm/AgentExecutor.ts +1 -1
- package/packages/core/src/swarm/EventBridge.ts +1 -1
- package/packages/core/src/swarm/index.ts +12 -9
- package/packages/core/src/tool-runtime/index.ts +146 -23
- package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
- package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
- package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
- package/packages/core/src/tools/agents/get-available-models.ts +36 -54
- package/packages/core/src/tools/agents/index.ts +784 -292
- package/packages/core/src/tools/api/api-request.test.ts +164 -0
- package/packages/core/src/tools/api/api-request.ts +174 -0
- package/packages/core/src/tools/api/index.ts +16 -0
- package/packages/core/src/tools/cli/index.ts +4 -0
- package/packages/core/src/tools/core/index.ts +281 -112
- package/packages/core/src/tools/cron/index.ts +121 -124
- package/packages/core/src/tools/index.ts +63 -78
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
- package/packages/core/src/tools/types.ts +3 -1
- package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
- package/packages/core/src/tools/web/browser-backend.ts +129 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
- package/packages/core/src/tools/web/browser-service.ts +80 -35
- package/packages/core/src/tools/web/browser-type.ts +3 -8
- package/packages/core/src/tools/web/index.ts +4 -4
- package/packages/core/src/tools/web/webview-backend.ts +412 -0
- package/packages/core/src/voice/index.ts +89 -63
- package/packages/core/src/workers/agent.worker.ts +2 -2
- package/packages/core/src/workers/workers.test.ts +3 -10
- package/scripts/bump-version.ts +248 -0
- package/scripts/generate-skill-bundle.ts +108 -0
- package/test/acceptance-checks.test.ts +403 -0
- package/test/agent-loop-terminal-synthesis.test.ts +32 -0
- package/test/browser-backend.test.ts +308 -0
- package/test/catalog-agents-stay-enabled.test.ts +117 -0
- package/test/causal-events.test.ts +117 -0
- package/test/compaction.test.ts +105 -0
- package/test/context-compiler.test.ts +269 -0
- package/test/curator.test.ts +130 -0
- package/test/durable-queue.test.ts +114 -0
- package/test/harness-barrel.test.ts +64 -0
- package/test/hive-helpers.test.ts +130 -0
- package/test/hivedb-search.test.ts +189 -0
- package/test/internal-turns.test.ts +166 -0
- package/test/job-idempotency.test.ts +68 -0
- package/test/job-retry-backoff.test.ts +184 -0
- package/test/job-store.test.ts +381 -0
- package/test/llm-retry.test.ts +97 -0
- package/test/memory-perf.test.ts +774 -0
- package/test/minimal-loadout.test.ts +78 -0
- package/test/model-catalog.test.ts +105 -0
- package/test/preload.ts +12 -0
- package/test/reflector.test.ts +320 -0
- package/test/retention-cap.test.ts +91 -0
- package/test/retired-capabilities-pruned.test.ts +192 -0
- package/test/run-store.test.ts +355 -0
- package/test/scratchpad.test.ts +74 -0
- package/test/secrets-durability.test.ts +119 -0
- package/test/seed-model-reseed.test.ts +155 -0
- package/test/setup-agent-seed.test.ts +264 -0
- package/test/tool-inventory.test.ts +65 -0
- package/test/tool-runtime.test.ts +258 -0
- package/test/tool-selector-runtime-tools.test.ts +117 -0
- package/test/toon.test.ts +429 -0
- package/tsconfig.json +2 -0
- package/packages/core/src/ace/Curator.ts +0 -158
- package/packages/core/src/ace/Reflector.ts +0 -200
- package/packages/core/src/ace/index.ts +0 -4
- package/packages/core/src/agent/AgentRunner.ts +0 -711
- package/packages/core/src/agent/ContextCompiler.ts +0 -567
- package/packages/core/src/agent/ContextGuard.ts +0 -91
- package/packages/core/src/agent/ConversationStore.ts +0 -254
- package/packages/core/src/agent/Hooks.ts +0 -166
- package/packages/core/src/agent/StuckLoop.ts +0 -133
- package/packages/core/src/agent/providers/LLMClient.ts +0 -149
- package/packages/core/src/agent/providers/anthropic.ts +0 -212
- package/packages/core/src/agent/providers/openai-compat.ts +0 -231
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
- package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
- package/packages/core/src/agent/selectors/index.ts +0 -6
- package/packages/core/src/auth/auth.ts +0 -121
- package/packages/core/src/auth/index.ts +0 -1
- package/packages/core/src/canvas/CanvasManager.ts +0 -390
- package/packages/core/src/canvas/canvas-tools.ts +0 -448
- package/packages/core/src/harness/collections.ts +0 -98
- package/packages/core/src/harness/goal-verifier.ts +0 -141
- package/packages/core/src/harness/harness.test.ts +0 -236
- package/packages/core/src/harness/reconcile.ts +0 -149
- package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
- package/packages/core/src/multimodal/VisionService.ts +0 -293
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
- package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
- package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
- package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
- package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
- package/packages/core/src/scheduler/dag/errors.ts +0 -37
- package/packages/core/src/scheduler/dag/index.ts +0 -26
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
- package/packages/core/src/storage/HiveDBStorage.ts +0 -64
- package/packages/core/src/storage/SQLiteStorage.ts +0 -414
- package/packages/core/src/storage/hiveSeed.ts +0 -308
- package/packages/core/src/storage/hiveStorage.test.ts +0 -38
- package/packages/core/src/storage/schema.ts +0 -689
- package/packages/core/src/storage/storage.test.ts +0 -37
- package/packages/core/src/swarm/AgentBus.ts +0 -460
- package/packages/core/src/swarm/EventBus.ts +0 -169
- package/packages/core/src/swarm/WorkerPool.ts +0 -236
- package/packages/core/src/tools/bridge-events.ts +0 -26
- package/packages/core/src/tools/canvas/index.ts +0 -375
- package/packages/core/src/tools/codebridge/index.ts +0 -342
- package/packages/core/src/tools/meeting/index.ts +0 -353
- package/packages/core/src/tools/projects/index.ts +0 -37
- package/packages/core/src/tools/projects/project-create.ts +0 -94
- package/packages/core/src/tools/projects/project-done.ts +0 -66
- package/packages/core/src/tools/projects/project-fail.ts +0 -66
- package/packages/core/src/tools/projects/project-list.ts +0 -96
- package/packages/core/src/tools/projects/project-update.ts +0 -72
- package/packages/core/src/tools/projects/task-create.ts +0 -68
- package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
- package/packages/core/src/tools/projects/task-update.ts +0 -93
- package/packages/core/src/tools/voice/index.ts +0 -104
- package/packages/core/src/tools/web/api-request.test.ts +0 -170
- package/packages/core/src/tools/web/api-request.ts +0 -239
- package/test/setup-db.ts +0 -216
- /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
|
@@ -6,7 +6,8 @@ import { createDiscordChannel, type DiscordConfig } from "./discord.ts";
|
|
|
6
6
|
import { createWebChatChannel, type WebChatConfig } from "./webchat.ts";
|
|
7
7
|
import { createWhatsAppChannel, WhatsAppChannel, type WhatsAppConfig } from "./whatsapp.ts";
|
|
8
8
|
import { createSlackChannel, type SlackConfig } from "./slack.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { col } from "../storage/hive.ts";
|
|
10
|
+
import type { ChannelDoc, AgentDoc, UserIdentityDoc } from "../storage/collections.ts";
|
|
10
11
|
import { loadChannelConfig } from "../storage/crypto.ts";
|
|
11
12
|
|
|
12
13
|
export class ChannelManager {
|
|
@@ -14,11 +15,44 @@ export class ChannelManager {
|
|
|
14
15
|
private channels: Map<string, IChannel> = new Map();
|
|
15
16
|
private messageHandler?: MessageHandler;
|
|
16
17
|
private log = logger.child("channels");
|
|
18
|
+
/**
|
|
19
|
+
* `channel:sessionId` → accountId of the account that received the message.
|
|
20
|
+
*
|
|
21
|
+
* Outbound calls (agent reply, narration, notify tools, scheduler) only carry
|
|
22
|
+
* a channel name and a routing session id. With two accounts of the same type
|
|
23
|
+
* connected — two WhatsApp numbers — picking a channel by name alone sends the
|
|
24
|
+
* reply out through whichever account happens to be first, to a peer that may
|
|
25
|
+
* not even exist there. Every inbound message records its account here so the
|
|
26
|
+
* reply goes back out the way it came in.
|
|
27
|
+
*
|
|
28
|
+
* Known limit: if the same peer id talks to two accounts of one type, the
|
|
29
|
+
* routing session id is identical for both and the most recent inbound
|
|
30
|
+
* message wins. Distinguishing them would require the account in the session
|
|
31
|
+
* id itself.
|
|
32
|
+
*/
|
|
33
|
+
private sessionAccounts: Map<string, string> = new Map();
|
|
17
34
|
|
|
18
35
|
constructor(config: Config) {
|
|
19
36
|
this.config = config;
|
|
20
37
|
}
|
|
21
38
|
|
|
39
|
+
private sessionKey(channelName: string, sessionId: string): string {
|
|
40
|
+
return `${channelName}:${sessionId}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolves which account owns an outbound session: an explicit account wins,
|
|
45
|
+
* then the account that last received a message on it.
|
|
46
|
+
*/
|
|
47
|
+
private resolveAccountId(
|
|
48
|
+
channelName: string,
|
|
49
|
+
sessionId: string,
|
|
50
|
+
explicit?: string
|
|
51
|
+
): string | undefined {
|
|
52
|
+
if (explicit) return explicit;
|
|
53
|
+
return this.sessionAccounts.get(this.sessionKey(channelName, sessionId));
|
|
54
|
+
}
|
|
55
|
+
|
|
22
56
|
onMessage(handler: MessageHandler): void {
|
|
23
57
|
this.messageHandler = handler;
|
|
24
58
|
}
|
|
@@ -32,16 +66,36 @@ export class ChannelManager {
|
|
|
32
66
|
await this.initializeFromConfig();
|
|
33
67
|
}
|
|
34
68
|
|
|
69
|
+
await this.seedSessionAccounts();
|
|
70
|
+
|
|
35
71
|
this.log.info(`Initialized ${this.channels.size} channel(s)`);
|
|
36
72
|
}
|
|
37
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Restores session→account routing from persisted identities. Without this, a
|
|
76
|
+
* durable worker that finishes after a restart would have no record of which
|
|
77
|
+
* account its conversation came in on.
|
|
78
|
+
*/
|
|
79
|
+
private async seedSessionAccounts(): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
const identitiesCol = await col<UserIdentityDoc>("userIdentities");
|
|
82
|
+
for (const { doc } of await identitiesCol.scan({})) {
|
|
83
|
+
if (!doc.account_id || !doc.channel_user_id) continue;
|
|
84
|
+
// Skip identities whose account is gone (disconnected and replaced by
|
|
85
|
+
// another one) — pinning a session to it would only fail every send.
|
|
86
|
+
if (!this.channels.has(`${doc.channel}:${doc.account_id}`)) continue;
|
|
87
|
+
this.sessionAccounts.set(this.sessionKey(doc.channel, doc.channel_user_id), doc.account_id);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.log.debug(`Could not seed session accounts: ${(error as Error).message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
38
94
|
private async initializeFromDB(): Promise<void> {
|
|
39
95
|
try {
|
|
40
|
-
const
|
|
96
|
+
const channelsCol = await col<ChannelDoc>("channels");
|
|
41
97
|
// Load all active channels - config may be empty for webchat
|
|
42
|
-
const rows =
|
|
43
|
-
SELECT id, type, enabled, active FROM channels WHERE enabled = 1 AND active = 1
|
|
44
|
-
`).all() as Array<{ id: string; type: string; enabled: number; active: number }>;
|
|
98
|
+
const rows = (await channelsCol.scan({})).filter(e => e.doc.enabled && e.doc.active).map(e => e.doc);
|
|
45
99
|
|
|
46
100
|
for (const row of rows) {
|
|
47
101
|
let config: Record<string, unknown> = {};
|
|
@@ -85,6 +139,26 @@ export class ChannelManager {
|
|
|
85
139
|
}
|
|
86
140
|
}
|
|
87
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Wires a channel into the manager: inbound messages record which account
|
|
144
|
+
* received them before reaching the handler, so replies can be routed back.
|
|
145
|
+
*/
|
|
146
|
+
private registerChannel(key: string, channel: IChannel): void {
|
|
147
|
+
channel.onMessage(async (message: IncomingMessage) => {
|
|
148
|
+
if (message.accountId && message.sessionId) {
|
|
149
|
+
this.sessionAccounts.set(
|
|
150
|
+
this.sessionKey(message.channel, message.sessionId),
|
|
151
|
+
message.accountId
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (this.messageHandler) {
|
|
155
|
+
await this.messageHandler(message);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.channels.set(key, channel);
|
|
160
|
+
}
|
|
161
|
+
|
|
88
162
|
private async createChannel(
|
|
89
163
|
channelName: string,
|
|
90
164
|
accountId: string,
|
|
@@ -125,7 +199,8 @@ export class ChannelManager {
|
|
|
125
199
|
case "whatsapp": {
|
|
126
200
|
let coordinatorId = "main";
|
|
127
201
|
try {
|
|
128
|
-
const
|
|
202
|
+
const agentsCol = await col<AgentDoc>("agents");
|
|
203
|
+
const coordinator = (await agentsCol.findBy("role", "coordinator", { limit: 1 }))[0];
|
|
129
204
|
if (coordinator?.id) coordinatorId = coordinator.id;
|
|
130
205
|
} catch { /* fallback to "main" */ }
|
|
131
206
|
channel = createWhatsAppChannel({
|
|
@@ -158,14 +233,8 @@ export class ChannelManager {
|
|
|
158
233
|
return;
|
|
159
234
|
}
|
|
160
235
|
|
|
161
|
-
channel.onMessage(async (message: IncomingMessage) => {
|
|
162
|
-
if (this.messageHandler) {
|
|
163
|
-
await this.messageHandler(message);
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
236
|
const key = `${channelName}:${accountId}`;
|
|
168
|
-
this.
|
|
237
|
+
this.registerChannel(key, channel);
|
|
169
238
|
|
|
170
239
|
this.log.info(`Created channel: ${key}`);
|
|
171
240
|
} catch (error) {
|
|
@@ -217,22 +286,35 @@ export class ChannelManager {
|
|
|
217
286
|
|
|
218
287
|
getChannel(channelName: string, accountId?: string): IChannel | undefined {
|
|
219
288
|
if (accountId) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return channel;
|
|
289
|
+
const exact = this.channels.get(`${channelName}:${accountId}`);
|
|
290
|
+
// Deliberately no fallback: delivering through a different account would
|
|
291
|
+
// send someone else's conversation out of the wrong number.
|
|
292
|
+
if (!exact) {
|
|
293
|
+
this.log.warn(`Channel ${channelName}:${accountId} is not instantiated — refusing to fall back to another account`);
|
|
226
294
|
}
|
|
295
|
+
return exact;
|
|
227
296
|
}
|
|
228
297
|
|
|
229
|
-
|
|
298
|
+
const matches = [...this.channels.entries()].filter(([key]) => key.startsWith(`${channelName}:`));
|
|
299
|
+
if (matches.length > 1) {
|
|
300
|
+
this.log.warn(
|
|
301
|
+
`${matches.length} "${channelName}" accounts are connected and no account was given — routing through ${matches[0]![0]}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return matches[0]?.[1];
|
|
230
305
|
}
|
|
231
306
|
|
|
232
307
|
async removeChannel(channelName: string, accountId: string): Promise<void> {
|
|
233
308
|
const key = `${channelName}:${accountId}`;
|
|
234
309
|
await this.stopChannel(channelName, accountId);
|
|
235
310
|
this.channels.delete(key);
|
|
311
|
+
// Drop sessions pinned to this account, otherwise every later send to them
|
|
312
|
+
// resolves to a channel that no longer exists.
|
|
313
|
+
for (const [sessionKey, account] of this.sessionAccounts) {
|
|
314
|
+
if (account === accountId && sessionKey.startsWith(`${channelName}:`)) {
|
|
315
|
+
this.sessionAccounts.delete(sessionKey);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
236
318
|
this.log.info(`Removed channel: ${key}`);
|
|
237
319
|
}
|
|
238
320
|
|
|
@@ -398,12 +480,22 @@ export class ChannelManager {
|
|
|
398
480
|
});
|
|
399
481
|
}
|
|
400
482
|
|
|
483
|
+
/** Resolves the account that owns this session (see `sessionAccounts`). */
|
|
484
|
+
private channelForSession(
|
|
485
|
+
channelName: string,
|
|
486
|
+
sessionId: string,
|
|
487
|
+
accountId?: string
|
|
488
|
+
): IChannel | undefined {
|
|
489
|
+
return this.getChannel(channelName, this.resolveAccountId(channelName, sessionId, accountId));
|
|
490
|
+
}
|
|
491
|
+
|
|
401
492
|
async send(
|
|
402
493
|
channelName: string,
|
|
403
494
|
sessionId: string,
|
|
404
|
-
message: unknown
|
|
495
|
+
message: unknown,
|
|
496
|
+
accountId?: string
|
|
405
497
|
): Promise<void> {
|
|
406
|
-
const channel = this.
|
|
498
|
+
const channel = this.channelForSession(channelName, sessionId, accountId);
|
|
407
499
|
|
|
408
500
|
if (!channel) {
|
|
409
501
|
throw new Error(`Channel not found: ${channelName}`);
|
|
@@ -412,29 +504,29 @@ export class ChannelManager {
|
|
|
412
504
|
await channel.send(sessionId, message as any);
|
|
413
505
|
}
|
|
414
506
|
|
|
415
|
-
async startTyping(channelName: string, sessionId: string): Promise<void> {
|
|
416
|
-
const channel = this.
|
|
507
|
+
async startTyping(channelName: string, sessionId: string, accountId?: string): Promise<void> {
|
|
508
|
+
const channel = this.channelForSession(channelName, sessionId, accountId);
|
|
417
509
|
if (channel?.startTyping) {
|
|
418
510
|
await channel.startTyping(sessionId);
|
|
419
511
|
}
|
|
420
512
|
}
|
|
421
513
|
|
|
422
|
-
async stopTyping(channelName: string, sessionId: string): Promise<void> {
|
|
423
|
-
const channel = this.
|
|
514
|
+
async stopTyping(channelName: string, sessionId: string, accountId?: string): Promise<void> {
|
|
515
|
+
const channel = this.channelForSession(channelName, sessionId, accountId);
|
|
424
516
|
if (channel?.stopTyping) {
|
|
425
517
|
await channel.stopTyping(sessionId);
|
|
426
518
|
}
|
|
427
519
|
}
|
|
428
520
|
|
|
429
|
-
async markAsRead(channelName: string, sessionId: string, messageId?: string): Promise<void> {
|
|
430
|
-
const channel = this.
|
|
521
|
+
async markAsRead(channelName: string, sessionId: string, messageId?: string, accountId?: string): Promise<void> {
|
|
522
|
+
const channel = this.channelForSession(channelName, sessionId, accountId);
|
|
431
523
|
if (channel?.markAsRead) {
|
|
432
524
|
await channel.markAsRead(sessionId, messageId);
|
|
433
525
|
}
|
|
434
526
|
}
|
|
435
527
|
|
|
436
|
-
async sendAudio(channelName: string, sessionId: string, audio: Buffer, mimeType: string): Promise<void> {
|
|
437
|
-
const channel = this.
|
|
528
|
+
async sendAudio(channelName: string, sessionId: string, audio: Buffer, mimeType: string, accountId?: string): Promise<void> {
|
|
529
|
+
const channel = this.channelForSession(channelName, sessionId, accountId);
|
|
438
530
|
if (!channel) {
|
|
439
531
|
throw new Error(`Channel not found: ${channelName}`);
|
|
440
532
|
}
|
|
@@ -2,7 +2,8 @@ import { App, ExpressReceiver, type SlashCommand } from "@slack/bolt";
|
|
|
2
2
|
import type { ChannelConfig, IncomingMessage, OutboundMessage } from "./base.ts";
|
|
3
3
|
import { BaseChannel } from "./base.ts";
|
|
4
4
|
import { logger } from "../utils/logger.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { updateDoc } from "../storage/hive.ts";
|
|
6
|
+
import type { ChannelDoc } from "../storage/collections.ts";
|
|
6
7
|
|
|
7
8
|
export interface SlackConfig extends ChannelConfig {
|
|
8
9
|
accountId?: string;
|
|
@@ -95,7 +96,7 @@ export class SlackChannel extends BaseChannel {
|
|
|
95
96
|
this.connectionState.status = "connected";
|
|
96
97
|
this.log.info(`Slack channel started on port ${port}`);
|
|
97
98
|
try {
|
|
98
|
-
|
|
99
|
+
await updateDoc<ChannelDoc>("channels", this.accountId, { status: "connected" });
|
|
99
100
|
} catch { /* ignore DB errors */ }
|
|
100
101
|
|
|
101
102
|
} catch (error) {
|
|
@@ -103,7 +104,7 @@ export class SlackChannel extends BaseChannel {
|
|
|
103
104
|
this.connectionState.error = (error as Error).message;
|
|
104
105
|
this.log.error(`Slack connection error: ${(error as Error).message}`);
|
|
105
106
|
try {
|
|
106
|
-
|
|
107
|
+
await updateDoc<ChannelDoc>("channels", this.accountId, { status: "error" });
|
|
107
108
|
} catch { /* ignore DB errors */ }
|
|
108
109
|
throw error;
|
|
109
110
|
}
|
|
@@ -124,7 +125,7 @@ export class SlackChannel extends BaseChannel {
|
|
|
124
125
|
this.connectionState.status = "disconnected";
|
|
125
126
|
this.log.info("Slack channel stopped");
|
|
126
127
|
try {
|
|
127
|
-
|
|
128
|
+
await updateDoc<ChannelDoc>("channels", this.accountId, { status: "disconnected" });
|
|
128
129
|
} catch { /* ignore DB errors */ }
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Bot, GrammyError, InputFile, type Context } from "grammy";
|
|
2
2
|
import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "./base.ts";
|
|
3
3
|
import { logger } from "../utils/logger.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { col, updateDoc } from "../storage/hive.ts";
|
|
5
|
+
import type { ChannelDoc, UserIdentityDoc } from "../storage/collections.ts";
|
|
6
|
+
import { resolveUserId } from "../storage/onboarding";
|
|
5
7
|
|
|
6
8
|
export interface TelegramConfig extends ChannelConfig {
|
|
7
9
|
botToken: string;
|
|
@@ -55,22 +57,45 @@ export class TelegramChannel extends BaseChannel {
|
|
|
55
57
|
});
|
|
56
58
|
|
|
57
59
|
this.bot.start({
|
|
58
|
-
onStart: () => {
|
|
60
|
+
onStart: async () => {
|
|
59
61
|
this.running = true;
|
|
60
62
|
this.log.info(`Telegram bot started: @${this.bot?.botInfo?.username ?? "unknown"}`);
|
|
61
63
|
try {
|
|
62
|
-
|
|
64
|
+
await updateDoc<ChannelDoc>("channels", this.accountId, { status: "connected" });
|
|
63
65
|
} catch { /* ignore DB errors */ }
|
|
64
66
|
},
|
|
65
|
-
}).catch((error: Error) => {
|
|
67
|
+
}).catch(async (error: Error) => {
|
|
66
68
|
this.log.error(`Telegram bot error: ${error.message}`);
|
|
67
69
|
this.running = false;
|
|
68
70
|
try {
|
|
69
|
-
|
|
71
|
+
await updateDoc<ChannelDoc>("channels", this.accountId, { status: "error" });
|
|
70
72
|
} catch { /* ignore DB errors */ }
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Ensures a userIdentities row links this Telegram chat to the single-user
|
|
78
|
+
* account, so cron/task notifications can route to Telegram even when the
|
|
79
|
+
* user's first-ever contact with the bot was a command (e.g. /start) that
|
|
80
|
+
* otherwise replies and returns before reaching the normal
|
|
81
|
+
* handleMessage() → resolveContext() auto-link path.
|
|
82
|
+
*/
|
|
83
|
+
private async linkIdentity(channelUserId: string): Promise<void> {
|
|
84
|
+
try {
|
|
85
|
+
const userId = await resolveUserId({});
|
|
86
|
+
if (!userId) return;
|
|
87
|
+
const identitiesCol = await col<UserIdentityDoc>("userIdentities");
|
|
88
|
+
await identitiesCol.put(`${userId}:telegram`, {
|
|
89
|
+
user_id: userId,
|
|
90
|
+
channel: "telegram",
|
|
91
|
+
channel_user_id: channelUserId,
|
|
92
|
+
linked_at: Date.now(),
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
this.log.warn(`Failed to auto-link telegram identity: ${(err as Error).message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
74
99
|
private async handleTelegramMessage(ctx: Context): Promise<void> {
|
|
75
100
|
const message = ctx.message;
|
|
76
101
|
if (!message) return;
|
|
@@ -100,6 +125,11 @@ export class TelegramChannel extends BaseChannel {
|
|
|
100
125
|
if (now - ts > 60_000) this.recentlyProcessed.delete(id);
|
|
101
126
|
}
|
|
102
127
|
|
|
128
|
+
// Auto-link this chat on first contact, before any command short-circuits.
|
|
129
|
+
if (!isGroup && this.isUserAllowed(chatId)) {
|
|
130
|
+
await this.linkIdentity(chatId);
|
|
131
|
+
}
|
|
132
|
+
|
|
103
133
|
const text = message.text;
|
|
104
134
|
const isCommand = text?.startsWith("/") ?? false;
|
|
105
135
|
|
|
@@ -306,7 +336,7 @@ export class TelegramChannel extends BaseChannel {
|
|
|
306
336
|
this.running = false;
|
|
307
337
|
this.log.info("Telegram bot stopped");
|
|
308
338
|
try {
|
|
309
|
-
|
|
339
|
+
await updateDoc<ChannelDoc>("channels", this.accountId, { status: "disconnected" });
|
|
310
340
|
} catch { /* ignore DB errors */ }
|
|
311
341
|
}
|
|
312
342
|
}
|
|
@@ -19,17 +19,22 @@ export class WebChatChannel extends BaseChannel {
|
|
|
19
19
|
accountId: string;
|
|
20
20
|
config: WebChatConfig;
|
|
21
21
|
|
|
22
|
+
private explicitAccountId?: string;
|
|
22
23
|
private connections: Map<string, ServerWebSocket<WebSocketData>> = new Map();
|
|
23
24
|
private log = logger.child("webchat");
|
|
24
25
|
|
|
25
26
|
constructor(config: WebChatConfig) {
|
|
26
27
|
super();
|
|
27
28
|
this.config = config;
|
|
28
|
-
|
|
29
|
-
this.accountId = config.accountId ||
|
|
29
|
+
this.explicitAccountId = config.accountId;
|
|
30
|
+
this.accountId = config.accountId || "webchat";
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
async start(): Promise<void> {
|
|
34
|
+
// Resolve accountId from database (single user) if not provided explicitly
|
|
35
|
+
if (!this.explicitAccountId) {
|
|
36
|
+
this.accountId = (await resolveUserId({})) || "webchat";
|
|
37
|
+
}
|
|
33
38
|
this.running = true;
|
|
34
39
|
this.log.info("WebChat channel ready");
|
|
35
40
|
}
|
|
@@ -46,7 +51,8 @@ export class WebChatChannel extends BaseChannel {
|
|
|
46
51
|
this.log.debug(`WebChat connection registered: ${data.sessionId}`);
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
unregisterConnection(sessionId: string): void {
|
|
54
|
+
unregisterConnection(sessionId: string, ws?: ServerWebSocket<WebSocketData>): void {
|
|
55
|
+
if (ws && this.connections.get(sessionId) !== ws) return;
|
|
50
56
|
this.connections.delete(sessionId);
|
|
51
57
|
this.log.debug(`WebChat connection unregistered: ${sessionId}`);
|
|
52
58
|
}
|
|
@@ -86,15 +92,10 @@ export class WebChatChannel extends BaseChannel {
|
|
|
86
92
|
const ws = this.connections.get(sessionId);
|
|
87
93
|
|
|
88
94
|
if (!ws) {
|
|
89
|
-
|
|
90
|
-
return;
|
|
95
|
+
throw new Error(`No WebChat connection for session: ${sessionId}`);
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
ws.send(JSON.stringify(message));
|
|
95
|
-
} catch (error) {
|
|
96
|
-
this.log.error(`Failed to send WebChat message: ${(error as Error).message}`);
|
|
97
|
-
}
|
|
98
|
+
ws.send(JSON.stringify(message));
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
async sendAudio(sessionId: string, audio: Buffer, mimeType: string): Promise<void> {
|
|
@@ -11,8 +11,10 @@ import type { ChannelConfig, IncomingMessage, OutboundMessage } from "./base.ts"
|
|
|
11
11
|
import { BaseChannel } from "./base.ts";
|
|
12
12
|
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
+
import { homedir } from "node:os";
|
|
14
15
|
import { logger } from "../utils/logger.ts";
|
|
15
|
-
import {
|
|
16
|
+
import { updateDoc } from "../storage/hive.ts";
|
|
17
|
+
import type { ChannelDoc } from "../storage/collections.ts";
|
|
16
18
|
// @ts-ignore — no type definitions for qrcode-terminal
|
|
17
19
|
import qrcodeTerminal from "qrcode-terminal";
|
|
18
20
|
|
|
@@ -69,7 +71,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
private getAuthPath(agentId: string, accountId: string): string {
|
|
72
|
-
const baseDir =
|
|
74
|
+
const baseDir = homedir();
|
|
73
75
|
const authDir = path.join(baseDir, ".hive", "agents", agentId, "whatsapp", accountId);
|
|
74
76
|
|
|
75
77
|
if (!existsSync(authDir)) {
|
|
@@ -117,11 +119,22 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
117
119
|
this.connectionState.waVersion = version.join(".");
|
|
118
120
|
this.log.info(`Using WhatsApp Web v${version.join(".")}`);
|
|
119
121
|
|
|
122
|
+
const baileysLogger = {
|
|
123
|
+
level: "silent",
|
|
124
|
+
child: () => baileysLogger,
|
|
125
|
+
trace: () => {},
|
|
126
|
+
debug: () => {},
|
|
127
|
+
info: (msg: unknown) => { if (typeof msg === "object" && msg !== null) this.log.debug((msg as any).msg ?? JSON.stringify(msg)); },
|
|
128
|
+
warn: (msg: unknown) => { if (typeof msg === "object" && msg !== null) this.log.warn((msg as any).msg ?? JSON.stringify(msg)); },
|
|
129
|
+
error: (msg: unknown) => { if (typeof msg === "object" && msg !== null) this.log.error((msg as any).msg ?? JSON.stringify(msg)); },
|
|
130
|
+
};
|
|
131
|
+
|
|
120
132
|
this.socket = makeWASocket({
|
|
121
133
|
version,
|
|
122
134
|
auth: state,
|
|
123
135
|
printQRInTerminal: false,
|
|
124
136
|
syncFullHistory: false,
|
|
137
|
+
logger: baileysLogger as any,
|
|
125
138
|
getMessage: async () => ({ conversation: "" }),
|
|
126
139
|
});
|
|
127
140
|
|
|
@@ -169,8 +182,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
169
182
|
this.log.warn(`WhatsApp disconnected: ${statusCode}`);
|
|
170
183
|
|
|
171
184
|
try {
|
|
172
|
-
|
|
173
|
-
.run(shouldReconnect ? "connecting" : "disconnected", this.accountId);
|
|
185
|
+
await updateDoc<ChannelDoc>("channels", this.accountId, { status: shouldReconnect ? "connecting" : "disconnected" });
|
|
174
186
|
} catch { /* ignore DB errors */ }
|
|
175
187
|
|
|
176
188
|
const needsSessionClear =
|
|
@@ -207,8 +219,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
207
219
|
}
|
|
208
220
|
|
|
209
221
|
try {
|
|
210
|
-
|
|
211
|
-
.run(Date.now(), this.accountId);
|
|
222
|
+
await updateDoc<ChannelDoc>("channels", this.accountId, { status: "connected", last_active: Date.now() });
|
|
212
223
|
} catch { /* ignore DB errors */ }
|
|
213
224
|
}
|
|
214
225
|
}
|
|
@@ -471,7 +482,11 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
471
482
|
throw new Error("WhatsApp not connected");
|
|
472
483
|
}
|
|
473
484
|
|
|
474
|
-
|
|
485
|
+
// Progress narration arrives mid-turn. Clearing "typing…" on it would leave
|
|
486
|
+
// the user watching interim messages with no sign the agent is still
|
|
487
|
+
// working — only a final message ends the indicator.
|
|
488
|
+
const isInterim = message.type === "progress";
|
|
489
|
+
if (!isInterim) await this.stopTyping(sessionId);
|
|
475
490
|
|
|
476
491
|
const text = message.content ?? message.chunk ?? "";
|
|
477
492
|
if (!text) return;
|
|
@@ -479,6 +494,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
479
494
|
const jid = this.getJid(sessionId);
|
|
480
495
|
|
|
481
496
|
await this.socket.sendMessage(jid, { text });
|
|
497
|
+
if (isInterim) await this.startTyping(sessionId);
|
|
482
498
|
this.log.debug(`Sent message to ${jid}`);
|
|
483
499
|
}
|
|
484
500
|
|
|
@@ -1,2 +1,13 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
1
|
+
export {
|
|
2
|
+
loadEnv,
|
|
3
|
+
getHiveDir,
|
|
4
|
+
loadConfig,
|
|
5
|
+
expandConfigPath,
|
|
6
|
+
expandPath,
|
|
7
|
+
type Config,
|
|
8
|
+
type ProviderConfig,
|
|
9
|
+
type MCPServerConfig,
|
|
10
|
+
type AgentEntry,
|
|
11
|
+
type Binding,
|
|
12
|
+
type UserConfig as ConfigUserConfig
|
|
13
|
+
} from "./loader.ts";
|