@mariozechner/pi-mom 0.9.4 → 0.10.1
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 +69 -0
- package/README.md +291 -104
- package/dist/agent.d.ts +3 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +326 -46
- package/dist/agent.js.map +1 -1
- package/dist/log.d.ts +35 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +195 -0
- package/dist/log.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +34 -24
- package/dist/main.js.map +1 -1
- package/dist/slack.d.ts +6 -0
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +68 -9
- package/dist/slack.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +19 -3
- package/dist/store.js.map +1 -1
- package/package.json +5 -4
package/dist/slack.js
CHANGED
|
@@ -2,6 +2,7 @@ import { SocketModeClient } from "@slack/socket-mode";
|
|
|
2
2
|
import { WebClient } from "@slack/web-api";
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
import { basename } from "path";
|
|
5
|
+
import * as log from "./log.js";
|
|
5
6
|
import { ChannelStore } from "./store.js";
|
|
6
7
|
export class MomBot {
|
|
7
8
|
socketClient;
|
|
@@ -45,7 +46,7 @@ export class MomBot {
|
|
|
45
46
|
const slackEvent = event;
|
|
46
47
|
// Log the mention (message event may not fire for app_mention)
|
|
47
48
|
await this.logMessage(slackEvent);
|
|
48
|
-
const ctx = this.createContext(slackEvent);
|
|
49
|
+
const ctx = await this.createContext(slackEvent);
|
|
49
50
|
await this.handler.onChannelMention(ctx);
|
|
50
51
|
});
|
|
51
52
|
// Handle all messages (for logging) and DMs (for triggering handler)
|
|
@@ -77,7 +78,7 @@ export class MomBot {
|
|
|
77
78
|
});
|
|
78
79
|
// Only trigger handler for DMs (channel mentions are handled by app_mention event)
|
|
79
80
|
if (slackEvent.channel_type === "im") {
|
|
80
|
-
const ctx = this.createContext({
|
|
81
|
+
const ctx = await this.createContext({
|
|
81
82
|
text: slackEvent.text || "",
|
|
82
83
|
channel: slackEvent.channel,
|
|
83
84
|
user: slackEvent.user,
|
|
@@ -92,6 +93,7 @@ export class MomBot {
|
|
|
92
93
|
const attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];
|
|
93
94
|
const { userName, displayName } = await this.getUserInfo(event.user);
|
|
94
95
|
await this.store.logMessage(event.channel, {
|
|
96
|
+
date: new Date(parseFloat(event.ts) * 1000).toISOString(),
|
|
95
97
|
ts: event.ts,
|
|
96
98
|
user: event.user,
|
|
97
99
|
userName,
|
|
@@ -101,25 +103,42 @@ export class MomBot {
|
|
|
101
103
|
isBot: false,
|
|
102
104
|
});
|
|
103
105
|
}
|
|
104
|
-
createContext(event) {
|
|
106
|
+
async createContext(event) {
|
|
105
107
|
const rawText = event.text;
|
|
106
108
|
const text = rawText.replace(/<@[A-Z0-9]+>/gi, "").trim();
|
|
109
|
+
// Get user info for logging
|
|
110
|
+
const { userName } = await this.getUserInfo(event.user);
|
|
111
|
+
// Get channel name for logging (best effort)
|
|
112
|
+
let channelName;
|
|
113
|
+
try {
|
|
114
|
+
if (event.channel.startsWith("C")) {
|
|
115
|
+
const result = await this.webClient.conversations.info({ channel: event.channel });
|
|
116
|
+
channelName = result.channel?.name ? `#${result.channel.name}` : undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Ignore errors - we'll just use the channel ID
|
|
121
|
+
}
|
|
107
122
|
// Process attachments (for context, already logged by message handler)
|
|
108
123
|
const attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];
|
|
109
124
|
// Track the single message for this run
|
|
110
125
|
let messageTs = null;
|
|
111
126
|
let accumulatedText = "";
|
|
112
127
|
let isThinking = true; // Track if we're still in "thinking" state
|
|
128
|
+
let isWorking = true; // Track if still processing
|
|
129
|
+
const workingIndicator = " ...";
|
|
113
130
|
let updatePromise = Promise.resolve();
|
|
114
131
|
return {
|
|
115
132
|
message: {
|
|
116
133
|
text,
|
|
117
134
|
rawText,
|
|
118
135
|
user: event.user,
|
|
136
|
+
userName,
|
|
119
137
|
channel: event.channel,
|
|
120
138
|
ts: event.ts,
|
|
121
139
|
attachments,
|
|
122
140
|
},
|
|
141
|
+
channelName,
|
|
123
142
|
store: this.store,
|
|
124
143
|
respond: async (responseText) => {
|
|
125
144
|
// Queue updates to avoid race conditions
|
|
@@ -133,19 +152,21 @@ export class MomBot {
|
|
|
133
152
|
// Subsequent responses get appended
|
|
134
153
|
accumulatedText += "\n" + responseText;
|
|
135
154
|
}
|
|
155
|
+
// Add working indicator if still working
|
|
156
|
+
const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
|
|
136
157
|
if (messageTs) {
|
|
137
158
|
// Update existing message
|
|
138
159
|
await this.webClient.chat.update({
|
|
139
160
|
channel: event.channel,
|
|
140
161
|
ts: messageTs,
|
|
141
|
-
text:
|
|
162
|
+
text: displayText,
|
|
142
163
|
});
|
|
143
164
|
}
|
|
144
165
|
else {
|
|
145
166
|
// Post initial message
|
|
146
167
|
const result = await this.webClient.chat.postMessage({
|
|
147
168
|
channel: event.channel,
|
|
148
|
-
text:
|
|
169
|
+
text: displayText,
|
|
149
170
|
});
|
|
150
171
|
messageTs = result.ts;
|
|
151
172
|
}
|
|
@@ -172,8 +193,8 @@ export class MomBot {
|
|
|
172
193
|
},
|
|
173
194
|
setTyping: async (isTyping) => {
|
|
174
195
|
if (isTyping && !messageTs) {
|
|
175
|
-
// Post initial "thinking" message
|
|
176
|
-
accumulatedText = "
|
|
196
|
+
// Post initial "thinking" message (... auto-appended by working indicator)
|
|
197
|
+
accumulatedText = "_Thinking_";
|
|
177
198
|
const result = await this.webClient.chat.postMessage({
|
|
178
199
|
channel: event.channel,
|
|
179
200
|
text: accumulatedText,
|
|
@@ -192,17 +213,55 @@ export class MomBot {
|
|
|
192
213
|
title: fileName,
|
|
193
214
|
});
|
|
194
215
|
},
|
|
216
|
+
replaceMessage: async (text) => {
|
|
217
|
+
updatePromise = updatePromise.then(async () => {
|
|
218
|
+
// Replace the accumulated text entirely
|
|
219
|
+
accumulatedText = text;
|
|
220
|
+
const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
|
|
221
|
+
if (messageTs) {
|
|
222
|
+
await this.webClient.chat.update({
|
|
223
|
+
channel: event.channel,
|
|
224
|
+
ts: messageTs,
|
|
225
|
+
text: displayText,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Post initial message
|
|
230
|
+
const result = await this.webClient.chat.postMessage({
|
|
231
|
+
channel: event.channel,
|
|
232
|
+
text: displayText,
|
|
233
|
+
});
|
|
234
|
+
messageTs = result.ts;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
await updatePromise;
|
|
238
|
+
},
|
|
239
|
+
setWorking: async (working) => {
|
|
240
|
+
updatePromise = updatePromise.then(async () => {
|
|
241
|
+
isWorking = working;
|
|
242
|
+
// If we have a message, update it to add/remove indicator
|
|
243
|
+
if (messageTs) {
|
|
244
|
+
const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
|
|
245
|
+
await this.webClient.chat.update({
|
|
246
|
+
channel: event.channel,
|
|
247
|
+
ts: messageTs,
|
|
248
|
+
text: displayText,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
await updatePromise;
|
|
253
|
+
},
|
|
195
254
|
};
|
|
196
255
|
}
|
|
197
256
|
async start() {
|
|
198
257
|
const auth = await this.webClient.auth.test();
|
|
199
258
|
this.botUserId = auth.user_id;
|
|
200
259
|
await this.socketClient.start();
|
|
201
|
-
|
|
260
|
+
log.logConnected();
|
|
202
261
|
}
|
|
203
262
|
async stop() {
|
|
204
263
|
await this.socketClient.disconnect();
|
|
205
|
-
|
|
264
|
+
log.logDisconnected();
|
|
206
265
|
}
|
|
207
266
|
}
|
|
208
267
|
//# sourceMappingURL=slack.js.map
|
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 { 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\tchannel: string; // channel ID\n\tts: string; // timestamp (for threading)\n\tattachments: Attachment[]; // file attachments\n}\n\nexport interface SlackContext {\n\tmessage: SlackMessage;\n\tstore: ChannelStore;\n\t/** Send/update the main message (accumulates text) */\n\trespond(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}\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 = 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 = 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\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 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}): SlackContext {\n\t\tconst rawText = event.text;\n\t\tconst text = rawText.replace(/<@[A-Z0-9]+>/gi, \"\").trim();\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 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\tchannel: event.channel,\n\t\t\t\tts: event.ts,\n\t\t\t\tattachments,\n\t\t\t},\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\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: accumulatedText,\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: accumulatedText,\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\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};\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\tconsole.log(\"⚡️ Mom bot connected and listening!\");\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tawait this.socketClient.disconnect();\n\t\tconsole.log(\"Mom bot disconnected.\");\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/** 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"]}
|
package/dist/store.d.ts
CHANGED
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,UAAU;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAQD,qBAAa,YAAY;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,gBAAgB,CAAyB;IACjD,OAAO,CAAC,aAAa,CAAS;IAE9B,YAAY,MAAM,EAAE,kBAAkB,EAQrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMvC;IAED;;OAEG;IACH,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAMrE;IAED;;;OAGG;IACH,kBAAkB,CACjB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EACnF,SAAS,EAAE,MAAM,GACf,UAAU,EAAE,CAuBd;IAED;;OAEG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBzE;IAED;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/E;YAKa,oBAAoB;YAwBpB,kBAAkB;CAsBhC","sourcesContent":["import { existsSync, mkdirSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\n\nexport interface Attachment {\n\toriginal: string; // original filename from uploader\n\tlocal: string; // path relative to working dir (e.g., \"C12345/attachments/1732531234567_file.png\")\n}\n\nexport interface LoggedMessage {\n\tdate: string; // ISO 8601 date (e.g., \"2025-11-26T10:44:00.000Z\") for easy grepping\n\tts: string; // slack timestamp or epoch ms\n\tuser: string; // user ID (or \"bot\" for bot responses)\n\tuserName?: string; // handle (e.g., \"mario\")\n\tdisplayName?: string; // display name (e.g., \"Mario Zechner\")\n\ttext: string;\n\tattachments: Attachment[];\n\tisBot: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n\tbotToken: string; // needed for authenticated file downloads\n}\n\ninterface PendingDownload {\n\tchannelId: string;\n\tlocalPath: string; // relative path\n\turl: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\tprivate botToken: string;\n\tprivate pendingDownloads: PendingDownload[] = [];\n\tprivate isDownloading = false;\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\t\tthis.botToken = config.botToken;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Generate a unique local filename for an attachment\n\t */\n\tgenerateLocalFilename(originalName: string, timestamp: string): string {\n\t\t// Convert slack timestamp (1234567890.123456) to milliseconds\n\t\tconst ts = Math.floor(parseFloat(timestamp) * 1000);\n\t\t// Sanitize original name (remove problematic characters)\n\t\tconst sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n\t\treturn `${ts}_${sanitized}`;\n\t}\n\n\t/**\n\t * Process attachments from a Slack message event\n\t * Returns attachment metadata and queues downloads\n\t */\n\tprocessAttachments(\n\t\tchannelId: string,\n\t\tfiles: Array<{ name: string; url_private_download?: string; url_private?: string }>,\n\t\ttimestamp: string,\n\t): Attachment[] {\n\t\tconst attachments: Attachment[] = [];\n\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url) continue;\n\n\t\t\tconst filename = this.generateLocalFilename(file.name, timestamp);\n\t\t\tconst localPath = `${channelId}/attachments/${filename}`;\n\n\t\t\tattachments.push({\n\t\t\t\toriginal: file.name,\n\t\t\t\tlocal: localPath,\n\t\t\t});\n\n\t\t\t// Queue for background download\n\t\t\tthis.pendingDownloads.push({ channelId, localPath, url });\n\t\t}\n\n\t\t// Trigger background download\n\t\tthis.processDownloadQueue();\n\n\t\treturn attachments;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<void> {\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\t// Parse timestamp to get date\n\t\t\tlet date: Date;\n\t\t\tif (message.ts.includes(\".\")) {\n\t\t\t\t// Slack timestamp format (1234567890.123456)\n\t\t\t\tdate = new Date(parseFloat(message.ts) * 1000);\n\t\t\t} else {\n\t\t\t\t// Epoch milliseconds\n\t\t\t\tdate = new Date(parseInt(message.ts, 10));\n\t\t\t}\n\t\t\tmessage.date = date.toISOString();\n\t\t}\n\n\t\tconst line = JSON.stringify(message) + \"\\n\";\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tattachments: [],\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Process the download queue in the background\n\t */\n\tprivate async processDownloadQueue(): Promise<void> {\n\t\tif (this.isDownloading || this.pendingDownloads.length === 0) return;\n\n\t\tthis.isDownloading = true;\n\n\t\twhile (this.pendingDownloads.length > 0) {\n\t\t\tconst item = this.pendingDownloads.shift();\n\t\t\tif (!item) break;\n\n\t\t\ttry {\n\t\t\t\tawait this.downloadAttachment(item.localPath, item.url);\n\t\t\t\t// Success - could add success logging here if we have context\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMsg = error instanceof Error ? error.message : String(error);\n\t\t\t\tlog.logWarning(`Failed to download attachment`, `${item.localPath}: ${errorMsg}`);\n\t\t\t}\n\t\t}\n\n\t\tthis.isDownloading = false;\n\t}\n\n\t/**\n\t * Download a single attachment\n\t */\n\tprivate async downloadAttachment(localPath: string, url: string): Promise<void> {\n\t\tconst filePath = join(this.workingDir, localPath);\n\n\t\t// Ensure directory exists\n\t\tconst dir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst response = await fetch(url, {\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.botToken}`,\n\t\t\t},\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`HTTP ${response.status}: ${response.statusText}`);\n\t\t}\n\n\t\tconst buffer = await response.arrayBuffer();\n\t\tawait writeFile(filePath, Buffer.from(buffer));\n\t}\n}\n"]}
|
package/dist/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from "fs";
|
|
2
2
|
import { appendFile, writeFile } from "fs/promises";
|
|
3
3
|
import { join } from "path";
|
|
4
|
+
import * as log from "./log.js";
|
|
4
5
|
export class ChannelStore {
|
|
5
6
|
workingDir;
|
|
6
7
|
botToken;
|
|
@@ -62,6 +63,20 @@ export class ChannelStore {
|
|
|
62
63
|
*/
|
|
63
64
|
async logMessage(channelId, message) {
|
|
64
65
|
const logPath = join(this.getChannelDir(channelId), "log.jsonl");
|
|
66
|
+
// Ensure message has a date field
|
|
67
|
+
if (!message.date) {
|
|
68
|
+
// Parse timestamp to get date
|
|
69
|
+
let date;
|
|
70
|
+
if (message.ts.includes(".")) {
|
|
71
|
+
// Slack timestamp format (1234567890.123456)
|
|
72
|
+
date = new Date(parseFloat(message.ts) * 1000);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Epoch milliseconds
|
|
76
|
+
date = new Date(parseInt(message.ts, 10));
|
|
77
|
+
}
|
|
78
|
+
message.date = date.toISOString();
|
|
79
|
+
}
|
|
65
80
|
const line = JSON.stringify(message) + "\n";
|
|
66
81
|
await appendFile(logPath, line, "utf-8");
|
|
67
82
|
}
|
|
@@ -70,6 +85,7 @@ export class ChannelStore {
|
|
|
70
85
|
*/
|
|
71
86
|
async logBotResponse(channelId, text, ts) {
|
|
72
87
|
await this.logMessage(channelId, {
|
|
88
|
+
date: new Date().toISOString(),
|
|
73
89
|
ts,
|
|
74
90
|
user: "bot",
|
|
75
91
|
text,
|
|
@@ -90,11 +106,11 @@ export class ChannelStore {
|
|
|
90
106
|
break;
|
|
91
107
|
try {
|
|
92
108
|
await this.downloadAttachment(item.localPath, item.url);
|
|
93
|
-
|
|
109
|
+
// Success - could add success logging here if we have context
|
|
94
110
|
}
|
|
95
111
|
catch (error) {
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
113
|
+
log.logWarning(`Failed to download attachment`, `${item.localPath}: ${errorMsg}`);
|
|
98
114
|
}
|
|
99
115
|
}
|
|
100
116
|
this.isDownloading = false;
|
package/dist/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\n\nexport interface Attachment {\n\toriginal: string; // original filename from uploader\n\tlocal: string; // path relative to working dir (e.g., \"C12345/attachments/1732531234567_file.png\")\n}\n\nexport interface LoggedMessage {\n\tts: string; // slack timestamp\n\tuser: string; // user ID (or \"bot\" for bot responses)\n\tuserName?: string; // handle (e.g., \"mario\")\n\tdisplayName?: string; // display name (e.g., \"Mario Zechner\")\n\ttext: string;\n\tattachments: Attachment[];\n\tisBot: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n\tbotToken: string; // needed for authenticated file downloads\n}\n\ninterface PendingDownload {\n\tchannelId: string;\n\tlocalPath: string; // relative path\n\turl: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\tprivate botToken: string;\n\tprivate pendingDownloads: PendingDownload[] = [];\n\tprivate isDownloading = false;\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\t\tthis.botToken = config.botToken;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Generate a unique local filename for an attachment\n\t */\n\tgenerateLocalFilename(originalName: string, timestamp: string): string {\n\t\t// Convert slack timestamp (1234567890.123456) to milliseconds\n\t\tconst ts = Math.floor(parseFloat(timestamp) * 1000);\n\t\t// Sanitize original name (remove problematic characters)\n\t\tconst sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n\t\treturn `${ts}_${sanitized}`;\n\t}\n\n\t/**\n\t * Process attachments from a Slack message event\n\t * Returns attachment metadata and queues downloads\n\t */\n\tprocessAttachments(\n\t\tchannelId: string,\n\t\tfiles: Array<{ name: string; url_private_download?: string; url_private?: string }>,\n\t\ttimestamp: string,\n\t): Attachment[] {\n\t\tconst attachments: Attachment[] = [];\n\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url) continue;\n\n\t\t\tconst filename = this.generateLocalFilename(file.name, timestamp);\n\t\t\tconst localPath = `${channelId}/attachments/${filename}`;\n\n\t\t\tattachments.push({\n\t\t\t\toriginal: file.name,\n\t\t\t\tlocal: localPath,\n\t\t\t});\n\n\t\t\t// Queue for background download\n\t\t\tthis.pendingDownloads.push({ channelId, localPath, url });\n\t\t}\n\n\t\t// Trigger background download\n\t\tthis.processDownloadQueue();\n\n\t\treturn attachments;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<void> {\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\t\tconst line = JSON.stringify(message) + \"\\n\";\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tattachments: [],\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Process the download queue in the background\n\t */\n\tprivate async processDownloadQueue(): Promise<void> {\n\t\tif (this.isDownloading || this.pendingDownloads.length === 0) return;\n\n\t\tthis.isDownloading = true;\n\n\t\twhile (this.pendingDownloads.length > 0) {\n\t\t\tconst item = this.pendingDownloads.shift();\n\t\t\tif (!item) break;\n\n\t\t\ttry {\n\t\t\t\tawait this.downloadAttachment(item.localPath, item.url);\n\t\t\t\tconsole.log(`Downloaded: ${item.localPath}`);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`Failed to download ${item.localPath}:`, error);\n\t\t\t\t// Could re-queue for retry here\n\t\t\t}\n\t\t}\n\n\t\tthis.isDownloading = false;\n\t}\n\n\t/**\n\t * Download a single attachment\n\t */\n\tprivate async downloadAttachment(localPath: string, url: string): Promise<void> {\n\t\tconst filePath = join(this.workingDir, localPath);\n\n\t\t// Ensure directory exists\n\t\tconst dir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst response = await fetch(url, {\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.botToken}`,\n\t\t\t},\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`HTTP ${response.status}: ${response.statusText}`);\n\t\t}\n\n\t\tconst buffer = await response.arrayBuffer();\n\t\tawait writeFile(filePath, Buffer.from(buffer));\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AA6BhC,MAAM,OAAO,YAAY;IAChB,UAAU,CAAS;IACnB,QAAQ,CAAS;IACjB,gBAAgB,GAAsB,EAAE,CAAC;IACzC,aAAa,GAAG,KAAK,CAAC;IAE9B,YAAY,MAA0B,EAAE;QACvC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QACpC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QAEhC,kCAAkC;QAClC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAClC,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC;IAAA,CACD;IAED;;OAEG;IACH,aAAa,CAAC,SAAiB,EAAU;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrC,CAAC;QACD,OAAO,GAAG,CAAC;IAAA,CACX;IAED;;OAEG;IACH,qBAAqB,CAAC,YAAoB,EAAE,SAAiB,EAAU;QACtE,8DAA8D;QAC9D,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC;QACpD,yDAAyD;QACzD,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;QAChE,OAAO,GAAG,EAAE,IAAI,SAAS,EAAE,CAAC;IAAA,CAC5B;IAED;;;OAGG;IACH,kBAAkB,CACjB,SAAiB,EACjB,KAAmF,EACnF,SAAiB,EACF;QACf,MAAM,WAAW,GAAiB,EAAE,CAAC;QAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC,WAAW,CAAC;YAC1D,IAAI,CAAC,GAAG;gBAAE,SAAS;YAEnB,MAAM,QAAQ,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;YAClE,MAAM,SAAS,GAAG,GAAG,SAAS,gBAAgB,QAAQ,EAAE,CAAC;YAEzD,WAAW,CAAC,IAAI,CAAC;gBAChB,QAAQ,EAAE,IAAI,CAAC,IAAI;gBACnB,KAAK,EAAE,SAAS;aAChB,CAAC,CAAC;YAEH,gCAAgC;YAChC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;QAC3D,CAAC;QAED,8BAA8B;QAC9B,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAE5B,OAAO,WAAW,CAAC;IAAA,CACnB;IAED;;OAEG;IACH,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,OAAsB,EAAiB;QAC1E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,CAAC;QAEjE,kCAAkC;QAClC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACnB,8BAA8B;YAC9B,IAAI,IAAU,CAAC;YACf,IAAI,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC9B,6CAA6C;gBAC7C,IAAI,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YAChD,CAAC;iBAAM,CAAC;gBACP,qBAAqB;gBACrB,IAAI,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;YAC3C,CAAC;YACD,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QACnC,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;QAC5C,MAAM,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAAA,CACzC;IAED;;OAEG;IACH,KAAK,CAAC,cAAc,CAAC,SAAiB,EAAE,IAAY,EAAE,EAAU,EAAiB;QAChF,MAAM,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE;YAChC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC9B,EAAE;YACF,IAAI,EAAE,KAAK;YACX,IAAI;YACJ,WAAW,EAAE,EAAE;YACf,KAAK,EAAE,IAAI;SACX,CAAC,CAAC;IAAA,CACH;IAED;;OAEG;IACK,KAAK,CAAC,oBAAoB,GAAkB;QACnD,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,gBAAgB,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAErE,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAE1B,OAAO,IAAI,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;YAC3C,IAAI,CAAC,IAAI;gBAAE,MAAM;YAEjB,IAAI,CAAC;gBACJ,MAAM,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;gBACxD,8DAA8D;YAC/D,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,MAAM,QAAQ,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACxE,GAAG,CAAC,UAAU,CAAC,+BAA+B,EAAE,GAAG,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC,CAAC;YACnF,CAAC;QACF,CAAC;QAED,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;IAAA,CAC3B;IAED;;OAEG;IACK,KAAK,CAAC,kBAAkB,CAAC,SAAiB,EAAE,GAAW,EAAiB;QAC/E,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAElD,0BAA0B;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACtF,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrC,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YACjC,OAAO,EAAE;gBACR,aAAa,EAAE,UAAU,IAAI,CAAC,QAAQ,EAAE;aACxC;SACD,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;QACpE,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC5C,MAAM,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAAA,CAC/C;CACD","sourcesContent":["import { existsSync, mkdirSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\n\nexport interface Attachment {\n\toriginal: string; // original filename from uploader\n\tlocal: string; // path relative to working dir (e.g., \"C12345/attachments/1732531234567_file.png\")\n}\n\nexport interface LoggedMessage {\n\tdate: string; // ISO 8601 date (e.g., \"2025-11-26T10:44:00.000Z\") for easy grepping\n\tts: string; // slack timestamp or epoch ms\n\tuser: string; // user ID (or \"bot\" for bot responses)\n\tuserName?: string; // handle (e.g., \"mario\")\n\tdisplayName?: string; // display name (e.g., \"Mario Zechner\")\n\ttext: string;\n\tattachments: Attachment[];\n\tisBot: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n\tbotToken: string; // needed for authenticated file downloads\n}\n\ninterface PendingDownload {\n\tchannelId: string;\n\tlocalPath: string; // relative path\n\turl: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\tprivate botToken: string;\n\tprivate pendingDownloads: PendingDownload[] = [];\n\tprivate isDownloading = false;\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\t\tthis.botToken = config.botToken;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Generate a unique local filename for an attachment\n\t */\n\tgenerateLocalFilename(originalName: string, timestamp: string): string {\n\t\t// Convert slack timestamp (1234567890.123456) to milliseconds\n\t\tconst ts = Math.floor(parseFloat(timestamp) * 1000);\n\t\t// Sanitize original name (remove problematic characters)\n\t\tconst sanitized = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n\t\treturn `${ts}_${sanitized}`;\n\t}\n\n\t/**\n\t * Process attachments from a Slack message event\n\t * Returns attachment metadata and queues downloads\n\t */\n\tprocessAttachments(\n\t\tchannelId: string,\n\t\tfiles: Array<{ name: string; url_private_download?: string; url_private?: string }>,\n\t\ttimestamp: string,\n\t): Attachment[] {\n\t\tconst attachments: Attachment[] = [];\n\n\t\tfor (const file of files) {\n\t\t\tconst url = file.url_private_download || file.url_private;\n\t\t\tif (!url) continue;\n\n\t\t\tconst filename = this.generateLocalFilename(file.name, timestamp);\n\t\t\tconst localPath = `${channelId}/attachments/${filename}`;\n\n\t\t\tattachments.push({\n\t\t\t\toriginal: file.name,\n\t\t\t\tlocal: localPath,\n\t\t\t});\n\n\t\t\t// Queue for background download\n\t\t\tthis.pendingDownloads.push({ channelId, localPath, url });\n\t\t}\n\n\t\t// Trigger background download\n\t\tthis.processDownloadQueue();\n\n\t\treturn attachments;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<void> {\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\t// Parse timestamp to get date\n\t\t\tlet date: Date;\n\t\t\tif (message.ts.includes(\".\")) {\n\t\t\t\t// Slack timestamp format (1234567890.123456)\n\t\t\t\tdate = new Date(parseFloat(message.ts) * 1000);\n\t\t\t} else {\n\t\t\t\t// Epoch milliseconds\n\t\t\t\tdate = new Date(parseInt(message.ts, 10));\n\t\t\t}\n\t\t\tmessage.date = date.toISOString();\n\t\t}\n\n\t\tconst line = JSON.stringify(message) + \"\\n\";\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tattachments: [],\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Process the download queue in the background\n\t */\n\tprivate async processDownloadQueue(): Promise<void> {\n\t\tif (this.isDownloading || this.pendingDownloads.length === 0) return;\n\n\t\tthis.isDownloading = true;\n\n\t\twhile (this.pendingDownloads.length > 0) {\n\t\t\tconst item = this.pendingDownloads.shift();\n\t\t\tif (!item) break;\n\n\t\t\ttry {\n\t\t\t\tawait this.downloadAttachment(item.localPath, item.url);\n\t\t\t\t// Success - could add success logging here if we have context\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMsg = error instanceof Error ? error.message : String(error);\n\t\t\t\tlog.logWarning(`Failed to download attachment`, `${item.localPath}: ${errorMsg}`);\n\t\t\t}\n\t\t}\n\n\t\tthis.isDownloading = false;\n\t}\n\n\t/**\n\t * Download a single attachment\n\t */\n\tprivate async downloadAttachment(localPath: string, url: string): Promise<void> {\n\t\tconst filePath = join(this.workingDir, localPath);\n\n\t\t// Ensure directory exists\n\t\tconst dir = join(this.workingDir, localPath.substring(0, localPath.lastIndexOf(\"/\")));\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst response = await fetch(url, {\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.botToken}`,\n\t\t\t},\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`HTTP ${response.status}: ${response.statusText}`);\n\t\t}\n\n\t\tconst buffer = await response.arrayBuffer();\n\t\tawait writeFile(filePath, Buffer.from(buffer));\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.10.1",
|
|
4
4
|
"description": "Slack bot that delegates messages to the pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,16 +16,17 @@
|
|
|
16
16
|
"clean": "rm -rf dist",
|
|
17
17
|
"build": "tsgo -p tsconfig.build.json && chmod +x dist/main.js",
|
|
18
18
|
"dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
19
|
-
"check": "tsgo --noEmit",
|
|
19
|
+
"check": "biome check --write . && tsgo --noEmit",
|
|
20
20
|
"prepublishOnly": "npm run clean && npm run build"
|
|
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.10.1",
|
|
25
|
+
"@mariozechner/pi-ai": "^0.10.1",
|
|
26
26
|
"@sinclair/typebox": "^0.34.0",
|
|
27
27
|
"@slack/socket-mode": "^2.0.0",
|
|
28
28
|
"@slack/web-api": "^7.0.0",
|
|
29
|
+
"chalk": "^5.6.2",
|
|
29
30
|
"diff": "^8.0.2"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|