@johpaz/hive 1.1.0
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/CONTRIBUTING.md +44 -0
- package/README.md +310 -0
- package/package.json +96 -0
- package/packages/cli/package.json +28 -0
- package/packages/cli/src/commands/agent-run.ts +168 -0
- package/packages/cli/src/commands/agents.ts +398 -0
- package/packages/cli/src/commands/chat.ts +142 -0
- package/packages/cli/src/commands/config.ts +50 -0
- package/packages/cli/src/commands/cron.ts +161 -0
- package/packages/cli/src/commands/dev.ts +95 -0
- package/packages/cli/src/commands/doctor.ts +133 -0
- package/packages/cli/src/commands/gateway.ts +443 -0
- package/packages/cli/src/commands/logs.ts +57 -0
- package/packages/cli/src/commands/mcp.ts +175 -0
- package/packages/cli/src/commands/message.ts +77 -0
- package/packages/cli/src/commands/onboard.ts +1868 -0
- package/packages/cli/src/commands/security.ts +144 -0
- package/packages/cli/src/commands/service.ts +50 -0
- package/packages/cli/src/commands/sessions.ts +116 -0
- package/packages/cli/src/commands/skills.ts +187 -0
- package/packages/cli/src/commands/update.ts +25 -0
- package/packages/cli/src/index.ts +185 -0
- package/packages/cli/src/utils/token.ts +6 -0
- package/packages/code-bridge/README.md +78 -0
- package/packages/code-bridge/package.json +18 -0
- package/packages/code-bridge/src/index.ts +95 -0
- package/packages/code-bridge/src/process-manager.ts +212 -0
- package/packages/code-bridge/src/schemas.ts +133 -0
- package/packages/core/package.json +46 -0
- package/packages/core/src/agent/agent-loop.ts +369 -0
- package/packages/core/src/agent/compaction.ts +140 -0
- package/packages/core/src/agent/context-compiler.ts +378 -0
- package/packages/core/src/agent/context-guard.ts +91 -0
- package/packages/core/src/agent/context.ts +138 -0
- package/packages/core/src/agent/conversation-store.ts +198 -0
- package/packages/core/src/agent/curator.ts +158 -0
- package/packages/core/src/agent/hooks.ts +166 -0
- package/packages/core/src/agent/index.ts +116 -0
- package/packages/core/src/agent/llm-client.ts +503 -0
- package/packages/core/src/agent/native-tools.ts +505 -0
- package/packages/core/src/agent/prompt-builder.ts +532 -0
- package/packages/core/src/agent/providers/index.ts +167 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/reflector.ts +170 -0
- package/packages/core/src/agent/service.ts +64 -0
- package/packages/core/src/agent/stuck-loop.ts +133 -0
- package/packages/core/src/agent/supervisor.ts +39 -0
- package/packages/core/src/agent/tracer.ts +102 -0
- package/packages/core/src/agent/workspace.ts +110 -0
- package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
- package/packages/core/src/canvas/canvas-manager.ts +319 -0
- package/packages/core/src/canvas/canvas-tools.ts +420 -0
- package/packages/core/src/canvas/emitter.ts +115 -0
- package/packages/core/src/canvas/index.ts +2 -0
- package/packages/core/src/channels/base.ts +138 -0
- package/packages/core/src/channels/discord.ts +260 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +383 -0
- package/packages/core/src/channels/slack.ts +287 -0
- package/packages/core/src/channels/telegram.ts +502 -0
- package/packages/core/src/channels/webchat.ts +128 -0
- package/packages/core/src/channels/whatsapp.ts +375 -0
- package/packages/core/src/config/index.ts +12 -0
- package/packages/core/src/config/loader.ts +529 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/index.ts +5 -0
- package/packages/core/src/gateway/initializer.ts +290 -0
- package/packages/core/src/gateway/lane-queue.ts +169 -0
- package/packages/core/src/gateway/resolver.ts +108 -0
- package/packages/core/src/gateway/router.ts +124 -0
- package/packages/core/src/gateway/server.ts +3317 -0
- package/packages/core/src/gateway/session.ts +95 -0
- package/packages/core/src/gateway/slash-commands.ts +192 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +19 -0
- package/packages/core/src/integrations/catalog.ts +286 -0
- package/packages/core/src/integrations/env.ts +64 -0
- package/packages/core/src/integrations/index.ts +2 -0
- package/packages/core/src/memory/index.ts +1 -0
- package/packages/core/src/memory/notes.ts +68 -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/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/pairing.ts +250 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/db-context.ts +333 -0
- package/packages/core/src/storage/onboarding.ts +1087 -0
- package/packages/core/src/storage/schema.ts +541 -0
- package/packages/core/src/storage/seed.ts +571 -0
- package/packages/core/src/storage/sqlite.ts +387 -0
- package/packages/core/src/storage/usage.ts +212 -0
- package/packages/core/src/tools/bridge-events.ts +74 -0
- package/packages/core/src/tools/browser.ts +275 -0
- package/packages/core/src/tools/codebridge.ts +421 -0
- package/packages/core/src/tools/coordinator-tools.ts +179 -0
- package/packages/core/src/tools/cron.ts +611 -0
- package/packages/core/src/tools/exec.ts +140 -0
- package/packages/core/src/tools/fs.ts +364 -0
- package/packages/core/src/tools/index.ts +12 -0
- package/packages/core/src/tools/memory.ts +176 -0
- package/packages/core/src/tools/notify.ts +113 -0
- package/packages/core/src/tools/project-management.ts +376 -0
- package/packages/core/src/tools/project.ts +375 -0
- package/packages/core/src/tools/read.ts +158 -0
- package/packages/core/src/tools/web.ts +436 -0
- package/packages/core/src/tools/workspace.ts +171 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +4 -0
- package/packages/core/src/utils/logger.ts +388 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/voice/index.ts +583 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/mcp/package.json +26 -0
- package/packages/mcp/src/config.ts +13 -0
- package/packages/mcp/src/index.ts +1 -0
- package/packages/mcp/src/logger.ts +42 -0
- package/packages/mcp/src/manager.ts +434 -0
- package/packages/mcp/src/transports/index.ts +67 -0
- package/packages/mcp/src/transports/sse.ts +241 -0
- package/packages/mcp/src/transports/websocket.ts +159 -0
- package/packages/skills/package.json +21 -0
- package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
- package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
- package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
- package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
- package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
- package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
- package/packages/skills/src/bundled/memory/SKILL.md +42 -0
- package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
- package/packages/skills/src/bundled/shell/SKILL.md +43 -0
- package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
- package/packages/skills/src/bundled/voice/SKILL.md +25 -0
- package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +282 -0
- package/packages/tools/package.json +43 -0
- package/packages/tools/src/browser/browser.test.ts +111 -0
- package/packages/tools/src/browser/index.ts +272 -0
- package/packages/tools/src/canvas/index.ts +220 -0
- package/packages/tools/src/cron/cron.test.ts +164 -0
- package/packages/tools/src/cron/index.ts +304 -0
- package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
- package/packages/tools/src/filesystem/index.ts +379 -0
- package/packages/tools/src/git/index.ts +239 -0
- package/packages/tools/src/index.ts +4 -0
- package/packages/tools/src/shell/detect-env.ts +70 -0
- package/packages/tools/tsconfig.json +9 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.ts";
|
|
2
|
+
|
|
3
|
+
export interface TokenBucketConfig {
|
|
4
|
+
maxTokens: number;
|
|
5
|
+
refillRate: number;
|
|
6
|
+
refillIntervalMs?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TokenBucket {
|
|
10
|
+
tokens: number;
|
|
11
|
+
lastUpdate: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RateLimitResult {
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
remaining: number;
|
|
17
|
+
retryAfterMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TokenBucketStats {
|
|
21
|
+
totalBuckets: number;
|
|
22
|
+
activeBuckets: number;
|
|
23
|
+
totalTokensAvailable: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TokenBucketRateLimiter {
|
|
27
|
+
private buckets: Map<string, TokenBucket> = new Map();
|
|
28
|
+
private config: Required<TokenBucketConfig>;
|
|
29
|
+
private log = logger.child("token-bucket-limiter");
|
|
30
|
+
|
|
31
|
+
constructor(config: TokenBucketConfig) {
|
|
32
|
+
this.config = {
|
|
33
|
+
maxTokens: config.maxTokens,
|
|
34
|
+
refillRate: config.refillRate,
|
|
35
|
+
refillIntervalMs: config.refillIntervalMs ?? 1000,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
this.startCleanup();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
canProceed(key: string): RateLimitResult {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
let bucket = this.buckets.get(key);
|
|
44
|
+
|
|
45
|
+
if (!bucket) {
|
|
46
|
+
bucket = {
|
|
47
|
+
tokens: this.config.maxTokens,
|
|
48
|
+
lastUpdate: now,
|
|
49
|
+
};
|
|
50
|
+
this.buckets.set(key, bucket);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const elapsed = now - bucket.lastUpdate;
|
|
54
|
+
const tokensToAdd = (elapsed / this.config.refillIntervalMs) * this.config.refillRate;
|
|
55
|
+
bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
|
|
56
|
+
bucket.lastUpdate = now;
|
|
57
|
+
|
|
58
|
+
if (bucket.tokens >= 1) {
|
|
59
|
+
bucket.tokens--;
|
|
60
|
+
return {
|
|
61
|
+
allowed: true,
|
|
62
|
+
remaining: Math.floor(bucket.tokens),
|
|
63
|
+
retryAfterMs: 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const retryAfterMs = Math.ceil(
|
|
68
|
+
((1 - bucket.tokens) / this.config.refillRate) * this.config.refillIntervalMs
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
this.log.debug(`Rate limit hit for ${key}, retry after ${retryAfterMs}ms`);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
allowed: false,
|
|
75
|
+
remaining: 0,
|
|
76
|
+
retryAfterMs,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
consume(key: string, tokens: number = 1): RateLimitResult {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
let bucket = this.buckets.get(key);
|
|
83
|
+
|
|
84
|
+
if (!bucket) {
|
|
85
|
+
bucket = {
|
|
86
|
+
tokens: this.config.maxTokens,
|
|
87
|
+
lastUpdate: now,
|
|
88
|
+
};
|
|
89
|
+
this.buckets.set(key, bucket);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const elapsed = now - bucket.lastUpdate;
|
|
93
|
+
const tokensToAdd = (elapsed / this.config.refillIntervalMs) * this.config.refillRate;
|
|
94
|
+
bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
|
|
95
|
+
bucket.lastUpdate = now;
|
|
96
|
+
|
|
97
|
+
if (bucket.tokens >= tokens) {
|
|
98
|
+
bucket.tokens -= tokens;
|
|
99
|
+
return {
|
|
100
|
+
allowed: true,
|
|
101
|
+
remaining: Math.floor(bucket.tokens),
|
|
102
|
+
retryAfterMs: 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const retryAfterMs = Math.ceil(
|
|
107
|
+
((tokens - bucket.tokens) / this.config.refillRate) * this.config.refillIntervalMs
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
allowed: false,
|
|
112
|
+
remaining: 0,
|
|
113
|
+
retryAfterMs,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
peek(key: string): number {
|
|
118
|
+
const bucket = this.buckets.get(key);
|
|
119
|
+
if (!bucket) return this.config.maxTokens;
|
|
120
|
+
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const elapsed = now - bucket.lastUpdate;
|
|
123
|
+
const tokensToAdd = (elapsed / this.config.refillIntervalMs) * this.config.refillRate;
|
|
124
|
+
|
|
125
|
+
return Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
refill(key: string, tokens?: number): void {
|
|
129
|
+
const bucket = this.buckets.get(key);
|
|
130
|
+
if (!bucket) return;
|
|
131
|
+
|
|
132
|
+
bucket.tokens = Math.min(
|
|
133
|
+
this.config.maxTokens,
|
|
134
|
+
bucket.tokens + (tokens ?? this.config.maxTokens)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
reset(key: string): void {
|
|
139
|
+
this.buckets.delete(key);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
resetAll(): void {
|
|
143
|
+
this.buckets.clear();
|
|
144
|
+
this.log.info("All rate limit buckets cleared");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getStats(): TokenBucketStats {
|
|
148
|
+
let totalTokens = 0;
|
|
149
|
+
let activeBuckets = 0;
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
|
|
152
|
+
for (const bucket of this.buckets.values()) {
|
|
153
|
+
const elapsed = now - bucket.lastUpdate;
|
|
154
|
+
const tokens = Math.min(
|
|
155
|
+
this.config.maxTokens,
|
|
156
|
+
bucket.tokens + (elapsed / this.config.refillIntervalMs) * this.config.refillRate
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (tokens < this.config.maxTokens) {
|
|
160
|
+
activeBuckets++;
|
|
161
|
+
}
|
|
162
|
+
totalTokens += tokens;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
totalBuckets: this.buckets.size,
|
|
167
|
+
activeBuckets,
|
|
168
|
+
totalTokensAvailable: Math.floor(totalTokens),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private startCleanup(): void {
|
|
173
|
+
setInterval(() => {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
for (const [key, bucket] of this.buckets) {
|
|
176
|
+
const elapsed = now - bucket.lastUpdate;
|
|
177
|
+
const fullTokens =
|
|
178
|
+
bucket.tokens + (elapsed / this.config.refillIntervalMs) * this.config.refillRate;
|
|
179
|
+
|
|
180
|
+
if (fullTokens >= this.config.maxTokens) {
|
|
181
|
+
this.buckets.delete(key);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}, 5 * 60 * 1000);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface SlidingWindowConfig {
|
|
189
|
+
windowMs: number;
|
|
190
|
+
maxRequests: number;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface SlidingWindowEntry {
|
|
194
|
+
timestamps: number[];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export class SlidingWindowRateLimiter {
|
|
198
|
+
private windows: Map<string, SlidingWindowEntry> = new Map();
|
|
199
|
+
private config: SlidingWindowConfig;
|
|
200
|
+
private log = logger.child("sliding-window-limiter");
|
|
201
|
+
|
|
202
|
+
constructor(config: SlidingWindowConfig) {
|
|
203
|
+
this.config = config;
|
|
204
|
+
this.startCleanup();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
check(key: string): RateLimitResult {
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
const windowStart = now - this.config.windowMs;
|
|
210
|
+
|
|
211
|
+
let entry = this.windows.get(key);
|
|
212
|
+
if (!entry) {
|
|
213
|
+
entry = { timestamps: [] };
|
|
214
|
+
this.windows.set(key, entry);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
|
|
218
|
+
|
|
219
|
+
if (entry.timestamps.length >= this.config.maxRequests) {
|
|
220
|
+
const oldestInWindow = entry.timestamps[0];
|
|
221
|
+
const retryAfterMs = oldestInWindow + this.config.windowMs - now;
|
|
222
|
+
|
|
223
|
+
this.log.debug(`Sliding window limit hit for ${key}`);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
allowed: false,
|
|
227
|
+
remaining: 0,
|
|
228
|
+
retryAfterMs: Math.max(0, retryAfterMs),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
entry.timestamps.push(now);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
allowed: true,
|
|
236
|
+
remaining: this.config.maxRequests - entry.timestamps.length,
|
|
237
|
+
retryAfterMs: 0,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
reset(key: string): void {
|
|
242
|
+
this.windows.delete(key);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
resetAll(): void {
|
|
246
|
+
this.windows.clear();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private startCleanup(): void {
|
|
250
|
+
setInterval(() => {
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
const windowStart = now - this.config.windowMs;
|
|
253
|
+
|
|
254
|
+
for (const [key, entry] of this.windows) {
|
|
255
|
+
entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
|
|
256
|
+
if (entry.timestamps.length === 0) {
|
|
257
|
+
this.windows.delete(key);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}, 60 * 1000);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function createTokenBucketLimiter(config: TokenBucketConfig): TokenBucketRateLimiter {
|
|
265
|
+
return new TokenBucketRateLimiter(config);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function createSlidingWindowLimiter(config: SlidingWindowConfig): SlidingWindowRateLimiter {
|
|
269
|
+
return new SlidingWindowRateLimiter(config);
|
|
270
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
2
|
+
import { BaseChannel, type ChannelConfig, type IncomingMessage, type OutboundMessage } from "../channels/base.ts";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
import { pairingService } from "./pairing.ts";
|
|
5
|
+
|
|
6
|
+
export interface SignalConfig extends ChannelConfig {
|
|
7
|
+
phoneNumber: string;
|
|
8
|
+
dataDir?: string;
|
|
9
|
+
signalCliPath?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SignalMessage {
|
|
13
|
+
envelope: {
|
|
14
|
+
source: string;
|
|
15
|
+
sourceNumber?: string;
|
|
16
|
+
sourceName?: string;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
dataMessage?: {
|
|
19
|
+
message?: string;
|
|
20
|
+
expiresInSeconds?: number;
|
|
21
|
+
};
|
|
22
|
+
syncMessage?: {
|
|
23
|
+
sentMessage?: {
|
|
24
|
+
destination?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class SignalChannel extends BaseChannel {
|
|
32
|
+
name = "signal";
|
|
33
|
+
accountId: string;
|
|
34
|
+
config: SignalConfig;
|
|
35
|
+
|
|
36
|
+
private signalCli?: ChildProcess;
|
|
37
|
+
private log = logger.child("signal");
|
|
38
|
+
private messageBuffer = "";
|
|
39
|
+
private chatIdCache: Map<string, string> = new Map();
|
|
40
|
+
|
|
41
|
+
constructor(accountId: string, config: SignalConfig) {
|
|
42
|
+
super();
|
|
43
|
+
this.accountId = accountId;
|
|
44
|
+
this.config = {
|
|
45
|
+
...config,
|
|
46
|
+
dmPolicy: config.dmPolicy ?? "pairing",
|
|
47
|
+
allowFrom: config.allowFrom ?? [],
|
|
48
|
+
enabled: config.enabled ?? true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async start(): Promise<void> {
|
|
53
|
+
if (!this.config.phoneNumber) {
|
|
54
|
+
throw new Error("Signal phone number not configured");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const signalCliPath = this.config.signalCliPath ?? "signal-cli";
|
|
58
|
+
const dataDir = this.config.dataDir;
|
|
59
|
+
|
|
60
|
+
const args = ["daemon", "--system"];
|
|
61
|
+
if (dataDir) {
|
|
62
|
+
args.push("--config", dataDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.log.info(`Starting signal-cli: ${signalCliPath} ${args.join(" ")}`);
|
|
66
|
+
|
|
67
|
+
this.signalCli = spawn(signalCliPath, args, {
|
|
68
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.signalCli.stdout?.on("data", (data: Buffer) => {
|
|
72
|
+
this.handleSignalOutput(data.toString());
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.signalCli.stderr?.on("data", (data: Buffer) => {
|
|
76
|
+
this.log.error(`signal-cli stderr: ${data.toString()}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.signalCli.on("error", (error: Error) => {
|
|
80
|
+
this.log.error(`signal-cli error: ${error.message}`);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.signalCli.on("exit", (code, signal) => {
|
|
84
|
+
this.log.warn(`signal-cli exited with code ${code}, signal ${signal}`);
|
|
85
|
+
this.running = false;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
89
|
+
|
|
90
|
+
this.running = true;
|
|
91
|
+
this.log.info(`Signal channel started for ${this.config.phoneNumber}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private handleSignalOutput(output: string): void {
|
|
95
|
+
this.messageBuffer += output;
|
|
96
|
+
|
|
97
|
+
const lines = this.messageBuffer.split("\n");
|
|
98
|
+
this.messageBuffer = lines.pop() ?? "";
|
|
99
|
+
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
if (line.trim()) {
|
|
102
|
+
this.parseSignalMessage(line);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private parseSignalMessage(line: string): void {
|
|
108
|
+
try {
|
|
109
|
+
const data: SignalMessage = JSON.parse(line);
|
|
110
|
+
|
|
111
|
+
if (data.envelope?.dataMessage?.message) {
|
|
112
|
+
const from = data.envelope.source;
|
|
113
|
+
const content = data.envelope.dataMessage.message;
|
|
114
|
+
const timestamp = data.envelope.timestamp;
|
|
115
|
+
|
|
116
|
+
this.handleIncomingMessage({
|
|
117
|
+
from,
|
|
118
|
+
content,
|
|
119
|
+
timestamp,
|
|
120
|
+
name: data.envelope.sourceName,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
this.log.debug(`Non-JSON output: ${line.slice(0, 100)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async handleIncomingMessage(msg: {
|
|
129
|
+
from: string;
|
|
130
|
+
content: string;
|
|
131
|
+
timestamp: number;
|
|
132
|
+
name?: string;
|
|
133
|
+
}): Promise<void> {
|
|
134
|
+
const peerId = msg.from;
|
|
135
|
+
const kind = "direct";
|
|
136
|
+
|
|
137
|
+
if (msg.content === "/myid") {
|
|
138
|
+
await this.sendDirectMessage(
|
|
139
|
+
peerId,
|
|
140
|
+
`🆔 Tu Signal ID es: ${peerId}\n\n` +
|
|
141
|
+
`Para emparejar, usa el comando:\n` +
|
|
142
|
+
`\`hive pairing generate signal ${peerId}\``
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (msg.content === "/pair" || msg.content.startsWith("/pair ")) {
|
|
148
|
+
const code = msg.content.split(" ")[1]?.trim();
|
|
149
|
+
|
|
150
|
+
if (!code) {
|
|
151
|
+
await this.sendDirectMessage(
|
|
152
|
+
peerId,
|
|
153
|
+
"🔐 Envía /pair CODIGO para emparejar.\n" +
|
|
154
|
+
"Solicita un código de emparejamiento al administrador."
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = pairingService.approve(code);
|
|
160
|
+
if (result.success) {
|
|
161
|
+
await this.sendDirectMessage(peerId, "✅ ¡Emparejamiento exitoso! Ya puedes usar el bot.");
|
|
162
|
+
} else {
|
|
163
|
+
await this.sendDirectMessage(peerId, `❌ ${result.error}`);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.config.dmPolicy === "pairing" && !pairingService.isAllowed("signal", peerId)) {
|
|
169
|
+
this.log.debug(`Message from unpaired user: ${peerId}`);
|
|
170
|
+
await this.sendDirectMessage(
|
|
171
|
+
peerId,
|
|
172
|
+
"⛔ No estás emparejado.\n\n" +
|
|
173
|
+
"Tu Signal ID: " + peerId + "\n\n" +
|
|
174
|
+
"Solicita un código de emparejamiento al administrador y envíalo con:\n" +
|
|
175
|
+
"/pair CODIGO"
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!this.isUserAllowed(peerId)) {
|
|
181
|
+
this.log.debug(`Message from unauthorized user: ${peerId}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const sessionId = this.formatSessionId(peerId, kind);
|
|
186
|
+
this.chatIdCache.set(sessionId, peerId);
|
|
187
|
+
|
|
188
|
+
const incomingMessage: IncomingMessage = {
|
|
189
|
+
sessionId,
|
|
190
|
+
channel: "signal",
|
|
191
|
+
accountId: this.accountId,
|
|
192
|
+
peerId,
|
|
193
|
+
peerKind: kind,
|
|
194
|
+
content: msg.content,
|
|
195
|
+
metadata: {
|
|
196
|
+
signal: {
|
|
197
|
+
phoneNumber: peerId,
|
|
198
|
+
name: msg.name,
|
|
199
|
+
timestamp: msg.timestamp,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
await this.handleMessage(incomingMessage);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async stop(): Promise<void> {
|
|
208
|
+
if (this.signalCli) {
|
|
209
|
+
this.signalCli.kill("SIGTERM");
|
|
210
|
+
this.running = false;
|
|
211
|
+
this.log.info("Signal channel stopped");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async send(sessionId: string, message: OutboundMessage): Promise<void> {
|
|
216
|
+
const content = message.content ?? "";
|
|
217
|
+
|
|
218
|
+
if (!content || content.trim().length === 0) {
|
|
219
|
+
this.log.warn("Empty response, skipping send");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const phoneNumber = this.chatIdCache.get(sessionId) ?? this.extractPhoneFromSession(sessionId);
|
|
224
|
+
|
|
225
|
+
if (!phoneNumber) {
|
|
226
|
+
throw new Error(`Cannot determine phone number for session: ${sessionId}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await this.sendDirectMessage(phoneNumber, content);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async sendDirectMessage(phoneNumber: string, content: string): Promise<void> {
|
|
233
|
+
const chunks = this.chunkMessage(content, 2000);
|
|
234
|
+
|
|
235
|
+
for (const chunk of chunks) {
|
|
236
|
+
await this.executeCli([
|
|
237
|
+
"send",
|
|
238
|
+
"-a", this.config.phoneNumber,
|
|
239
|
+
"-m", chunk,
|
|
240
|
+
phoneNumber,
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
if (chunks.length > 1) {
|
|
244
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async executeCli(args: string[]): Promise<string> {
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
const signalCliPath = this.config.signalCliPath ?? "signal-cli";
|
|
252
|
+
const fullArgs = this.config.dataDir
|
|
253
|
+
? ["--config", this.config.dataDir, ...args]
|
|
254
|
+
: args;
|
|
255
|
+
|
|
256
|
+
const proc = spawn(signalCliPath, fullArgs, {
|
|
257
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
let stdout = "";
|
|
261
|
+
let stderr = "";
|
|
262
|
+
|
|
263
|
+
proc.stdout?.on("data", (data: Buffer) => {
|
|
264
|
+
stdout += data.toString();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
proc.stderr?.on("data", (data: Buffer) => {
|
|
268
|
+
stderr += data.toString();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
proc.on("close", (code) => {
|
|
272
|
+
if (code === 0) {
|
|
273
|
+
resolve(stdout);
|
|
274
|
+
} else {
|
|
275
|
+
reject(new Error(`signal-cli failed: ${stderr || stdout}`));
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
proc.on("error", (error) => {
|
|
280
|
+
reject(error);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private extractPhoneFromSession(sessionId: string): string | undefined {
|
|
286
|
+
const parts = sessionId.split(":");
|
|
287
|
+
return parts[4];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private chunkMessage(content: string, maxLength: number): string[] {
|
|
291
|
+
const chunks: string[] = [];
|
|
292
|
+
let remaining = content;
|
|
293
|
+
|
|
294
|
+
while (remaining.length > 0) {
|
|
295
|
+
if (remaining.length <= maxLength) {
|
|
296
|
+
chunks.push(remaining);
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let splitPoint = remaining.lastIndexOf("\n\n", maxLength);
|
|
301
|
+
if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
|
|
302
|
+
splitPoint = remaining.lastIndexOf("\n", maxLength);
|
|
303
|
+
}
|
|
304
|
+
if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
|
|
305
|
+
splitPoint = remaining.lastIndexOf(" ", maxLength);
|
|
306
|
+
}
|
|
307
|
+
if (splitPoint === -1 || splitPoint < maxLength * 0.5) {
|
|
308
|
+
splitPoint = maxLength;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
chunks.push(remaining.slice(0, splitPoint));
|
|
312
|
+
remaining = remaining.slice(splitPoint).trim();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return chunks;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function createSignalChannel(accountId: string, config: SignalConfig): SignalChannel {
|
|
320
|
+
return new SignalChannel(accountId, config);
|
|
321
|
+
}
|