verybot 0.1.8
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/README.md +167 -0
- package/dist/aliases/store.d.ts +21 -0
- package/dist/aliases/store.js +148 -0
- package/dist/aliases/types.d.ts +6 -0
- package/dist/aliases/types.js +1 -0
- package/dist/brain/agent-registry.d.ts +96 -0
- package/dist/brain/agent-registry.js +141 -0
- package/dist/brain/agent.d.ts +167 -0
- package/dist/brain/agent.js +932 -0
- package/dist/brain/channel-store.d.ts +27 -0
- package/dist/brain/channel-store.js +78 -0
- package/dist/brain/compaction.d.ts +37 -0
- package/dist/brain/compaction.js +214 -0
- package/dist/brain/context.d.ts +43 -0
- package/dist/brain/context.js +139 -0
- package/dist/brain/delegation-store.d.ts +33 -0
- package/dist/brain/delegation-store.js +106 -0
- package/dist/brain/loop.d.ts +24 -0
- package/dist/brain/loop.js +318 -0
- package/dist/brain/mcp-adapter.d.ts +43 -0
- package/dist/brain/mcp-adapter.js +244 -0
- package/dist/brain/memory-extractor.d.ts +26 -0
- package/dist/brain/memory-extractor.js +82 -0
- package/dist/brain/providers.d.ts +14 -0
- package/dist/brain/providers.js +85 -0
- package/dist/brain/queue.d.ts +18 -0
- package/dist/brain/queue.js +111 -0
- package/dist/brain/run-tools.d.ts +50 -0
- package/dist/brain/run-tools.js +136 -0
- package/dist/brain/session-key.d.ts +23 -0
- package/dist/brain/session-key.js +41 -0
- package/dist/brain/session-state.d.ts +36 -0
- package/dist/brain/session-state.js +51 -0
- package/dist/brain/session-store.d.ts +50 -0
- package/dist/brain/session-store.js +207 -0
- package/dist/brain/session.d.ts +32 -0
- package/dist/brain/session.js +75 -0
- package/dist/brain/task-subscriber.d.ts +56 -0
- package/dist/brain/task-subscriber.js +317 -0
- package/dist/brain/user-content.d.ts +16 -0
- package/dist/brain/user-content.js +32 -0
- package/dist/brain/utils.d.ts +4 -0
- package/dist/brain/utils.js +26 -0
- package/dist/brain/worker-coordinator.d.ts +25 -0
- package/dist/brain/worker-coordinator.js +83 -0
- package/dist/channels/commands.d.ts +50 -0
- package/dist/channels/commands.js +132 -0
- package/dist/channels/discord/channel.d.ts +29 -0
- package/dist/channels/discord/channel.js +159 -0
- package/dist/channels/discord/markdown.d.ts +19 -0
- package/dist/channels/discord/markdown.js +62 -0
- package/dist/channels/manager.d.ts +29 -0
- package/dist/channels/manager.js +100 -0
- package/dist/channels/slack/channel.d.ts +37 -0
- package/dist/channels/slack/channel.js +227 -0
- package/dist/channels/slack/markdown.d.ts +19 -0
- package/dist/channels/slack/markdown.js +62 -0
- package/dist/channels/specs.d.ts +32 -0
- package/dist/channels/specs.js +99 -0
- package/dist/channels/telegram/channel.d.ts +29 -0
- package/dist/channels/telegram/channel.js +182 -0
- package/dist/channels/telegram/markdown.d.ts +17 -0
- package/dist/channels/telegram/markdown.js +66 -0
- package/dist/channels/types.d.ts +26 -0
- package/dist/channels/types.js +1 -0
- package/dist/channels/whatsapp/channel.d.ts +34 -0
- package/dist/channels/whatsapp/channel.js +276 -0
- package/dist/channels/whatsapp/markdown.d.ts +20 -0
- package/dist/channels/whatsapp/markdown.js +51 -0
- package/dist/cli/claude-login.d.ts +5 -0
- package/dist/cli/claude-login.js +47 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +78 -0
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.js +96 -0
- package/dist/computer/browser/actions.d.ts +31 -0
- package/dist/computer/browser/actions.js +148 -0
- package/dist/computer/browser/context-manager.d.ts +28 -0
- package/dist/computer/browser/context-manager.js +78 -0
- package/dist/computer/browser/manager.d.ts +91 -0
- package/dist/computer/browser/manager.js +344 -0
- package/dist/computer/browser/profile-badge.d.ts +13 -0
- package/dist/computer/browser/profile-badge.js +67 -0
- package/dist/computer/browser/screenshot.d.ts +5 -0
- package/dist/computer/browser/screenshot.js +21 -0
- package/dist/computer/browser/snapshot.d.ts +30 -0
- package/dist/computer/browser/snapshot.js +242 -0
- package/dist/computer/browser/tools.d.ts +5 -0
- package/dist/computer/browser/tools.js +167 -0
- package/dist/computer/browser/types.d.ts +26 -0
- package/dist/computer/browser/types.js +1 -0
- package/dist/computer/desktop/adapter.d.ts +25 -0
- package/dist/computer/desktop/adapter.js +11 -0
- package/dist/computer/desktop/macos.d.ts +24 -0
- package/dist/computer/desktop/macos.js +223 -0
- package/dist/computer/desktop/tools.d.ts +25 -0
- package/dist/computer/desktop/tools.js +114 -0
- package/dist/config/agent-config.d.ts +55 -0
- package/dist/config/agent-config.js +16 -0
- package/dist/config/model-catalog.d.ts +22 -0
- package/dist/config/model-catalog.js +112 -0
- package/dist/config/model-spec.d.ts +8 -0
- package/dist/config/model-spec.js +66 -0
- package/dist/config/store.d.ts +25 -0
- package/dist/config/store.js +143 -0
- package/dist/config.d.ts +110 -0
- package/dist/config.js +259 -0
- package/dist/control-ui/assets/index-Cbl7G5Sc.css +1 -0
- package/dist/control-ui/assets/index-Cu1P4C62.js +266 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-ext-wght-normal-DSNfmdVt.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-wght-normal-B2hlT84T.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-devanagari-wght-normal-Cv-Vwajv.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-ext-wght-normal-12T8GTDR.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-wght-normal-Ymb6dZNd.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-ext-wght-normal-W1qJv59z.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-wght-normal-BYSzYMf3.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-vietnamese-wght-normal-DLTJy58D.woff2 +0 -0
- package/dist/control-ui/index.html +14 -0
- package/dist/control-ui/vite.svg +1 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +11 -0
- package/dist/gateway/broadcast.d.ts +5 -0
- package/dist/gateway/broadcast.js +33 -0
- package/dist/gateway/methods/aliases.d.ts +17 -0
- package/dist/gateway/methods/aliases.js +22 -0
- package/dist/gateway/methods/chat.d.ts +33 -0
- package/dist/gateway/methods/chat.js +37 -0
- package/dist/gateway/methods/config.d.ts +14 -0
- package/dist/gateway/methods/config.js +24 -0
- package/dist/gateway/methods/models.d.ts +10 -0
- package/dist/gateway/methods/models.js +14 -0
- package/dist/gateway/methods/playbooks.d.ts +45 -0
- package/dist/gateway/methods/playbooks.js +488 -0
- package/dist/gateway/methods/prompt-templates.d.ts +27 -0
- package/dist/gateway/methods/prompt-templates.js +106 -0
- package/dist/gateway/methods/scheduler.d.ts +62 -0
- package/dist/gateway/methods/scheduler.js +129 -0
- package/dist/gateway/methods/sessions.d.ts +44 -0
- package/dist/gateway/methods/sessions.js +111 -0
- package/dist/gateway/methods/system.d.ts +12 -0
- package/dist/gateway/methods/system.js +39 -0
- package/dist/gateway/methods/tasks.d.ts +40 -0
- package/dist/gateway/methods/tasks.js +151 -0
- package/dist/gateway/methods/teams.d.ts +69 -0
- package/dist/gateway/methods/teams.js +376 -0
- package/dist/gateway/methods/tools.d.ts +6 -0
- package/dist/gateway/methods/tools.js +7 -0
- package/dist/gateway/methods/whatsapp.d.ts +19 -0
- package/dist/gateway/methods/whatsapp.js +35 -0
- package/dist/gateway/rpc.d.ts +38 -0
- package/dist/gateway/rpc.js +79 -0
- package/dist/gateway/server.d.ts +9 -0
- package/dist/gateway/server.js +137 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +254 -0
- package/dist/integrations/github.d.ts +7 -0
- package/dist/integrations/github.js +133 -0
- package/dist/integrations/mcp.d.ts +7 -0
- package/dist/integrations/mcp.js +106 -0
- package/dist/integrations/registry.d.ts +47 -0
- package/dist/integrations/registry.js +332 -0
- package/dist/integrations/scanner.d.ts +10 -0
- package/dist/integrations/scanner.js +122 -0
- package/dist/integrations/twitter.d.ts +10 -0
- package/dist/integrations/twitter.js +120 -0
- package/dist/integrations/types.d.ts +72 -0
- package/dist/integrations/types.js +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +104 -0
- package/dist/markdown/chunk.d.ts +9 -0
- package/dist/markdown/chunk.js +52 -0
- package/dist/markdown/ir.d.ts +37 -0
- package/dist/markdown/ir.js +529 -0
- package/dist/markdown/render.d.ts +22 -0
- package/dist/markdown/render.js +148 -0
- package/dist/markdown/table-render.d.ts +43 -0
- package/dist/markdown/table-render.js +219 -0
- package/dist/markdown/tables.d.ts +17 -0
- package/dist/markdown/tables.js +27 -0
- package/dist/memory/embedding.d.ts +16 -0
- package/dist/memory/embedding.js +66 -0
- package/dist/memory/explicit.d.ts +16 -0
- package/dist/memory/explicit.js +29 -0
- package/dist/memory/extractor.d.ts +13 -0
- package/dist/memory/extractor.js +82 -0
- package/dist/memory/search.d.ts +15 -0
- package/dist/memory/search.js +57 -0
- package/dist/memory/session-learning.d.ts +23 -0
- package/dist/memory/session-learning.js +55 -0
- package/dist/memory/store.d.ts +36 -0
- package/dist/memory/store.js +334 -0
- package/dist/memory/types.d.ts +9 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.d.ts +28 -0
- package/dist/paths.js +48 -0
- package/dist/prompt-templates/builtins/index.d.ts +4 -0
- package/dist/prompt-templates/builtins/index.js +5 -0
- package/dist/prompt-templates/builtins/planner.d.ts +4 -0
- package/dist/prompt-templates/builtins/planner.js +77 -0
- package/dist/prompt-templates/store.d.ts +45 -0
- package/dist/prompt-templates/store.js +224 -0
- package/dist/prompt-templates/types.d.ts +10 -0
- package/dist/prompt-templates/types.js +1 -0
- package/dist/scheduler/connected-channels.d.ts +24 -0
- package/dist/scheduler/connected-channels.js +57 -0
- package/dist/scheduler/scheduler.d.ts +22 -0
- package/dist/scheduler/scheduler.js +132 -0
- package/dist/scheduler/store.d.ts +27 -0
- package/dist/scheduler/store.js +205 -0
- package/dist/scheduler/types.d.ts +29 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/security/command-validator.d.ts +22 -0
- package/dist/security/command-validator.js +160 -0
- package/dist/security/docker-sandbox.d.ts +48 -0
- package/dist/security/docker-sandbox.js +218 -0
- package/dist/security/env-filter.d.ts +8 -0
- package/dist/security/env-filter.js +41 -0
- package/dist/skills/loader.d.ts +33 -0
- package/dist/skills/loader.js +132 -0
- package/dist/skills/prompt.d.ts +6 -0
- package/dist/skills/prompt.js +17 -0
- package/dist/skills/read-tool.d.ts +7 -0
- package/dist/skills/read-tool.js +24 -0
- package/dist/skills/scanner.d.ts +6 -0
- package/dist/skills/scanner.js +73 -0
- package/dist/skills/types.d.ts +15 -0
- package/dist/skills/types.js +1 -0
- package/dist/tasks/inline-attachment-content.d.ts +9 -0
- package/dist/tasks/inline-attachment-content.js +64 -0
- package/dist/tasks/store.d.ts +112 -0
- package/dist/tasks/store.js +519 -0
- package/dist/tasks/types.d.ts +129 -0
- package/dist/tasks/types.js +80 -0
- package/dist/teams/status-config.d.ts +8 -0
- package/dist/teams/status-config.js +40 -0
- package/dist/teams/store.d.ts +111 -0
- package/dist/teams/store.js +671 -0
- package/dist/teams/types.d.ts +30 -0
- package/dist/teams/types.js +1 -0
- package/dist/tools/bash.d.ts +18 -0
- package/dist/tools/bash.js +64 -0
- package/dist/tools/channel-history.d.ts +10 -0
- package/dist/tools/channel-history.js +43 -0
- package/dist/tools/delegate.d.ts +20 -0
- package/dist/tools/delegate.js +299 -0
- package/dist/tools/fs.d.ts +4 -0
- package/dist/tools/fs.js +335 -0
- package/dist/tools/integration-toggle.d.ts +14 -0
- package/dist/tools/integration-toggle.js +47 -0
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/memory.js +59 -0
- package/dist/tools/prompt-templates.d.ts +7 -0
- package/dist/tools/prompt-templates.js +133 -0
- package/dist/tools/registry.d.ts +6 -0
- package/dist/tools/registry.js +9 -0
- package/dist/tools/schedule.d.ts +8 -0
- package/dist/tools/schedule.js +219 -0
- package/dist/tools/speak.d.ts +10 -0
- package/dist/tools/speak.js +56 -0
- package/dist/tools/tasks.d.ts +67 -0
- package/dist/tools/tasks.js +288 -0
- package/dist/tools/teams.d.ts +22 -0
- package/dist/tools/teams.js +470 -0
- package/dist/tools/web-fetch.d.ts +3 -0
- package/dist/tools/web-fetch.js +22 -0
- package/dist/tts/edge.d.ts +10 -0
- package/dist/tts/edge.js +60 -0
- package/dist/tts/speak.d.ts +12 -0
- package/dist/tts/speak.js +81 -0
- package/dist/tts/transcribe.d.ts +5 -0
- package/dist/tts/transcribe.js +40 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +22 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +13 -0
- package/package.json +102 -0
- package/verybot.js +2 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Structured command result — channels format parts with their own escaping
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
const LEARN_COMMAND = "/learn";
|
|
5
|
+
const REMEMBER_COMMAND = "/remember";
|
|
6
|
+
const REMEMBER_USAGE = `${REMEMBER_COMMAND} <fact>`;
|
|
7
|
+
export class CommandRouter {
|
|
8
|
+
opts;
|
|
9
|
+
activeTeams = new Map();
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.opts = opts;
|
|
12
|
+
}
|
|
13
|
+
/** Resolve the active team for a channel, falling back to configured defaultTeamId. */
|
|
14
|
+
resolveTeamId(channelId) {
|
|
15
|
+
return this.activeTeams.get(channelId) ?? this.opts.defaultTeamId;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse raw text for slash-style commands (e.g. "/clear", "/team foo").
|
|
19
|
+
* Returns `null` if the text is not a recognised command.
|
|
20
|
+
*/
|
|
21
|
+
async handle(channelType, channelId, text) {
|
|
22
|
+
const trimmed = text.trim();
|
|
23
|
+
const lower = trimmed.toLowerCase();
|
|
24
|
+
if (lower === "/clear" || lower === "/reset") {
|
|
25
|
+
return this.handleClear(channelType, channelId);
|
|
26
|
+
}
|
|
27
|
+
if ((lower === "/team" || lower.startsWith("/team ")) && this.opts.listTeams) {
|
|
28
|
+
const arg = trimmed.slice("/team".length).trim();
|
|
29
|
+
return this.handleTeam(channelType, channelId, arg);
|
|
30
|
+
}
|
|
31
|
+
if (this.opts.onLearn) {
|
|
32
|
+
if (lower === LEARN_COMMAND || lower.startsWith(`${LEARN_COMMAND} `)) {
|
|
33
|
+
const topic = trimmed.slice(LEARN_COMMAND.length).trim();
|
|
34
|
+
return this.handleLearn(channelType, channelId, topic || undefined);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (this.opts.onRemember) {
|
|
38
|
+
if (lower === REMEMBER_COMMAND || lower.startsWith(`${REMEMBER_COMMAND} `)) {
|
|
39
|
+
const fact = trimmed.slice(REMEMBER_COMMAND.length).trim();
|
|
40
|
+
return this.handleRemember(channelType, channelId, fact);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/** Handle /clear (or /reset). */
|
|
46
|
+
async handleClear(channelType, channelId) {
|
|
47
|
+
if (this.opts.onClear) {
|
|
48
|
+
await this.opts.onClear(channelType, channelId, this.resolveTeamId(channelId));
|
|
49
|
+
}
|
|
50
|
+
return { parts: ["Session cleared."] };
|
|
51
|
+
}
|
|
52
|
+
/** Handle /team [name]. */
|
|
53
|
+
async handleTeam(channelType, channelId, arg) {
|
|
54
|
+
const teams = this.opts.listTeams?.() ?? [];
|
|
55
|
+
// No argument → list teams with the active one highlighted.
|
|
56
|
+
if (!arg) {
|
|
57
|
+
const currentTeamId = this.resolveTeamId(channelId);
|
|
58
|
+
const parts = ["Teams:\n"];
|
|
59
|
+
for (const t of teams) {
|
|
60
|
+
if (t.id === currentTeamId) {
|
|
61
|
+
parts.push("• ", { bold: t.name }, " (active)\n");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
parts.push("• ", t.name, "\n");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
parts.push("\nSwitch with ", { code: "/team <name>" });
|
|
68
|
+
return { parts };
|
|
69
|
+
}
|
|
70
|
+
// Find team by name (case-insensitive).
|
|
71
|
+
const target = teams.find((t) => t.name.toLowerCase() === arg.toLowerCase());
|
|
72
|
+
if (!target) {
|
|
73
|
+
return {
|
|
74
|
+
parts: ['Unknown team "', arg, '". Use ', { code: "/team" }, " to list available teams."],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
this.activeTeams.set(channelId, target.id);
|
|
78
|
+
return {
|
|
79
|
+
parts: ["Switched to team ", { bold: target.name }, "."],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Handle /learn [topic]. */
|
|
83
|
+
async handleLearn(channelType, channelId, topic) {
|
|
84
|
+
if (!this.opts.onLearn) {
|
|
85
|
+
return { parts: ["Memory is not enabled"] };
|
|
86
|
+
}
|
|
87
|
+
let result;
|
|
88
|
+
try {
|
|
89
|
+
result = await this.opts.onLearn(channelType, channelId, topic, this.resolveTeamId(channelId));
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
const msg = err instanceof Error ? err.message : "Failed to save memory";
|
|
93
|
+
return { parts: [msg] };
|
|
94
|
+
}
|
|
95
|
+
if (result.extracted === 0) {
|
|
96
|
+
return result.topic
|
|
97
|
+
? { parts: ['No learnable facts found about "', result.topic, '" in the current session.'] }
|
|
98
|
+
: { parts: ["No learnable facts found in the current session."] };
|
|
99
|
+
}
|
|
100
|
+
const topicSuffix = result.topic ? ` about "${result.topic}"` : "";
|
|
101
|
+
const summary = result.saved > 0
|
|
102
|
+
? `Learned ${result.saved} ${pluralize("fact", result.saved)}${topicSuffix}.`
|
|
103
|
+
: `Found ${result.extracted} ${pluralize("fact", result.extracted)}${topicSuffix}, but all were already known.`;
|
|
104
|
+
if (result.savedFacts.length === 0) {
|
|
105
|
+
return { parts: [summary] };
|
|
106
|
+
}
|
|
107
|
+
const factLines = result.savedFacts.map((fact) => `• ${fact}`).join("\n");
|
|
108
|
+
return { parts: [`${summary}\n${factLines}`] };
|
|
109
|
+
}
|
|
110
|
+
/** Handle /remember [fact]. */
|
|
111
|
+
async handleRemember(channelType, channelId, fact) {
|
|
112
|
+
if (!this.opts.onRemember) {
|
|
113
|
+
return { parts: ["Memory is not enabled"] };
|
|
114
|
+
}
|
|
115
|
+
if (!fact) {
|
|
116
|
+
return { parts: ["Usage: ", { code: REMEMBER_USAGE }] };
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const result = await this.opts.onRemember(channelType, channelId, fact, this.resolveTeamId(channelId));
|
|
120
|
+
return result.saved
|
|
121
|
+
? { parts: ['Learned: "', result.fact, '"'] }
|
|
122
|
+
: { parts: ['Already known: "', result.fact, '"'] };
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
const msg = err instanceof Error ? err.message : "Failed to save memory";
|
|
126
|
+
return { parts: [msg] };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function pluralize(word, count) {
|
|
131
|
+
return count === 1 ? word : `${word}s`;
|
|
132
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Channel, MessageHandler } from "../types.js";
|
|
2
|
+
import type { MarkdownTableMode } from "../../markdown/ir.js";
|
|
3
|
+
/** Callback that resolves the current table mode from config at send time. */
|
|
4
|
+
export type ResolveTableMode = () => MarkdownTableMode;
|
|
5
|
+
export interface CreateDiscordChannelOpts {
|
|
6
|
+
token: string;
|
|
7
|
+
onMessage: MessageHandler;
|
|
8
|
+
onClear?: (channelType: string, channelId: string, teamId?: string) => Promise<void>;
|
|
9
|
+
onLearn?: (channelType: string, channelId: string, topic?: string, teamId?: string) => Promise<{
|
|
10
|
+
topic?: string;
|
|
11
|
+
extracted: number;
|
|
12
|
+
saved: number;
|
|
13
|
+
skipped: number;
|
|
14
|
+
savedFacts: string[];
|
|
15
|
+
}>;
|
|
16
|
+
onRemember?: (channelType: string, channelId: string, fact: string, teamId?: string) => Promise<{
|
|
17
|
+
saved: boolean;
|
|
18
|
+
fact: string;
|
|
19
|
+
}>;
|
|
20
|
+
/** List available teams for the /team command. */
|
|
21
|
+
listTeams?: () => {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
}[];
|
|
25
|
+
/** Fallback team when no active team is set for a channel. */
|
|
26
|
+
defaultTeamId?: string;
|
|
27
|
+
resolveTableMode?: ResolveTableMode;
|
|
28
|
+
}
|
|
29
|
+
export declare function createDiscordChannel(opts: CreateDiscordChannelOpts): Channel;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Client, GatewayIntentBits, Partials, Events } from "discord.js";
|
|
2
|
+
import { logger } from "../../logger.js";
|
|
3
|
+
import { markdownToDiscordChunks, escapeDiscord, DISCORD_TEXT_LIMIT } from "./markdown.js";
|
|
4
|
+
import { CommandRouter } from "../commands.js";
|
|
5
|
+
/** Discord typing indicator expires after ~10s; refresh every 8s. */
|
|
6
|
+
const TYPING_INTERVAL_MS = 8000;
|
|
7
|
+
export function createDiscordChannel(opts) {
|
|
8
|
+
const { token, onMessage, onClear, onLearn, onRemember, listTeams, defaultTeamId, resolveTableMode = () => "code", } = opts;
|
|
9
|
+
const client = new Client({
|
|
10
|
+
intents: [
|
|
11
|
+
GatewayIntentBits.Guilds,
|
|
12
|
+
GatewayIntentBits.GuildMessages,
|
|
13
|
+
GatewayIntentBits.MessageContent,
|
|
14
|
+
GatewayIntentBits.DirectMessages,
|
|
15
|
+
],
|
|
16
|
+
partials: [Partials.Channel],
|
|
17
|
+
});
|
|
18
|
+
const typingIntervals = new Map();
|
|
19
|
+
const commands = new CommandRouter({ onClear, onLearn, onRemember, listTeams, defaultTeamId });
|
|
20
|
+
/** Format a CommandResult using Discord markdown syntax. */
|
|
21
|
+
function formatDiscordResult(result) {
|
|
22
|
+
return result.parts
|
|
23
|
+
.map((p) => {
|
|
24
|
+
if (typeof p === "string")
|
|
25
|
+
return escapeDiscord(p);
|
|
26
|
+
if ("bold" in p)
|
|
27
|
+
return `**${escapeDiscord(p.bold)}**`;
|
|
28
|
+
return `\`${p.code}\``;
|
|
29
|
+
})
|
|
30
|
+
.join("");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Handle text-based commands (e.g. "/clear", "/team foo").
|
|
34
|
+
* Returns the formatted response if the message was consumed, or null.
|
|
35
|
+
*/
|
|
36
|
+
async function handleTextCommand(text, channelId) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await commands.handle("discord", channelId, text);
|
|
39
|
+
if (!result)
|
|
40
|
+
return null;
|
|
41
|
+
return formatDiscordResult(result);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
45
|
+
logger.error(`[discord] command error: ${msg}`);
|
|
46
|
+
return "Failed to process command. Please try again.";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function startTyping(channelId) {
|
|
50
|
+
stopTyping(channelId);
|
|
51
|
+
const discordChannel = client.channels.cache.get(channelId);
|
|
52
|
+
if (!discordChannel?.isTextBased() || !("sendTyping" in discordChannel))
|
|
53
|
+
return;
|
|
54
|
+
// Send immediately, then refresh on interval
|
|
55
|
+
discordChannel.sendTyping().catch((err) => logger.error(`[discord] typing error: ${err}`));
|
|
56
|
+
const interval = setInterval(() => {
|
|
57
|
+
discordChannel.sendTyping().catch((err) => logger.error(`[discord] typing interval error: ${err}`));
|
|
58
|
+
}, TYPING_INTERVAL_MS);
|
|
59
|
+
typingIntervals.set(channelId, interval);
|
|
60
|
+
}
|
|
61
|
+
function stopTyping(channelId) {
|
|
62
|
+
const interval = typingIntervals.get(channelId);
|
|
63
|
+
if (interval) {
|
|
64
|
+
clearInterval(interval);
|
|
65
|
+
typingIntervals.delete(channelId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Send chunked Discord markdown to a channel. */
|
|
69
|
+
async function sendChunks(channelId, text) {
|
|
70
|
+
const discordChannel = client.channels.cache.get(channelId);
|
|
71
|
+
if (!discordChannel?.isTextBased() || !("send" in discordChannel)) {
|
|
72
|
+
logger.error(`[discord] cannot send to channel ${channelId}: not a text channel`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const tableMode = resolveTableMode();
|
|
76
|
+
const chunks = markdownToDiscordChunks(text, DISCORD_TEXT_LIMIT, { tableMode });
|
|
77
|
+
const noMentions = { allowedMentions: { parse: [] } };
|
|
78
|
+
if (chunks.length === 0 && text) {
|
|
79
|
+
await discordChannel.send({ content: text, ...noMentions });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const chunk of chunks) {
|
|
83
|
+
await discordChannel.send({ content: chunk, ...noMentions });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Strip the bot mention prefix (e.g. `<@123456>`) from message text. */
|
|
87
|
+
function stripBotMention(text) {
|
|
88
|
+
const botId = client.user?.id;
|
|
89
|
+
if (!botId)
|
|
90
|
+
return text.trim();
|
|
91
|
+
const mentionRegex = new RegExp(`^\\s*<@!?${botId}>\\s*`);
|
|
92
|
+
return text.replace(mentionRegex, "").trim();
|
|
93
|
+
}
|
|
94
|
+
const channel = {
|
|
95
|
+
name: "discord",
|
|
96
|
+
start: async () => {
|
|
97
|
+
client.on(Events.MessageCreate, async (message) => {
|
|
98
|
+
try {
|
|
99
|
+
const botUser = client.user;
|
|
100
|
+
if (!botUser)
|
|
101
|
+
return;
|
|
102
|
+
// Ignore bot's own messages and other bots
|
|
103
|
+
if (message.author.id === botUser.id)
|
|
104
|
+
return;
|
|
105
|
+
if (message.author.bot)
|
|
106
|
+
return;
|
|
107
|
+
const isDM = !message.guild;
|
|
108
|
+
const isMentioned = message.mentions.has(botUser);
|
|
109
|
+
// In guilds, only respond to mentions; in DMs, always respond
|
|
110
|
+
if (!isDM && !isMentioned)
|
|
111
|
+
return;
|
|
112
|
+
const channelId = message.channelId;
|
|
113
|
+
const userId = message.author.id;
|
|
114
|
+
const rawText = message.content ?? "";
|
|
115
|
+
const text = isDM ? rawText.trim() : stripBotMention(rawText);
|
|
116
|
+
if (!text)
|
|
117
|
+
return;
|
|
118
|
+
// Try command handling first
|
|
119
|
+
const cmdResponse = await handleTextCommand(text, channelId);
|
|
120
|
+
if (cmdResponse) {
|
|
121
|
+
await message.reply({ content: cmdResponse, allowedMentions: { parse: [] } });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
logger.info(`[discord] message from ${userId} in ${channelId} (${isDM ? "DM" : "guild"})`);
|
|
125
|
+
startTyping(channelId);
|
|
126
|
+
try {
|
|
127
|
+
await onMessage({
|
|
128
|
+
channelType: "discord",
|
|
129
|
+
channelId,
|
|
130
|
+
userId,
|
|
131
|
+
text,
|
|
132
|
+
teamId: commands.resolveTeamId(channelId),
|
|
133
|
+
}, channel);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
stopTyping(channelId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
141
|
+
logger.error(`[discord] error in MessageCreate handler: ${msg}`);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
await client.login(token);
|
|
145
|
+
logger.info(`[discord] connected as ${client.user?.tag}`);
|
|
146
|
+
},
|
|
147
|
+
stop: async () => {
|
|
148
|
+
for (const chatId of typingIntervals.keys())
|
|
149
|
+
stopTyping(chatId);
|
|
150
|
+
await client.destroy();
|
|
151
|
+
logger.info("[discord] disconnected");
|
|
152
|
+
},
|
|
153
|
+
send: async (channelId, text) => {
|
|
154
|
+
stopTyping(channelId);
|
|
155
|
+
await sendChunks(channelId, text);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
return channel;
|
|
159
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → Discord markdown conversion.
|
|
3
|
+
* Discord supports standard markdown natively, with minor escaping needs.
|
|
4
|
+
*/
|
|
5
|
+
import { type MarkdownTableMode } from "../../markdown/ir.js";
|
|
6
|
+
/** Discord message character limit. */
|
|
7
|
+
export declare const DISCORD_TEXT_LIMIT = 2000;
|
|
8
|
+
/** Escape Discord markdown special characters in plain text. */
|
|
9
|
+
export declare function escapeDiscord(text: string): string;
|
|
10
|
+
export declare function markdownToDiscord(markdown: string, options?: {
|
|
11
|
+
tableMode?: MarkdownTableMode;
|
|
12
|
+
}): string;
|
|
13
|
+
/**
|
|
14
|
+
* Convert markdown to chunked Discord markdown strings.
|
|
15
|
+
* Each chunk is ≤ `limit` characters of plain-text IR.
|
|
16
|
+
*/
|
|
17
|
+
export declare function markdownToDiscordChunks(markdown: string, limit?: number, options?: {
|
|
18
|
+
tableMode?: MarkdownTableMode;
|
|
19
|
+
}): string[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → Discord markdown conversion.
|
|
3
|
+
* Discord supports standard markdown natively, with minor escaping needs.
|
|
4
|
+
*/
|
|
5
|
+
import { chunkMarkdownIR, markdownToIR, } from "../../markdown/ir.js";
|
|
6
|
+
import { renderMarkdownWithMarkers } from "../../markdown/render.js";
|
|
7
|
+
const SAFE_LINK_PROTOCOL = /^https?:\/\/|^mailto:|^tel:/i;
|
|
8
|
+
/** Discord message character limit. */
|
|
9
|
+
export const DISCORD_TEXT_LIMIT = 2000;
|
|
10
|
+
/** Escape Discord markdown special characters in plain text. */
|
|
11
|
+
export function escapeDiscord(text) {
|
|
12
|
+
return text.replace(/([*_~`|>\\])/g, "\\$1");
|
|
13
|
+
}
|
|
14
|
+
function buildDiscordLink(link, _text) {
|
|
15
|
+
const href = link.href.trim();
|
|
16
|
+
if (!href || link.start === link.end)
|
|
17
|
+
return null;
|
|
18
|
+
if (!SAFE_LINK_PROTOCOL.test(href))
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
start: link.start,
|
|
22
|
+
end: link.end,
|
|
23
|
+
open: "[",
|
|
24
|
+
close: `](${href})`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function renderDiscordMarkdown(ir) {
|
|
28
|
+
return renderMarkdownWithMarkers(ir, {
|
|
29
|
+
styleMarkers: {
|
|
30
|
+
bold: { open: "**", close: "**" },
|
|
31
|
+
italic: { open: "*", close: "*" },
|
|
32
|
+
strikethrough: { open: "~~", close: "~~" },
|
|
33
|
+
code: { open: "`", close: "`" },
|
|
34
|
+
code_block: { open: "```\n", close: "\n```" },
|
|
35
|
+
},
|
|
36
|
+
escapeText: escapeDiscord,
|
|
37
|
+
buildLink: buildDiscordLink,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function markdownToDiscord(markdown, options = {}) {
|
|
41
|
+
const ir = markdownToIR(markdown ?? "", {
|
|
42
|
+
linkify: true,
|
|
43
|
+
headingStyle: "none",
|
|
44
|
+
blockquotePrefix: "> ",
|
|
45
|
+
tableMode: options.tableMode,
|
|
46
|
+
});
|
|
47
|
+
return renderDiscordMarkdown(ir);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Convert markdown to chunked Discord markdown strings.
|
|
51
|
+
* Each chunk is ≤ `limit` characters of plain-text IR.
|
|
52
|
+
*/
|
|
53
|
+
export function markdownToDiscordChunks(markdown, limit = DISCORD_TEXT_LIMIT, options = {}) {
|
|
54
|
+
const ir = markdownToIR(markdown ?? "", {
|
|
55
|
+
linkify: true,
|
|
56
|
+
headingStyle: "none",
|
|
57
|
+
blockquotePrefix: "> ",
|
|
58
|
+
tableMode: options.tableMode,
|
|
59
|
+
});
|
|
60
|
+
const chunks = chunkMarkdownIR(ir, limit);
|
|
61
|
+
return chunks.map((chunk) => renderDiscordMarkdown(chunk));
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Channel } from "./types.js";
|
|
2
|
+
export interface ChannelSpec {
|
|
3
|
+
name: string;
|
|
4
|
+
/** Opaque fingerprint (e.g. hash of token). Change triggers stop→start cycle. */
|
|
5
|
+
fingerprint: string;
|
|
6
|
+
/** Factory that creates a fresh Channel instance. */
|
|
7
|
+
create: () => Channel;
|
|
8
|
+
}
|
|
9
|
+
/** Hash a token to produce a safe fingerprint (never store/log raw secrets). */
|
|
10
|
+
export declare function channelFingerprint(token: string): string;
|
|
11
|
+
export declare class ChannelManager {
|
|
12
|
+
private channels;
|
|
13
|
+
private byName;
|
|
14
|
+
private fingerprints;
|
|
15
|
+
private reconciling;
|
|
16
|
+
register(channel: Channel, fingerprint?: string): void;
|
|
17
|
+
/** Look up a channel by name (e.g. "telegram"). */
|
|
18
|
+
get(name: string): Channel | undefined;
|
|
19
|
+
startAll(): Promise<void>;
|
|
20
|
+
stopAll(): Promise<void>;
|
|
21
|
+
/** Stop and unregister a single channel by name. */
|
|
22
|
+
private stopOne;
|
|
23
|
+
/**
|
|
24
|
+
* Diff current channels against desired specs. Stop removed/changed channels,
|
|
25
|
+
* start new/changed ones. Self-guarding: concurrent calls are skipped
|
|
26
|
+
* (the next reloadConfig cycle will catch the mismatch).
|
|
27
|
+
*/
|
|
28
|
+
reconcile(specs: ChannelSpec[]): Promise<void>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
/** Hash a token to produce a safe fingerprint (never store/log raw secrets). */
|
|
4
|
+
export function channelFingerprint(token) {
|
|
5
|
+
return createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
6
|
+
}
|
|
7
|
+
/** Settle delay between stopping an old channel and starting the replacement.
|
|
8
|
+
* Telegram's getUpdates long-poll needs ~2s to release server-side. */
|
|
9
|
+
const SETTLE_MS = 2_000;
|
|
10
|
+
export class ChannelManager {
|
|
11
|
+
channels = [];
|
|
12
|
+
byName = new Map();
|
|
13
|
+
fingerprints = new Map();
|
|
14
|
+
reconciling = false;
|
|
15
|
+
register(channel, fingerprint) {
|
|
16
|
+
this.channels.push(channel);
|
|
17
|
+
this.byName.set(channel.name, channel);
|
|
18
|
+
if (fingerprint)
|
|
19
|
+
this.fingerprints.set(channel.name, fingerprint);
|
|
20
|
+
}
|
|
21
|
+
/** Look up a channel by name (e.g. "telegram"). */
|
|
22
|
+
get(name) {
|
|
23
|
+
return this.byName.get(name);
|
|
24
|
+
}
|
|
25
|
+
async startAll() {
|
|
26
|
+
for (const ch of this.channels) {
|
|
27
|
+
try {
|
|
28
|
+
await ch.start();
|
|
29
|
+
logger.info(`Channel started: ${ch.name}`);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
logger.error(`Failed to start channel ${ch.name}: ${err}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async stopAll() {
|
|
37
|
+
await Promise.all(this.channels.map((c) => c.stop()));
|
|
38
|
+
}
|
|
39
|
+
/** Stop and unregister a single channel by name. */
|
|
40
|
+
async stopOne(name) {
|
|
41
|
+
const ch = this.byName.get(name);
|
|
42
|
+
if (!ch)
|
|
43
|
+
return false;
|
|
44
|
+
try {
|
|
45
|
+
await ch.stop();
|
|
46
|
+
logger.info(`Channel stopped: ${name}`);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
logger.error(`Failed to stop channel ${name}: ${err}`);
|
|
50
|
+
}
|
|
51
|
+
this.byName.delete(name);
|
|
52
|
+
this.fingerprints.delete(name);
|
|
53
|
+
this.channels = this.channels.filter((c) => c.name !== name);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Diff current channels against desired specs. Stop removed/changed channels,
|
|
58
|
+
* start new/changed ones. Self-guarding: concurrent calls are skipped
|
|
59
|
+
* (the next reloadConfig cycle will catch the mismatch).
|
|
60
|
+
*/
|
|
61
|
+
async reconcile(specs) {
|
|
62
|
+
if (this.reconciling) {
|
|
63
|
+
logger.info("Channel reconcile already in progress, skipping");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.reconciling = true;
|
|
67
|
+
try {
|
|
68
|
+
const desired = new Map(specs.map((s) => [s.name, s]));
|
|
69
|
+
// Stop channels that are no longer in config
|
|
70
|
+
for (const name of [...this.byName.keys()]) {
|
|
71
|
+
if (!desired.has(name)) {
|
|
72
|
+
await this.stopOne(name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Add or replace channels whose fingerprint changed
|
|
76
|
+
for (const [name, spec] of desired) {
|
|
77
|
+
if (this.fingerprints.get(name) === spec.fingerprint)
|
|
78
|
+
continue;
|
|
79
|
+
const hadExisting = await this.stopOne(name);
|
|
80
|
+
// Settle delay so server-side connections release (e.g. Telegram long-poll)
|
|
81
|
+
if (hadExisting) {
|
|
82
|
+
await new Promise((r) => setTimeout(r, SETTLE_MS));
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const channel = spec.create();
|
|
86
|
+
await channel.start();
|
|
87
|
+
this.register(channel, spec.fingerprint);
|
|
88
|
+
logger.info(`Channel hot-reloaded: ${name}`);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// Don't register — next reconcile will retry since no fingerprint is stored
|
|
92
|
+
logger.error(`Failed to start channel ${name} during reconcile: ${err}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
this.reconciling = false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Channel, MessageHandler } from "../types.js";
|
|
2
|
+
import type { MarkdownTableMode } from "../../markdown/ir.js";
|
|
3
|
+
export interface SlackSchedulerOpts {
|
|
4
|
+
/** Register a Slack channel as a connected scheduler channel. */
|
|
5
|
+
connectScheduler: (teamId: string, channelId: string, sendFn: (text: string) => Promise<void>) => string;
|
|
6
|
+
/** Unregister a Slack channel from the scheduler. */
|
|
7
|
+
disconnectScheduler: (teamId: string, key: string) => void;
|
|
8
|
+
}
|
|
9
|
+
/** Callback that resolves the current table mode from config at send time. */
|
|
10
|
+
export type ResolveTableMode = () => MarkdownTableMode;
|
|
11
|
+
export interface CreateSlackChannelOpts {
|
|
12
|
+
botToken: string;
|
|
13
|
+
appToken: string;
|
|
14
|
+
onMessage: MessageHandler;
|
|
15
|
+
onClear?: (channelType: string, channelId: string, teamId?: string) => Promise<void>;
|
|
16
|
+
onLearn?: (channelType: string, channelId: string, topic?: string, teamId?: string) => Promise<{
|
|
17
|
+
topic?: string;
|
|
18
|
+
extracted: number;
|
|
19
|
+
saved: number;
|
|
20
|
+
skipped: number;
|
|
21
|
+
savedFacts: string[];
|
|
22
|
+
}>;
|
|
23
|
+
onRemember?: (channelType: string, channelId: string, fact: string, teamId?: string) => Promise<{
|
|
24
|
+
saved: boolean;
|
|
25
|
+
fact: string;
|
|
26
|
+
}>;
|
|
27
|
+
/** List available teams for the /team command. */
|
|
28
|
+
listTeams?: () => {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
}[];
|
|
32
|
+
/** Fallback team when no active team is set for a channel. */
|
|
33
|
+
defaultTeamId?: string;
|
|
34
|
+
schedulerOpts?: SlackSchedulerOpts;
|
|
35
|
+
resolveTableMode?: ResolveTableMode;
|
|
36
|
+
}
|
|
37
|
+
export declare function createSlackChannel(opts: CreateSlackChannelOpts): Channel;
|