@mariozechner/pi-mom 0.10.1 → 0.11.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/CHANGELOG.md +44 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +221 -145
- package/dist/agent.js.map +1 -1
- package/dist/log.d.ts +1 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +3 -0
- package/dist/log.js.map +1 -1
- package/dist/slack.d.ts +31 -2
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +111 -6
- package/dist/slack.js.map +1 -1
- package/package.json +3 -3
package/dist/slack.js
CHANGED
|
@@ -11,6 +11,7 @@ export class MomBot {
|
|
|
11
11
|
botUserId = null;
|
|
12
12
|
store;
|
|
13
13
|
userCache = new Map();
|
|
14
|
+
channelCache = new Map(); // id -> name
|
|
14
15
|
constructor(handler, config) {
|
|
15
16
|
this.handler = handler;
|
|
16
17
|
this.socketClient = new SocketModeClient({ appToken: config.appToken });
|
|
@@ -21,6 +22,102 @@ export class MomBot {
|
|
|
21
22
|
});
|
|
22
23
|
this.setupEventHandlers();
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Fetch all channels the bot is a member of
|
|
27
|
+
*/
|
|
28
|
+
async fetchChannels() {
|
|
29
|
+
try {
|
|
30
|
+
let cursor;
|
|
31
|
+
do {
|
|
32
|
+
const result = await this.webClient.conversations.list({
|
|
33
|
+
types: "public_channel,private_channel",
|
|
34
|
+
exclude_archived: true,
|
|
35
|
+
limit: 200,
|
|
36
|
+
cursor,
|
|
37
|
+
});
|
|
38
|
+
const channels = result.channels;
|
|
39
|
+
if (channels) {
|
|
40
|
+
for (const channel of channels) {
|
|
41
|
+
if (channel.id && channel.name && channel.is_member) {
|
|
42
|
+
this.channelCache.set(channel.id, channel.name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
cursor = result.response_metadata?.next_cursor;
|
|
47
|
+
} while (cursor);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
log.logWarning("Failed to fetch channels", String(error));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Fetch all workspace users
|
|
55
|
+
*/
|
|
56
|
+
async fetchUsers() {
|
|
57
|
+
try {
|
|
58
|
+
let cursor;
|
|
59
|
+
do {
|
|
60
|
+
const result = await this.webClient.users.list({
|
|
61
|
+
limit: 200,
|
|
62
|
+
cursor,
|
|
63
|
+
});
|
|
64
|
+
const members = result.members;
|
|
65
|
+
if (members) {
|
|
66
|
+
for (const user of members) {
|
|
67
|
+
if (user.id && user.name && !user.deleted) {
|
|
68
|
+
this.userCache.set(user.id, {
|
|
69
|
+
userName: user.name,
|
|
70
|
+
displayName: user.real_name || user.name,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
cursor = result.response_metadata?.next_cursor;
|
|
76
|
+
} while (cursor);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
log.logWarning("Failed to fetch users", String(error));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get all known channels (id -> name)
|
|
84
|
+
*/
|
|
85
|
+
getChannels() {
|
|
86
|
+
return Array.from(this.channelCache.entries()).map(([id, name]) => ({ id, name }));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get all known users
|
|
90
|
+
*/
|
|
91
|
+
getUsers() {
|
|
92
|
+
return Array.from(this.userCache.entries()).map(([id, { userName, displayName }]) => ({
|
|
93
|
+
id,
|
|
94
|
+
userName,
|
|
95
|
+
displayName,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Obfuscate usernames and user IDs in text to prevent pinging people
|
|
100
|
+
* e.g., "nate" -> "n_a_t_e", "@mario" -> "@m_a_r_i_o", "<@U123>" -> "<@U_1_2_3>"
|
|
101
|
+
*/
|
|
102
|
+
obfuscateUsernames(text) {
|
|
103
|
+
let result = text;
|
|
104
|
+
// Obfuscate user IDs like <@U16LAL8LS>
|
|
105
|
+
result = result.replace(/<@([A-Z0-9]+)>/gi, (_match, id) => {
|
|
106
|
+
return `<@${id.split("").join("_")}>`;
|
|
107
|
+
});
|
|
108
|
+
// Obfuscate usernames
|
|
109
|
+
for (const { userName } of this.userCache.values()) {
|
|
110
|
+
// Escape special regex characters in username
|
|
111
|
+
const escaped = userName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
112
|
+
// Match @username, <@username>, or bare username (case insensitive, word boundary)
|
|
113
|
+
const pattern = new RegExp(`(<@|@)?(\\b${escaped}\\b)`, "gi");
|
|
114
|
+
result = result.replace(pattern, (_match, prefix, name) => {
|
|
115
|
+
const obfuscated = name.split("").join("_");
|
|
116
|
+
return (prefix || "") + obfuscated;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
24
121
|
async getUserInfo(userId) {
|
|
25
122
|
if (this.userCache.has(userId)) {
|
|
26
123
|
return this.userCache.get(userId);
|
|
@@ -41,11 +138,10 @@ export class MomBot {
|
|
|
41
138
|
}
|
|
42
139
|
setupEventHandlers() {
|
|
43
140
|
// Handle @mentions in channels
|
|
141
|
+
// Note: We don't log here - the message event handler logs all messages
|
|
44
142
|
this.socketClient.on("app_mention", async ({ event, ack }) => {
|
|
45
143
|
await ack();
|
|
46
144
|
const slackEvent = event;
|
|
47
|
-
// Log the mention (message event may not fire for app_mention)
|
|
48
|
-
await this.logMessage(slackEvent);
|
|
49
145
|
const ctx = await this.createContext(slackEvent);
|
|
50
146
|
await this.handler.onChannelMention(ctx);
|
|
51
147
|
});
|
|
@@ -140,7 +236,9 @@ export class MomBot {
|
|
|
140
236
|
},
|
|
141
237
|
channelName,
|
|
142
238
|
store: this.store,
|
|
143
|
-
|
|
239
|
+
channels: this.getChannels(),
|
|
240
|
+
users: this.getUsers(),
|
|
241
|
+
respond: async (responseText, log = true) => {
|
|
144
242
|
// Queue updates to avoid race conditions
|
|
145
243
|
updatePromise = updatePromise.then(async () => {
|
|
146
244
|
if (isThinking) {
|
|
@@ -170,8 +268,10 @@ export class MomBot {
|
|
|
170
268
|
});
|
|
171
269
|
messageTs = result.ts;
|
|
172
270
|
}
|
|
173
|
-
// Log the response
|
|
174
|
-
|
|
271
|
+
// Log the response if requested
|
|
272
|
+
if (log) {
|
|
273
|
+
await this.store.logBotResponse(event.channel, responseText, messageTs);
|
|
274
|
+
}
|
|
175
275
|
});
|
|
176
276
|
await updatePromise;
|
|
177
277
|
},
|
|
@@ -182,11 +282,13 @@ export class MomBot {
|
|
|
182
282
|
// No main message yet, just skip
|
|
183
283
|
return;
|
|
184
284
|
}
|
|
285
|
+
// Obfuscate usernames to avoid pinging people in thread details
|
|
286
|
+
const obfuscatedText = this.obfuscateUsernames(threadText);
|
|
185
287
|
// Post in thread under the main message
|
|
186
288
|
await this.webClient.chat.postMessage({
|
|
187
289
|
channel: event.channel,
|
|
188
290
|
thread_ts: messageTs,
|
|
189
|
-
text:
|
|
291
|
+
text: obfuscatedText,
|
|
190
292
|
});
|
|
191
293
|
});
|
|
192
294
|
await updatePromise;
|
|
@@ -256,6 +358,9 @@ export class MomBot {
|
|
|
256
358
|
async start() {
|
|
257
359
|
const auth = await this.webClient.auth.test();
|
|
258
360
|
this.botUserId = auth.user_id;
|
|
361
|
+
// Fetch channels and users in parallel
|
|
362
|
+
await Promise.all([this.fetchChannels(), this.fetchUsers()]);
|
|
363
|
+
log.logInfo(`Loaded ${this.channelCache.size} channels, ${this.userCache.size} users`);
|
|
259
364
|
await this.socketClient.start();
|
|
260
365
|
log.logConnected();
|
|
261
366
|
}
|
package/dist/slack.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack.js","sourceRoot":"","sources":["../src/slack.ts"],"names":[],"mappings":"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","sourcesContent":["import { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { readFileSync } from \"fs\";\nimport { basename } from \"path\";\nimport * as log from \"./log.js\";\nimport { type Attachment, ChannelStore } from \"./store.js\";\n\nexport interface SlackMessage {\n\ttext: string; // message content (mentions stripped)\n\trawText: string; // original text with mentions\n\tuser: string; // user ID\n\tuserName?: string; // user handle\n\tchannel: string; // channel ID\n\tts: string; // timestamp (for threading)\n\tattachments: Attachment[]; // file attachments\n}\n\nexport interface SlackContext {\n\tmessage: SlackMessage;\n\tchannelName?: string; // channel name for logging (e.g., #dev-team)\n\tstore: ChannelStore;\n\t/** Send/update the main message (accumulates text) */\n\trespond(text: string): Promise<void>;\n\t/** Replace the entire message text (not append) */\n\treplaceMessage(text: string): Promise<void>;\n\t/** Post a message in the thread under the main message (for verbose details) */\n\trespondInThread(text: string): Promise<void>;\n\t/** Show/hide typing indicator */\n\tsetTyping(isTyping: boolean): Promise<void>;\n\t/** Upload a file to the channel */\n\tuploadFile(filePath: string, title?: string): Promise<void>;\n\t/** Set working state (adds/removes working indicator emoji) */\n\tsetWorking(working: boolean): Promise<void>;\n}\n\nexport interface MomHandler {\n\tonChannelMention(ctx: SlackContext): Promise<void>;\n\tonDirectMessage(ctx: SlackContext): Promise<void>;\n}\n\nexport interface MomBotConfig {\n\tappToken: string;\n\tbotToken: string;\n\tworkingDir: string; // directory for channel data and attachments\n}\n\nexport class MomBot {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate handler: MomHandler;\n\tprivate botUserId: string | null = null;\n\tpublic readonly store: ChannelStore;\n\tprivate userCache: Map<string, { userName: string; displayName: string }> = new Map();\n\n\tconstructor(handler: MomHandler, config: MomBotConfig) {\n\t\tthis.handler = handler;\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t\tthis.store = new ChannelStore({\n\t\t\tworkingDir: config.workingDir,\n\t\t\tbotToken: config.botToken,\n\t\t});\n\n\t\tthis.setupEventHandlers();\n\t}\n\n\tprivate async getUserInfo(userId: string): Promise<{ userName: string; displayName: string }> {\n\t\tif (this.userCache.has(userId)) {\n\t\t\treturn this.userCache.get(userId)!;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.webClient.users.info({ user: userId });\n\t\t\tconst user = result.user as { name?: string; real_name?: string };\n\t\t\tconst info = {\n\t\t\t\tuserName: user?.name || userId,\n\t\t\t\tdisplayName: user?.real_name || user?.name || userId,\n\t\t\t};\n\t\t\tthis.userCache.set(userId, info);\n\t\t\treturn info;\n\t\t} catch {\n\t\t\treturn { userName: userId, displayName: userId };\n\t\t}\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\t// Handle @mentions in channels\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\n\t\t\tconst slackEvent = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t\t\t};\n\n\t\t\t// Log the mention (message event may not fire for app_mention)\n\t\t\tawait this.logMessage(slackEvent);\n\n\t\t\tconst ctx = await this.createContext(slackEvent);\n\t\t\tawait this.handler.onChannelMention(ctx);\n\t\t});\n\n\t\t// Handle all messages (for logging) and DMs (for triggering handler)\n\t\tthis.socketClient.on(\"message\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\n\t\t\tconst slackEvent = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t\t\t};\n\n\t\t\t// Ignore bot messages\n\t\t\tif (slackEvent.bot_id) return;\n\t\t\t// Ignore message edits, etc. (but allow file_share)\n\t\t\tif (slackEvent.subtype !== undefined && slackEvent.subtype !== \"file_share\") return;\n\t\t\t// Ignore if no user\n\t\t\tif (!slackEvent.user) return;\n\t\t\t// Ignore messages from the bot itself\n\t\t\tif (slackEvent.user === this.botUserId) return;\n\t\t\t// Ignore if no text AND no files\n\t\t\tif (!slackEvent.text && (!slackEvent.files || slackEvent.files.length === 0)) return;\n\n\t\t\t// Log ALL messages (channel and DM)\n\t\t\tawait this.logMessage({\n\t\t\t\ttext: slackEvent.text || \"\",\n\t\t\t\tchannel: slackEvent.channel,\n\t\t\t\tuser: slackEvent.user,\n\t\t\t\tts: slackEvent.ts,\n\t\t\t\tfiles: slackEvent.files,\n\t\t\t});\n\n\t\t\t// Only trigger handler for DMs (channel mentions are handled by app_mention event)\n\t\t\tif (slackEvent.channel_type === \"im\") {\n\t\t\t\tconst ctx = await this.createContext({\n\t\t\t\t\ttext: slackEvent.text || \"\",\n\t\t\t\t\tchannel: slackEvent.channel,\n\t\t\t\t\tuser: slackEvent.user,\n\t\t\t\t\tts: slackEvent.ts,\n\t\t\t\t\tfiles: slackEvent.files,\n\t\t\t\t});\n\t\t\t\tawait this.handler.onDirectMessage(ctx);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async logMessage(event: {\n\t\ttext: string;\n\t\tchannel: string;\n\t\tuser: string;\n\t\tts: string;\n\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t}): Promise<void> {\n\t\tconst attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];\n\t\tconst { userName, displayName } = await this.getUserInfo(event.user);\n\n\t\tawait this.store.logMessage(event.channel, {\n\t\t\tdate: new Date(parseFloat(event.ts) * 1000).toISOString(),\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName,\n\t\t\tdisplayName,\n\t\t\ttext: event.text,\n\t\t\tattachments,\n\t\t\tisBot: false,\n\t\t});\n\t}\n\n\tprivate async createContext(event: {\n\t\ttext: string;\n\t\tchannel: string;\n\t\tuser: string;\n\t\tts: string;\n\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t}): Promise<SlackContext> {\n\t\tconst rawText = event.text;\n\t\tconst text = rawText.replace(/<@[A-Z0-9]+>/gi, \"\").trim();\n\n\t\t// Get user info for logging\n\t\tconst { userName } = await this.getUserInfo(event.user);\n\n\t\t// Get channel name for logging (best effort)\n\t\tlet channelName: string | undefined;\n\t\ttry {\n\t\t\tif (event.channel.startsWith(\"C\")) {\n\t\t\t\tconst result = await this.webClient.conversations.info({ channel: event.channel });\n\t\t\t\tchannelName = result.channel?.name ? `#${result.channel.name}` : undefined;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors - we'll just use the channel ID\n\t\t}\n\n\t\t// Process attachments (for context, already logged by message handler)\n\t\tconst attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];\n\n\t\t// Track the single message for this run\n\t\tlet messageTs: string | null = null;\n\t\tlet accumulatedText = \"\";\n\t\tlet isThinking = true; // Track if we're still in \"thinking\" state\n\t\tlet isWorking = true; // Track if still processing\n\t\tconst workingIndicator = \" ...\";\n\t\tlet updatePromise: Promise<void> = Promise.resolve();\n\n\t\treturn {\n\t\t\tmessage: {\n\t\t\t\ttext,\n\t\t\t\trawText,\n\t\t\t\tuser: event.user,\n\t\t\t\tuserName,\n\t\t\t\tchannel: event.channel,\n\t\t\t\tts: event.ts,\n\t\t\t\tattachments,\n\t\t\t},\n\t\t\tchannelName,\n\t\t\tstore: this.store,\n\t\t\trespond: async (responseText: string) => {\n\t\t\t\t// Queue updates to avoid race conditions\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tif (isThinking) {\n\t\t\t\t\t\t// First real response replaces \"Thinking...\"\n\t\t\t\t\t\taccumulatedText = responseText;\n\t\t\t\t\t\tisThinking = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Subsequent responses get appended\n\t\t\t\t\t\taccumulatedText += \"\\n\" + responseText;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Add working indicator if still working\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\t// Update existing message\n\t\t\t\t\t\tawait this.webClient.chat.update({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\tts: messageTs,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Post initial message\n\t\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Log the response\n\t\t\t\t\tawait this.store.logBotResponse(event.channel, responseText, messageTs!);\n\t\t\t\t});\n\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\trespondInThread: async (threadText: string) => {\n\t\t\t\t// Queue thread posts to maintain order\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tif (!messageTs) {\n\t\t\t\t\t\t// No main message yet, just skip\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// Post in thread under the main message\n\t\t\t\t\tawait this.webClient.chat.postMessage({\n\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\tthread_ts: messageTs,\n\t\t\t\t\t\ttext: threadText,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\t\tif (isTyping && !messageTs) {\n\t\t\t\t\t// Post initial \"thinking\" message (... auto-appended by working indicator)\n\t\t\t\t\taccumulatedText = \"_Thinking_\";\n\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\ttext: accumulatedText,\n\t\t\t\t\t});\n\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t}\n\t\t\t\t// We don't delete/clear anymore - message persists and gets updated\n\t\t\t},\n\t\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\t\tconst fileName = title || basename(filePath);\n\t\t\t\tconst fileContent = readFileSync(filePath);\n\n\t\t\t\tawait this.webClient.files.uploadV2({\n\t\t\t\t\tchannel_id: event.channel,\n\t\t\t\t\tfile: fileContent,\n\t\t\t\t\tfilename: fileName,\n\t\t\t\t\ttitle: fileName,\n\t\t\t\t});\n\t\t\t},\n\t\t\treplaceMessage: async (text: string) => {\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\t// Replace the accumulated text entirely\n\t\t\t\t\taccumulatedText = text;\n\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\tawait this.webClient.chat.update({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\tts: messageTs,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Post initial message\n\t\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\tsetWorking: async (working: boolean) => {\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tisWorking = working;\n\n\t\t\t\t\t// If we have a message, update it to add/remove indicator\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\t\t\tawait this.webClient.chat.update({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\tts: messageTs,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t};\n\t}\n\n\tasync start(): Promise<void> {\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\t\tawait this.socketClient.start();\n\t\tlog.logConnected();\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tawait this.socketClient.disconnect();\n\t\tlog.logDisconnected();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"slack.js","sourceRoot":"","sources":["../src/slack.ts"],"names":[],"mappings":"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","sourcesContent":["import { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { readFileSync } from \"fs\";\nimport { basename } from \"path\";\nimport * as log from \"./log.js\";\nimport { type Attachment, ChannelStore } from \"./store.js\";\n\nexport interface SlackMessage {\n\ttext: string; // message content (mentions stripped)\n\trawText: string; // original text with mentions\n\tuser: string; // user ID\n\tuserName?: string; // user handle\n\tchannel: string; // channel ID\n\tts: string; // timestamp (for threading)\n\tattachments: Attachment[]; // file attachments\n}\n\nexport interface SlackContext {\n\tmessage: SlackMessage;\n\tchannelName?: string; // channel name for logging (e.g., #dev-team)\n\tstore: ChannelStore;\n\t/** All channels the bot is a member of */\n\tchannels: ChannelInfo[];\n\t/** All known users in the workspace */\n\tusers: UserInfo[];\n\t/** Send/update the main message (accumulates text). Set log=false to skip logging. */\n\trespond(text: string, log?: boolean): Promise<void>;\n\t/** Replace the entire message text (not append) */\n\treplaceMessage(text: string): Promise<void>;\n\t/** Post a message in the thread under the main message (for verbose details) */\n\trespondInThread(text: string): Promise<void>;\n\t/** Show/hide typing indicator */\n\tsetTyping(isTyping: boolean): Promise<void>;\n\t/** Upload a file to the channel */\n\tuploadFile(filePath: string, title?: string): Promise<void>;\n\t/** Set working state (adds/removes working indicator emoji) */\n\tsetWorking(working: boolean): Promise<void>;\n}\n\nexport interface MomHandler {\n\tonChannelMention(ctx: SlackContext): Promise<void>;\n\tonDirectMessage(ctx: SlackContext): Promise<void>;\n}\n\nexport interface MomBotConfig {\n\tappToken: string;\n\tbotToken: string;\n\tworkingDir: string; // directory for channel data and attachments\n}\n\nexport interface ChannelInfo {\n\tid: string;\n\tname: string;\n}\n\nexport interface UserInfo {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\nexport class MomBot {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate handler: MomHandler;\n\tprivate botUserId: string | null = null;\n\tpublic readonly store: ChannelStore;\n\tprivate userCache: Map<string, { userName: string; displayName: string }> = new Map();\n\tprivate channelCache: Map<string, string> = new Map(); // id -> name\n\n\tconstructor(handler: MomHandler, config: MomBotConfig) {\n\t\tthis.handler = handler;\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t\tthis.store = new ChannelStore({\n\t\t\tworkingDir: config.workingDir,\n\t\t\tbotToken: config.botToken,\n\t\t});\n\n\t\tthis.setupEventHandlers();\n\t}\n\n\t/**\n\t * Fetch all channels the bot is a member of\n\t */\n\tprivate async fetchChannels(): Promise<void> {\n\t\ttry {\n\t\t\tlet cursor: string | undefined;\n\t\t\tdo {\n\t\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\t\ttypes: \"public_channel,private_channel\",\n\t\t\t\t\texclude_archived: true,\n\t\t\t\t\tlimit: 200,\n\t\t\t\t\tcursor,\n\t\t\t\t});\n\n\t\t\t\tconst channels = result.channels as Array<{ id?: string; name?: string; is_member?: boolean }> | undefined;\n\t\t\t\tif (channels) {\n\t\t\t\t\tfor (const channel of channels) {\n\t\t\t\t\t\tif (channel.id && channel.name && channel.is_member) {\n\t\t\t\t\t\t\tthis.channelCache.set(channel.id, channel.name);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t\t} while (cursor);\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to fetch channels\", String(error));\n\t\t}\n\t}\n\n\t/**\n\t * Fetch all workspace users\n\t */\n\tprivate async fetchUsers(): Promise<void> {\n\t\ttry {\n\t\t\tlet cursor: string | undefined;\n\t\t\tdo {\n\t\t\t\tconst result = await this.webClient.users.list({\n\t\t\t\t\tlimit: 200,\n\t\t\t\t\tcursor,\n\t\t\t\t});\n\n\t\t\t\tconst members = result.members as\n\t\t\t\t\t| Array<{ id?: string; name?: string; real_name?: string; deleted?: boolean }>\n\t\t\t\t\t| undefined;\n\t\t\t\tif (members) {\n\t\t\t\t\tfor (const user of members) {\n\t\t\t\t\t\tif (user.id && user.name && !user.deleted) {\n\t\t\t\t\t\t\tthis.userCache.set(user.id, {\n\t\t\t\t\t\t\t\tuserName: user.name,\n\t\t\t\t\t\t\t\tdisplayName: user.real_name || user.name,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t\t} while (cursor);\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to fetch users\", String(error));\n\t\t}\n\t}\n\n\t/**\n\t * Get all known channels (id -> name)\n\t */\n\tgetChannels(): ChannelInfo[] {\n\t\treturn Array.from(this.channelCache.entries()).map(([id, name]) => ({ id, name }));\n\t}\n\n\t/**\n\t * Get all known users\n\t */\n\tgetUsers(): UserInfo[] {\n\t\treturn Array.from(this.userCache.entries()).map(([id, { userName, displayName }]) => ({\n\t\t\tid,\n\t\t\tuserName,\n\t\t\tdisplayName,\n\t\t}));\n\t}\n\n\t/**\n\t * Obfuscate usernames and user IDs in text to prevent pinging people\n\t * e.g., \"nate\" -> \"n_a_t_e\", \"@mario\" -> \"@m_a_r_i_o\", \"<@U123>\" -> \"<@U_1_2_3>\"\n\t */\n\tprivate obfuscateUsernames(text: string): string {\n\t\tlet result = text;\n\n\t\t// Obfuscate user IDs like <@U16LAL8LS>\n\t\tresult = result.replace(/<@([A-Z0-9]+)>/gi, (_match, id) => {\n\t\t\treturn `<@${id.split(\"\").join(\"_\")}>`;\n\t\t});\n\n\t\t// Obfuscate usernames\n\t\tfor (const { userName } of this.userCache.values()) {\n\t\t\t// Escape special regex characters in username\n\t\t\tconst escaped = userName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\t// Match @username, <@username>, or bare username (case insensitive, word boundary)\n\t\t\tconst pattern = new RegExp(`(<@|@)?(\\\\b${escaped}\\\\b)`, \"gi\");\n\t\t\tresult = result.replace(pattern, (_match, prefix, name) => {\n\t\t\t\tconst obfuscated = name.split(\"\").join(\"_\");\n\t\t\t\treturn (prefix || \"\") + obfuscated;\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t}\n\n\tprivate async getUserInfo(userId: string): Promise<{ userName: string; displayName: string }> {\n\t\tif (this.userCache.has(userId)) {\n\t\t\treturn this.userCache.get(userId)!;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.webClient.users.info({ user: userId });\n\t\t\tconst user = result.user as { name?: string; real_name?: string };\n\t\t\tconst info = {\n\t\t\t\tuserName: user?.name || userId,\n\t\t\t\tdisplayName: user?.real_name || user?.name || userId,\n\t\t\t};\n\t\t\tthis.userCache.set(userId, info);\n\t\t\treturn info;\n\t\t} catch {\n\t\t\treturn { userName: userId, displayName: userId };\n\t\t}\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\t// Handle @mentions in channels\n\t\t// Note: We don't log here - the message event handler logs all messages\n\t\tthis.socketClient.on(\"app_mention\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\n\t\t\tconst slackEvent = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t\t\t};\n\n\t\t\tconst ctx = await this.createContext(slackEvent);\n\t\t\tawait this.handler.onChannelMention(ctx);\n\t\t});\n\n\t\t// Handle all messages (for logging) and DMs (for triggering handler)\n\t\tthis.socketClient.on(\"message\", async ({ event, ack }) => {\n\t\t\tawait ack();\n\n\t\t\tconst slackEvent = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t\t\t};\n\n\t\t\t// Ignore bot messages\n\t\t\tif (slackEvent.bot_id) return;\n\t\t\t// Ignore message edits, etc. (but allow file_share)\n\t\t\tif (slackEvent.subtype !== undefined && slackEvent.subtype !== \"file_share\") return;\n\t\t\t// Ignore if no user\n\t\t\tif (!slackEvent.user) return;\n\t\t\t// Ignore messages from the bot itself\n\t\t\tif (slackEvent.user === this.botUserId) return;\n\t\t\t// Ignore if no text AND no files\n\t\t\tif (!slackEvent.text && (!slackEvent.files || slackEvent.files.length === 0)) return;\n\n\t\t\t// Log ALL messages (channel and DM)\n\t\t\tawait this.logMessage({\n\t\t\t\ttext: slackEvent.text || \"\",\n\t\t\t\tchannel: slackEvent.channel,\n\t\t\t\tuser: slackEvent.user,\n\t\t\t\tts: slackEvent.ts,\n\t\t\t\tfiles: slackEvent.files,\n\t\t\t});\n\n\t\t\t// Only trigger handler for DMs (channel mentions are handled by app_mention event)\n\t\t\tif (slackEvent.channel_type === \"im\") {\n\t\t\t\tconst ctx = await this.createContext({\n\t\t\t\t\ttext: slackEvent.text || \"\",\n\t\t\t\t\tchannel: slackEvent.channel,\n\t\t\t\t\tuser: slackEvent.user,\n\t\t\t\t\tts: slackEvent.ts,\n\t\t\t\t\tfiles: slackEvent.files,\n\t\t\t\t});\n\t\t\t\tawait this.handler.onDirectMessage(ctx);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate async logMessage(event: {\n\t\ttext: string;\n\t\tchannel: string;\n\t\tuser: string;\n\t\tts: string;\n\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t}): Promise<void> {\n\t\tconst attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];\n\t\tconst { userName, displayName } = await this.getUserInfo(event.user);\n\n\t\tawait this.store.logMessage(event.channel, {\n\t\t\tdate: new Date(parseFloat(event.ts) * 1000).toISOString(),\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName,\n\t\t\tdisplayName,\n\t\t\ttext: event.text,\n\t\t\tattachments,\n\t\t\tisBot: false,\n\t\t});\n\t}\n\n\tprivate async createContext(event: {\n\t\ttext: string;\n\t\tchannel: string;\n\t\tuser: string;\n\t\tts: string;\n\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t}): Promise<SlackContext> {\n\t\tconst rawText = event.text;\n\t\tconst text = rawText.replace(/<@[A-Z0-9]+>/gi, \"\").trim();\n\n\t\t// Get user info for logging\n\t\tconst { userName } = await this.getUserInfo(event.user);\n\n\t\t// Get channel name for logging (best effort)\n\t\tlet channelName: string | undefined;\n\t\ttry {\n\t\t\tif (event.channel.startsWith(\"C\")) {\n\t\t\t\tconst result = await this.webClient.conversations.info({ channel: event.channel });\n\t\t\t\tchannelName = result.channel?.name ? `#${result.channel.name}` : undefined;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors - we'll just use the channel ID\n\t\t}\n\n\t\t// Process attachments (for context, already logged by message handler)\n\t\tconst attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];\n\n\t\t// Track the single message for this run\n\t\tlet messageTs: string | null = null;\n\t\tlet accumulatedText = \"\";\n\t\tlet isThinking = true; // Track if we're still in \"thinking\" state\n\t\tlet isWorking = true; // Track if still processing\n\t\tconst workingIndicator = \" ...\";\n\t\tlet updatePromise: Promise<void> = Promise.resolve();\n\n\t\treturn {\n\t\t\tmessage: {\n\t\t\t\ttext,\n\t\t\t\trawText,\n\t\t\t\tuser: event.user,\n\t\t\t\tuserName,\n\t\t\t\tchannel: event.channel,\n\t\t\t\tts: event.ts,\n\t\t\t\tattachments,\n\t\t\t},\n\t\t\tchannelName,\n\t\t\tstore: this.store,\n\t\t\tchannels: this.getChannels(),\n\t\t\tusers: this.getUsers(),\n\t\t\trespond: async (responseText: string, log = true) => {\n\t\t\t\t// Queue updates to avoid race conditions\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tif (isThinking) {\n\t\t\t\t\t\t// First real response replaces \"Thinking...\"\n\t\t\t\t\t\taccumulatedText = responseText;\n\t\t\t\t\t\tisThinking = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Subsequent responses get appended\n\t\t\t\t\t\taccumulatedText += \"\\n\" + responseText;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Add working indicator if still working\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\t// Update existing message\n\t\t\t\t\t\tawait this.webClient.chat.update({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\tts: messageTs,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Post initial message\n\t\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Log the response if requested\n\t\t\t\t\tif (log) {\n\t\t\t\t\t\tawait this.store.logBotResponse(event.channel, responseText, messageTs!);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\trespondInThread: async (threadText: string) => {\n\t\t\t\t// Queue thread posts to maintain order\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tif (!messageTs) {\n\t\t\t\t\t\t// No main message yet, just skip\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t// Obfuscate usernames to avoid pinging people in thread details\n\t\t\t\t\tconst obfuscatedText = this.obfuscateUsernames(threadText);\n\t\t\t\t\t// Post in thread under the main message\n\t\t\t\t\tawait this.webClient.chat.postMessage({\n\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\tthread_ts: messageTs,\n\t\t\t\t\t\ttext: obfuscatedText,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\t\tif (isTyping && !messageTs) {\n\t\t\t\t\t// Post initial \"thinking\" message (... auto-appended by working indicator)\n\t\t\t\t\taccumulatedText = \"_Thinking_\";\n\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\ttext: accumulatedText,\n\t\t\t\t\t});\n\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t}\n\t\t\t\t// We don't delete/clear anymore - message persists and gets updated\n\t\t\t},\n\t\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\t\tconst fileName = title || basename(filePath);\n\t\t\t\tconst fileContent = readFileSync(filePath);\n\n\t\t\t\tawait this.webClient.files.uploadV2({\n\t\t\t\t\tchannel_id: event.channel,\n\t\t\t\t\tfile: fileContent,\n\t\t\t\t\tfilename: fileName,\n\t\t\t\t\ttitle: fileName,\n\t\t\t\t});\n\t\t\t},\n\t\t\treplaceMessage: async (text: string) => {\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\t// Replace the accumulated text entirely\n\t\t\t\t\taccumulatedText = text;\n\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\tawait this.webClient.chat.update({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\tts: messageTs,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Post initial message\n\t\t\t\t\t\tconst result = await this.webClient.chat.postMessage({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tmessageTs = result.ts as string;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t\tsetWorking: async (working: boolean) => {\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\tisWorking = working;\n\n\t\t\t\t\t// If we have a message, update it to add/remove indicator\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\t\t\tawait this.webClient.chat.update({\n\t\t\t\t\t\t\tchannel: event.channel,\n\t\t\t\t\t\t\tts: messageTs,\n\t\t\t\t\t\t\ttext: displayText,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t},\n\t\t};\n\t}\n\n\tasync start(): Promise<void> {\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\n\t\t// Fetch channels and users in parallel\n\t\tawait Promise.all([this.fetchChannels(), this.fetchUsers()]);\n\t\tlog.logInfo(`Loaded ${this.channelCache.size} channels, ${this.userCache.size} users`);\n\n\t\tawait this.socketClient.start();\n\t\tlog.logConnected();\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tawait this.socketClient.disconnect();\n\t\tlog.logDisconnected();\n\t}\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mariozechner/pi-mom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Slack bot that delegates messages to the pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@anthropic-ai/sandbox-runtime": "^0.0.16",
|
|
24
|
-
"@mariozechner/pi-agent-core": "^0.
|
|
25
|
-
"@mariozechner/pi-ai": "^0.
|
|
24
|
+
"@mariozechner/pi-agent-core": "^0.11.0",
|
|
25
|
+
"@mariozechner/pi-ai": "^0.11.0",
|
|
26
26
|
"@sinclair/typebox": "^0.34.0",
|
|
27
27
|
"@slack/socket-mode": "^2.0.0",
|
|
28
28
|
"@slack/web-api": "^7.0.0",
|