clementine-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +44 -0
- package/LICENSE +21 -0
- package/README.md +795 -0
- package/dist/agent/agent-manager.d.ts +69 -0
- package/dist/agent/agent-manager.js +441 -0
- package/dist/agent/assistant.d.ts +225 -0
- package/dist/agent/assistant.js +3888 -0
- package/dist/agent/auto-update.d.ts +32 -0
- package/dist/agent/auto-update.js +186 -0
- package/dist/agent/daily-planner.d.ts +24 -0
- package/dist/agent/daily-planner.js +379 -0
- package/dist/agent/execution-advisor.d.ts +10 -0
- package/dist/agent/execution-advisor.js +272 -0
- package/dist/agent/hooks.d.ts +45 -0
- package/dist/agent/hooks.js +564 -0
- package/dist/agent/insight-engine.d.ts +66 -0
- package/dist/agent/insight-engine.js +225 -0
- package/dist/agent/intent-classifier.d.ts +48 -0
- package/dist/agent/intent-classifier.js +214 -0
- package/dist/agent/link-extractor.d.ts +19 -0
- package/dist/agent/link-extractor.js +90 -0
- package/dist/agent/mcp-bridge.d.ts +62 -0
- package/dist/agent/mcp-bridge.js +435 -0
- package/dist/agent/metacognition.d.ts +66 -0
- package/dist/agent/metacognition.js +221 -0
- package/dist/agent/orchestrator.d.ts +81 -0
- package/dist/agent/orchestrator.js +790 -0
- package/dist/agent/profiles.d.ts +22 -0
- package/dist/agent/profiles.js +91 -0
- package/dist/agent/prompt-cache.d.ts +24 -0
- package/dist/agent/prompt-cache.js +68 -0
- package/dist/agent/prompt-evolver.d.ts +28 -0
- package/dist/agent/prompt-evolver.js +279 -0
- package/dist/agent/role-scaffolds.d.ts +28 -0
- package/dist/agent/role-scaffolds.js +433 -0
- package/dist/agent/safe-restart.d.ts +41 -0
- package/dist/agent/safe-restart.js +150 -0
- package/dist/agent/self-improve.d.ts +66 -0
- package/dist/agent/self-improve.js +1706 -0
- package/dist/agent/session-event-log.d.ts +114 -0
- package/dist/agent/session-event-log.js +233 -0
- package/dist/agent/skill-extractor.d.ts +72 -0
- package/dist/agent/skill-extractor.js +435 -0
- package/dist/agent/source-mods.d.ts +61 -0
- package/dist/agent/source-mods.js +230 -0
- package/dist/agent/source-preflight.d.ts +25 -0
- package/dist/agent/source-preflight.js +100 -0
- package/dist/agent/stall-guard.d.ts +62 -0
- package/dist/agent/stall-guard.js +109 -0
- package/dist/agent/strategic-planner.d.ts +60 -0
- package/dist/agent/strategic-planner.js +352 -0
- package/dist/agent/team-bus.d.ts +89 -0
- package/dist/agent/team-bus.js +556 -0
- package/dist/agent/team-router.d.ts +26 -0
- package/dist/agent/team-router.js +37 -0
- package/dist/agent/tool-loop-detector.d.ts +59 -0
- package/dist/agent/tool-loop-detector.js +242 -0
- package/dist/agent/workflow-runner.d.ts +36 -0
- package/dist/agent/workflow-runner.js +317 -0
- package/dist/agent/workflow-variables.d.ts +16 -0
- package/dist/agent/workflow-variables.js +62 -0
- package/dist/channels/discord-agent-bot.d.ts +101 -0
- package/dist/channels/discord-agent-bot.js +881 -0
- package/dist/channels/discord-bot-manager.d.ts +80 -0
- package/dist/channels/discord-bot-manager.js +262 -0
- package/dist/channels/discord-utils.d.ts +51 -0
- package/dist/channels/discord-utils.js +293 -0
- package/dist/channels/discord.d.ts +12 -0
- package/dist/channels/discord.js +1832 -0
- package/dist/channels/slack-agent-bot.d.ts +73 -0
- package/dist/channels/slack-agent-bot.js +320 -0
- package/dist/channels/slack-bot-manager.d.ts +66 -0
- package/dist/channels/slack-bot-manager.js +236 -0
- package/dist/channels/slack-utils.d.ts +39 -0
- package/dist/channels/slack-utils.js +189 -0
- package/dist/channels/slack.d.ts +11 -0
- package/dist/channels/slack.js +196 -0
- package/dist/channels/telegram.d.ts +10 -0
- package/dist/channels/telegram.js +235 -0
- package/dist/channels/webhook.d.ts +9 -0
- package/dist/channels/webhook.js +78 -0
- package/dist/channels/whatsapp.d.ts +11 -0
- package/dist/channels/whatsapp.js +181 -0
- package/dist/cli/chat.d.ts +14 -0
- package/dist/cli/chat.js +220 -0
- package/dist/cli/cron.d.ts +17 -0
- package/dist/cli/cron.js +552 -0
- package/dist/cli/dashboard.d.ts +15 -0
- package/dist/cli/dashboard.js +17677 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +2474 -0
- package/dist/cli/routes/delegations.d.ts +19 -0
- package/dist/cli/routes/delegations.js +154 -0
- package/dist/cli/routes/digest.d.ts +17 -0
- package/dist/cli/routes/digest.js +375 -0
- package/dist/cli/routes/goals.d.ts +14 -0
- package/dist/cli/routes/goals.js +258 -0
- package/dist/cli/routes/workflows.d.ts +18 -0
- package/dist/cli/routes/workflows.js +97 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +619 -0
- package/dist/cli/tunnel.d.ts +35 -0
- package/dist/cli/tunnel.js +141 -0
- package/dist/config.d.ts +145 -0
- package/dist/config.js +278 -0
- package/dist/events/bus.d.ts +43 -0
- package/dist/events/bus.js +136 -0
- package/dist/gateway/cron-scheduler.d.ts +166 -0
- package/dist/gateway/cron-scheduler.js +1767 -0
- package/dist/gateway/delivery-queue.d.ts +30 -0
- package/dist/gateway/delivery-queue.js +110 -0
- package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
- package/dist/gateway/heartbeat-scheduler.js +1298 -0
- package/dist/gateway/heartbeat.d.ts +3 -0
- package/dist/gateway/heartbeat.js +3 -0
- package/dist/gateway/lanes.d.ts +24 -0
- package/dist/gateway/lanes.js +76 -0
- package/dist/gateway/notifications.d.ts +29 -0
- package/dist/gateway/notifications.js +75 -0
- package/dist/gateway/router.d.ts +210 -0
- package/dist/gateway/router.js +1330 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1015 -0
- package/dist/memory/chunker.d.ts +28 -0
- package/dist/memory/chunker.js +226 -0
- package/dist/memory/consolidation.d.ts +44 -0
- package/dist/memory/consolidation.js +171 -0
- package/dist/memory/context-assembler.d.ts +50 -0
- package/dist/memory/context-assembler.js +149 -0
- package/dist/memory/embeddings.d.ts +38 -0
- package/dist/memory/embeddings.js +180 -0
- package/dist/memory/graph-store.d.ts +66 -0
- package/dist/memory/graph-store.js +613 -0
- package/dist/memory/mmr.d.ts +21 -0
- package/dist/memory/mmr.js +75 -0
- package/dist/memory/search.d.ts +26 -0
- package/dist/memory/search.js +67 -0
- package/dist/memory/store.d.ts +530 -0
- package/dist/memory/store.js +2022 -0
- package/dist/security/integrity.d.ts +24 -0
- package/dist/security/integrity.js +58 -0
- package/dist/security/patterns.d.ts +34 -0
- package/dist/security/patterns.js +110 -0
- package/dist/security/scanner.d.ts +32 -0
- package/dist/security/scanner.js +263 -0
- package/dist/tools/admin-tools.d.ts +12 -0
- package/dist/tools/admin-tools.js +1278 -0
- package/dist/tools/external-tools.d.ts +11 -0
- package/dist/tools/external-tools.js +1327 -0
- package/dist/tools/goal-tools.d.ts +9 -0
- package/dist/tools/goal-tools.js +159 -0
- package/dist/tools/mcp-server.d.ts +13 -0
- package/dist/tools/mcp-server.js +141 -0
- package/dist/tools/memory-tools.d.ts +10 -0
- package/dist/tools/memory-tools.js +568 -0
- package/dist/tools/session-tools.d.ts +6 -0
- package/dist/tools/session-tools.js +146 -0
- package/dist/tools/shared.d.ts +216 -0
- package/dist/tools/shared.js +340 -0
- package/dist/tools/team-tools.d.ts +6 -0
- package/dist/tools/team-tools.js +447 -0
- package/dist/tools/tool-meta.d.ts +34 -0
- package/dist/tools/tool-meta.js +133 -0
- package/dist/tools/vault-tools.d.ts +8 -0
- package/dist/tools/vault-tools.js +457 -0
- package/dist/types.d.ts +716 -0
- package/dist/types.js +16 -0
- package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
- package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
- package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
- package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
- package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
- package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
- package/dist/vault-migrations/helpers.d.ts +14 -0
- package/dist/vault-migrations/helpers.js +44 -0
- package/dist/vault-migrations/runner.d.ts +14 -0
- package/dist/vault-migrations/runner.js +139 -0
- package/dist/vault-migrations/types.d.ts +42 -0
- package/dist/vault-migrations/types.js +9 -0
- package/install.sh +320 -0
- package/package.json +84 -0
- package/scripts/postinstall.js +125 -0
- package/vault/00-System/AGENTS.md +66 -0
- package/vault/00-System/CRON.md +71 -0
- package/vault/00-System/HEARTBEAT.md +58 -0
- package/vault/00-System/MEMORY.md +16 -0
- package/vault/00-System/SOUL.md +96 -0
- package/vault/05-Tasks/TASKS.md +19 -0
- package/vault/06-Templates/_Daily-Template.md +28 -0
- package/vault/06-Templates/_People-Template.md +22 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Slack agent bot client.
|
|
3
|
+
*
|
|
4
|
+
* A @slack/bolt App wrapper for a single agent.
|
|
5
|
+
* Handles: DMs + channel messages → gateway → stream response.
|
|
6
|
+
* Uses Socket Mode (requires both bot token + app token).
|
|
7
|
+
*
|
|
8
|
+
* Channel discovery (in priority order):
|
|
9
|
+
* 1. Explicit `slackChannelId` from agent config
|
|
10
|
+
* 2. Auto-discover by matching `channelName` via conversations.list
|
|
11
|
+
* 3. Falls back to listening in ALL channels the bot is in
|
|
12
|
+
*
|
|
13
|
+
* DMs are always enabled for the owner.
|
|
14
|
+
*/
|
|
15
|
+
import type { AgentProfile } from '../types.js';
|
|
16
|
+
import type { Gateway } from '../gateway/router.js';
|
|
17
|
+
export interface SlackAgentBotConfig {
|
|
18
|
+
slug: string;
|
|
19
|
+
botToken: string;
|
|
20
|
+
appToken: string;
|
|
21
|
+
ownerId: string;
|
|
22
|
+
profile: AgentProfile;
|
|
23
|
+
/** Explicit channel IDs to listen in. If empty, auto-discovered on connect. */
|
|
24
|
+
channelIds?: string[];
|
|
25
|
+
}
|
|
26
|
+
export type SlackAgentBotStatus = 'offline' | 'connecting' | 'online' | 'error';
|
|
27
|
+
export declare class SlackAgentBotClient {
|
|
28
|
+
private app;
|
|
29
|
+
private config;
|
|
30
|
+
private gateway;
|
|
31
|
+
private status;
|
|
32
|
+
private errorMessage?;
|
|
33
|
+
/** Bot's own user ID (set after auth.test). */
|
|
34
|
+
private botUserId?;
|
|
35
|
+
/** Resolved channel IDs (set on connect, after auto-discovery). */
|
|
36
|
+
private resolvedChannelIds;
|
|
37
|
+
constructor(config: SlackAgentBotConfig, gateway: Gateway);
|
|
38
|
+
start(): Promise<void>;
|
|
39
|
+
stop(): Promise<void>;
|
|
40
|
+
getStatus(): {
|
|
41
|
+
status: SlackAgentBotStatus;
|
|
42
|
+
botUserId?: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
};
|
|
45
|
+
getChannelIds(): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Discover which channels this bot should listen in.
|
|
48
|
+
*
|
|
49
|
+
* Priority:
|
|
50
|
+
* 1. Explicit channelIds from config (e.g. slackChannelId in agent.md)
|
|
51
|
+
* 2. Match by channelName via conversations.list
|
|
52
|
+
* 3. All channels the bot is a member of (fallback)
|
|
53
|
+
*/
|
|
54
|
+
private discoverChannels;
|
|
55
|
+
/** Check if this bot participates in a shared team chat channel. */
|
|
56
|
+
isTeamChat(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Check if this agent is being addressed in a team chat message.
|
|
59
|
+
* Matches: @mention (Slack format <@UXXXXXX>), agent name, agent slug, or broadcast keywords.
|
|
60
|
+
*/
|
|
61
|
+
private isAddressedInTeamChat;
|
|
62
|
+
/**
|
|
63
|
+
* Collect recent messages from the channel for team chat context.
|
|
64
|
+
*/
|
|
65
|
+
private gatherTeamChatContext;
|
|
66
|
+
/**
|
|
67
|
+
* Receive an inter-agent team message. Posts a formatted message showing
|
|
68
|
+
* the incoming content, then triggers the agent to process and respond.
|
|
69
|
+
*/
|
|
70
|
+
receiveTeamMessage(fromName: string, fromSlug: string, content: string): Promise<string>;
|
|
71
|
+
private handleMessage;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=slack-agent-bot.d.ts.map
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Slack agent bot client.
|
|
3
|
+
*
|
|
4
|
+
* A @slack/bolt App wrapper for a single agent.
|
|
5
|
+
* Handles: DMs + channel messages → gateway → stream response.
|
|
6
|
+
* Uses Socket Mode (requires both bot token + app token).
|
|
7
|
+
*
|
|
8
|
+
* Channel discovery (in priority order):
|
|
9
|
+
* 1. Explicit `slackChannelId` from agent config
|
|
10
|
+
* 2. Auto-discover by matching `channelName` via conversations.list
|
|
11
|
+
* 3. Falls back to listening in ALL channels the bot is in
|
|
12
|
+
*
|
|
13
|
+
* DMs are always enabled for the owner.
|
|
14
|
+
*/
|
|
15
|
+
import { App } from '@slack/bolt';
|
|
16
|
+
import pino from 'pino';
|
|
17
|
+
import { SlackStreamingMessage } from './slack-utils.js';
|
|
18
|
+
import { friendlyToolName } from './discord-utils.js';
|
|
19
|
+
const logger = pino({ name: 'clementine.slack-agent-bot' });
|
|
20
|
+
export class SlackAgentBotClient {
|
|
21
|
+
app;
|
|
22
|
+
config;
|
|
23
|
+
gateway;
|
|
24
|
+
status = 'offline';
|
|
25
|
+
errorMessage;
|
|
26
|
+
/** Bot's own user ID (set after auth.test). */
|
|
27
|
+
botUserId;
|
|
28
|
+
/** Resolved channel IDs (set on connect, after auto-discovery). */
|
|
29
|
+
resolvedChannelIds = [];
|
|
30
|
+
constructor(config, gateway) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.gateway = gateway;
|
|
33
|
+
this.app = new App({
|
|
34
|
+
token: config.botToken,
|
|
35
|
+
appToken: config.appToken,
|
|
36
|
+
socketMode: true,
|
|
37
|
+
});
|
|
38
|
+
// Catch Socket Mode errors so they don't crash the daemon
|
|
39
|
+
this.app.error(async (error) => {
|
|
40
|
+
this.status = 'error';
|
|
41
|
+
this.errorMessage = String(error);
|
|
42
|
+
logger.error({ err: error, slug: config.slug }, 'Slack agent bot error — continuing');
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async start() {
|
|
46
|
+
this.status = 'connecting';
|
|
47
|
+
try {
|
|
48
|
+
// Get bot identity
|
|
49
|
+
const authResult = await this.app.client.auth.test({ token: this.config.botToken });
|
|
50
|
+
this.botUserId = authResult.user_id;
|
|
51
|
+
// Discover channels
|
|
52
|
+
this.resolvedChannelIds = await this.discoverChannels();
|
|
53
|
+
// Register message handler
|
|
54
|
+
this.app.message(async ({ message, client }) => {
|
|
55
|
+
try {
|
|
56
|
+
await this.handleMessage(message, client);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
logger.error({ err, slug: this.config.slug }, 'Unhandled error in Slack agent bot message handler');
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
await this.app.start();
|
|
63
|
+
this.status = 'online';
|
|
64
|
+
this.errorMessage = undefined;
|
|
65
|
+
logger.info({ slug: this.config.slug, botUserId: this.botUserId, channels: this.resolvedChannelIds }, `Slack agent bot online: ${this.config.profile.name}`);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
this.status = 'error';
|
|
69
|
+
this.errorMessage = String(err);
|
|
70
|
+
logger.error({ err, slug: this.config.slug }, 'Slack agent bot start failed');
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async stop() {
|
|
75
|
+
try {
|
|
76
|
+
await this.app.stop();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
this.status = 'offline';
|
|
82
|
+
logger.info({ slug: this.config.slug }, 'Slack agent bot stopped');
|
|
83
|
+
}
|
|
84
|
+
getStatus() {
|
|
85
|
+
return {
|
|
86
|
+
status: this.status,
|
|
87
|
+
botUserId: this.botUserId,
|
|
88
|
+
error: this.errorMessage,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
getChannelIds() {
|
|
92
|
+
return this.resolvedChannelIds;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Discover which channels this bot should listen in.
|
|
96
|
+
*
|
|
97
|
+
* Priority:
|
|
98
|
+
* 1. Explicit channelIds from config (e.g. slackChannelId in agent.md)
|
|
99
|
+
* 2. Match by channelName via conversations.list
|
|
100
|
+
* 3. All channels the bot is a member of (fallback)
|
|
101
|
+
*/
|
|
102
|
+
async discoverChannels() {
|
|
103
|
+
// 1. Explicit IDs
|
|
104
|
+
if (this.config.channelIds && this.config.channelIds.length > 0) {
|
|
105
|
+
logger.info({ slug: this.config.slug, channelIds: this.config.channelIds }, 'Using explicit channel IDs');
|
|
106
|
+
return this.config.channelIds;
|
|
107
|
+
}
|
|
108
|
+
// Fetch all channels the bot is a member of (paginate fully)
|
|
109
|
+
const allBotChannels = [];
|
|
110
|
+
let cursor;
|
|
111
|
+
do {
|
|
112
|
+
const result = await this.app.client.conversations.list({
|
|
113
|
+
token: this.config.botToken,
|
|
114
|
+
types: 'public_channel,private_channel',
|
|
115
|
+
exclude_archived: true,
|
|
116
|
+
limit: 200,
|
|
117
|
+
cursor,
|
|
118
|
+
});
|
|
119
|
+
for (const ch of result.channels ?? []) {
|
|
120
|
+
if (ch.is_member && ch.id && ch.name) {
|
|
121
|
+
allBotChannels.push({ id: ch.id, name: ch.name });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
cursor = result.response_metadata?.next_cursor || undefined;
|
|
125
|
+
} while (cursor);
|
|
126
|
+
// 2. Match by channelName
|
|
127
|
+
const channelNameConfig = this.config.profile.team?.channelName;
|
|
128
|
+
if (channelNameConfig) {
|
|
129
|
+
const channelNames = Array.isArray(channelNameConfig) ? channelNameConfig : [channelNameConfig];
|
|
130
|
+
const matched = allBotChannels
|
|
131
|
+
.filter(ch => channelNames.includes(ch.name))
|
|
132
|
+
.map(ch => ch.id);
|
|
133
|
+
if (matched.length > 0) {
|
|
134
|
+
logger.info({ slug: this.config.slug, channelNames, matched }, 'Auto-discovered Slack channels by name');
|
|
135
|
+
return matched;
|
|
136
|
+
}
|
|
137
|
+
logger.warn({ slug: this.config.slug, channelNames }, 'No Slack channels found matching channelName(s) — falling back to all bot channels');
|
|
138
|
+
}
|
|
139
|
+
// 3. Fallback: all channels the bot is a member of
|
|
140
|
+
const all = allBotChannels.map(ch => ch.id);
|
|
141
|
+
logger.info({ slug: this.config.slug, count: all.length }, 'Fallback: listening in all Slack channels bot is a member of');
|
|
142
|
+
return all;
|
|
143
|
+
}
|
|
144
|
+
/** Check if this bot participates in a shared team chat channel. */
|
|
145
|
+
isTeamChat() {
|
|
146
|
+
return this.config.profile.team?.teamChat === true;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Check if this agent is being addressed in a team chat message.
|
|
150
|
+
* Matches: @mention (Slack format <@UXXXXXX>), agent name, agent slug, or broadcast keywords.
|
|
151
|
+
*/
|
|
152
|
+
isAddressedInTeamChat(text) {
|
|
153
|
+
// Direct @mention of this bot
|
|
154
|
+
if (this.botUserId && text.includes(`<@${this.botUserId}>`)) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
const lower = text.toLowerCase();
|
|
158
|
+
// Broadcast keywords
|
|
159
|
+
const broadcastPatterns = [
|
|
160
|
+
/\b@?team\b/,
|
|
161
|
+
/\beveryone\b/,
|
|
162
|
+
/\ball\s+agents?\b/,
|
|
163
|
+
/\bthe\s+team\b/,
|
|
164
|
+
];
|
|
165
|
+
if (broadcastPatterns.some(p => p.test(lower))) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
// Individual agent name or slug at word boundaries
|
|
169
|
+
const name = this.config.profile.name.toLowerCase();
|
|
170
|
+
const slug = this.config.slug.toLowerCase();
|
|
171
|
+
const namePattern = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
|
172
|
+
const slugPattern = new RegExp(`\\b${slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
|
173
|
+
return namePattern.test(lower) || slugPattern.test(lower);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Collect recent messages from the channel for team chat context.
|
|
177
|
+
*/
|
|
178
|
+
async gatherTeamChatContext(channel, beforeTs, limit = 10) {
|
|
179
|
+
try {
|
|
180
|
+
const result = await this.app.client.conversations.history({
|
|
181
|
+
token: this.config.botToken,
|
|
182
|
+
channel,
|
|
183
|
+
latest: beforeTs,
|
|
184
|
+
limit: limit + 1,
|
|
185
|
+
inclusive: false,
|
|
186
|
+
});
|
|
187
|
+
const contextLines = [];
|
|
188
|
+
for (const msg of (result.messages ?? []).reverse()) {
|
|
189
|
+
const authorName = msg.bot_id ? (msg.username ?? 'Bot') : 'Owner';
|
|
190
|
+
const preview = (msg.text ?? '').slice(0, 300);
|
|
191
|
+
if (preview) {
|
|
192
|
+
contextLines.push(`[${authorName}]: ${preview}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (contextLines.length === 0)
|
|
196
|
+
return '';
|
|
197
|
+
return `\n\n[Recent team chat context]\n${contextLines.join('\n')}\n[End context]`;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Receive an inter-agent team message. Posts a formatted message showing
|
|
205
|
+
* the incoming content, then triggers the agent to process and respond.
|
|
206
|
+
*/
|
|
207
|
+
async receiveTeamMessage(fromName, fromSlug, content) {
|
|
208
|
+
if (this.resolvedChannelIds.length === 0) {
|
|
209
|
+
logger.warn({ slug: this.config.slug }, 'No Slack channels to deliver team message to');
|
|
210
|
+
return '(no channels available)';
|
|
211
|
+
}
|
|
212
|
+
const channelId = this.resolvedChannelIds[0];
|
|
213
|
+
// Post the incoming message so it's visible in the channel
|
|
214
|
+
await this.app.client.chat.postMessage({
|
|
215
|
+
token: this.config.botToken,
|
|
216
|
+
channel: channelId,
|
|
217
|
+
text: `*${fromName}* via team message:\n${content.slice(0, 3000)}`,
|
|
218
|
+
});
|
|
219
|
+
// Run the task through the unleashed pipeline — gives the agent full
|
|
220
|
+
// multi-phase autonomous execution instead of the 5-minute chat timeout.
|
|
221
|
+
const streamer = new SlackStreamingMessage(this.app.client, channelId);
|
|
222
|
+
await streamer.start();
|
|
223
|
+
try {
|
|
224
|
+
const response = await this.gateway.handleTeamTask(fromName, fromSlug, content, this.config.profile, async (token) => {
|
|
225
|
+
await streamer.update(token);
|
|
226
|
+
});
|
|
227
|
+
await streamer.finalize(response);
|
|
228
|
+
logger.info({ slug: this.config.slug, from: fromSlug }, 'Processed Slack team message');
|
|
229
|
+
return response;
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
logger.error({ err, slug: this.config.slug }, 'Failed to process Slack team message');
|
|
233
|
+
const errMsg = `Something went wrong processing a team message: ${err}`;
|
|
234
|
+
await streamer.finalize(errMsg);
|
|
235
|
+
return errMsg;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async handleMessage(message, client) {
|
|
239
|
+
// Ignore own messages
|
|
240
|
+
if (message.user === this.botUserId)
|
|
241
|
+
return;
|
|
242
|
+
// Ignore bot messages
|
|
243
|
+
if (message.bot_id)
|
|
244
|
+
return;
|
|
245
|
+
// Ignore subtypes (joins, leaves, etc.)
|
|
246
|
+
if (message.subtype)
|
|
247
|
+
return;
|
|
248
|
+
const channel = message.channel;
|
|
249
|
+
const isDm = message.channel_type === 'im';
|
|
250
|
+
const isWatchedChannel = !isDm && this.resolvedChannelIds.includes(channel);
|
|
251
|
+
// Respond in DMs or watched channels
|
|
252
|
+
if (!isDm && !isWatchedChannel)
|
|
253
|
+
return;
|
|
254
|
+
const isTeamChatChannel = isWatchedChannel && this.isTeamChat();
|
|
255
|
+
// Owner-only check
|
|
256
|
+
if (this.config.ownerId && message.user !== this.config.ownerId) {
|
|
257
|
+
logger.warn({ slug: this.config.slug, author: message.user }, 'Ignored Slack message from non-owner');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// In team chat: respond to all if respondToAll is set, otherwise only when addressed
|
|
261
|
+
const respondToAll = this.config.profile.team?.respondToAll === true;
|
|
262
|
+
if (isTeamChatChannel && !respondToAll && !this.isAddressedInTeamChat(message.text ?? '')) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
let text = message.text ?? '';
|
|
266
|
+
// Extract file attachments
|
|
267
|
+
if (message.files && Array.isArray(message.files) && message.files.length > 0) {
|
|
268
|
+
const fileLines = message.files.map((file) => {
|
|
269
|
+
if (file.mimetype?.startsWith('image/')) {
|
|
270
|
+
return `[Image attached: ${file.name} (${file.url_private})]`;
|
|
271
|
+
}
|
|
272
|
+
return `[File attached: ${file.name}, ${file.mimetype || 'unknown type'}, ${file.url_private}]`;
|
|
273
|
+
});
|
|
274
|
+
text = fileLines.join('\n') + (text ? '\n' + text : '');
|
|
275
|
+
}
|
|
276
|
+
if (!text)
|
|
277
|
+
return;
|
|
278
|
+
// !clear command
|
|
279
|
+
if (text === '!clear') {
|
|
280
|
+
const sessionKey = isDm
|
|
281
|
+
? `slack:agent:${this.config.slug}:${message.user}`
|
|
282
|
+
: `slack:channel:${channel}:${this.config.slug}:${message.user}`;
|
|
283
|
+
this.gateway.clearSession(sessionKey);
|
|
284
|
+
await client.chat.postMessage({ channel, text: 'Session cleared.', thread_ts: message.ts });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// In team chat, use agent-scoped session key
|
|
288
|
+
const sessionKey = isDm
|
|
289
|
+
? `slack:agent:${this.config.slug}:${message.user}`
|
|
290
|
+
: isTeamChatChannel
|
|
291
|
+
? `slack:channel:${channel}:${this.config.slug}:${message.user}`
|
|
292
|
+
: `slack:channel:${channel}:${message.user}`;
|
|
293
|
+
// Set the agent profile for this session
|
|
294
|
+
this.gateway.setSessionProfile(sessionKey, this.config.slug);
|
|
295
|
+
// In team chat, gather recent messages for context
|
|
296
|
+
if (isTeamChatChannel) {
|
|
297
|
+
const teamContext = await this.gatherTeamChatContext(channel, message.ts, 10);
|
|
298
|
+
if (teamContext) {
|
|
299
|
+
text += teamContext;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Stream response
|
|
303
|
+
const threadTs = message.thread_ts ?? message.ts;
|
|
304
|
+
const streamer = new SlackStreamingMessage(client, channel, threadTs);
|
|
305
|
+
await streamer.start();
|
|
306
|
+
try {
|
|
307
|
+
const response = await this.gateway.handleMessage(sessionKey, text, async (token) => {
|
|
308
|
+
await streamer.update(token);
|
|
309
|
+
}, undefined, // model
|
|
310
|
+
undefined, // maxTurns
|
|
311
|
+
async (toolName, toolInput) => { streamer.setToolStatus(friendlyToolName(toolName, toolInput)); });
|
|
312
|
+
await streamer.finalize(response);
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
logger.error({ err, slug: this.config.slug }, 'Slack agent bot message handling error');
|
|
316
|
+
await streamer.finalize(`Something went wrong: ${err}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=slack-agent-bot.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Slack bot manager.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the lifecycle of Slack agent bot clients. Agents with both
|
|
5
|
+
* `slackBotToken` AND `slackAppToken` in their profile get their own
|
|
6
|
+
* dedicated @slack/bolt App.
|
|
7
|
+
*/
|
|
8
|
+
import type { Gateway } from '../gateway/router.js';
|
|
9
|
+
import { type SlackAgentBotStatus } from './slack-agent-bot.js';
|
|
10
|
+
export interface SlackBotManagerConfig {
|
|
11
|
+
gateway: Gateway;
|
|
12
|
+
ownerId: string;
|
|
13
|
+
statusFilePath?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SlackBotStatus {
|
|
16
|
+
slug: string;
|
|
17
|
+
status: SlackAgentBotStatus;
|
|
18
|
+
botUserId?: string;
|
|
19
|
+
channelIds: string[];
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class SlackBotManager {
|
|
23
|
+
private bots;
|
|
24
|
+
private gateway;
|
|
25
|
+
private ownerId;
|
|
26
|
+
private statusFilePath;
|
|
27
|
+
private pollInterval?;
|
|
28
|
+
private statusInterval?;
|
|
29
|
+
constructor(config: SlackBotManagerConfig);
|
|
30
|
+
/**
|
|
31
|
+
* Scan all agents for slackBotToken + slackAppToken, start bots, return owned channel IDs.
|
|
32
|
+
*/
|
|
33
|
+
startAll(): Promise<string[]>;
|
|
34
|
+
startBot(slug: string): Promise<void>;
|
|
35
|
+
stopBot(slug: string): Promise<void>;
|
|
36
|
+
stopAll(): Promise<void>;
|
|
37
|
+
getStatuses(): Map<string, SlackBotStatus>;
|
|
38
|
+
/**
|
|
39
|
+
* Get all channel IDs managed by Slack agent bots.
|
|
40
|
+
* Main Slack bot should NOT watch these.
|
|
41
|
+
*/
|
|
42
|
+
getOwnedChannelIds(): string[];
|
|
43
|
+
/** Get channel IDs that are shared team chat channels. */
|
|
44
|
+
getTeamChatChannelIds(): string[];
|
|
45
|
+
/** Get the primary channel ID for a specific agent bot. */
|
|
46
|
+
getChannelForAgent(slug: string): string | null;
|
|
47
|
+
/** Reverse lookup: which agent slug owns a given Slack channel ID? */
|
|
48
|
+
getAgentForChannel(channelId: string): string | null;
|
|
49
|
+
/** Get the owner ID. */
|
|
50
|
+
getOwnerId(): string;
|
|
51
|
+
/** Check if an agent has a running Slack bot. */
|
|
52
|
+
hasBot(slug: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Deliver a team message to an agent's Slack bot.
|
|
55
|
+
* Returns the agent's response text, or null if delivery failed.
|
|
56
|
+
*/
|
|
57
|
+
deliverTeamMessage(toSlug: string, fromName: string, fromSlug: string, content: string): Promise<string | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Poll for new/removed agents with Slack tokens at the given interval.
|
|
60
|
+
*/
|
|
61
|
+
startPolling(intervalMs: number): void;
|
|
62
|
+
private pollForChanges;
|
|
63
|
+
/** Write status to disk so the dashboard can read it. */
|
|
64
|
+
private startStatusWriter;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=slack-bot-manager.d.ts.map
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — Slack bot manager.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the lifecycle of Slack agent bot clients. Agents with both
|
|
5
|
+
* `slackBotToken` AND `slackAppToken` in their profile get their own
|
|
6
|
+
* dedicated @slack/bolt App.
|
|
7
|
+
*/
|
|
8
|
+
import { writeFileSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import pino from 'pino';
|
|
11
|
+
import { SlackAgentBotClient } from './slack-agent-bot.js';
|
|
12
|
+
const logger = pino({ name: 'clementine.slack-bot-manager' });
|
|
13
|
+
export class SlackBotManager {
|
|
14
|
+
bots = new Map();
|
|
15
|
+
gateway;
|
|
16
|
+
ownerId;
|
|
17
|
+
statusFilePath;
|
|
18
|
+
pollInterval;
|
|
19
|
+
statusInterval;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.gateway = config.gateway;
|
|
22
|
+
this.ownerId = config.ownerId;
|
|
23
|
+
this.statusFilePath = config.statusFilePath ??
|
|
24
|
+
path.join(process.env.CLEMENTINE_HOME || path.join(process.env.HOME || '', '.clementine'), '.slack-bot-status.json');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Scan all agents for slackBotToken + slackAppToken, start bots, return owned channel IDs.
|
|
28
|
+
*/
|
|
29
|
+
async startAll() {
|
|
30
|
+
const mgr = this.gateway.getAgentManager();
|
|
31
|
+
const allAgents = mgr.listAll();
|
|
32
|
+
logger.info({ agentCount: allAgents.length }, 'Scanning agents for Slack tokens');
|
|
33
|
+
for (const agent of allAgents) {
|
|
34
|
+
if (!agent.slackBotToken || !agent.slackAppToken)
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
await this.startBot(agent.slug);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
logger.error({ err, slug: agent.slug }, 'Failed to start Slack agent bot');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Start status file writer
|
|
44
|
+
this.startStatusWriter();
|
|
45
|
+
return this.getOwnedChannelIds();
|
|
46
|
+
}
|
|
47
|
+
async startBot(slug) {
|
|
48
|
+
// If already running, stop first
|
|
49
|
+
if (this.bots.has(slug)) {
|
|
50
|
+
await this.stopBot(slug);
|
|
51
|
+
}
|
|
52
|
+
const mgr = this.gateway.getAgentManager();
|
|
53
|
+
const profile = mgr.get(slug);
|
|
54
|
+
if (!profile) {
|
|
55
|
+
throw new Error(`Agent '${slug}' not found`);
|
|
56
|
+
}
|
|
57
|
+
if (!profile.slackBotToken || !profile.slackAppToken) {
|
|
58
|
+
throw new Error(`Agent '${slug}' missing slackBotToken or slackAppToken`);
|
|
59
|
+
}
|
|
60
|
+
// Build channel IDs from explicit config
|
|
61
|
+
const explicitChannelIds = profile.slackChannelId
|
|
62
|
+
? [profile.slackChannelId]
|
|
63
|
+
: undefined;
|
|
64
|
+
const bot = new SlackAgentBotClient({
|
|
65
|
+
slug,
|
|
66
|
+
botToken: profile.slackBotToken,
|
|
67
|
+
appToken: profile.slackAppToken,
|
|
68
|
+
ownerId: this.ownerId,
|
|
69
|
+
profile,
|
|
70
|
+
channelIds: explicitChannelIds,
|
|
71
|
+
}, this.gateway);
|
|
72
|
+
await bot.start();
|
|
73
|
+
this.bots.set(slug, bot);
|
|
74
|
+
logger.info({ slug }, 'Slack agent bot started');
|
|
75
|
+
}
|
|
76
|
+
async stopBot(slug) {
|
|
77
|
+
const bot = this.bots.get(slug);
|
|
78
|
+
if (!bot)
|
|
79
|
+
return;
|
|
80
|
+
await bot.stop();
|
|
81
|
+
this.bots.delete(slug);
|
|
82
|
+
}
|
|
83
|
+
async stopAll() {
|
|
84
|
+
const slugs = [...this.bots.keys()];
|
|
85
|
+
await Promise.all(slugs.map(slug => this.stopBot(slug)));
|
|
86
|
+
if (this.pollInterval) {
|
|
87
|
+
clearInterval(this.pollInterval);
|
|
88
|
+
this.pollInterval = undefined;
|
|
89
|
+
}
|
|
90
|
+
if (this.statusInterval) {
|
|
91
|
+
clearInterval(this.statusInterval);
|
|
92
|
+
this.statusInterval = undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
getStatuses() {
|
|
96
|
+
const result = new Map();
|
|
97
|
+
for (const [slug, bot] of this.bots) {
|
|
98
|
+
const s = bot.getStatus();
|
|
99
|
+
result.set(slug, {
|
|
100
|
+
slug,
|
|
101
|
+
status: s.status,
|
|
102
|
+
botUserId: s.botUserId,
|
|
103
|
+
channelIds: bot.getChannelIds(),
|
|
104
|
+
error: s.error,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get all channel IDs managed by Slack agent bots.
|
|
111
|
+
* Main Slack bot should NOT watch these.
|
|
112
|
+
*/
|
|
113
|
+
getOwnedChannelIds() {
|
|
114
|
+
const ids = [];
|
|
115
|
+
for (const bot of this.bots.values()) {
|
|
116
|
+
ids.push(...bot.getChannelIds());
|
|
117
|
+
}
|
|
118
|
+
return ids;
|
|
119
|
+
}
|
|
120
|
+
/** Get channel IDs that are shared team chat channels. */
|
|
121
|
+
getTeamChatChannelIds() {
|
|
122
|
+
const ids = [];
|
|
123
|
+
for (const bot of this.bots.values()) {
|
|
124
|
+
if (bot.isTeamChat()) {
|
|
125
|
+
ids.push(...bot.getChannelIds());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [...new Set(ids)];
|
|
129
|
+
}
|
|
130
|
+
/** Get the primary channel ID for a specific agent bot. */
|
|
131
|
+
getChannelForAgent(slug) {
|
|
132
|
+
const bot = this.bots.get(slug);
|
|
133
|
+
if (!bot)
|
|
134
|
+
return null;
|
|
135
|
+
const channels = bot.getChannelIds();
|
|
136
|
+
return channels[0] ?? null;
|
|
137
|
+
}
|
|
138
|
+
/** Reverse lookup: which agent slug owns a given Slack channel ID? */
|
|
139
|
+
getAgentForChannel(channelId) {
|
|
140
|
+
for (const [slug, bot] of this.bots) {
|
|
141
|
+
if (bot.getChannelIds().includes(channelId))
|
|
142
|
+
return slug;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
/** Get the owner ID. */
|
|
147
|
+
getOwnerId() {
|
|
148
|
+
return this.ownerId;
|
|
149
|
+
}
|
|
150
|
+
/** Check if an agent has a running Slack bot. */
|
|
151
|
+
hasBot(slug) {
|
|
152
|
+
return this.bots.has(slug);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Deliver a team message to an agent's Slack bot.
|
|
156
|
+
* Returns the agent's response text, or null if delivery failed.
|
|
157
|
+
*/
|
|
158
|
+
async deliverTeamMessage(toSlug, fromName, fromSlug, content) {
|
|
159
|
+
const bot = this.bots.get(toSlug);
|
|
160
|
+
if (!bot)
|
|
161
|
+
return null;
|
|
162
|
+
try {
|
|
163
|
+
return await bot.receiveTeamMessage(fromName, fromSlug, content);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
logger.error({ err, toSlug, fromSlug }, 'Failed to deliver Slack team message via bot');
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Poll for new/removed agents with Slack tokens at the given interval.
|
|
172
|
+
*/
|
|
173
|
+
startPolling(intervalMs) {
|
|
174
|
+
if (this.pollInterval)
|
|
175
|
+
clearInterval(this.pollInterval);
|
|
176
|
+
this.pollInterval = setInterval(async () => {
|
|
177
|
+
try {
|
|
178
|
+
await this.pollForChanges();
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
logger.error({ err }, 'Slack bot polling error');
|
|
182
|
+
}
|
|
183
|
+
}, intervalMs);
|
|
184
|
+
}
|
|
185
|
+
async pollForChanges() {
|
|
186
|
+
const mgr = this.gateway.getAgentManager();
|
|
187
|
+
mgr.invalidateCache();
|
|
188
|
+
const allAgents = mgr.listAll();
|
|
189
|
+
// Find agents that should have Slack bots (need both tokens)
|
|
190
|
+
const shouldHaveBot = new Set();
|
|
191
|
+
for (const agent of allAgents) {
|
|
192
|
+
if (agent.slackBotToken && agent.slackAppToken) {
|
|
193
|
+
shouldHaveBot.add(agent.slug);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Start new bots
|
|
197
|
+
for (const slug of shouldHaveBot) {
|
|
198
|
+
if (!this.bots.has(slug)) {
|
|
199
|
+
logger.info({ slug }, 'Detected new agent with Slack tokens — starting bot');
|
|
200
|
+
try {
|
|
201
|
+
await this.startBot(slug);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
logger.error({ err, slug }, 'Failed to start new Slack agent bot');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Stop removed bots
|
|
209
|
+
for (const slug of this.bots.keys()) {
|
|
210
|
+
if (!shouldHaveBot.has(slug)) {
|
|
211
|
+
logger.info({ slug }, 'Agent no longer has Slack tokens — stopping bot');
|
|
212
|
+
await this.stopBot(slug);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Write status to disk so the dashboard can read it. */
|
|
217
|
+
startStatusWriter() {
|
|
218
|
+
if (this.statusInterval)
|
|
219
|
+
clearInterval(this.statusInterval);
|
|
220
|
+
const writeStatus = () => {
|
|
221
|
+
try {
|
|
222
|
+
const statuses = {};
|
|
223
|
+
for (const [slug, status] of this.getStatuses()) {
|
|
224
|
+
statuses[slug] = status;
|
|
225
|
+
}
|
|
226
|
+
writeFileSync(this.statusFilePath, JSON.stringify(statuses, null, 2));
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Non-fatal
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
writeStatus();
|
|
233
|
+
this.statusInterval = setInterval(writeStatus, 10_000);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=slack-bot-manager.js.map
|