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.
Files changed (190) hide show
  1. package/.env.example +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +795 -0
  4. package/dist/agent/agent-manager.d.ts +69 -0
  5. package/dist/agent/agent-manager.js +441 -0
  6. package/dist/agent/assistant.d.ts +225 -0
  7. package/dist/agent/assistant.js +3888 -0
  8. package/dist/agent/auto-update.d.ts +32 -0
  9. package/dist/agent/auto-update.js +186 -0
  10. package/dist/agent/daily-planner.d.ts +24 -0
  11. package/dist/agent/daily-planner.js +379 -0
  12. package/dist/agent/execution-advisor.d.ts +10 -0
  13. package/dist/agent/execution-advisor.js +272 -0
  14. package/dist/agent/hooks.d.ts +45 -0
  15. package/dist/agent/hooks.js +564 -0
  16. package/dist/agent/insight-engine.d.ts +66 -0
  17. package/dist/agent/insight-engine.js +225 -0
  18. package/dist/agent/intent-classifier.d.ts +48 -0
  19. package/dist/agent/intent-classifier.js +214 -0
  20. package/dist/agent/link-extractor.d.ts +19 -0
  21. package/dist/agent/link-extractor.js +90 -0
  22. package/dist/agent/mcp-bridge.d.ts +62 -0
  23. package/dist/agent/mcp-bridge.js +435 -0
  24. package/dist/agent/metacognition.d.ts +66 -0
  25. package/dist/agent/metacognition.js +221 -0
  26. package/dist/agent/orchestrator.d.ts +81 -0
  27. package/dist/agent/orchestrator.js +790 -0
  28. package/dist/agent/profiles.d.ts +22 -0
  29. package/dist/agent/profiles.js +91 -0
  30. package/dist/agent/prompt-cache.d.ts +24 -0
  31. package/dist/agent/prompt-cache.js +68 -0
  32. package/dist/agent/prompt-evolver.d.ts +28 -0
  33. package/dist/agent/prompt-evolver.js +279 -0
  34. package/dist/agent/role-scaffolds.d.ts +28 -0
  35. package/dist/agent/role-scaffolds.js +433 -0
  36. package/dist/agent/safe-restart.d.ts +41 -0
  37. package/dist/agent/safe-restart.js +150 -0
  38. package/dist/agent/self-improve.d.ts +66 -0
  39. package/dist/agent/self-improve.js +1706 -0
  40. package/dist/agent/session-event-log.d.ts +114 -0
  41. package/dist/agent/session-event-log.js +233 -0
  42. package/dist/agent/skill-extractor.d.ts +72 -0
  43. package/dist/agent/skill-extractor.js +435 -0
  44. package/dist/agent/source-mods.d.ts +61 -0
  45. package/dist/agent/source-mods.js +230 -0
  46. package/dist/agent/source-preflight.d.ts +25 -0
  47. package/dist/agent/source-preflight.js +100 -0
  48. package/dist/agent/stall-guard.d.ts +62 -0
  49. package/dist/agent/stall-guard.js +109 -0
  50. package/dist/agent/strategic-planner.d.ts +60 -0
  51. package/dist/agent/strategic-planner.js +352 -0
  52. package/dist/agent/team-bus.d.ts +89 -0
  53. package/dist/agent/team-bus.js +556 -0
  54. package/dist/agent/team-router.d.ts +26 -0
  55. package/dist/agent/team-router.js +37 -0
  56. package/dist/agent/tool-loop-detector.d.ts +59 -0
  57. package/dist/agent/tool-loop-detector.js +242 -0
  58. package/dist/agent/workflow-runner.d.ts +36 -0
  59. package/dist/agent/workflow-runner.js +317 -0
  60. package/dist/agent/workflow-variables.d.ts +16 -0
  61. package/dist/agent/workflow-variables.js +62 -0
  62. package/dist/channels/discord-agent-bot.d.ts +101 -0
  63. package/dist/channels/discord-agent-bot.js +881 -0
  64. package/dist/channels/discord-bot-manager.d.ts +80 -0
  65. package/dist/channels/discord-bot-manager.js +262 -0
  66. package/dist/channels/discord-utils.d.ts +51 -0
  67. package/dist/channels/discord-utils.js +293 -0
  68. package/dist/channels/discord.d.ts +12 -0
  69. package/dist/channels/discord.js +1832 -0
  70. package/dist/channels/slack-agent-bot.d.ts +73 -0
  71. package/dist/channels/slack-agent-bot.js +320 -0
  72. package/dist/channels/slack-bot-manager.d.ts +66 -0
  73. package/dist/channels/slack-bot-manager.js +236 -0
  74. package/dist/channels/slack-utils.d.ts +39 -0
  75. package/dist/channels/slack-utils.js +189 -0
  76. package/dist/channels/slack.d.ts +11 -0
  77. package/dist/channels/slack.js +196 -0
  78. package/dist/channels/telegram.d.ts +10 -0
  79. package/dist/channels/telegram.js +235 -0
  80. package/dist/channels/webhook.d.ts +9 -0
  81. package/dist/channels/webhook.js +78 -0
  82. package/dist/channels/whatsapp.d.ts +11 -0
  83. package/dist/channels/whatsapp.js +181 -0
  84. package/dist/cli/chat.d.ts +14 -0
  85. package/dist/cli/chat.js +220 -0
  86. package/dist/cli/cron.d.ts +17 -0
  87. package/dist/cli/cron.js +552 -0
  88. package/dist/cli/dashboard.d.ts +15 -0
  89. package/dist/cli/dashboard.js +17677 -0
  90. package/dist/cli/index.d.ts +3 -0
  91. package/dist/cli/index.js +2474 -0
  92. package/dist/cli/routes/delegations.d.ts +19 -0
  93. package/dist/cli/routes/delegations.js +154 -0
  94. package/dist/cli/routes/digest.d.ts +17 -0
  95. package/dist/cli/routes/digest.js +375 -0
  96. package/dist/cli/routes/goals.d.ts +14 -0
  97. package/dist/cli/routes/goals.js +258 -0
  98. package/dist/cli/routes/workflows.d.ts +18 -0
  99. package/dist/cli/routes/workflows.js +97 -0
  100. package/dist/cli/setup.d.ts +8 -0
  101. package/dist/cli/setup.js +619 -0
  102. package/dist/cli/tunnel.d.ts +35 -0
  103. package/dist/cli/tunnel.js +141 -0
  104. package/dist/config.d.ts +145 -0
  105. package/dist/config.js +278 -0
  106. package/dist/events/bus.d.ts +43 -0
  107. package/dist/events/bus.js +136 -0
  108. package/dist/gateway/cron-scheduler.d.ts +166 -0
  109. package/dist/gateway/cron-scheduler.js +1767 -0
  110. package/dist/gateway/delivery-queue.d.ts +30 -0
  111. package/dist/gateway/delivery-queue.js +110 -0
  112. package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
  113. package/dist/gateway/heartbeat-scheduler.js +1298 -0
  114. package/dist/gateway/heartbeat.d.ts +3 -0
  115. package/dist/gateway/heartbeat.js +3 -0
  116. package/dist/gateway/lanes.d.ts +24 -0
  117. package/dist/gateway/lanes.js +76 -0
  118. package/dist/gateway/notifications.d.ts +29 -0
  119. package/dist/gateway/notifications.js +75 -0
  120. package/dist/gateway/router.d.ts +210 -0
  121. package/dist/gateway/router.js +1330 -0
  122. package/dist/index.d.ts +12 -0
  123. package/dist/index.js +1015 -0
  124. package/dist/memory/chunker.d.ts +28 -0
  125. package/dist/memory/chunker.js +226 -0
  126. package/dist/memory/consolidation.d.ts +44 -0
  127. package/dist/memory/consolidation.js +171 -0
  128. package/dist/memory/context-assembler.d.ts +50 -0
  129. package/dist/memory/context-assembler.js +149 -0
  130. package/dist/memory/embeddings.d.ts +38 -0
  131. package/dist/memory/embeddings.js +180 -0
  132. package/dist/memory/graph-store.d.ts +66 -0
  133. package/dist/memory/graph-store.js +613 -0
  134. package/dist/memory/mmr.d.ts +21 -0
  135. package/dist/memory/mmr.js +75 -0
  136. package/dist/memory/search.d.ts +26 -0
  137. package/dist/memory/search.js +67 -0
  138. package/dist/memory/store.d.ts +530 -0
  139. package/dist/memory/store.js +2022 -0
  140. package/dist/security/integrity.d.ts +24 -0
  141. package/dist/security/integrity.js +58 -0
  142. package/dist/security/patterns.d.ts +34 -0
  143. package/dist/security/patterns.js +110 -0
  144. package/dist/security/scanner.d.ts +32 -0
  145. package/dist/security/scanner.js +263 -0
  146. package/dist/tools/admin-tools.d.ts +12 -0
  147. package/dist/tools/admin-tools.js +1278 -0
  148. package/dist/tools/external-tools.d.ts +11 -0
  149. package/dist/tools/external-tools.js +1327 -0
  150. package/dist/tools/goal-tools.d.ts +9 -0
  151. package/dist/tools/goal-tools.js +159 -0
  152. package/dist/tools/mcp-server.d.ts +13 -0
  153. package/dist/tools/mcp-server.js +141 -0
  154. package/dist/tools/memory-tools.d.ts +10 -0
  155. package/dist/tools/memory-tools.js +568 -0
  156. package/dist/tools/session-tools.d.ts +6 -0
  157. package/dist/tools/session-tools.js +146 -0
  158. package/dist/tools/shared.d.ts +216 -0
  159. package/dist/tools/shared.js +340 -0
  160. package/dist/tools/team-tools.d.ts +6 -0
  161. package/dist/tools/team-tools.js +447 -0
  162. package/dist/tools/tool-meta.d.ts +34 -0
  163. package/dist/tools/tool-meta.js +133 -0
  164. package/dist/tools/vault-tools.d.ts +8 -0
  165. package/dist/tools/vault-tools.js +457 -0
  166. package/dist/types.d.ts +716 -0
  167. package/dist/types.js +16 -0
  168. package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
  169. package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
  170. package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
  171. package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
  172. package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
  173. package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
  174. package/dist/vault-migrations/helpers.d.ts +14 -0
  175. package/dist/vault-migrations/helpers.js +44 -0
  176. package/dist/vault-migrations/runner.d.ts +14 -0
  177. package/dist/vault-migrations/runner.js +139 -0
  178. package/dist/vault-migrations/types.d.ts +42 -0
  179. package/dist/vault-migrations/types.js +9 -0
  180. package/install.sh +320 -0
  181. package/package.json +84 -0
  182. package/scripts/postinstall.js +125 -0
  183. package/vault/00-System/AGENTS.md +66 -0
  184. package/vault/00-System/CRON.md +71 -0
  185. package/vault/00-System/HEARTBEAT.md +58 -0
  186. package/vault/00-System/MEMORY.md +16 -0
  187. package/vault/00-System/SOUL.md +96 -0
  188. package/vault/05-Tasks/TASKS.md +19 -0
  189. package/vault/06-Templates/_Daily-Template.md +28 -0
  190. 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