verybot 0.1.3
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.
Potentially problematic release.
This version of verybot might be problematic. Click here for more details.
- package/dist/brain/agent-registry.d.ts +75 -0
- package/dist/brain/agent-registry.js +124 -0
- package/dist/brain/agent.d.ts +146 -0
- package/dist/brain/agent.js +680 -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 +33 -0
- package/dist/brain/context.js +77 -0
- package/dist/brain/delegation-store.d.ts +33 -0
- package/dist/brain/delegation-store.js +106 -0
- package/dist/brain/loop.d.ts +21 -0
- package/dist/brain/loop.js +161 -0
- package/dist/brain/mcp-adapter.d.ts +39 -0
- package/dist/brain/mcp-adapter.js +227 -0
- package/dist/brain/memory-extractor.d.ts +26 -0
- package/dist/brain/memory-extractor.js +82 -0
- package/dist/brain/providers.d.ts +10 -0
- package/dist/brain/providers.js +69 -0
- package/dist/brain/queue.d.ts +18 -0
- package/dist/brain/queue.js +84 -0
- package/dist/brain/run-tools.d.ts +47 -0
- package/dist/brain/run-tools.js +84 -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/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 +35 -0
- package/dist/channels/commands.js +65 -0
- package/dist/channels/discord/channel.d.ts +18 -0
- package/dist/channels/discord/channel.js +154 -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 +26 -0
- package/dist/channels/slack/channel.js +207 -0
- package/dist/channels/slack/markdown.d.ts +19 -0
- package/dist/channels/slack/markdown.js +62 -0
- package/dist/channels/specs.d.ts +21 -0
- package/dist/channels/specs.js +96 -0
- package/dist/channels/telegram/channel.d.ts +18 -0
- package/dist/channels/telegram/channel.js +156 -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 +23 -0
- package/dist/channels/whatsapp/channel.js +242 -0
- package/dist/channels/whatsapp/markdown.d.ts +20 -0
- package/dist/channels/whatsapp/markdown.js +51 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +78 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +13 -0
- package/dist/computer/browser/actions.d.ts +31 -0
- package/dist/computer/browser/actions.js +148 -0
- package/dist/computer/browser/manager.d.ts +55 -0
- package/dist/computer/browser/manager.js +496 -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/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 +41 -0
- package/dist/config/agent-config.js +14 -0
- package/dist/config/model-catalog.d.ts +22 -0
- package/dist/config/model-catalog.js +99 -0
- package/dist/config/store.d.ts +25 -0
- package/dist/config/store.js +143 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.js +224 -0
- package/dist/control-ui/assets/index-BANXNUyt.js +143 -0
- package/dist/control-ui/assets/index-BSUFrP9R.css +1 -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/chat.d.ts +24 -0
- package/dist/gateway/methods/chat.js +19 -0
- package/dist/gateway/methods/config.d.ts +13 -0
- package/dist/gateway/methods/config.js +14 -0
- package/dist/gateway/methods/models.d.ts +10 -0
- package/dist/gateway/methods/models.js +14 -0
- package/dist/gateway/methods/prompt-templates.d.ts +23 -0
- package/dist/gateway/methods/prompt-templates.js +82 -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 +26 -0
- package/dist/gateway/methods/sessions.js +54 -0
- package/dist/gateway/methods/skills.d.ts +35 -0
- package/dist/gateway/methods/skills.js +202 -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 +21 -0
- package/dist/gateway/methods/tasks.js +46 -0
- package/dist/gateway/methods/teams.d.ts +70 -0
- package/dist/gateway/methods/teams.js +374 -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 +75 -0
- package/dist/gateway/server.d.ts +4 -0
- package/dist/gateway/server.js +133 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +212 -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 +43 -0
- package/dist/integrations/registry.js +258 -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/extractor.d.ts +6 -0
- package/dist/memory/extractor.js +72 -0
- package/dist/memory/search.d.ts +15 -0
- package/dist/memory/search.js +57 -0
- package/dist/memory/store.d.ts +34 -0
- package/dist/memory/store.js +328 -0
- package/dist/memory/types.d.ts +9 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +29 -0
- package/dist/prompt-templates/builtins.d.ts +2 -0
- package/dist/prompt-templates/builtins.js +72 -0
- package/dist/prompt-templates/store.d.ts +39 -0
- package/dist/prompt-templates/store.js +174 -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/store.d.ts +47 -0
- package/dist/tasks/store.js +193 -0
- package/dist/tasks/types.d.ts +75 -0
- package/dist/tasks/types.js +32 -0
- package/dist/teams/store.d.ts +78 -0
- package/dist/teams/store.js +420 -0
- package/dist/teams/types.d.ts +23 -0
- package/dist/teams/types.js +1 -0
- package/dist/tools/bash.d.ts +16 -0
- package/dist/tools/bash.js +62 -0
- package/dist/tools/channel-history.d.ts +10 -0
- package/dist/tools/channel-history.js +43 -0
- package/dist/tools/delegate.d.ts +16 -0
- package/dist/tools/delegate.js +216 -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 +65 -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 +29 -0
- package/dist/tools/tasks.js +92 -0
- package/dist/tools/teams.d.ts +7 -0
- package/dist/tools/teams.js +180 -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/package.json +90 -0
- package/verybot.js +2 -0
|
@@ -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,26 @@
|
|
|
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
|
+
/** List available teams for the /team command. */
|
|
17
|
+
listTeams?: () => {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
}[];
|
|
21
|
+
/** Fallback team when no active team is set for a channel. */
|
|
22
|
+
defaultTeamId?: string;
|
|
23
|
+
schedulerOpts?: SlackSchedulerOpts;
|
|
24
|
+
resolveTableMode?: ResolveTableMode;
|
|
25
|
+
}
|
|
26
|
+
export declare function createSlackChannel(opts: CreateSlackChannelOpts): Channel;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { App } from "@slack/bolt";
|
|
2
|
+
import { logger } from "../../logger.js";
|
|
3
|
+
import { markdownToSlackMrkdwnChunks, escapeSlackMrkdwn, SLACK_TEXT_LIMIT } from "./markdown.js";
|
|
4
|
+
import { CommandRouter } from "../commands.js";
|
|
5
|
+
/** Emoji added while the bot is processing a message. */
|
|
6
|
+
const TYPING_EMOJI = "eyes";
|
|
7
|
+
/** Send chunked Slack mrkdwn to a channel. Optionally in-thread via threadTs. */
|
|
8
|
+
async function sendMrkdwnChunks(app, channelId, text, resolveTableMode, threadTs) {
|
|
9
|
+
const tableMode = resolveTableMode();
|
|
10
|
+
const chunks = markdownToSlackMrkdwnChunks(text, SLACK_TEXT_LIMIT, { tableMode });
|
|
11
|
+
if (chunks.length === 0 && text) {
|
|
12
|
+
await app.client.chat.postMessage({ channel: channelId, text, thread_ts: threadTs });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
for (const chunk of chunks) {
|
|
16
|
+
await app.client.chat.postMessage({ channel: channelId, text: chunk, thread_ts: threadTs });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Strip the bot mention prefix (e.g. `<@U12345>`) from message text. */
|
|
20
|
+
function stripBotMention(text, botUserId) {
|
|
21
|
+
const prefix = `<@${botUserId}>`;
|
|
22
|
+
const trimmed = text.trimStart();
|
|
23
|
+
if (trimmed.startsWith(prefix)) {
|
|
24
|
+
return trimmed.slice(prefix.length).trim();
|
|
25
|
+
}
|
|
26
|
+
return text.trim();
|
|
27
|
+
}
|
|
28
|
+
export function createSlackChannel(opts) {
|
|
29
|
+
const { botToken, appToken, onMessage, onClear, listTeams, defaultTeamId, schedulerOpts, resolveTableMode = () => "code", } = opts;
|
|
30
|
+
const app = new App({
|
|
31
|
+
token: botToken,
|
|
32
|
+
appToken,
|
|
33
|
+
socketMode: true,
|
|
34
|
+
});
|
|
35
|
+
const commands = new CommandRouter({ onClear, listTeams, defaultTeamId });
|
|
36
|
+
/** Map<channelId, { teamId, connectionKey }> for active scheduler connections. */
|
|
37
|
+
const schedulerConnections = new Map();
|
|
38
|
+
/**
|
|
39
|
+
* Track thread context per channelId so replies stay in-thread.
|
|
40
|
+
* Note: in channels with concurrent @mentions, the latest message wins.
|
|
41
|
+
* DMs are safe since each DM conversation has a unique channelId.
|
|
42
|
+
*/
|
|
43
|
+
const threadMap = new Map();
|
|
44
|
+
/** Format a CommandResult using Slack mrkdwn syntax. */
|
|
45
|
+
function formatSlackResult(result) {
|
|
46
|
+
return result.parts
|
|
47
|
+
.map((p) => {
|
|
48
|
+
if (typeof p === "string")
|
|
49
|
+
return escapeSlackMrkdwn(p);
|
|
50
|
+
if ("bold" in p)
|
|
51
|
+
return `*${escapeSlackMrkdwn(p.bold)}*`;
|
|
52
|
+
return `\`${p.code}\``;
|
|
53
|
+
})
|
|
54
|
+
.join("");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Handle text-based commands (e.g. "/clear", "/team foo").
|
|
58
|
+
* Returns true if the message was consumed as a command.
|
|
59
|
+
*/
|
|
60
|
+
async function handleTextCommand(text, channelId) {
|
|
61
|
+
const result = await commands.handle("slack", channelId, text);
|
|
62
|
+
if (!result)
|
|
63
|
+
return false;
|
|
64
|
+
await app.client.chat.postMessage({ channel: channelId, text: formatSlackResult(result) });
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
/** Add a reaction as typing proxy; silently ignore failures. */
|
|
68
|
+
async function addTypingReaction(channelId, ts) {
|
|
69
|
+
try {
|
|
70
|
+
await app.client.reactions.add({ channel: channelId, timestamp: ts, name: TYPING_EMOJI });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Reaction may already exist or bot lacks permission — safe to ignore
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Remove typing reaction; silently ignore failures. */
|
|
77
|
+
async function removeTypingReaction(channelId, ts) {
|
|
78
|
+
try {
|
|
79
|
+
await app.client.reactions.remove({ channel: channelId, timestamp: ts, name: TYPING_EMOJI });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Safe to ignore
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const channel = {
|
|
86
|
+
name: "slack",
|
|
87
|
+
start: async () => {
|
|
88
|
+
// --- @mention events (channels/groups) ---
|
|
89
|
+
app.event("app_mention", async ({ event, context }) => {
|
|
90
|
+
const channelId = event.channel;
|
|
91
|
+
const userId = event.user ?? "unknown";
|
|
92
|
+
const botUserId = context.botUserId ?? "";
|
|
93
|
+
const rawText = event.text ?? "";
|
|
94
|
+
const text = stripBotMention(rawText, botUserId);
|
|
95
|
+
const ts = event.ts;
|
|
96
|
+
const threadTs = event.thread_ts ?? ts;
|
|
97
|
+
if (!text)
|
|
98
|
+
return;
|
|
99
|
+
if (await handleTextCommand(text, channelId))
|
|
100
|
+
return;
|
|
101
|
+
logger.info(`[slack] mention from ${userId} in ${channelId}`);
|
|
102
|
+
threadMap.set(channelId, { threadTs, messageTs: ts });
|
|
103
|
+
await addTypingReaction(channelId, ts);
|
|
104
|
+
try {
|
|
105
|
+
await onMessage({
|
|
106
|
+
channelType: "slack",
|
|
107
|
+
channelId,
|
|
108
|
+
userId,
|
|
109
|
+
text,
|
|
110
|
+
teamId: commands.resolveTeamId(channelId),
|
|
111
|
+
}, channel);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
115
|
+
logger.error(`[slack] error handling mention in ${channelId}: ${msg}`);
|
|
116
|
+
await removeTypingReaction(channelId, ts);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// --- DM events ---
|
|
120
|
+
app.event("message", async ({ event, context }) => {
|
|
121
|
+
// Only handle direct messages (im channel type)
|
|
122
|
+
// Skip bot messages, changed messages, etc.
|
|
123
|
+
if (!("channel_type" in event) || event.channel_type !== "im")
|
|
124
|
+
return;
|
|
125
|
+
if ("subtype" in event && event.subtype)
|
|
126
|
+
return;
|
|
127
|
+
if (!("user" in event) || !event.user)
|
|
128
|
+
return;
|
|
129
|
+
if (!("text" in event) || !event.text)
|
|
130
|
+
return;
|
|
131
|
+
// Ignore messages from the bot itself
|
|
132
|
+
if (event.user === context.botUserId)
|
|
133
|
+
return;
|
|
134
|
+
const channelId = event.channel;
|
|
135
|
+
const userId = event.user;
|
|
136
|
+
const botUserId = context.botUserId ?? "";
|
|
137
|
+
// Strip bot mention so "@bot /clear" is matched as "/clear"
|
|
138
|
+
const text = stripBotMention(event.text, botUserId);
|
|
139
|
+
const ts = event.ts;
|
|
140
|
+
const threadTs = ("thread_ts" in event ? event.thread_ts : undefined) ?? ts;
|
|
141
|
+
if (await handleTextCommand(text, channelId))
|
|
142
|
+
return;
|
|
143
|
+
logger.info(`[slack] DM from ${userId}`);
|
|
144
|
+
threadMap.set(channelId, { threadTs, messageTs: ts });
|
|
145
|
+
await addTypingReaction(channelId, ts);
|
|
146
|
+
try {
|
|
147
|
+
await onMessage({
|
|
148
|
+
channelType: "slack",
|
|
149
|
+
channelId,
|
|
150
|
+
userId,
|
|
151
|
+
text,
|
|
152
|
+
teamId: commands.resolveTeamId(channelId),
|
|
153
|
+
}, channel);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
157
|
+
logger.error(`[slack] error handling DM from ${userId}: ${msg}`);
|
|
158
|
+
await removeTypingReaction(channelId, ts);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
await app.start();
|
|
162
|
+
logger.info("[slack] connected via Socket Mode");
|
|
163
|
+
},
|
|
164
|
+
stop: async () => {
|
|
165
|
+
for (const [, conn] of schedulerConnections) {
|
|
166
|
+
schedulerOpts?.disconnectScheduler(conn.teamId, conn.connectionKey);
|
|
167
|
+
}
|
|
168
|
+
schedulerConnections.clear();
|
|
169
|
+
await app.stop();
|
|
170
|
+
logger.info("[slack] disconnected");
|
|
171
|
+
},
|
|
172
|
+
send: async (channelId, text) => {
|
|
173
|
+
const entry = threadMap.get(channelId);
|
|
174
|
+
await sendMrkdwnChunks(app, channelId, text, resolveTableMode, entry?.threadTs);
|
|
175
|
+
if (entry) {
|
|
176
|
+
await removeTypingReaction(channelId, entry.messageTs);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
readHistory: async (channelId, limit = 20, threadTs) => {
|
|
180
|
+
logger.info(`[slack] readHistory channel=${channelId} limit=${limit} threadTs=${threadTs ?? "none"}`);
|
|
181
|
+
let raw;
|
|
182
|
+
if (threadTs) {
|
|
183
|
+
const result = await app.client.conversations.replies({
|
|
184
|
+
channel: channelId,
|
|
185
|
+
ts: threadTs,
|
|
186
|
+
limit,
|
|
187
|
+
});
|
|
188
|
+
raw = result.messages ?? [];
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const result = await app.client.conversations.history({
|
|
192
|
+
channel: channelId,
|
|
193
|
+
limit,
|
|
194
|
+
});
|
|
195
|
+
// conversations.history returns newest-first, reverse for chronological
|
|
196
|
+
raw = (result.messages ?? []).reverse();
|
|
197
|
+
}
|
|
198
|
+
logger.info(`[slack] readHistory returned ${raw.length} messages for ${channelId}`);
|
|
199
|
+
return raw.map((msg) => ({
|
|
200
|
+
user: msg.user ?? "bot",
|
|
201
|
+
text: msg.text ?? "",
|
|
202
|
+
ts: msg.ts ?? "",
|
|
203
|
+
}));
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
return channel;
|
|
207
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → Slack mrkdwn conversion.
|
|
3
|
+
* Slack uses a custom markdown-like format called "mrkdwn".
|
|
4
|
+
*/
|
|
5
|
+
import { type MarkdownTableMode } from "../../markdown/ir.js";
|
|
6
|
+
/** Slack mrkdwn uses &, <, > as control chars — escape them in plain text. */
|
|
7
|
+
export declare function escapeSlackMrkdwn(text: string): string;
|
|
8
|
+
export declare function markdownToSlackMrkdwn(markdown: string, options?: {
|
|
9
|
+
tableMode?: MarkdownTableMode;
|
|
10
|
+
}): string;
|
|
11
|
+
/** Slack message limit (text blocks). */
|
|
12
|
+
export declare const SLACK_TEXT_LIMIT = 4000;
|
|
13
|
+
/**
|
|
14
|
+
* Convert markdown to chunked Slack mrkdwn strings.
|
|
15
|
+
* Each chunk is ≤ `limit` characters of plain-text IR.
|
|
16
|
+
*/
|
|
17
|
+
export declare function markdownToSlackMrkdwnChunks(markdown: string, limit?: number, options?: {
|
|
18
|
+
tableMode?: MarkdownTableMode;
|
|
19
|
+
}): string[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → Slack mrkdwn conversion.
|
|
3
|
+
* Slack uses a custom markdown-like format called "mrkdwn".
|
|
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
|
+
/** Slack mrkdwn uses &, <, > as control chars — escape them in plain text. */
|
|
9
|
+
export function escapeSlackMrkdwn(text) {
|
|
10
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
11
|
+
}
|
|
12
|
+
function buildSlackLink(link, _text) {
|
|
13
|
+
const href = link.href.trim();
|
|
14
|
+
if (!href || link.start === link.end)
|
|
15
|
+
return null;
|
|
16
|
+
if (!SAFE_LINK_PROTOCOL.test(href))
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
start: link.start,
|
|
20
|
+
end: link.end,
|
|
21
|
+
open: `<${escapeSlackMrkdwn(href)}|`,
|
|
22
|
+
close: ">",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function renderSlackMrkdwn(ir) {
|
|
26
|
+
return renderMarkdownWithMarkers(ir, {
|
|
27
|
+
styleMarkers: {
|
|
28
|
+
bold: { open: "*", close: "*" },
|
|
29
|
+
italic: { open: "_", close: "_" },
|
|
30
|
+
strikethrough: { open: "~", close: "~" },
|
|
31
|
+
code: { open: "`", close: "`" },
|
|
32
|
+
code_block: { open: "```\n", close: "\n```" },
|
|
33
|
+
},
|
|
34
|
+
escapeText: escapeSlackMrkdwn,
|
|
35
|
+
buildLink: buildSlackLink,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function markdownToSlackMrkdwn(markdown, options = {}) {
|
|
39
|
+
const ir = markdownToIR(markdown ?? "", {
|
|
40
|
+
linkify: true,
|
|
41
|
+
headingStyle: "none",
|
|
42
|
+
blockquotePrefix: "> ",
|
|
43
|
+
tableMode: options.tableMode,
|
|
44
|
+
});
|
|
45
|
+
return renderSlackMrkdwn(ir);
|
|
46
|
+
}
|
|
47
|
+
/** Slack message limit (text blocks). */
|
|
48
|
+
export const SLACK_TEXT_LIMIT = 4000;
|
|
49
|
+
/**
|
|
50
|
+
* Convert markdown to chunked Slack mrkdwn strings.
|
|
51
|
+
* Each chunk is ≤ `limit` characters of plain-text IR.
|
|
52
|
+
*/
|
|
53
|
+
export function markdownToSlackMrkdwnChunks(markdown, limit = SLACK_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) => renderSlackMrkdwn(chunk));
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MessageHandler } from "./types.js";
|
|
2
|
+
import type { ChannelSpec } from "./manager.js";
|
|
3
|
+
import type { MarkdownTableMode } from "../markdown/ir.js";
|
|
4
|
+
import type { ConnectedChannelRegistry } from "../scheduler/connected-channels.js";
|
|
5
|
+
import type { Config } from "../config.js";
|
|
6
|
+
export interface ChannelCallbacks {
|
|
7
|
+
onMessage: MessageHandler;
|
|
8
|
+
onClear: (channelType: string, channelId: string, teamId?: string) => Promise<void>;
|
|
9
|
+
listTeams: () => {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}[];
|
|
13
|
+
connectedChannels: ConnectedChannelRegistry | null;
|
|
14
|
+
/** Return the raw per-channel table config value (resolved lazily at send time). */
|
|
15
|
+
getChannelTableConfig: (channel: string) => MarkdownTableMode | string | null | undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build channel specs from config. Pure function — all side effects are
|
|
19
|
+
* deferred via callbacks so agent.ts stays thin.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildChannelSpecs(config: Config, cb: ChannelCallbacks): ChannelSpec[];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { channelFingerprint } from "./manager.js";
|
|
2
|
+
import { createTelegramChannel } from "./telegram/channel.js";
|
|
3
|
+
import { createDiscordChannel } from "./discord/channel.js";
|
|
4
|
+
import { createSlackChannel } from "./slack/channel.js";
|
|
5
|
+
import { createWhatsAppChannel } from "./whatsapp/channel.js";
|
|
6
|
+
import { resolveMarkdownTableMode } from "../markdown/tables.js";
|
|
7
|
+
import { DEFAULT_TEAM_ID } from "../config/agent-config.js";
|
|
8
|
+
/** Build scheduler opts shared across channels. */
|
|
9
|
+
function buildSchedulerOpts(channelType, connectedChannels) {
|
|
10
|
+
return {
|
|
11
|
+
connectScheduler: (teamId, channelId, sendFn) => connectedChannels.connect(teamId, {
|
|
12
|
+
channelType,
|
|
13
|
+
channelId,
|
|
14
|
+
send: sendFn,
|
|
15
|
+
}),
|
|
16
|
+
disconnectScheduler: (teamId, key) => connectedChannels.disconnect(teamId, key),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build channel specs from config. Pure function — all side effects are
|
|
21
|
+
* deferred via callbacks so agent.ts stays thin.
|
|
22
|
+
*/
|
|
23
|
+
export function buildChannelSpecs(config, cb) {
|
|
24
|
+
const specs = [];
|
|
25
|
+
if (config.channels.telegram) {
|
|
26
|
+
const token = config.channels.telegram.token;
|
|
27
|
+
const resolveTableMode = () => resolveMarkdownTableMode({ channel: "telegram", channelTableMode: cb.getChannelTableConfig("telegram") });
|
|
28
|
+
specs.push({
|
|
29
|
+
name: "telegram",
|
|
30
|
+
fingerprint: channelFingerprint(token),
|
|
31
|
+
create: () => createTelegramChannel({
|
|
32
|
+
token,
|
|
33
|
+
defaultTeamId: DEFAULT_TEAM_ID,
|
|
34
|
+
listTeams: cb.listTeams,
|
|
35
|
+
onMessage: cb.onMessage,
|
|
36
|
+
onClear: cb.onClear,
|
|
37
|
+
resolveTableMode,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (config.channels.discord) {
|
|
42
|
+
const token = config.channels.discord.token;
|
|
43
|
+
const resolveTableMode = () => resolveMarkdownTableMode({ channel: "discord", channelTableMode: cb.getChannelTableConfig("discord") });
|
|
44
|
+
specs.push({
|
|
45
|
+
name: "discord",
|
|
46
|
+
fingerprint: channelFingerprint(token),
|
|
47
|
+
create: () => createDiscordChannel({
|
|
48
|
+
token,
|
|
49
|
+
defaultTeamId: DEFAULT_TEAM_ID,
|
|
50
|
+
listTeams: cb.listTeams,
|
|
51
|
+
onMessage: cb.onMessage,
|
|
52
|
+
onClear: cb.onClear,
|
|
53
|
+
resolveTableMode,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (config.channels.slack) {
|
|
58
|
+
const { botToken, appToken } = config.channels.slack;
|
|
59
|
+
const resolveTableMode = () => resolveMarkdownTableMode({ channel: "slack", channelTableMode: cb.getChannelTableConfig("slack") });
|
|
60
|
+
const schedulerOpts = cb.connectedChannels
|
|
61
|
+
? buildSchedulerOpts("slack", cb.connectedChannels)
|
|
62
|
+
: undefined;
|
|
63
|
+
specs.push({
|
|
64
|
+
name: "slack",
|
|
65
|
+
fingerprint: channelFingerprint(botToken + appToken),
|
|
66
|
+
create: () => createSlackChannel({
|
|
67
|
+
botToken,
|
|
68
|
+
appToken,
|
|
69
|
+
defaultTeamId: DEFAULT_TEAM_ID,
|
|
70
|
+
listTeams: cb.listTeams,
|
|
71
|
+
onMessage: cb.onMessage,
|
|
72
|
+
onClear: cb.onClear,
|
|
73
|
+
schedulerOpts,
|
|
74
|
+
resolveTableMode,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (config.channels.whatsapp) {
|
|
79
|
+
const { phoneId } = config.channels.whatsapp;
|
|
80
|
+
const resolveTableMode = () => resolveMarkdownTableMode({ channel: "whatsapp", channelTableMode: cb.getChannelTableConfig("whatsapp") });
|
|
81
|
+
specs.push({
|
|
82
|
+
name: "whatsapp",
|
|
83
|
+
fingerprint: channelFingerprint(phoneId),
|
|
84
|
+
create: () => createWhatsAppChannel({
|
|
85
|
+
phoneId,
|
|
86
|
+
selfOnly: config.channels.whatsapp?.selfOnly,
|
|
87
|
+
defaultTeamId: DEFAULT_TEAM_ID,
|
|
88
|
+
listTeams: cb.listTeams,
|
|
89
|
+
onMessage: cb.onMessage,
|
|
90
|
+
onClear: cb.onClear,
|
|
91
|
+
resolveTableMode,
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return specs;
|
|
96
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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 CreateTelegramChannelOpts {
|
|
6
|
+
token: string;
|
|
7
|
+
onMessage: MessageHandler;
|
|
8
|
+
onClear?: (channelType: string, channelId: string, teamId?: string) => Promise<void>;
|
|
9
|
+
/** List available teams for the /team command. */
|
|
10
|
+
listTeams?: () => {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}[];
|
|
14
|
+
/** Fallback team when no active team is set for a chat. */
|
|
15
|
+
defaultTeamId?: string;
|
|
16
|
+
resolveTableMode?: ResolveTableMode;
|
|
17
|
+
}
|
|
18
|
+
export declare function createTelegramChannel(opts: CreateTelegramChannelOpts): Channel;
|