@johpaz/hive-core 1.0.6 → 1.0.7
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/package.json +5 -2
- package/src/agent/index.ts +145 -17
- package/src/agent/providers/index.ts +20 -1
- package/src/agent/workspace.ts +111 -0
- package/src/channels/base.ts +28 -2
- package/src/channels/discord.ts +58 -10
- package/src/channels/manager.ts +114 -3
- package/src/channels/slack.ts +38 -4
- package/src/channels/telegram.ts +263 -43
- package/src/channels/webchat.ts +22 -0
- package/src/channels/whatsapp.ts +51 -3
- package/src/config/loader.ts +47 -8
- package/src/gateway/server.ts +612 -240
- package/src/gateway/session.ts +2 -1
- package/src/gateway/slash-commands.ts +7 -14
- package/src/memory/notes.ts +28 -130
- package/src/multi-agent/manager.ts +28 -0
- package/src/storage/sqlite.ts +230 -0
- package/src/tools/workspace.ts +171 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johpaz/hive-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Hive Gateway — Personal AI agent runtime",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -23,9 +23,12 @@
|
|
|
23
23
|
"@slack/bolt": "latest",
|
|
24
24
|
"grammy": "latest",
|
|
25
25
|
"discord.js": "latest",
|
|
26
|
+
"@sapphire/snowflake": "latest",
|
|
26
27
|
"js-yaml": "latest",
|
|
27
28
|
"zod": "latest",
|
|
28
|
-
"qrcode-terminal": "latest"
|
|
29
|
+
"qrcode-terminal": "latest",
|
|
30
|
+
"@johpaz/hive-skills": "workspace:*",
|
|
31
|
+
"@johpaz/hive-mcp": "workspace:*"
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"typescript": "latest",
|
package/src/agent/index.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { Config } from "../config/loader.ts";
|
|
2
|
-
import { loadSoul } from "./soul.ts";
|
|
3
|
-
import { loadUser } from "./user.ts";
|
|
4
|
-
import { loadEthics, type EthicsConfig, DEFAULT_ETHICS } from "./ethics.ts";
|
|
2
|
+
import { loadSoul, watchSoul, type SoulConfig } from "./soul.ts";
|
|
3
|
+
import { loadUser, watchUser, type UserConfig } from "./user.ts";
|
|
4
|
+
import { loadEthics, watchEthics, type EthicsConfig, DEFAULT_ETHICS } from "./ethics.ts";
|
|
5
5
|
import { buildSystemPrompt } from "./context.ts";
|
|
6
6
|
import { logger } from "../utils/logger.ts";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import { SkillLoader, type Skill } from "@johpaz/hive-skills";
|
|
8
|
+
import { MCPClientManager, createMCPManager } from "@johpaz/hive-mcp";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
|
|
12
|
+
export { watchEthics, watchSoul, watchUser };
|
|
9
13
|
|
|
10
14
|
export interface AgentOptions {
|
|
11
15
|
agentId: string;
|
|
@@ -18,6 +22,12 @@ export class Agent {
|
|
|
18
22
|
readonly workspacePath: string;
|
|
19
23
|
private config: Config;
|
|
20
24
|
private ethics: EthicsConfig | null = null;
|
|
25
|
+
private soul: SoulConfig | null = null;
|
|
26
|
+
private user: UserConfig | null = null;
|
|
27
|
+
private skills: Skill[] = [];
|
|
28
|
+
private skillLoader: SkillLoader | null = null;
|
|
29
|
+
private mcpManager: MCPClientManager | null = null;
|
|
30
|
+
private memory: string[] = [];
|
|
21
31
|
private log = logger.child("agent");
|
|
22
32
|
|
|
23
33
|
constructor(options: AgentOptions) {
|
|
@@ -28,26 +38,75 @@ export class Agent {
|
|
|
28
38
|
|
|
29
39
|
async initialize(): Promise<void> {
|
|
30
40
|
this.log.info(`Initializing agent: ${this.agentId}`);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
|
|
42
|
+
this.soul = loadSoul(path.join(this.workspacePath, "SOUL.md"));
|
|
43
|
+
this.user = loadUser(path.join(this.workspacePath, "USER.md"));
|
|
34
44
|
this.ethics = await loadEthics(path.join(this.workspacePath, "ETHICS.md"));
|
|
35
45
|
|
|
36
|
-
if (soul) {
|
|
46
|
+
if (this.soul) {
|
|
37
47
|
this.log.debug(`Loaded SOUL.md for agent ${this.agentId}`);
|
|
38
48
|
}
|
|
39
|
-
if (user) {
|
|
49
|
+
if (this.user) {
|
|
40
50
|
this.log.debug(`Loaded USER.md for agent ${this.agentId}`);
|
|
41
51
|
}
|
|
42
52
|
if (this.ethics) {
|
|
43
53
|
this.log.debug(`Loaded ETHICS.md for agent ${this.agentId}`);
|
|
44
54
|
}
|
|
55
|
+
|
|
56
|
+
// Load skills
|
|
57
|
+
this.skillLoader = new SkillLoader({
|
|
58
|
+
skills: this.config.skills,
|
|
59
|
+
workspacePath: this.workspacePath,
|
|
60
|
+
});
|
|
61
|
+
this.skills = this.skillLoader.loadAllSkills();
|
|
62
|
+
this.log.debug(`Loaded ${this.skills.length} skills for agent ${this.agentId}`);
|
|
63
|
+
|
|
64
|
+
// Load MCP
|
|
65
|
+
if (this.config.mcp?.enabled !== false) {
|
|
66
|
+
this.mcpManager = createMCPManager(this.config.mcp || {});
|
|
67
|
+
await this.mcpManager.initialize();
|
|
68
|
+
try {
|
|
69
|
+
await this.mcpManager.connectAll();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.log.warn(`Some MCP servers failed to connect: ${(error as Error).message}`);
|
|
72
|
+
}
|
|
73
|
+
this.log.debug(`MCP Manager initialized for agent ${this.agentId}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Load memory notes
|
|
77
|
+
this.memory = this.loadMemoryNotes();
|
|
78
|
+
if (this.memory.length > 0) {
|
|
79
|
+
this.log.debug(`Loaded ${this.memory.length} memory notes`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private loadMemoryNotes(): string[] {
|
|
84
|
+
const memoryDir = path.join(this.workspacePath, "memory");
|
|
85
|
+
const notes: string[] = [];
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(memoryDir)) {
|
|
88
|
+
return notes;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const files = fs.readdirSync(memoryDir).filter(f => f.endsWith(".md"));
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
const content = fs.readFileSync(path.join(memoryDir, file), "utf-8");
|
|
95
|
+
// Extract body from frontmatter if present
|
|
96
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
97
|
+
const body = match ? match[1]!.trim() : content.trim();
|
|
98
|
+
if (body) {
|
|
99
|
+
notes.push(body);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
this.log.warn(`Failed to load memory notes: ${(error as Error).message}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return notes;
|
|
45
107
|
}
|
|
46
108
|
|
|
47
109
|
buildPrompt(): string {
|
|
48
|
-
const soul = loadSoul(path.join(this.workspacePath, "SOUL.md"));
|
|
49
|
-
const user = loadUser(path.join(this.workspacePath, "USER.md"));
|
|
50
|
-
|
|
51
110
|
const ethicsConfig = this.ethics ?? {
|
|
52
111
|
raw: DEFAULT_ETHICS,
|
|
53
112
|
loadedAt: new Date(),
|
|
@@ -56,7 +115,7 @@ export class Agent {
|
|
|
56
115
|
|
|
57
116
|
return buildSystemPrompt({
|
|
58
117
|
ethics: ethicsConfig,
|
|
59
|
-
soul: soul ?? {
|
|
118
|
+
soul: this.soul ?? {
|
|
60
119
|
identity: "",
|
|
61
120
|
personality: "",
|
|
62
121
|
boundaries: "",
|
|
@@ -64,13 +123,78 @@ export class Agent {
|
|
|
64
123
|
capabilities: [],
|
|
65
124
|
raw: "",
|
|
66
125
|
},
|
|
67
|
-
user,
|
|
68
|
-
skills:
|
|
69
|
-
memory:
|
|
126
|
+
user: this.user,
|
|
127
|
+
skills: this.skills.map(s => s.content),
|
|
128
|
+
memory: this.memory,
|
|
70
129
|
maxTokens: this.config.agent?.context?.maxTokens ?? 128000,
|
|
71
130
|
});
|
|
72
131
|
}
|
|
73
132
|
|
|
133
|
+
reloadSoul(): void {
|
|
134
|
+
this.soul = loadSoul(path.join(this.workspacePath, "SOUL.md"));
|
|
135
|
+
this.log.info("SOUL.md recargado");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
reloadUser(): void {
|
|
139
|
+
this.user = loadUser(path.join(this.workspacePath, "USER.md"));
|
|
140
|
+
this.log.info("USER.md recargado");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async reloadEthics(): Promise<void> {
|
|
144
|
+
this.ethics = await loadEthics(path.join(this.workspacePath, "ETHICS.md"));
|
|
145
|
+
this.log.info("ETHICS.md recargado");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
reloadSkills(): void {
|
|
149
|
+
if (this.skillLoader) {
|
|
150
|
+
this.skillLoader.clearCache();
|
|
151
|
+
this.skills = this.skillLoader.loadAllSkills();
|
|
152
|
+
this.log.info(`Skills recargadas: ${this.skills.length} skills`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
reloadMemory(): void {
|
|
157
|
+
this.memory = this.loadMemoryNotes();
|
|
158
|
+
this.log.info(`Memoria recargada: ${this.memory.length} notas`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async reload(): Promise<void> {
|
|
162
|
+
this.soul = loadSoul(path.join(this.workspacePath, "SOUL.md"));
|
|
163
|
+
this.user = loadUser(path.join(this.workspacePath, "USER.md"));
|
|
164
|
+
this.ethics = await loadEthics(path.join(this.workspacePath, "ETHICS.md"));
|
|
165
|
+
this.reloadSkills();
|
|
166
|
+
this.reloadMemory();
|
|
167
|
+
this.log.info("Agente recargado completamente");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async updateConfig(config: Config): Promise<void> {
|
|
171
|
+
this.config = config;
|
|
172
|
+
if (this.mcpManager) {
|
|
173
|
+
await this.mcpManager.updateConfig(this.config.mcp || {});
|
|
174
|
+
}
|
|
175
|
+
this.log.info("Configuración actualizada");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getEthics(): EthicsConfig | null {
|
|
179
|
+
return this.ethics;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getSoul(): SoulConfig | null {
|
|
183
|
+
return this.soul;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getUser(): UserConfig | null {
|
|
187
|
+
return this.user;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getSkills(): Skill[] {
|
|
191
|
+
return this.skills;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getMemory(): string[] {
|
|
195
|
+
return this.memory;
|
|
196
|
+
}
|
|
197
|
+
|
|
74
198
|
getConfig(): Config {
|
|
75
199
|
return this.config;
|
|
76
200
|
}
|
|
@@ -78,4 +202,8 @@ export class Agent {
|
|
|
78
202
|
getWorkspacePath(): string {
|
|
79
203
|
return this.workspacePath;
|
|
80
204
|
}
|
|
205
|
+
|
|
206
|
+
getMCPManager(): MCPClientManager | null {
|
|
207
|
+
return this.mcpManager;
|
|
208
|
+
}
|
|
81
209
|
}
|
|
@@ -218,6 +218,14 @@ export class AgentRunner {
|
|
|
218
218
|
options.onToken(chunk);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
if (!fullText || fullText.trim().length === 0) {
|
|
222
|
+
this.log.warn(`Empty response from LLM (streaming)`, {
|
|
223
|
+
provider,
|
|
224
|
+
model: resolved.name,
|
|
225
|
+
});
|
|
226
|
+
fullText = "...";
|
|
227
|
+
}
|
|
228
|
+
|
|
221
229
|
return {
|
|
222
230
|
content: fullText,
|
|
223
231
|
usage: {
|
|
@@ -236,6 +244,17 @@ export class AgentRunner {
|
|
|
236
244
|
temperature: options.temperature ?? 0.7,
|
|
237
245
|
});
|
|
238
246
|
|
|
247
|
+
let responseText = result.text ?? "";
|
|
248
|
+
|
|
249
|
+
if (!responseText || responseText.trim().length === 0) {
|
|
250
|
+
this.log.warn(`Empty response from LLM, using fallback`, {
|
|
251
|
+
provider,
|
|
252
|
+
model: resolved.name,
|
|
253
|
+
hadToolCalls: (result.toolCalls?.length ?? 0) > 0,
|
|
254
|
+
});
|
|
255
|
+
responseText = "...";
|
|
256
|
+
}
|
|
257
|
+
|
|
239
258
|
const toolCalls = result.toolCalls?.map((tc) => {
|
|
240
259
|
const t = tc as unknown as { toolCallId: string; toolName: string; input: Record<string, unknown> };
|
|
241
260
|
return {
|
|
@@ -246,7 +265,7 @@ export class AgentRunner {
|
|
|
246
265
|
});
|
|
247
266
|
|
|
248
267
|
return {
|
|
249
|
-
content:
|
|
268
|
+
content: responseText,
|
|
250
269
|
toolCalls,
|
|
251
270
|
usage: {
|
|
252
271
|
promptTokens: 0,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as fsPromises from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { logger } from "../utils/logger.ts";
|
|
5
|
+
|
|
6
|
+
export type WorkspaceFile = "soul" | "user" | "ethics";
|
|
7
|
+
|
|
8
|
+
export class WorkspaceLoader {
|
|
9
|
+
private cache: Map<WorkspaceFile, string> = new Map();
|
|
10
|
+
private watchers: Map<WorkspaceFile, fs.FSWatcher> = new Map();
|
|
11
|
+
private workspaceDir: string;
|
|
12
|
+
|
|
13
|
+
constructor(workspaceDir: string) {
|
|
14
|
+
this.workspaceDir = workspaceDir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private getPath(file: WorkspaceFile): string {
|
|
18
|
+
const names: Record<WorkspaceFile, string> = {
|
|
19
|
+
soul: "SOUL.md",
|
|
20
|
+
user: "USER.md",
|
|
21
|
+
ethics: "ETHICS.md",
|
|
22
|
+
};
|
|
23
|
+
return path.join(this.workspaceDir, names[file]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async read(file: WorkspaceFile): Promise<string> {
|
|
27
|
+
const filePath = this.getPath(file);
|
|
28
|
+
try {
|
|
29
|
+
const content = await fsPromises.readFile(filePath, "utf-8");
|
|
30
|
+
this.cache.set(file, content);
|
|
31
|
+
return content;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
34
|
+
this.cache.set(file, "");
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
logger.error(`Failed to read ${file}.md: ${(error as Error).message}`);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async write(file: WorkspaceFile, content: string): Promise<void> {
|
|
43
|
+
if (file === "ethics") {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"ETHICS.md cannot be modified programmatically. Only the operator can edit it directly."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const filePath = this.getPath(file);
|
|
50
|
+
await fsPromises.writeFile(filePath, content, "utf-8");
|
|
51
|
+
await fsPromises.chmod(filePath, 0o644);
|
|
52
|
+
this.cache.set(file, content);
|
|
53
|
+
logger.info(`Workspace: Updated ${file}.md`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async patch(file: WorkspaceFile, section: string, newContent: string): Promise<void> {
|
|
57
|
+
if (file === "ethics") {
|
|
58
|
+
throw new Error("ETHICS.md cannot be modified programmatically.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const current = await this.read(file);
|
|
62
|
+
const patched = this.patchSection(current, section, newContent);
|
|
63
|
+
await this.write(file, patched);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async append(file: WorkspaceFile, section: string, content: string): Promise<void> {
|
|
67
|
+
if (file === "ethics") {
|
|
68
|
+
throw new Error("ETHICS.md cannot be modified programmatically.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const current = await this.read(file);
|
|
72
|
+
const sectionHeader = `## ${section}`;
|
|
73
|
+
|
|
74
|
+
if (current.includes(sectionHeader)) {
|
|
75
|
+
const patched = current.replace(
|
|
76
|
+
new RegExp(`(${sectionHeader}[\\s\\S]*?)(?=\\n## |$)`),
|
|
77
|
+
`$1\n${content}`
|
|
78
|
+
);
|
|
79
|
+
await this.write(file, patched);
|
|
80
|
+
} else {
|
|
81
|
+
await this.write(file, `${current}\n\n${sectionHeader}\n${content}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async reload(): Promise<void> {
|
|
86
|
+
for (const file of ["soul", "user", "ethics"] as WorkspaceFile[]) {
|
|
87
|
+
await this.read(file);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private patchSection(content: string, section: string, newContent: string): string {
|
|
92
|
+
const header = `## ${section}`;
|
|
93
|
+
const regex = new RegExp(
|
|
94
|
+
`(${header})([\\s\\S]*?)(?=\\n## |$)`,
|
|
95
|
+
"m"
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (content.match(regex)) {
|
|
99
|
+
return content.replace(regex, `$1\n\n${newContent}\n`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return `${content}\n\n${header}\n\n${newContent}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
closeWatchers(): void {
|
|
106
|
+
for (const watcher of this.watchers.values()) {
|
|
107
|
+
watcher.close();
|
|
108
|
+
}
|
|
109
|
+
this.watchers.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/channels/base.ts
CHANGED
|
@@ -39,6 +39,9 @@ export interface IChannel {
|
|
|
39
39
|
send(sessionId: string, message: OutboundMessage): Promise<void>;
|
|
40
40
|
onMessage(handler: MessageHandler): void;
|
|
41
41
|
isRunning(): boolean;
|
|
42
|
+
startTyping?(sessionId: string): Promise<void>;
|
|
43
|
+
stopTyping?(sessionId: string): Promise<void>;
|
|
44
|
+
markAsRead?(sessionId: string, messageId?: string): Promise<void>;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
export type MessageHandler = (message: IncomingMessage) => Promise<void>;
|
|
@@ -50,6 +53,7 @@ export abstract class BaseChannel implements IChannel {
|
|
|
50
53
|
|
|
51
54
|
protected messageHandler?: MessageHandler;
|
|
52
55
|
protected running = false;
|
|
56
|
+
protected typingIntervals: Map<string, Timer> = new Map();
|
|
53
57
|
|
|
54
58
|
abstract start(): Promise<void>;
|
|
55
59
|
abstract stop(): Promise<void>;
|
|
@@ -63,6 +67,22 @@ export abstract class BaseChannel implements IChannel {
|
|
|
63
67
|
return this.running;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
async startTyping(_sessionId: string): Promise<void> {
|
|
71
|
+
// Default: no-op, override in subclasses
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async stopTyping(sessionId: string): Promise<void> {
|
|
75
|
+
const interval = this.typingIntervals.get(sessionId);
|
|
76
|
+
if (interval) {
|
|
77
|
+
clearInterval(interval);
|
|
78
|
+
this.typingIntervals.delete(sessionId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async markAsRead(_sessionId: string, _messageId?: string): Promise<void> {
|
|
83
|
+
// Default: no-op, override in subclasses
|
|
84
|
+
}
|
|
85
|
+
|
|
66
86
|
protected async handleMessage(message: IncomingMessage): Promise<void> {
|
|
67
87
|
if (this.messageHandler) {
|
|
68
88
|
await this.messageHandler(message);
|
|
@@ -74,12 +94,18 @@ export abstract class BaseChannel implements IChannel {
|
|
|
74
94
|
return true;
|
|
75
95
|
}
|
|
76
96
|
|
|
97
|
+
const normalizedPeerId = `tg:${peerId}`;
|
|
98
|
+
|
|
77
99
|
if (this.config.dmPolicy === "allowlist") {
|
|
78
|
-
return this.config.allowFrom.
|
|
100
|
+
return this.config.allowFrom.some(
|
|
101
|
+
(allowed) => allowed === peerId || allowed === normalizedPeerId
|
|
102
|
+
);
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
if (this.config.dmPolicy === "pairing") {
|
|
82
|
-
return this.config.allowFrom.
|
|
106
|
+
return this.config.allowFrom.some(
|
|
107
|
+
(allowed) => allowed === peerId || allowed === normalizedPeerId
|
|
108
|
+
);
|
|
83
109
|
}
|
|
84
110
|
|
|
85
111
|
return false;
|
package/src/channels/discord.ts
CHANGED
|
@@ -25,6 +25,7 @@ export class DiscordChannel extends BaseChannel {
|
|
|
25
25
|
|
|
26
26
|
private client?: Client;
|
|
27
27
|
private log = logger.child("discord");
|
|
28
|
+
private channelCache: Map<string, DiscordTextChannel> = new Map();
|
|
28
29
|
|
|
29
30
|
constructor(accountId: string, config: DiscordConfig) {
|
|
30
31
|
super();
|
|
@@ -81,8 +82,13 @@ export class DiscordChannel extends BaseChannel {
|
|
|
81
82
|
return;
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
const sessionId = this.formatSessionId(peerId, kind);
|
|
86
|
+
if (message.channel.isTextBased()) {
|
|
87
|
+
this.channelCache.set(sessionId, message.channel as DiscordTextChannel);
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
const incomingMessage: IncomingMessage = {
|
|
85
|
-
sessionId
|
|
91
|
+
sessionId,
|
|
86
92
|
channel: "discord",
|
|
87
93
|
accountId: this.accountId,
|
|
88
94
|
peerId,
|
|
@@ -114,10 +120,11 @@ export class DiscordChannel extends BaseChannel {
|
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
async
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
private async getChannel(sessionId: string): Promise<DiscordTextChannel | null> {
|
|
124
|
+
const cached = this.channelCache.get(sessionId);
|
|
125
|
+
if (cached) return cached;
|
|
126
|
+
|
|
127
|
+
if (!this.client) return null;
|
|
121
128
|
|
|
122
129
|
const parts = sessionId.split(":");
|
|
123
130
|
const peerPart = parts.slice(3).join(":");
|
|
@@ -130,10 +137,51 @@ export class DiscordChannel extends BaseChannel {
|
|
|
130
137
|
channelId = peerPart;
|
|
131
138
|
}
|
|
132
139
|
|
|
133
|
-
|
|
140
|
+
try {
|
|
141
|
+
const channel = await this.client.channels.fetch(channelId);
|
|
142
|
+
if (channel && channel.isTextBased()) {
|
|
143
|
+
this.channelCache.set(sessionId, channel as DiscordTextChannel);
|
|
144
|
+
return channel as DiscordTextChannel;
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Channel not found
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async startTyping(sessionId: string): Promise<void> {
|
|
154
|
+
const channel = await this.getChannel(sessionId);
|
|
155
|
+
if (!channel) return;
|
|
156
|
+
|
|
157
|
+
await channel.sendTyping();
|
|
158
|
+
|
|
159
|
+
const interval = setInterval(async () => {
|
|
160
|
+
try {
|
|
161
|
+
await channel.sendTyping();
|
|
162
|
+
} catch {
|
|
163
|
+
this.stopTyping(sessionId);
|
|
164
|
+
}
|
|
165
|
+
}, 8000);
|
|
166
|
+
|
|
167
|
+
this.typingIntervals.set(sessionId, interval);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async stopTyping(sessionId: string): Promise<void> {
|
|
171
|
+
const interval = this.typingIntervals.get(sessionId);
|
|
172
|
+
if (interval) {
|
|
173
|
+
clearInterval(interval);
|
|
174
|
+
this.typingIntervals.delete(sessionId);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async send(sessionId: string, message: OutboundMessage): Promise<void> {
|
|
179
|
+
await this.stopTyping(sessionId);
|
|
180
|
+
|
|
181
|
+
const channel = await this.getChannel(sessionId);
|
|
134
182
|
|
|
135
|
-
if (!channel
|
|
136
|
-
throw new Error(`Channel not found
|
|
183
|
+
if (!channel) {
|
|
184
|
+
throw new Error(`Channel not found for session: ${sessionId}`);
|
|
137
185
|
}
|
|
138
186
|
|
|
139
187
|
const content = message.content ?? "";
|
|
@@ -141,11 +189,11 @@ export class DiscordChannel extends BaseChannel {
|
|
|
141
189
|
|
|
142
190
|
try {
|
|
143
191
|
if (content.length <= maxLength) {
|
|
144
|
-
await
|
|
192
|
+
await channel.send(content);
|
|
145
193
|
} else {
|
|
146
194
|
const chunks = this.chunkMessage(content, maxLength);
|
|
147
195
|
for (const chunk of chunks) {
|
|
148
|
-
await
|
|
196
|
+
await channel.send(chunk);
|
|
149
197
|
}
|
|
150
198
|
}
|
|
151
199
|
} catch (error) {
|