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,881 @@
1
+ /**
2
+ * Clementine TypeScript — Discord agent bot client.
3
+ *
4
+ * A discord.js Client wrapper for a single agent.
5
+ * Handles: DMs + guild channel messages → gateway → stream response.
6
+ * Slash commands: /plan, /deep, /quick, /opus, /model, /clear, /help.
7
+ *
8
+ * Channel discovery (in priority order):
9
+ * 1. Explicit `discordChannelId` from agent config
10
+ * 2. Auto-discover by matching `channelName` in the guild
11
+ * 3. Falls back to listening in ALL text channels the bot can see
12
+ *
13
+ * DMs are always enabled for the owner.
14
+ */
15
+ import { ActionRowBuilder, ActivityType, ChannelType, Client, EmbedBuilder, Events, GatewayIntentBits, ModalBuilder, Partials, REST, Routes, SlashCommandBuilder, TextInputBuilder, TextInputStyle, } from 'discord.js';
16
+ import pino from 'pino';
17
+ import { chunkText, sendChunked, DiscordStreamingMessage, friendlyToolName, sanitizeResponse } from './discord-utils.js';
18
+ import { MODELS } from '../config.js';
19
+ import * as cronParser from 'cron-parser';
20
+ const logger = pino({ name: 'clementine.agent-bot' });
21
+ /** Format a duration in minutes to a compact human string. */
22
+ function formatAgentDuration(minutes) {
23
+ if (minutes < 1)
24
+ return '<1m';
25
+ if (minutes < 60)
26
+ return `${minutes}m`;
27
+ if (minutes < 1440) {
28
+ const h = Math.floor(minutes / 60);
29
+ const m = minutes % 60;
30
+ return m > 0 ? `${h}h${m}m` : `${h}h`;
31
+ }
32
+ const d = Math.floor(minutes / 1440);
33
+ const h = Math.floor((minutes % 1440) / 60);
34
+ return h > 0 ? `${d}d${h}h` : `${d}d`;
35
+ }
36
+ // ── Slash commands shared by all agent bots ──────────────────────────
37
+ const agentSlashCommands = [
38
+ new SlashCommandBuilder().setName('plan').setDescription('Break a task into parallel steps')
39
+ .addStringOption(o => o.setName('task').setDescription('What to plan').setRequired(true)),
40
+ new SlashCommandBuilder().setName('deep').setDescription('Extended mode (100 turns) for heavy tasks')
41
+ .addStringOption(o => o.setName('message').setDescription('Your message').setRequired(true)),
42
+ new SlashCommandBuilder().setName('quick').setDescription('Quick reply using Haiku model')
43
+ .addStringOption(o => o.setName('message').setDescription('Your message').setRequired(true)),
44
+ new SlashCommandBuilder().setName('opus').setDescription('Deep reply using Opus model')
45
+ .addStringOption(o => o.setName('message').setDescription('Your message').setRequired(true)),
46
+ new SlashCommandBuilder().setName('model').setDescription('Switch default model')
47
+ .addStringOption(o => o.setName('tier').setDescription('Model tier').setRequired(true)
48
+ .addChoices({ name: 'Haiku', value: 'haiku' }, { name: 'Sonnet', value: 'sonnet' }, { name: 'Opus', value: 'opus' })),
49
+ new SlashCommandBuilder().setName('clear').setDescription('Reset conversation session'),
50
+ new SlashCommandBuilder().setName('help').setDescription('Show all available commands'),
51
+ ];
52
+ export class AgentBotClient {
53
+ client;
54
+ config;
55
+ gateway;
56
+ status = 'offline';
57
+ errorMessage;
58
+ /** Resolved channel IDs (set on ready, after auto-discovery). */
59
+ resolvedChannelIds = [];
60
+ /** Pinned status embed message (edited in-place on state changes). */
61
+ statusEmbedMessage = null;
62
+ statusEmbedDebounce = null;
63
+ /** Check if a user is authorized to interact with this agent bot. */
64
+ isAuthorized(userId) {
65
+ if (this.config.ownerId && userId === this.config.ownerId)
66
+ return true;
67
+ if (this.config.allowedUsers?.includes(userId))
68
+ return true;
69
+ return false;
70
+ }
71
+ /** Check if a user is the owner (not just an allowed member). */
72
+ isOwner(userId) {
73
+ return !!(this.config.ownerId && userId === this.config.ownerId);
74
+ }
75
+ /** Return the session key prefix for channel sessions based on user role. */
76
+ channelPrefix(userId) {
77
+ return this.isOwner(userId) ? 'discord:channel' : 'discord:member';
78
+ }
79
+ /** Return the session key prefix for DM sessions based on user role. */
80
+ dmPrefix(userId) {
81
+ return this.isOwner(userId) ? 'discord:agent' : 'discord:member-dm';
82
+ }
83
+ constructor(config, gateway) {
84
+ this.config = config;
85
+ this.gateway = gateway;
86
+ this.client = new Client({
87
+ intents: [
88
+ GatewayIntentBits.Guilds,
89
+ GatewayIntentBits.GuildMessages,
90
+ GatewayIntentBits.MessageContent,
91
+ GatewayIntentBits.DirectMessages,
92
+ ],
93
+ partials: [Partials.Channel], // Required for DM events
94
+ });
95
+ }
96
+ async start() {
97
+ this.status = 'connecting';
98
+ this.client.once(Events.ClientReady, async (readyClient) => {
99
+ this.status = 'online';
100
+ this.errorMessage = undefined;
101
+ // Resolve channels and pre-register them as "seen" in the gateway
102
+ // so the new-channel check-in gate doesn't fire for known agent channels
103
+ this.resolvedChannelIds = this.discoverChannels();
104
+ for (const chId of this.resolvedChannelIds) {
105
+ this.gateway.markChannelSeen(`discord:channel:${chId}`);
106
+ }
107
+ // Register slash commands for this bot
108
+ try {
109
+ const rest = new REST().setToken(this.config.token);
110
+ await rest.put(Routes.applicationCommands(readyClient.user.id), {
111
+ body: agentSlashCommands.map(c => c.toJSON()),
112
+ });
113
+ logger.info({ slug: this.config.slug, count: agentSlashCommands.length }, `Registered ${agentSlashCommands.length} slash commands`);
114
+ }
115
+ catch (err) {
116
+ logger.error({ err, slug: this.config.slug }, 'Failed to register slash commands');
117
+ }
118
+ logger.info({ slug: this.config.slug, botTag: readyClient.user.tag, channels: this.resolvedChannelIds }, `Agent bot online: ${this.config.profile.name}`);
119
+ // Set presence to show the agent's role
120
+ readyClient.user.setPresence({
121
+ status: 'online',
122
+ activities: [{
123
+ name: this.config.profile.description.slice(0, 128),
124
+ type: ActivityType.Custom,
125
+ }],
126
+ });
127
+ // Send startup status to owner's DMs
128
+ await this.sendStartupStatus();
129
+ // Send status embed to the agent's primary channel (if available)
130
+ if (this.config.cronScheduler && this.resolvedChannelIds.length > 0) {
131
+ await this.sendOrUpdateStatusEmbed();
132
+ // Auto-update status embed on state changes (debounced)
133
+ this.config.cronScheduler.onStatusChange(() => {
134
+ if (this.statusEmbedDebounce)
135
+ clearTimeout(this.statusEmbedDebounce);
136
+ this.statusEmbedDebounce = setTimeout(() => {
137
+ this.sendOrUpdateStatusEmbed().catch(() => { });
138
+ }, 2000);
139
+ });
140
+ }
141
+ });
142
+ this.client.on(Events.InteractionCreate, async (interaction) => {
143
+ try {
144
+ await this.handleInteraction(interaction);
145
+ }
146
+ catch (err) {
147
+ logger.error({ err, slug: this.config.slug }, 'Unhandled error in agent bot interaction handler');
148
+ }
149
+ });
150
+ this.client.on(Events.MessageCreate, async (message) => {
151
+ try {
152
+ await this.handleMessage(message);
153
+ }
154
+ catch (err) {
155
+ logger.error({ err, slug: this.config.slug }, 'Unhandled error in agent bot message handler');
156
+ }
157
+ });
158
+ this.client.on(Events.Error, (err) => {
159
+ this.status = 'error';
160
+ this.errorMessage = String(err);
161
+ logger.error({ err, slug: this.config.slug }, 'Agent bot error');
162
+ });
163
+ try {
164
+ await this.client.login(this.config.token);
165
+ }
166
+ catch (err) {
167
+ this.status = 'error';
168
+ this.errorMessage = String(err);
169
+ logger.error({ err, slug: this.config.slug }, 'Agent bot login failed');
170
+ throw err;
171
+ }
172
+ }
173
+ async stop() {
174
+ try {
175
+ this.client.destroy();
176
+ }
177
+ catch {
178
+ // ignore
179
+ }
180
+ this.status = 'offline';
181
+ logger.info({ slug: this.config.slug }, 'Agent bot stopped');
182
+ }
183
+ getStatus() {
184
+ return {
185
+ status: this.status,
186
+ botTag: this.client.user?.tag,
187
+ avatarUrl: this.client.user?.displayAvatarURL({ size: 128, extension: 'png' }),
188
+ error: this.errorMessage,
189
+ };
190
+ }
191
+ getChannelIds() {
192
+ return this.resolvedChannelIds;
193
+ }
194
+ /**
195
+ * Discover which channels this bot should listen in.
196
+ *
197
+ * Priority:
198
+ * 1. Explicit channelIds from config (e.g. discordChannelId in agent.md)
199
+ * 2. Match by channelName in any guild the bot is in
200
+ * 3. All text channels the bot can see (fallback for simple setups)
201
+ */
202
+ discoverChannels() {
203
+ // 1. Explicit IDs
204
+ if (this.config.channelIds && this.config.channelIds.length > 0) {
205
+ logger.info({ slug: this.config.slug, channelIds: this.config.channelIds }, 'Using explicit channel IDs');
206
+ return this.config.channelIds;
207
+ }
208
+ // 2. Match by channelName (supports single string or array of names)
209
+ const channelNameConfig = this.config.profile.team?.channelName;
210
+ if (channelNameConfig) {
211
+ const channelNames = Array.isArray(channelNameConfig) ? channelNameConfig : [channelNameConfig];
212
+ const matched = [];
213
+ for (const guild of this.client.guilds.cache.values()) {
214
+ for (const channel of guild.channels.cache.values()) {
215
+ if (channel.type === ChannelType.GuildText && channelNames.includes(channel.name)) {
216
+ matched.push(channel.id);
217
+ }
218
+ }
219
+ }
220
+ if (matched.length > 0) {
221
+ logger.info({ slug: this.config.slug, channelNames, matched }, 'Auto-discovered channels by name');
222
+ return matched;
223
+ }
224
+ logger.warn({ slug: this.config.slug, channelNames }, 'No channels found matching channelName(s) — falling back to all visible text channels');
225
+ }
226
+ // 3. Fallback: all text channels the bot can see
227
+ const all = [];
228
+ for (const guild of this.client.guilds.cache.values()) {
229
+ for (const channel of guild.channels.cache.values()) {
230
+ if (channel.type === ChannelType.GuildText) {
231
+ all.push(channel.id);
232
+ }
233
+ }
234
+ }
235
+ logger.info({ slug: this.config.slug, count: all.length }, 'Fallback: listening in all visible text channels');
236
+ return all;
237
+ }
238
+ /**
239
+ * Send a notification to the owner's DMs on behalf of this agent bot.
240
+ * Used by BotManager.sendAsAgent() for cron result routing.
241
+ */
242
+ async sendNotification(text, embed) {
243
+ if (this.status !== 'online')
244
+ throw new Error(`Bot ${this.config.slug} is not online`);
245
+ const owner = await this.client.users.fetch(this.config.ownerId, { force: true });
246
+ const dmChannel = await owner.createDM();
247
+ if (embed) {
248
+ await dmChannel.send({ embeds: [embed] });
249
+ }
250
+ else {
251
+ const { chunkText } = await import('./discord-utils.js');
252
+ for (const chunk of chunkText(text, 1900)) {
253
+ await dmChannel.send(chunk);
254
+ }
255
+ }
256
+ }
257
+ /** Send a startup status embed to the owner's DMs. */
258
+ async sendStartupStatus() {
259
+ if (!this.config.ownerId)
260
+ return;
261
+ try {
262
+ const owner = await this.client.users.fetch(this.config.ownerId, { force: true });
263
+ const dmChannel = await owner.createDM();
264
+ // Use the rich agent status embed if cronScheduler is available
265
+ const embed = this.config.cronScheduler
266
+ ? this.buildAgentStatusEmbed()
267
+ : new EmbedBuilder()
268
+ .setColor(0x22c55e)
269
+ .setTitle(`${this.config.profile.name} is online`)
270
+ .setDescription(this.config.profile.description)
271
+ .addFields({ name: 'Model', value: this.config.profile.model || 'sonnet', inline: true }, { name: 'Tier', value: String(this.config.profile.tier), inline: true })
272
+ .setFooter({ text: `Agent bot \u00b7 ${this.client.user?.tag ?? 'unknown'}` })
273
+ .setTimestamp();
274
+ await dmChannel.send({ embeds: [embed] });
275
+ logger.info({ slug: this.config.slug }, 'Sent startup status embed to owner DMs');
276
+ }
277
+ catch (err) {
278
+ logger.error({ err, slug: this.config.slug }, 'Failed to send startup status embed');
279
+ }
280
+ }
281
+ // ── Agent-scoped status embed ──────────────────────────────────────
282
+ buildAgentStatusEmbed() {
283
+ const now = new Date();
284
+ const slug = this.config.slug;
285
+ const profile = this.config.profile;
286
+ const cs = this.config.cronScheduler;
287
+ // Get agent-scoped job definitions
288
+ const allJobs = cs?.getJobDefinitions() ?? [];
289
+ const myJobs = allJobs.filter(j => j.agentSlug === slug);
290
+ const activeJobs = myJobs.filter(j => j.active);
291
+ // Agent-scoped today stats from run log
292
+ let myOk = 0, myErrors = 0, mySkipped = 0, myTotal = 0;
293
+ if (cs) {
294
+ const midnight = new Date();
295
+ midnight.setHours(0, 0, 0, 0);
296
+ const midnightISO = midnight.toISOString();
297
+ for (const job of myJobs) {
298
+ const entries = cs.runLog.readRecent(job.name, 50);
299
+ for (const e of entries) {
300
+ if (e.startedAt < midnightISO)
301
+ break;
302
+ myTotal++;
303
+ if (e.status === 'ok')
304
+ myOk++;
305
+ else if (e.status === 'error')
306
+ myErrors++;
307
+ else if (e.status === 'skipped')
308
+ mySkipped++;
309
+ }
310
+ }
311
+ }
312
+ // Running jobs for this agent
313
+ const runningJobs = cs?.getRunningJobs() ?? [];
314
+ const myRunning = runningJobs.filter(j => myJobs.some(mj => mj.name === j));
315
+ // Health color
316
+ const healthColor = myErrors > 0 ? 0xE74C3C
317
+ : myRunning.length > 0 ? 0xF39C12
318
+ : 0x2ECC71;
319
+ const embed = new EmbedBuilder()
320
+ .setTitle(`${profile.name} Status`)
321
+ .setDescription(profile.description)
322
+ .setColor(healthColor)
323
+ .setTimestamp(now)
324
+ .setFooter({ text: `Auto-updates \u00b7 !dashboard to refresh \u00b7 ${slug}` });
325
+ if (profile.avatar) {
326
+ embed.setThumbnail(profile.avatar);
327
+ }
328
+ // ── Active work
329
+ if (myRunning.length > 0) {
330
+ const runningItems = myRunning.map(j => {
331
+ // Strip agent slug prefix from job name
332
+ const display = j.startsWith(`${slug}:`) ? j.slice(slug.length + 1) : j;
333
+ return `\u23F3 ${display}`;
334
+ });
335
+ embed.addFields({
336
+ name: `\u2699\uFE0F Active (${runningItems.length})`,
337
+ value: runningItems.join('\n'),
338
+ inline: false,
339
+ });
340
+ }
341
+ // ── Today's stats
342
+ const statsLine = `\u2705 ${myOk} passed` +
343
+ (myErrors > 0 ? ` \u00b7 \u274C ${myErrors} failed` : '') +
344
+ (mySkipped > 0 ? ` \u00b7 \u23ED ${mySkipped} skipped` : '');
345
+ embed.addFields({ name: `\u{1F4CA} Today (${myTotal} runs)`, value: statsLine, inline: false });
346
+ // ── Last run results per job
347
+ if (cs && myJobs.length > 0) {
348
+ const lastRunLines = [];
349
+ for (const job of activeJobs) {
350
+ const recent = cs.runLog.readRecent(job.name, 1);
351
+ const display = job.name.startsWith(`${slug}:`) ? job.name.slice(slug.length + 1) : job.name;
352
+ if (recent.length > 0) {
353
+ const r = recent[0];
354
+ const icon = r.status === 'ok' ? '\u2705' : r.status === 'error' ? '\u274C' : '\u23ED';
355
+ const ago = Math.round((Date.now() - new Date(r.finishedAt).getTime()) / 60000);
356
+ lastRunLines.push(`${icon} ${display} \u2014 ${formatAgentDuration(ago)} ago`);
357
+ }
358
+ else {
359
+ lastRunLines.push(`\u2B1C ${display} \u2014 never run`);
360
+ }
361
+ }
362
+ if (lastRunLines.length > 0) {
363
+ embed.addFields({ name: '\u{1F4DD} Last Runs', value: lastRunLines.join('\n'), inline: false });
364
+ }
365
+ }
366
+ // ── Next runs for this agent
367
+ const upcoming = [];
368
+ for (const job of activeJobs) {
369
+ try {
370
+ const fields = job.schedule.trim().split(/\s+/);
371
+ const expr = fields.length === 6 ? fields.slice(1).join(' ') : job.schedule;
372
+ const interval = cronParser.CronExpressionParser.parse(expr);
373
+ const next = interval.next().toDate();
374
+ upcoming.push({ name: job.name, nextMs: next.getTime() });
375
+ }
376
+ catch { /* skip */ }
377
+ }
378
+ upcoming.sort((a, b) => a.nextMs - b.nextMs);
379
+ if (upcoming.length > 0) {
380
+ const nextLines = upcoming.slice(0, 5).map(u => {
381
+ const diffMs = u.nextMs - now.getTime();
382
+ const diffMin = Math.round(diffMs / 60000);
383
+ const display = u.name.startsWith(`${slug}:`) ? u.name.slice(slug.length + 1) : u.name;
384
+ return `\`${formatAgentDuration(diffMin).padStart(4)}\` ${display}`;
385
+ });
386
+ if (upcoming.length > 5)
387
+ nextLines.push(`_+${upcoming.length - 5} more_`);
388
+ embed.addFields({ name: '\u{1F4C5} Next Runs', value: nextLines.join('\n'), inline: false });
389
+ }
390
+ // ── Config + Schedule summary
391
+ const disabledCount = myJobs.length - activeJobs.length;
392
+ const configParts = [
393
+ `Model: ${profile.model || 'sonnet'}`,
394
+ `Tier: ${profile.tier}`,
395
+ `Jobs: ${activeJobs.length} active` + (disabledCount > 0 ? `, ${disabledCount} disabled` : ''),
396
+ ];
397
+ embed.addFields({ name: '\u{1F527} Config', value: configParts.join(' \u00b7 '), inline: false });
398
+ return embed;
399
+ }
400
+ async sendOrUpdateStatusEmbed() {
401
+ try {
402
+ const embed = this.buildAgentStatusEmbed();
403
+ if (this.statusEmbedMessage) {
404
+ try {
405
+ await this.statusEmbedMessage.edit({ embeds: [embed] });
406
+ return;
407
+ }
408
+ catch {
409
+ this.statusEmbedMessage = null;
410
+ }
411
+ }
412
+ // Send to the agent's primary channel
413
+ if (this.resolvedChannelIds.length > 0) {
414
+ const channelId = this.resolvedChannelIds[0];
415
+ const channel = this.client.channels.cache.get(channelId);
416
+ if (channel && 'send' in channel) {
417
+ this.statusEmbedMessage = await channel.send({ embeds: [embed] });
418
+ try {
419
+ await this.statusEmbedMessage.pin();
420
+ }
421
+ catch { /* may lack perms */ }
422
+ }
423
+ }
424
+ }
425
+ catch (err) {
426
+ logger.error({ err, slug: this.config.slug }, 'Failed to update agent status embed');
427
+ }
428
+ }
429
+ /**
430
+ * Receive an inter-agent team message. Posts an embed showing the incoming
431
+ * message, then triggers the agent to process and respond in-channel.
432
+ */
433
+ /** Track recent team message content hashes to prevent duplicate embeds. */
434
+ recentTeamMessageHashes = new Map();
435
+ async receiveTeamMessage(fromName, fromSlug, content) {
436
+ if (this.resolvedChannelIds.length === 0) {
437
+ logger.warn({ slug: this.config.slug }, 'No channels to deliver team message to');
438
+ return '(no channels available)';
439
+ }
440
+ const channelId = this.resolvedChannelIds[0];
441
+ const channel = this.client.channels.cache.get(channelId);
442
+ if (!channel || channel.type !== ChannelType.GuildText) {
443
+ logger.warn({ slug: this.config.slug, channelId }, 'Channel not found for team message delivery');
444
+ return '(channel not found)';
445
+ }
446
+ // Dedup: reject identical messages within 5 minutes (prevents embed spam)
447
+ const { createHash } = await import('node:crypto');
448
+ const contentHash = createHash('sha256').update(`${fromSlug}:${content.trim()}`).digest('hex').slice(0, 12);
449
+ const now = Date.now();
450
+ const lastSeen = this.recentTeamMessageHashes.get(contentHash) ?? 0;
451
+ if (now - lastSeen < 300_000) {
452
+ logger.info({ slug: this.config.slug, from: fromSlug }, 'Duplicate team message suppressed (already posted)');
453
+ return '(duplicate message suppressed — already delivered recently)';
454
+ }
455
+ this.recentTeamMessageHashes.set(contentHash, now);
456
+ // Prune old entries
457
+ if (this.recentTeamMessageHashes.size > 50) {
458
+ for (const [key, ts] of this.recentTeamMessageHashes) {
459
+ if (now - ts > 300_000)
460
+ this.recentTeamMessageHashes.delete(key);
461
+ }
462
+ }
463
+ // Post the incoming message as an embed so it's visible in the channel
464
+ const embed = new EmbedBuilder()
465
+ .setColor(0x5865F2) // Discord blurple
466
+ .setAuthor({ name: `${fromName} via team message` })
467
+ .setDescription(content.length > 4096 ? content.slice(0, 4093) + '...' : content)
468
+ .setTimestamp();
469
+ await channel.send({ embeds: [embed] });
470
+ // Run the task through the unleashed pipeline — gives the agent full
471
+ // multi-phase autonomous execution instead of the 5-minute chat timeout.
472
+ const streamer = new DiscordStreamingMessage(channel);
473
+ await streamer.start();
474
+ try {
475
+ const response = await this.gateway.handleTeamTask(fromName, fromSlug, content, this.config.profile, async (token) => {
476
+ await streamer.update(token);
477
+ });
478
+ await streamer.finalize(response);
479
+ logger.info({ slug: this.config.slug, from: fromSlug }, 'Processed team message');
480
+ return response;
481
+ }
482
+ catch (err) {
483
+ logger.error({ err, slug: this.config.slug }, 'Failed to process team message');
484
+ const errMsg = `Something went wrong processing a team message: ${sanitizeResponse(String(err))}`;
485
+ await streamer.finalize(errMsg);
486
+ return errMsg;
487
+ }
488
+ }
489
+ // ── Slash command + button interaction handler ──────────────────────
490
+ async handleInteraction(interaction) {
491
+ // ── Slash commands ──────────────────────────────────────────
492
+ if (interaction.isChatInputCommand()) {
493
+ const cmd = interaction;
494
+ // Access control: owner + allowedUsers
495
+ if (!this.isAuthorized(cmd.user.id)) {
496
+ await cmd.reply({ content: 'You don\'t have access to this agent.', ephemeral: true });
497
+ return;
498
+ }
499
+ const cmdPrefix = this.channelPrefix(cmd.user.id);
500
+ const cmdDmPrefix = this.dmPrefix(cmd.user.id);
501
+ const sessionKey = cmd.channel?.isDMBased()
502
+ ? `${cmdDmPrefix}:${this.config.slug}:${cmd.user.id}`
503
+ : `${cmdPrefix}:${cmd.channelId}:${cmd.user.id}`;
504
+ // Set agent profile for this session
505
+ this.gateway.setSessionProfile(sessionKey, this.config.slug);
506
+ const name = cmd.commandName;
507
+ // /help
508
+ if (name === 'help') {
509
+ const agentName = this.config.profile.name;
510
+ await cmd.reply([
511
+ `**${agentName} Commands**`,
512
+ '`/plan <task>` — Break a task into parallel steps',
513
+ '`/deep <msg>` — Extended mode (100 turns)',
514
+ '`/quick <msg>` — Quick reply (Haiku) · `/opus <msg>` — Deep reply (Opus)',
515
+ '`/model [haiku|sonnet|opus]` — Switch default model',
516
+ '`/clear` — Reset conversation · `/help` — This message',
517
+ ].join('\n'));
518
+ return;
519
+ }
520
+ // /clear
521
+ if (name === 'clear') {
522
+ this.gateway.clearSession(sessionKey);
523
+ await cmd.reply('Session cleared.');
524
+ return;
525
+ }
526
+ // /model
527
+ if (name === 'model') {
528
+ const tier = cmd.options.getString('tier', true);
529
+ const t = tier.toLowerCase();
530
+ if (t in MODELS) {
531
+ this.gateway.setSessionModel(sessionKey, MODELS[t]);
532
+ await cmd.reply(`Model switched to **${t}** (\`${MODELS[t]}\`).`);
533
+ }
534
+ else {
535
+ const current = this.gateway.getSessionModel(sessionKey) ?? 'default';
536
+ await cmd.reply(`Current model: \`${current}\`\nOptions: /model haiku, /model sonnet, /model opus`);
537
+ }
538
+ return;
539
+ }
540
+ // /plan — with approval buttons
541
+ if (name === 'plan') {
542
+ const task = cmd.options.getString('task', true);
543
+ await cmd.deferReply();
544
+ await cmd.editReply(`Planning: _${task.slice(0, 100)}_...`);
545
+ if (!cmd.channel) {
546
+ await cmd.editReply('Could not access channel for plan.');
547
+ return;
548
+ }
549
+ const streamer = new DiscordStreamingMessage(cmd.channel);
550
+ await streamer.start();
551
+ await streamer.update('Planning...');
552
+ try {
553
+ const result = await this.gateway.handlePlan(sessionKey, task, async (updates) => {
554
+ const lines = [
555
+ `**Plan:** ${task.slice(0, 100)}`,
556
+ '',
557
+ ...updates.map((u, i) => {
558
+ const num = `[${i + 1}/${updates.length}]`;
559
+ const desc = u.description.slice(0, 60);
560
+ switch (u.status) {
561
+ case 'done': return `${num} ${desc} \u2713 (${Math.round((u.durationMs ?? 0) / 1000)}s)`;
562
+ case 'running': return `${num} ${desc} \u23f3 running...`;
563
+ case 'failed': return `${num} ${desc} \u2717 failed`;
564
+ default: return `${num} ${desc} \u25cb waiting`;
565
+ }
566
+ }),
567
+ ];
568
+ await streamer.update(lines.join('\n').slice(0, 1800));
569
+ }, async (_planSummary, steps) => {
570
+ const planPreview = `**Plan:** ${task.slice(0, 100)}\n\n` +
571
+ steps.map((s, i) => `${i + 1}. **${s.id}** — ${s.description.slice(0, 60)}`).join('\n');
572
+ if ('send' in cmd.channel) {
573
+ await sendChunked(cmd.channel, planPreview);
574
+ }
575
+ // Send approval buttons
576
+ const requestId = `plan-${Date.now()}`;
577
+ const buttons = [
578
+ { type: 2, style: 3, label: 'Approve', custom_id: `plan_${requestId}_approve` },
579
+ { type: 2, style: 1, label: 'Revise', custom_id: `plan_${requestId}_revise` },
580
+ { type: 2, style: 4, label: 'Cancel', custom_id: `plan_${requestId}_deny` },
581
+ ];
582
+ if ('send' in cmd.channel) {
583
+ await cmd.channel.send({
584
+ content: 'Approve this plan?',
585
+ components: [{ type: 1, components: buttons }],
586
+ });
587
+ }
588
+ const approvalResult = await this.gateway.requestApproval('Pending approval', requestId);
589
+ if (typeof approvalResult === 'string') {
590
+ if ('send' in cmd.channel) {
591
+ await cmd.channel.send('\u2728 *Revising plan...*');
592
+ }
593
+ return approvalResult;
594
+ }
595
+ if (approvalResult) {
596
+ const newStreamer = new DiscordStreamingMessage(cmd.channel);
597
+ await newStreamer.start();
598
+ await newStreamer.update('Executing plan...');
599
+ Object.assign(streamer, {
600
+ message: newStreamer.message,
601
+ lastEdit: newStreamer.lastEdit,
602
+ pendingText: '',
603
+ lastFlushedText: '',
604
+ isFinal: false,
605
+ });
606
+ }
607
+ return approvalResult;
608
+ });
609
+ await streamer.finalize(result);
610
+ }
611
+ catch (err) {
612
+ logger.error({ err, slug: this.config.slug }, '/plan command failed');
613
+ await streamer.finalize(`Plan failed: ${err}`);
614
+ }
615
+ return;
616
+ }
617
+ // /deep, /quick, /opus — chat with model override
618
+ if (name === 'deep' || name === 'quick' || name === 'opus') {
619
+ const msg = cmd.options.getString('message', true);
620
+ const oneOffModel = name === 'quick' ? MODELS.haiku : name === 'opus' ? MODELS.opus : undefined;
621
+ const oneOffMaxTurns = name === 'deep' ? 100 : undefined;
622
+ await cmd.deferReply();
623
+ try {
624
+ const response = await this.gateway.handleMessage(sessionKey, msg, async () => { }, oneOffModel, oneOffMaxTurns);
625
+ const chunks = chunkText(response || '*(no response)*', 1900);
626
+ await cmd.editReply(chunks[0]);
627
+ for (let i = 1; i < chunks.length; i++) {
628
+ await cmd.followUp(chunks[i]);
629
+ }
630
+ }
631
+ catch (err) {
632
+ logger.error({ err, slug: this.config.slug }, `/${name} command failed`);
633
+ await cmd.editReply(`Something went wrong: ${err}`);
634
+ }
635
+ return;
636
+ }
637
+ return;
638
+ }
639
+ // ── Button interactions (plan approve/deny/revise) ──────────
640
+ if (interaction.isButton()) {
641
+ const button = interaction;
642
+ const customId = button.customId;
643
+ // Access control: owner + allowedUsers
644
+ if (!this.isAuthorized(button.user.id)) {
645
+ await button.reply({ content: 'You don\'t have access to this agent.', ephemeral: true });
646
+ return;
647
+ }
648
+ // Plan approval buttons: plan_{requestId}_{action}
649
+ const planMatch = customId.match(/^plan_(.+)_(approve|deny|revise)$/);
650
+ if (planMatch) {
651
+ const [, requestId, action] = planMatch;
652
+ if (action === 'approve') {
653
+ await button.deferUpdate();
654
+ this.gateway.resolveApproval(requestId, true);
655
+ }
656
+ else if (action === 'deny') {
657
+ await button.deferUpdate();
658
+ this.gateway.resolveApproval(requestId, false);
659
+ }
660
+ else if (action === 'revise') {
661
+ // Show modal for revision feedback
662
+ const modal = new ModalBuilder()
663
+ .setCustomId(`revise_modal_${requestId}`)
664
+ .setTitle('Revise Plan');
665
+ const input = new TextInputBuilder()
666
+ .setCustomId('revision_feedback')
667
+ .setLabel('What should be changed?')
668
+ .setStyle(TextInputStyle.Paragraph)
669
+ .setRequired(true);
670
+ modal.addComponents(new ActionRowBuilder().addComponents(input));
671
+ await button.showModal(modal);
672
+ }
673
+ // Disable buttons after click
674
+ try {
675
+ if (button.message) {
676
+ const rawComponents = button.message.components.map((row) => ({
677
+ type: 1,
678
+ components: (row.components ?? []).map((comp) => ({
679
+ type: comp.type ?? 2,
680
+ style: comp.style,
681
+ label: comp.label,
682
+ custom_id: comp.customId ?? comp.custom_id,
683
+ disabled: true,
684
+ })),
685
+ }));
686
+ await button.editReply({
687
+ content: button.message.content + `\n\n${action === 'approve' ? '\u2705 Approved' : action === 'deny' ? '\u274c Cancelled' : '\u270f\ufe0f Revising'}`,
688
+ components: rawComponents,
689
+ });
690
+ }
691
+ }
692
+ catch { /* non-fatal */ }
693
+ return;
694
+ }
695
+ }
696
+ // ── Modal submissions (revision feedback) ────────────────────
697
+ if (interaction.isModalSubmit()) {
698
+ const modal = interaction;
699
+ if (modal.customId.startsWith('revise_modal_')) {
700
+ const requestId = modal.customId.replace('revise_modal_', '');
701
+ const feedback = modal.fields.getTextInputValue('revision_feedback');
702
+ await modal.deferUpdate();
703
+ this.gateway.resolveApproval(requestId, feedback);
704
+ }
705
+ }
706
+ }
707
+ /** Check if this bot participates in a shared team chat channel. */
708
+ isTeamChat() {
709
+ return this.config.profile.team?.teamChat === true;
710
+ }
711
+ /**
712
+ * Check if this agent is being addressed in a team chat message.
713
+ * Matches: @mention, agent name, agent slug, or broadcast keywords.
714
+ */
715
+ isAddressedInTeamChat(message) {
716
+ // Direct @mention of this bot
717
+ if (this.client.user && message.mentions.users.has(this.client.user.id)) {
718
+ return true;
719
+ }
720
+ // @everyone and @here Discord mentions address all agents
721
+ if (message.mentions.everyone) {
722
+ return true;
723
+ }
724
+ const content = message.content.toLowerCase();
725
+ // Broadcast keywords — address all agents at once
726
+ const broadcastPatterns = [
727
+ /\b@?team\b/,
728
+ /\beveryone\b/,
729
+ /\ball\s+agents?\b/,
730
+ /\bthe\s+team\b/,
731
+ ];
732
+ if (broadcastPatterns.some(p => p.test(content))) {
733
+ return true;
734
+ }
735
+ // Individual agent name or slug at word boundaries
736
+ const name = this.config.profile.name.toLowerCase();
737
+ const slug = this.config.slug.toLowerCase();
738
+ const namePattern = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
739
+ const slugPattern = new RegExp(`\\b${slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
740
+ return namePattern.test(content) || slugPattern.test(content);
741
+ }
742
+ /**
743
+ * Collect recent messages from other bots in the same channel for context.
744
+ * Returns a formatted string of the last N messages from other agents.
745
+ */
746
+ async gatherTeamChatContext(message, limit = 10) {
747
+ try {
748
+ const channel = message.channel;
749
+ if (channel.isDMBased())
750
+ return '';
751
+ const recent = await channel.messages.fetch({ limit: limit + 1, before: message.id });
752
+ const contextLines = [];
753
+ for (const msg of recent.sort((a, b) => a.createdTimestamp - b.createdTimestamp).values()) {
754
+ const authorName = msg.author.bot ? msg.author.username : 'Owner';
755
+ const preview = msg.content.slice(0, 300);
756
+ if (preview) {
757
+ contextLines.push(`[${authorName}]: ${preview}`);
758
+ }
759
+ }
760
+ if (contextLines.length === 0)
761
+ return '';
762
+ return `\n\n[Recent team chat context]\n${contextLines.join('\n')}\n[End context]`;
763
+ }
764
+ catch {
765
+ return ''; // Non-fatal — proceed without context
766
+ }
767
+ }
768
+ async handleMessage(message) {
769
+ // Ignore own messages
770
+ if (message.author.id === this.client.user?.id)
771
+ return;
772
+ const isDm = message.channel.isDMBased();
773
+ const isWatchedChannel = !isDm && this.resolvedChannelIds.includes(message.channelId);
774
+ // Respond in DMs or watched channels
775
+ if (!isDm && !isWatchedChannel)
776
+ return;
777
+ const isTeamChatChannel = isWatchedChannel && this.isTeamChat();
778
+ // In team chat: ignore all bot messages (prevents loops).
779
+ // In solo channels: ignore all bot messages (original behavior).
780
+ if (message.author.bot)
781
+ return;
782
+ // Access control: owner + allowedUsers
783
+ if (!this.isAuthorized(message.author.id)) {
784
+ logger.debug({ slug: this.config.slug, author: message.author.tag }, 'Ignored message from unauthorized user');
785
+ return;
786
+ }
787
+ // In team chat: respond to all if respondToAll is set, otherwise only when addressed
788
+ const respondToAll = this.config.profile.team?.respondToAll === true;
789
+ if (isTeamChatChannel && !respondToAll && !this.isAddressedInTeamChat(message)) {
790
+ return;
791
+ }
792
+ // Extract attachments
793
+ let text = message.content;
794
+ if (message.attachments.size > 0) {
795
+ const attachmentLines = message.attachments.map(att => {
796
+ if (att.contentType?.startsWith('image/')) {
797
+ return `[Image attached: ${att.name} (${att.url})]`;
798
+ }
799
+ return `[File attached: ${att.name}, ${att.contentType || 'unknown type'}, ${att.url}]`;
800
+ });
801
+ text = attachmentLines.join('\n') + (text ? '\n' + text : '');
802
+ }
803
+ if (!text)
804
+ return;
805
+ // !dashboard command — show agent-scoped status embed
806
+ if (text === '!dashboard') {
807
+ if (this.config.cronScheduler) {
808
+ // Unpin old, send fresh, pin new
809
+ if (this.statusEmbedMessage) {
810
+ try {
811
+ await this.statusEmbedMessage.unpin();
812
+ }
813
+ catch { /* non-fatal */ }
814
+ }
815
+ const embed = this.buildAgentStatusEmbed();
816
+ if ('send' in message.channel) {
817
+ this.statusEmbedMessage = await message.channel.send({ embeds: [embed] });
818
+ try {
819
+ await this.statusEmbedMessage.pin();
820
+ }
821
+ catch { /* non-fatal */ }
822
+ }
823
+ }
824
+ else {
825
+ await message.reply('Status dashboard unavailable (no scheduler connected).');
826
+ }
827
+ return;
828
+ }
829
+ // !clear command
830
+ if (text === '!clear') {
831
+ const prefix = this.channelPrefix(message.author.id);
832
+ const dmPfx = this.dmPrefix(message.author.id);
833
+ const sessionKey = isDm
834
+ ? `${dmPfx}:${this.config.slug}:${message.author.id}`
835
+ : `${prefix}:${message.channelId}:${message.author.id}`;
836
+ this.gateway.clearSession(sessionKey);
837
+ await message.reply('Session cleared.');
838
+ return;
839
+ }
840
+ // In team chat, use agent-scoped session key so each agent has its own
841
+ // conversation memory in the shared channel
842
+ const prefix = this.channelPrefix(message.author.id);
843
+ const dmPfx = this.dmPrefix(message.author.id);
844
+ const sessionKey = isDm
845
+ ? `${dmPfx}:${this.config.slug}:${message.author.id}`
846
+ : isTeamChatChannel
847
+ ? `${prefix}:${message.channelId}:${this.config.slug}:${message.author.id}`
848
+ : `${prefix}:${message.channelId}:${message.author.id}`;
849
+ // Set the agent profile for this session
850
+ this.gateway.setSessionProfile(sessionKey, this.config.slug);
851
+ // Show queued indicator if session is busy
852
+ if (this.gateway.isSessionBusy(sessionKey)) {
853
+ await message.react('\u23f3'); // hourglass
854
+ }
855
+ // In team chat, gather recent messages from other agents as context
856
+ if (isTeamChatChannel) {
857
+ const teamContext = await this.gatherTeamChatContext(message);
858
+ if (teamContext) {
859
+ text += teamContext;
860
+ }
861
+ }
862
+ // Stream response as the bot's own identity
863
+ const streamer = new DiscordStreamingMessage(message.channel);
864
+ await streamer.start();
865
+ try {
866
+ const response = await this.gateway.handleMessage(sessionKey, text, async (token) => {
867
+ await streamer.update(token);
868
+ }, undefined, // model
869
+ undefined, // maxTurns
870
+ async (toolName, toolInput) => {
871
+ streamer.setToolStatus(friendlyToolName(toolName, toolInput));
872
+ });
873
+ await streamer.finalize(response);
874
+ }
875
+ catch (err) {
876
+ logger.error({ err, slug: this.config.slug }, 'Agent bot message handling error');
877
+ await streamer.finalize(`Something went wrong: ${sanitizeResponse(String(err))}`);
878
+ }
879
+ }
880
+ }
881
+ //# sourceMappingURL=discord-agent-bot.js.map