@mariozechner/pi-mom 0.18.3 → 0.18.5

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download.js","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"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","sourcesContent":["import { LogLevel, WebClient } from \"@slack/web-api\";\n\ninterface Message {\n\tts: string;\n\tuser?: string;\n\ttext?: string;\n\tthread_ts?: string;\n\treply_count?: number;\n\tfiles?: Array<{ name: string; url_private?: string }>;\n}\n\nfunction formatTs(ts: string): string {\n\tconst date = new Date(parseFloat(ts) * 1000);\n\treturn date\n\t\t.toISOString()\n\t\t.replace(\"T\", \" \")\n\t\t.replace(/\\.\\d+Z$/, \"\");\n}\n\nfunction formatMessage(ts: string, user: string, text: string, indent = \"\"): string {\n\tconst prefix = `[${formatTs(ts)}] ${user}: `;\n\tconst lines = text.split(\"\\n\");\n\tconst firstLine = `${indent}${prefix}${lines[0]}`;\n\tif (lines.length === 1) return firstLine;\n\t// All continuation lines get same indent as content start\n\tconst contentIndent = indent + \" \".repeat(prefix.length);\n\treturn [firstLine, ...lines.slice(1).map((l) => contentIndent + l)].join(\"\\n\");\n}\n\nexport async function downloadChannel(channelId: string, botToken: string): Promise<void> {\n\tconst client = new WebClient(botToken, { logLevel: LogLevel.ERROR });\n\n\tconsole.error(`Fetching channel info for ${channelId}...`);\n\n\t// Get channel info\n\tlet channelName = channelId;\n\ttry {\n\t\tconst info = await client.conversations.info({ channel: channelId });\n\t\tchannelName = (info.channel as any)?.name || channelId;\n\t} catch {\n\t\t// DM channels don't have names, that's fine\n\t}\n\n\tconsole.error(`Downloading history for #${channelName} (${channelId})...`);\n\n\t// Fetch all messages\n\tconst messages: Message[] = [];\n\tlet cursor: string | undefined;\n\n\tdo {\n\t\tconst response = await client.conversations.history({\n\t\t\tchannel: channelId,\n\t\t\tlimit: 200,\n\t\t\tcursor,\n\t\t});\n\n\t\tif (response.messages) {\n\t\t\tmessages.push(...(response.messages as Message[]));\n\t\t}\n\n\t\tcursor = response.response_metadata?.next_cursor;\n\t\tconsole.error(` Fetched ${messages.length} messages...`);\n\t} while (cursor);\n\n\t// Reverse to chronological order\n\tmessages.reverse();\n\n\t// Build map of thread replies\n\tconst threadReplies = new Map<string, Message[]>();\n\tconst threadsToFetch = messages.filter((m) => m.reply_count && m.reply_count > 0);\n\n\tconsole.error(`Fetching ${threadsToFetch.length} threads...`);\n\n\tfor (let i = 0; i < threadsToFetch.length; i++) {\n\t\tconst parent = threadsToFetch[i];\n\t\tconsole.error(` Thread ${i + 1}/${threadsToFetch.length} (${parent.reply_count} replies)...`);\n\n\t\tconst replies: Message[] = [];\n\t\tlet threadCursor: string | undefined;\n\n\t\tdo {\n\t\t\tconst response = await client.conversations.replies({\n\t\t\t\tchannel: channelId,\n\t\t\t\tts: parent.ts,\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor: threadCursor,\n\t\t\t});\n\n\t\t\tif (response.messages) {\n\t\t\t\t// Skip the first message (it's the parent)\n\t\t\t\treplies.push(...(response.messages as Message[]).slice(1));\n\t\t\t}\n\n\t\t\tthreadCursor = response.response_metadata?.next_cursor;\n\t\t} while (threadCursor);\n\n\t\tthreadReplies.set(parent.ts, replies);\n\t}\n\n\t// Output messages with thread replies interleaved\n\tlet totalReplies = 0;\n\tfor (const msg of messages) {\n\t\t// Output the message\n\t\tconsole.log(formatMessage(msg.ts, msg.user || \"unknown\", msg.text || \"\"));\n\n\t\t// Output thread replies right after parent (indented)\n\t\tconst replies = threadReplies.get(msg.ts);\n\t\tif (replies) {\n\t\t\tfor (const reply of replies) {\n\t\t\t\tconsole.log(formatMessage(reply.ts, reply.user || \"unknown\", reply.text || \"\", \" \"));\n\t\t\t\ttotalReplies++;\n\t\t\t}\n\t\t}\n\t}\n\n\tconsole.error(`Done! ${messages.length} messages, ${totalReplies} thread replies`);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport { type AgentRunner, getOrCreateRunner } from \"./agent.js\";\nimport { syncLogToContext } from \"./context.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { type MomHandler, type SlackBot, SlackBot as SlackBotClass, type SlackEvent } from \"./slack.js\";\nimport { ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;\nconst ANTHROPIC_OAUTH_TOKEN = process.env.ANTHROPIC_OAUTH_TOKEN;\n\nfunction parseArgs(): { workingDir: string; sandbox: SandboxConfig } {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t}\n\t}\n\n\tif (!workingDir) {\n\t\tconsole.error(\"Usage: mom [--sandbox=host|docker:<name>] <working-directory>\");\n\t\tprocess.exit(1);\n\t}\n\n\treturn { workingDir: resolve(workingDir), sandbox };\n}\n\nconst { workingDir, sandbox } = parseArgs();\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {\n\tconsole.error(\"Missing env: MOM_SLACK_APP_TOKEN, MOM_SLACK_BOT_TOKEN, ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN\");\n\tprocess.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstore: ChannelStore;\n\tstopRequested: boolean;\n\tstopMessageTs?: string;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\nfunction getState(channelId: string): ChannelState {\n\tlet state = channelStates.get(channelId);\n\tif (!state) {\n\t\tconst channelDir = join(workingDir, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: getOrCreateRunner(sandbox, channelId, channelDir),\n\t\t\tstore: new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! }),\n\t\t\tstopRequested: false,\n\t\t};\n\t\tchannelStates.set(channelId, state);\n\t}\n\treturn state;\n}\n\n// ============================================================================\n// Create SlackContext adapter\n// ============================================================================\n\nfunction createSlackContext(event: SlackEvent, slack: SlackBot, state: ChannelState) {\n\tlet messageTs: string | null = null;\n\tlet accumulatedText = \"\";\n\tlet isWorking = true;\n\tconst workingIndicator = \" ...\";\n\tlet updatePromise = Promise.resolve();\n\n\tconst user = slack.getUser(event.user);\n\n\treturn {\n\t\tmessage: {\n\t\t\ttext: event.text,\n\t\t\trawText: event.text,\n\t\t\tuser: event.user,\n\t\t\tuserName: user?.userName,\n\t\t\tchannel: event.channel,\n\t\t\tts: event.ts,\n\t\t\tattachments: [],\n\t\t},\n\t\tchannelName: slack.getChannel(event.channel)?.name,\n\t\tstore: state.store,\n\t\tchannels: slack.getAllChannels().map((c) => ({ id: c.id, name: c.name })),\n\t\tusers: slack.getAllUsers().map((u) => ({ id: u.id, userName: u.userName, displayName: u.displayName })),\n\n\t\trespond: async (text: string, shouldLog = true) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\taccumulatedText = accumulatedText ? accumulatedText + \"\\n\" + text : text;\n\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t} else {\n\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, displayText);\n\t\t\t\t}\n\n\t\t\t\tif (shouldLog && messageTs) {\n\t\t\t\t\tslack.logBotResponse(event.channel, text, messageTs);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\treplaceMessage: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\taccumulatedText = text;\n\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t} else {\n\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, displayText);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\trespondInThread: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.postInThread(event.channel, messageTs, text);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\tif (isTyping && !messageTs) {\n\t\t\t\taccumulatedText = \"_Thinking_\";\n\t\t\t\tmessageTs = await slack.postMessage(event.channel, accumulatedText + workingIndicator);\n\t\t\t}\n\t\t},\n\n\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\tawait slack.uploadFile(event.channel, filePath, title);\n\t\t},\n\n\t\tsetWorking: async (working: boolean) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\tisWorking = working;\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\t};\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: MomHandler = {\n\tisRunning(channelId: string): boolean {\n\t\tconst state = channelStates.get(channelId);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(channelId: string, slack: SlackBot): Promise<void> {\n\t\tconst state = channelStates.get(channelId);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tconst ts = await slack.postMessage(channelId, \"_Stopping..._\");\n\t\t\tstate.stopMessageTs = ts; // Save for updating later\n\t\t} else {\n\t\t\tawait slack.postMessage(channelId, \"_Nothing running_\");\n\t\t}\n\t},\n\n\tasync handleEvent(event: SlackEvent, slack: SlackBot): Promise<void> {\n\t\tconst state = getState(event.channel);\n\t\tconst channelDir = join(workingDir, event.channel);\n\n\t\t// Start run\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tlog.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n\t\ttry {\n\t\t\t// SYNC context from log.jsonl BEFORE processing\n\t\t\t// This adds any messages that were logged while mom wasn't running\n\t\t\t// Exclude messages >= current ts (will be handled by agent)\n\t\t\tconst syncedCount = syncLogToContext(channelDir, event.ts);\n\t\t\tif (syncedCount > 0) {\n\t\t\t\tlog.logInfo(`[${event.channel}] Synced ${syncedCount} messages from log to context`);\n\t\t\t}\n\n\t\t\t// Create context adapter\n\t\t\tconst ctx = createSlackContext(event, slack, state);\n\n\t\t\t// Run the agent\n\t\t\tawait ctx.setTyping(true);\n\t\t\tawait ctx.setWorking(true);\n\t\t\tconst result = await state.runner.run(ctx as any, state.store);\n\t\t\tawait ctx.setWorking(false);\n\n\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\tif (state.stopMessageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n\t\t\t\t\tstate.stopMessageTs = undefined;\n\t\t\t\t} else {\n\t\t\t\t\tawait slack.postMessage(event.channel, \"_Stopped_\");\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t} finally {\n\t\t\tstate.running = false;\n\t\t}\n\t},\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\nconst bot = new SlackBotClass(handler, {\n\tappToken: MOM_SLACK_APP_TOKEN,\n\tbotToken: MOM_SLACK_BOT_TOKEN,\n\tworkingDir,\n});\n\nbot.start();\n"]}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport { type AgentRunner, getOrCreateRunner } from \"./agent.js\";\nimport { syncLogToContext } from \"./context.js\";\nimport { downloadChannel } from \"./download.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { type MomHandler, type SlackBot, SlackBot as SlackBotClass, type SlackEvent } from \"./slack.js\";\nimport { ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;\nconst ANTHROPIC_OAUTH_TOKEN = process.env.ANTHROPIC_OAUTH_TOKEN;\n\ninterface ParsedArgs {\n\tworkingDir?: string;\n\tsandbox: SandboxConfig;\n\tdownloadChannel?: string;\n}\n\nfunction parseArgs(): ParsedArgs {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\tlet downloadChannelId: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (arg.startsWith(\"--download=\")) {\n\t\t\tdownloadChannelId = arg.slice(\"--download=\".length);\n\t\t} else if (arg === \"--download\") {\n\t\t\tdownloadChannelId = args[++i];\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t}\n\t}\n\n\treturn {\n\t\tworkingDir: workingDir ? resolve(workingDir) : undefined,\n\t\tsandbox,\n\t\tdownloadChannel: downloadChannelId,\n\t};\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --download mode\nif (parsedArgs.downloadChannel) {\n\tif (!MOM_SLACK_BOT_TOKEN) {\n\t\tconsole.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n\t\tprocess.exit(1);\n\t}\n\tawait downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n\tprocess.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n\tconsole.error(\"Usage: mom [--sandbox=host|docker:<name>] <working-directory>\");\n\tconsole.error(\" mom --download <channel-id>\");\n\tprocess.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {\n\tconsole.error(\"Missing env: MOM_SLACK_APP_TOKEN, MOM_SLACK_BOT_TOKEN, ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN\");\n\tprocess.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstore: ChannelStore;\n\tstopRequested: boolean;\n\tstopMessageTs?: string;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\nfunction getState(channelId: string): ChannelState {\n\tlet state = channelStates.get(channelId);\n\tif (!state) {\n\t\tconst channelDir = join(workingDir, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: getOrCreateRunner(sandbox, channelId, channelDir),\n\t\t\tstore: new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! }),\n\t\t\tstopRequested: false,\n\t\t};\n\t\tchannelStates.set(channelId, state);\n\t}\n\treturn state;\n}\n\n// ============================================================================\n// Create SlackContext adapter\n// ============================================================================\n\nfunction createSlackContext(event: SlackEvent, slack: SlackBot, state: ChannelState) {\n\tlet messageTs: string | null = null;\n\tlet accumulatedText = \"\";\n\tlet isWorking = true;\n\tconst workingIndicator = \" ...\";\n\tlet updatePromise = Promise.resolve();\n\n\tconst user = slack.getUser(event.user);\n\n\treturn {\n\t\tmessage: {\n\t\t\ttext: event.text,\n\t\t\trawText: event.text,\n\t\t\tuser: event.user,\n\t\t\tuserName: user?.userName,\n\t\t\tchannel: event.channel,\n\t\t\tts: event.ts,\n\t\t\tattachments: (event.attachments || []).map((a) => ({ local: a.local })),\n\t\t},\n\t\tchannelName: slack.getChannel(event.channel)?.name,\n\t\tstore: state.store,\n\t\tchannels: slack.getAllChannels().map((c) => ({ id: c.id, name: c.name })),\n\t\tusers: slack.getAllUsers().map((u) => ({ id: u.id, userName: u.userName, displayName: u.displayName })),\n\n\t\trespond: async (text: string, shouldLog = true) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\taccumulatedText = accumulatedText ? accumulatedText + \"\\n\" + text : text;\n\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t} else {\n\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, displayText);\n\t\t\t\t}\n\n\t\t\t\tif (shouldLog && messageTs) {\n\t\t\t\t\tslack.logBotResponse(event.channel, text, messageTs);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\treplaceMessage: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\taccumulatedText = text;\n\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t} else {\n\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, displayText);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\trespondInThread: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.postInThread(event.channel, messageTs, text);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\tif (isTyping && !messageTs) {\n\t\t\t\taccumulatedText = \"_Thinking_\";\n\t\t\t\tmessageTs = await slack.postMessage(event.channel, accumulatedText + workingIndicator);\n\t\t\t}\n\t\t},\n\n\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\tawait slack.uploadFile(event.channel, filePath, title);\n\t\t},\n\n\t\tsetWorking: async (working: boolean) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\tisWorking = working;\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\t};\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: MomHandler = {\n\tisRunning(channelId: string): boolean {\n\t\tconst state = channelStates.get(channelId);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(channelId: string, slack: SlackBot): Promise<void> {\n\t\tconst state = channelStates.get(channelId);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tconst ts = await slack.postMessage(channelId, \"_Stopping..._\");\n\t\t\tstate.stopMessageTs = ts; // Save for updating later\n\t\t} else {\n\t\t\tawait slack.postMessage(channelId, \"_Nothing running_\");\n\t\t}\n\t},\n\n\tasync handleEvent(event: SlackEvent, slack: SlackBot): Promise<void> {\n\t\tconst state = getState(event.channel);\n\t\tconst channelDir = join(workingDir, event.channel);\n\n\t\t// Start run\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tlog.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n\t\ttry {\n\t\t\t// SYNC context from log.jsonl BEFORE processing\n\t\t\t// This adds any messages that were logged while mom wasn't running\n\t\t\t// Exclude messages >= current ts (will be handled by agent)\n\t\t\tconst syncedCount = syncLogToContext(channelDir, event.ts);\n\t\t\tif (syncedCount > 0) {\n\t\t\t\tlog.logInfo(`[${event.channel}] Synced ${syncedCount} messages from log to context`);\n\t\t\t}\n\n\t\t\t// Create context adapter\n\t\t\tconst ctx = createSlackContext(event, slack, state);\n\n\t\t\t// Run the agent\n\t\t\tawait ctx.setTyping(true);\n\t\t\tawait ctx.setWorking(true);\n\t\t\tconst result = await state.runner.run(ctx as any, state.store);\n\t\t\tawait ctx.setWorking(false);\n\n\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\tif (state.stopMessageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n\t\t\t\t\tstate.stopMessageTs = undefined;\n\t\t\t\t} else {\n\t\t\t\t\tawait slack.postMessage(event.channel, \"_Stopped_\");\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t} finally {\n\t\t\tstate.running = false;\n\t\t}\n\t},\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\n// Shared store for attachment downloads (also used per-channel in getState)\nconst sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n\nconst bot = new SlackBotClass(handler, {\n\tappToken: MOM_SLACK_APP_TOKEN,\n\tbotToken: MOM_SLACK_BOT_TOKEN,\n\tworkingDir,\n\tstore: sharedStore,\n});\n\nbot.start();\n"]}
package/dist/main.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { join, resolve } from "path";
3
3
  import { getOrCreateRunner } from "./agent.js";
4
4
  import { syncLogToContext } from "./context.js";
5
+ import { downloadChannel } from "./download.js";
5
6
  import * as log from "./log.js";
6
7
  import { parseSandboxArg, validateSandbox } from "./sandbox.js";
7
8
  import { SlackBot as SlackBotClass } from "./slack.js";
@@ -17,6 +18,7 @@ function parseArgs() {
17
18
  const args = process.argv.slice(2);
18
19
  let sandbox = { type: "host" };
19
20
  let workingDir;
21
+ let downloadChannelId;
20
22
  for (let i = 0; i < args.length; i++) {
21
23
  const arg = args[i];
22
24
  if (arg.startsWith("--sandbox=")) {
@@ -25,17 +27,39 @@ function parseArgs() {
25
27
  else if (arg === "--sandbox") {
26
28
  sandbox = parseSandboxArg(args[++i] || "");
27
29
  }
30
+ else if (arg.startsWith("--download=")) {
31
+ downloadChannelId = arg.slice("--download=".length);
32
+ }
33
+ else if (arg === "--download") {
34
+ downloadChannelId = args[++i];
35
+ }
28
36
  else if (!arg.startsWith("-")) {
29
37
  workingDir = arg;
30
38
  }
31
39
  }
32
- if (!workingDir) {
33
- console.error("Usage: mom [--sandbox=host|docker:<name>] <working-directory>");
40
+ return {
41
+ workingDir: workingDir ? resolve(workingDir) : undefined,
42
+ sandbox,
43
+ downloadChannel: downloadChannelId,
44
+ };
45
+ }
46
+ const parsedArgs = parseArgs();
47
+ // Handle --download mode
48
+ if (parsedArgs.downloadChannel) {
49
+ if (!MOM_SLACK_BOT_TOKEN) {
50
+ console.error("Missing env: MOM_SLACK_BOT_TOKEN");
34
51
  process.exit(1);
35
52
  }
36
- return { workingDir: resolve(workingDir), sandbox };
53
+ await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);
54
+ process.exit(0);
55
+ }
56
+ // Normal bot mode - require working dir
57
+ if (!parsedArgs.workingDir) {
58
+ console.error("Usage: mom [--sandbox=host|docker:<name>] <working-directory>");
59
+ console.error(" mom --download <channel-id>");
60
+ process.exit(1);
37
61
  }
38
- const { workingDir, sandbox } = parseArgs();
62
+ const { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };
39
63
  if (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {
40
64
  console.error("Missing env: MOM_SLACK_APP_TOKEN, MOM_SLACK_BOT_TOKEN, ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN");
41
65
  process.exit(1);
@@ -74,7 +98,7 @@ function createSlackContext(event, slack, state) {
74
98
  userName: user?.userName,
75
99
  channel: event.channel,
76
100
  ts: event.ts,
77
- attachments: [],
101
+ attachments: (event.attachments || []).map((a) => ({ local: a.local })),
78
102
  },
79
103
  channelName: slack.getChannel(event.channel)?.name,
80
104
  store: state.store,
@@ -202,10 +226,13 @@ const handler = {
202
226
  // Start
203
227
  // ============================================================================
204
228
  log.logStartup(workingDir, sandbox.type === "host" ? "host" : `docker:${sandbox.container}`);
229
+ // Shared store for attachment downloads (also used per-channel in getState)
230
+ const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN });
205
231
  const bot = new SlackBotClass(handler, {
206
232
  appToken: MOM_SLACK_APP_TOKEN,
207
233
  botToken: MOM_SLACK_BOT_TOKEN,
208
234
  workingDir,
235
+ store: sharedStore,
209
236
  });
210
237
  bot.start();
211
238
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport { type AgentRunner, getOrCreateRunner } from \"./agent.js\";\nimport { syncLogToContext } from \"./context.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { type MomHandler, type SlackBot, SlackBot as SlackBotClass, type SlackEvent } from \"./slack.js\";\nimport { ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;\nconst ANTHROPIC_OAUTH_TOKEN = process.env.ANTHROPIC_OAUTH_TOKEN;\n\nfunction parseArgs(): { workingDir: string; sandbox: SandboxConfig } {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t}\n\t}\n\n\tif (!workingDir) {\n\t\tconsole.error(\"Usage: mom [--sandbox=host|docker:<name>] <working-directory>\");\n\t\tprocess.exit(1);\n\t}\n\n\treturn { workingDir: resolve(workingDir), sandbox };\n}\n\nconst { workingDir, sandbox } = parseArgs();\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {\n\tconsole.error(\"Missing env: MOM_SLACK_APP_TOKEN, MOM_SLACK_BOT_TOKEN, ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN\");\n\tprocess.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstore: ChannelStore;\n\tstopRequested: boolean;\n\tstopMessageTs?: string;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\nfunction getState(channelId: string): ChannelState {\n\tlet state = channelStates.get(channelId);\n\tif (!state) {\n\t\tconst channelDir = join(workingDir, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: getOrCreateRunner(sandbox, channelId, channelDir),\n\t\t\tstore: new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! }),\n\t\t\tstopRequested: false,\n\t\t};\n\t\tchannelStates.set(channelId, state);\n\t}\n\treturn state;\n}\n\n// ============================================================================\n// Create SlackContext adapter\n// ============================================================================\n\nfunction createSlackContext(event: SlackEvent, slack: SlackBot, state: ChannelState) {\n\tlet messageTs: string | null = null;\n\tlet accumulatedText = \"\";\n\tlet isWorking = true;\n\tconst workingIndicator = \" ...\";\n\tlet updatePromise = Promise.resolve();\n\n\tconst user = slack.getUser(event.user);\n\n\treturn {\n\t\tmessage: {\n\t\t\ttext: event.text,\n\t\t\trawText: event.text,\n\t\t\tuser: event.user,\n\t\t\tuserName: user?.userName,\n\t\t\tchannel: event.channel,\n\t\t\tts: event.ts,\n\t\t\tattachments: [],\n\t\t},\n\t\tchannelName: slack.getChannel(event.channel)?.name,\n\t\tstore: state.store,\n\t\tchannels: slack.getAllChannels().map((c) => ({ id: c.id, name: c.name })),\n\t\tusers: slack.getAllUsers().map((u) => ({ id: u.id, userName: u.userName, displayName: u.displayName })),\n\n\t\trespond: async (text: string, shouldLog = true) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\taccumulatedText = accumulatedText ? accumulatedText + \"\\n\" + text : text;\n\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t} else {\n\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, displayText);\n\t\t\t\t}\n\n\t\t\t\tif (shouldLog && messageTs) {\n\t\t\t\t\tslack.logBotResponse(event.channel, text, messageTs);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\treplaceMessage: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\taccumulatedText = text;\n\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t} else {\n\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, displayText);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\trespondInThread: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.postInThread(event.channel, messageTs, text);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\tif (isTyping && !messageTs) {\n\t\t\t\taccumulatedText = \"_Thinking_\";\n\t\t\t\tmessageTs = await slack.postMessage(event.channel, accumulatedText + workingIndicator);\n\t\t\t}\n\t\t},\n\n\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\tawait slack.uploadFile(event.channel, filePath, title);\n\t\t},\n\n\t\tsetWorking: async (working: boolean) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\tisWorking = working;\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\t};\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: MomHandler = {\n\tisRunning(channelId: string): boolean {\n\t\tconst state = channelStates.get(channelId);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(channelId: string, slack: SlackBot): Promise<void> {\n\t\tconst state = channelStates.get(channelId);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tconst ts = await slack.postMessage(channelId, \"_Stopping..._\");\n\t\t\tstate.stopMessageTs = ts; // Save for updating later\n\t\t} else {\n\t\t\tawait slack.postMessage(channelId, \"_Nothing running_\");\n\t\t}\n\t},\n\n\tasync handleEvent(event: SlackEvent, slack: SlackBot): Promise<void> {\n\t\tconst state = getState(event.channel);\n\t\tconst channelDir = join(workingDir, event.channel);\n\n\t\t// Start run\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tlog.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n\t\ttry {\n\t\t\t// SYNC context from log.jsonl BEFORE processing\n\t\t\t// This adds any messages that were logged while mom wasn't running\n\t\t\t// Exclude messages >= current ts (will be handled by agent)\n\t\t\tconst syncedCount = syncLogToContext(channelDir, event.ts);\n\t\t\tif (syncedCount > 0) {\n\t\t\t\tlog.logInfo(`[${event.channel}] Synced ${syncedCount} messages from log to context`);\n\t\t\t}\n\n\t\t\t// Create context adapter\n\t\t\tconst ctx = createSlackContext(event, slack, state);\n\n\t\t\t// Run the agent\n\t\t\tawait ctx.setTyping(true);\n\t\t\tawait ctx.setWorking(true);\n\t\t\tconst result = await state.runner.run(ctx as any, state.store);\n\t\t\tawait ctx.setWorking(false);\n\n\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\tif (state.stopMessageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n\t\t\t\t\tstate.stopMessageTs = undefined;\n\t\t\t\t} else {\n\t\t\t\t\tawait slack.postMessage(event.channel, \"_Stopped_\");\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t} finally {\n\t\t\tstate.running = false;\n\t\t}\n\t},\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\nconst bot = new SlackBotClass(handler, {\n\tappToken: MOM_SLACK_APP_TOKEN,\n\tbotToken: MOM_SLACK_BOT_TOKEN,\n\tworkingDir,\n});\n\nbot.start();\n"]}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport { type AgentRunner, getOrCreateRunner } from \"./agent.js\";\nimport { syncLogToContext } from \"./context.js\";\nimport { downloadChannel } from \"./download.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { type MomHandler, type SlackBot, SlackBot as SlackBotClass, type SlackEvent } from \"./slack.js\";\nimport { ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;\nconst ANTHROPIC_OAUTH_TOKEN = process.env.ANTHROPIC_OAUTH_TOKEN;\n\ninterface ParsedArgs {\n\tworkingDir?: string;\n\tsandbox: SandboxConfig;\n\tdownloadChannel?: string;\n}\n\nfunction parseArgs(): ParsedArgs {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\tlet downloadChannelId: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (arg.startsWith(\"--download=\")) {\n\t\t\tdownloadChannelId = arg.slice(\"--download=\".length);\n\t\t} else if (arg === \"--download\") {\n\t\t\tdownloadChannelId = args[++i];\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t}\n\t}\n\n\treturn {\n\t\tworkingDir: workingDir ? resolve(workingDir) : undefined,\n\t\tsandbox,\n\t\tdownloadChannel: downloadChannelId,\n\t};\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --download mode\nif (parsedArgs.downloadChannel) {\n\tif (!MOM_SLACK_BOT_TOKEN) {\n\t\tconsole.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n\t\tprocess.exit(1);\n\t}\n\tawait downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n\tprocess.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n\tconsole.error(\"Usage: mom [--sandbox=host|docker:<name>] <working-directory>\");\n\tconsole.error(\" mom --download <channel-id>\");\n\tprocess.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {\n\tconsole.error(\"Missing env: MOM_SLACK_APP_TOKEN, MOM_SLACK_BOT_TOKEN, ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN\");\n\tprocess.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstore: ChannelStore;\n\tstopRequested: boolean;\n\tstopMessageTs?: string;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\nfunction getState(channelId: string): ChannelState {\n\tlet state = channelStates.get(channelId);\n\tif (!state) {\n\t\tconst channelDir = join(workingDir, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: getOrCreateRunner(sandbox, channelId, channelDir),\n\t\t\tstore: new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! }),\n\t\t\tstopRequested: false,\n\t\t};\n\t\tchannelStates.set(channelId, state);\n\t}\n\treturn state;\n}\n\n// ============================================================================\n// Create SlackContext adapter\n// ============================================================================\n\nfunction createSlackContext(event: SlackEvent, slack: SlackBot, state: ChannelState) {\n\tlet messageTs: string | null = null;\n\tlet accumulatedText = \"\";\n\tlet isWorking = true;\n\tconst workingIndicator = \" ...\";\n\tlet updatePromise = Promise.resolve();\n\n\tconst user = slack.getUser(event.user);\n\n\treturn {\n\t\tmessage: {\n\t\t\ttext: event.text,\n\t\t\trawText: event.text,\n\t\t\tuser: event.user,\n\t\t\tuserName: user?.userName,\n\t\t\tchannel: event.channel,\n\t\t\tts: event.ts,\n\t\t\tattachments: (event.attachments || []).map((a) => ({ local: a.local })),\n\t\t},\n\t\tchannelName: slack.getChannel(event.channel)?.name,\n\t\tstore: state.store,\n\t\tchannels: slack.getAllChannels().map((c) => ({ id: c.id, name: c.name })),\n\t\tusers: slack.getAllUsers().map((u) => ({ id: u.id, userName: u.userName, displayName: u.displayName })),\n\n\t\trespond: async (text: string, shouldLog = true) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\taccumulatedText = accumulatedText ? accumulatedText + \"\\n\" + text : text;\n\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t} else {\n\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, displayText);\n\t\t\t\t}\n\n\t\t\t\tif (shouldLog && messageTs) {\n\t\t\t\t\tslack.logBotResponse(event.channel, text, messageTs);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\treplaceMessage: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\taccumulatedText = text;\n\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t} else {\n\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, displayText);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\trespondInThread: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.postInThread(event.channel, messageTs, text);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\tif (isTyping && !messageTs) {\n\t\t\t\taccumulatedText = \"_Thinking_\";\n\t\t\t\tmessageTs = await slack.postMessage(event.channel, accumulatedText + workingIndicator);\n\t\t\t}\n\t\t},\n\n\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\tawait slack.uploadFile(event.channel, filePath, title);\n\t\t},\n\n\t\tsetWorking: async (working: boolean) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\tisWorking = working;\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\t};\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: MomHandler = {\n\tisRunning(channelId: string): boolean {\n\t\tconst state = channelStates.get(channelId);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(channelId: string, slack: SlackBot): Promise<void> {\n\t\tconst state = channelStates.get(channelId);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tconst ts = await slack.postMessage(channelId, \"_Stopping..._\");\n\t\t\tstate.stopMessageTs = ts; // Save for updating later\n\t\t} else {\n\t\t\tawait slack.postMessage(channelId, \"_Nothing running_\");\n\t\t}\n\t},\n\n\tasync handleEvent(event: SlackEvent, slack: SlackBot): Promise<void> {\n\t\tconst state = getState(event.channel);\n\t\tconst channelDir = join(workingDir, event.channel);\n\n\t\t// Start run\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tlog.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n\t\ttry {\n\t\t\t// SYNC context from log.jsonl BEFORE processing\n\t\t\t// This adds any messages that were logged while mom wasn't running\n\t\t\t// Exclude messages >= current ts (will be handled by agent)\n\t\t\tconst syncedCount = syncLogToContext(channelDir, event.ts);\n\t\t\tif (syncedCount > 0) {\n\t\t\t\tlog.logInfo(`[${event.channel}] Synced ${syncedCount} messages from log to context`);\n\t\t\t}\n\n\t\t\t// Create context adapter\n\t\t\tconst ctx = createSlackContext(event, slack, state);\n\n\t\t\t// Run the agent\n\t\t\tawait ctx.setTyping(true);\n\t\t\tawait ctx.setWorking(true);\n\t\t\tconst result = await state.runner.run(ctx as any, state.store);\n\t\t\tawait ctx.setWorking(false);\n\n\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\tif (state.stopMessageTs) {\n\t\t\t\t\tawait slack.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n\t\t\t\t\tstate.stopMessageTs = undefined;\n\t\t\t\t} else {\n\t\t\t\t\tawait slack.postMessage(event.channel, \"_Stopped_\");\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t} finally {\n\t\t\tstate.running = false;\n\t\t}\n\t},\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\n// Shared store for attachment downloads (also used per-channel in getState)\nconst sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n\nconst bot = new SlackBotClass(handler, {\n\tappToken: MOM_SLACK_APP_TOKEN,\n\tbotToken: MOM_SLACK_BOT_TOKEN,\n\tworkingDir,\n\tstore: sharedStore,\n});\n\nbot.start();\n"]}
package/dist/slack.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Attachment, ChannelStore } from "./store.js";
1
2
  export interface SlackEvent {
2
3
  type: "mention" | "dm";
3
4
  channel: string;
@@ -5,10 +6,12 @@ export interface SlackEvent {
5
6
  user: string;
6
7
  text: string;
7
8
  files?: Array<{
8
- name: string;
9
+ name?: string;
9
10
  url_private_download?: string;
10
11
  url_private?: string;
11
12
  }>;
13
+ /** Processed attachments with local paths (populated after logUserMessage) */
14
+ attachments?: Attachment[];
12
15
  }
13
16
  export interface SlackUser {
14
17
  id: string;
@@ -71,6 +74,7 @@ export declare class SlackBot {
71
74
  private webClient;
72
75
  private handler;
73
76
  private workingDir;
77
+ private store;
74
78
  private botUserId;
75
79
  private startupTs;
76
80
  private users;
@@ -80,6 +84,7 @@ export declare class SlackBot {
80
84
  appToken: string;
81
85
  botToken: string;
82
86
  workingDir: string;
87
+ store: ChannelStore;
83
88
  });
84
89
  start(): Promise<void>;
85
90
  getUser(userId: string): SlackUser | undefined;
@@ -103,6 +108,7 @@ export declare class SlackBot {
103
108
  private setupEventHandlers;
104
109
  /**
105
110
  * Log a user message to log.jsonl (SYNC)
111
+ * Downloads attachments in background via store
106
112
  */
107
113
  private logUserMessage;
108
114
  private getExistingTimestamps;
@@ -1 +1 @@
1
- {"version":3,"file":"slack.d.ts","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 { appendFileSync, existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport * as log from \"./log.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface SlackEvent {\n\ttype: \"mention\" | \"dm\";\n\tchannel: string;\n\tts: string;\n\tuser: string;\n\ttext: string;\n\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n}\n\nexport interface SlackUser {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\nexport interface SlackChannel {\n\tid: string;\n\tname: string;\n}\n\n// Types used by agent.ts\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 interface SlackContext {\n\tmessage: {\n\t\ttext: string;\n\t\trawText: string;\n\t\tuser: string;\n\t\tuserName?: string;\n\t\tchannel: string;\n\t\tts: string;\n\t\tattachments: Array<{ local: string }>;\n\t};\n\tchannelName?: string;\n\tchannels: ChannelInfo[];\n\tusers: UserInfo[];\n\trespond: (text: string, shouldLog?: boolean) => Promise<void>;\n\treplaceMessage: (text: string) => Promise<void>;\n\trespondInThread: (text: string) => Promise<void>;\n\tsetTyping: (isTyping: boolean) => Promise<void>;\n\tuploadFile: (filePath: string, title?: string) => Promise<void>;\n\tsetWorking: (working: boolean) => Promise<void>;\n}\n\nexport interface MomHandler {\n\t/**\n\t * Check if channel is currently running (SYNC)\n\t */\n\tisRunning(channelId: string): boolean;\n\n\t/**\n\t * Handle an event that triggers mom (ASYNC)\n\t * Called only when isRunning() returned false\n\t */\n\thandleEvent(event: SlackEvent, slack: SlackBot): Promise<void>;\n\n\t/**\n\t * Handle stop command (ASYNC)\n\t * Called when user says \"stop\" while mom is running\n\t */\n\thandleStop(channelId: string, slack: SlackBot): Promise<void>;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n\tprivate queue: QueuedWork[] = [];\n\tprivate processing = false;\n\n\tenqueue(work: QueuedWork): void {\n\t\tthis.queue.push(work);\n\t\tthis.processNext();\n\t}\n\n\tprivate async processNext(): Promise<void> {\n\t\tif (this.processing || this.queue.length === 0) return;\n\t\tthis.processing = true;\n\t\tconst work = this.queue.shift()!;\n\t\ttry {\n\t\t\tawait work();\n\t\t} catch (err) {\n\t\t\tlog.logWarning(\"Queue error\", err instanceof Error ? err.message : String(err));\n\t\t}\n\t\tthis.processing = false;\n\t\tthis.processNext();\n\t}\n}\n\n// ============================================================================\n// SlackBot\n// ============================================================================\n\nexport class SlackBot {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate handler: MomHandler;\n\tprivate workingDir: string;\n\tprivate botUserId: string | null = null;\n\tprivate startupTs: string | null = null; // Messages older than this are just logged, not processed\n\n\tprivate users = new Map<string, SlackUser>();\n\tprivate channels = new Map<string, SlackChannel>();\n\tprivate queues = new Map<string, ChannelQueue>();\n\n\tconstructor(handler: MomHandler, config: { appToken: string; botToken: string; workingDir: string }) {\n\t\tthis.handler = handler;\n\t\tthis.workingDir = config.workingDir;\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t}\n\n\t// ==========================================================================\n\t// Public API\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\tawait Promise.all([this.fetchUsers(), this.fetchChannels()]);\n\t\tlog.logInfo(`Loaded ${this.channels.size} channels, ${this.users.size} users`);\n\n\t\tawait this.backfillAllChannels();\n\n\t\tthis.setupEventHandlers();\n\t\tawait this.socketClient.start();\n\n\t\t// Record startup time - messages older than this are just logged, not processed\n\t\tthis.startupTs = (Date.now() / 1000).toFixed(6);\n\n\t\tlog.logConnected();\n\t}\n\n\tgetUser(userId: string): SlackUser | undefined {\n\t\treturn this.users.get(userId);\n\t}\n\n\tgetChannel(channelId: string): SlackChannel | undefined {\n\t\treturn this.channels.get(channelId);\n\t}\n\n\tgetAllUsers(): SlackUser[] {\n\t\treturn Array.from(this.users.values());\n\t}\n\n\tgetAllChannels(): SlackChannel[] {\n\t\treturn Array.from(this.channels.values());\n\t}\n\n\tasync postMessage(channel: string, text: string): Promise<string> {\n\t\tconst result = await this.webClient.chat.postMessage({ channel, text });\n\t\treturn result.ts as string;\n\t}\n\n\tasync updateMessage(channel: string, ts: string, text: string): Promise<void> {\n\t\tawait this.webClient.chat.update({ channel, ts, text });\n\t}\n\n\tasync postInThread(channel: string, threadTs: string, text: string): Promise<void> {\n\t\tawait this.webClient.chat.postMessage({ channel, thread_ts: threadTs, text });\n\t}\n\n\tasync uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n\t\tconst fileName = title || basename(filePath);\n\t\tconst fileContent = readFileSync(filePath);\n\t\tawait this.webClient.files.uploadV2({\n\t\t\tchannel_id: channel,\n\t\t\tfile: fileContent,\n\t\t\tfilename: fileName,\n\t\t\ttitle: fileName,\n\t\t});\n\t}\n\n\t/**\n\t * Log a message to log.jsonl (SYNC)\n\t * This is the ONLY place messages are written to log.jsonl\n\t */\n\tlogToFile(channel: string, entry: object): void {\n\t\tconst dir = join(this.workingDir, channel);\n\t\tif (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\t\tappendFileSync(join(dir, \"log.jsonl\"), JSON.stringify(entry) + \"\\n\");\n\t}\n\n\t/**\n\t * Log a bot response to log.jsonl\n\t */\n\tlogBotResponse(channel: string, text: string, ts: string): void {\n\t\tthis.logToFile(channel, {\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// Private - Event Handlers\n\t// ==========================================================================\n\n\tprivate getQueue(channelId: string): ChannelQueue {\n\t\tlet queue = this.queues.get(channelId);\n\t\tif (!queue) {\n\t\t\tqueue = new ChannelQueue();\n\t\t\tthis.queues.set(channelId, queue);\n\t\t}\n\t\treturn queue;\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\t// Channel @mentions\n\t\tthis.socketClient.on(\"app_mention\", ({ event, ack }) => {\n\t\t\tconst e = 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// Skip DMs (handled by message event)\n\t\t\tif (e.channel.startsWith(\"D\")) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst slackEvent: SlackEvent = {\n\t\t\t\ttype: \"mention\",\n\t\t\t\tchannel: e.channel,\n\t\t\t\tts: e.ts,\n\t\t\t\tuser: e.user,\n\t\t\t\ttext: e.text.replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t};\n\n\t\t\t// SYNC: Log to log.jsonl (ALWAYS, even for old messages)\n\t\t\tthis.logUserMessage(slackEvent);\n\n\t\t\t// Only trigger processing for messages AFTER startup (not replayed old messages)\n\t\t\tif (this.startupTs && e.ts < this.startupTs) {\n\t\t\t\tlog.logInfo(\n\t\t\t\t\t`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`,\n\t\t\t\t);\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for stop command - execute immediately, don't queue!\n\t\t\tif (slackEvent.text.toLowerCase().trim() === \"stop\") {\n\t\t\t\tif (this.handler.isRunning(e.channel)) {\n\t\t\t\t\tthis.handler.handleStop(e.channel, this); // Don't await, don't queue\n\t\t\t\t} else {\n\t\t\t\t\tthis.postMessage(e.channel, \"_Nothing running_\");\n\t\t\t\t}\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// SYNC: Check if busy\n\t\t\tif (this.handler.isRunning(e.channel)) {\n\t\t\t\tthis.postMessage(e.channel, \"_Already working. Say `@mom stop` to cancel._\");\n\t\t\t} else {\n\t\t\t\tthis.getQueue(e.channel).enqueue(() => this.handler.handleEvent(slackEvent, this));\n\t\t\t}\n\n\t\t\tack();\n\t\t});\n\n\t\t// All messages (for logging) + DMs (for triggering)\n\t\tthis.socketClient.on(\"message\", ({ event, ack }) => {\n\t\t\tconst e = 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// Skip bot messages, edits, etc.\n\t\t\tif (e.bot_id || !e.user || e.user === this.botUserId) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (e.subtype !== undefined && e.subtype !== \"file_share\") {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!e.text && (!e.files || e.files.length === 0)) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst isDM = e.channel_type === \"im\";\n\t\t\tconst isBotMention = e.text?.includes(`<@${this.botUserId}>`);\n\n\t\t\t// Skip channel @mentions - already handled by app_mention event\n\t\t\tif (!isDM && isBotMention) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst slackEvent: SlackEvent = {\n\t\t\t\ttype: isDM ? \"dm\" : \"mention\",\n\t\t\t\tchannel: e.channel,\n\t\t\t\tts: e.ts,\n\t\t\t\tuser: e.user,\n\t\t\t\ttext: (e.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t};\n\n\t\t\t// SYNC: Log to log.jsonl (ALL messages - channel chatter and DMs)\n\t\t\tthis.logUserMessage(slackEvent);\n\n\t\t\t// Only trigger processing for messages AFTER startup (not replayed old messages)\n\t\t\tif (this.startupTs && e.ts < this.startupTs) {\n\t\t\t\tlog.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Only trigger handler for DMs\n\t\t\tif (isDM) {\n\t\t\t\t// Check for stop command - execute immediately, don't queue!\n\t\t\t\tif (slackEvent.text.toLowerCase().trim() === \"stop\") {\n\t\t\t\t\tif (this.handler.isRunning(e.channel)) {\n\t\t\t\t\t\tthis.handler.handleStop(e.channel, this); // Don't await, don't queue\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.postMessage(e.channel, \"_Nothing running_\");\n\t\t\t\t\t}\n\t\t\t\t\tack();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (this.handler.isRunning(e.channel)) {\n\t\t\t\t\tthis.postMessage(e.channel, \"_Already working. Say `stop` to cancel._\");\n\t\t\t\t} else {\n\t\t\t\t\tthis.getQueue(e.channel).enqueue(() => this.handler.handleEvent(slackEvent, this));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tack();\n\t\t});\n\t}\n\n\t/**\n\t * Log a user message to log.jsonl (SYNC)\n\t */\n\tprivate logUserMessage(event: SlackEvent): void {\n\t\tconst user = this.users.get(event.user);\n\t\tthis.logToFile(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: user?.userName,\n\t\t\tdisplayName: user?.displayName,\n\t\t\ttext: event.text,\n\t\t\tattachments: event.files?.map((f) => f.name) || [],\n\t\t\tisBot: false,\n\t\t});\n\t}\n\n\t// ==========================================================================\n\t// Private - Backfill\n\t// ==========================================================================\n\n\tprivate getExistingTimestamps(channelId: string): Set<string> {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tconst timestamps = new Set<string>();\n\t\tif (!existsSync(logPath)) return timestamps;\n\n\t\tconst content = readFileSync(logPath, \"utf-8\");\n\t\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\t\tfor (const line of lines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.ts) timestamps.add(entry.ts);\n\t\t\t} catch {}\n\t\t}\n\t\treturn timestamps;\n\t}\n\n\tprivate async backfillChannel(channelId: string): Promise<number> {\n\t\tconst existingTs = this.getExistingTimestamps(channelId);\n\n\t\t// Find the biggest ts in log.jsonl\n\t\tlet latestTs: string | undefined;\n\t\tfor (const ts of existingTs) {\n\t\t\tif (!latestTs || parseFloat(ts) > parseFloat(latestTs)) latestTs = ts;\n\t\t}\n\n\t\ttype Message = {\n\t\t\tuser?: string;\n\t\t\tbot_id?: string;\n\t\t\ttext?: string;\n\t\t\tts?: string;\n\t\t\tsubtype?: string;\n\t\t\tfiles?: Array<{ name: string }>;\n\t\t};\n\t\tconst allMessages: Message[] = [];\n\n\t\tlet cursor: string | undefined;\n\t\tlet pageCount = 0;\n\t\tconst maxPages = 3;\n\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.history({\n\t\t\t\tchannel: channelId,\n\t\t\t\toldest: latestTs, // Only fetch messages newer than what we have\n\t\t\t\tinclusive: false,\n\t\t\t\tlimit: 1000,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tif (result.messages) {\n\t\t\t\tallMessages.push(...(result.messages as Message[]));\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t\tpageCount++;\n\t\t} while (cursor && pageCount < maxPages);\n\n\t\t// Filter: include mom's messages, exclude other bots, skip already logged\n\t\tconst relevantMessages = allMessages.filter((msg) => {\n\t\t\tif (!msg.ts || existingTs.has(msg.ts)) return false; // Skip duplicates\n\t\t\tif (msg.user === this.botUserId) return true;\n\t\t\tif (msg.bot_id) return false;\n\t\t\tif (msg.subtype !== undefined && msg.subtype !== \"file_share\") return false;\n\t\t\tif (!msg.user) return false;\n\t\t\tif (!msg.text && (!msg.files || msg.files.length === 0)) return false;\n\t\t\treturn true;\n\t\t});\n\n\t\t// Reverse to chronological order\n\t\trelevantMessages.reverse();\n\n\t\t// Log each message to log.jsonl\n\t\tfor (const msg of relevantMessages) {\n\t\t\tconst isMomMessage = msg.user === this.botUserId;\n\t\t\tconst user = this.users.get(msg.user!);\n\t\t\t// Strip @mentions from text (same as live messages)\n\t\t\tconst text = (msg.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim();\n\n\t\t\tthis.logToFile(channelId, {\n\t\t\t\tdate: new Date(parseFloat(msg.ts!) * 1000).toISOString(),\n\t\t\t\tts: msg.ts!,\n\t\t\t\tuser: isMomMessage ? \"bot\" : msg.user!,\n\t\t\t\tuserName: isMomMessage ? undefined : user?.userName,\n\t\t\t\tdisplayName: isMomMessage ? undefined : user?.displayName,\n\t\t\t\ttext,\n\t\t\t\tattachments: msg.files?.map((f) => f.name) || [],\n\t\t\t\tisBot: isMomMessage,\n\t\t\t});\n\t\t}\n\n\t\treturn relevantMessages.length;\n\t}\n\n\tprivate async backfillAllChannels(): Promise<void> {\n\t\tconst startTime = Date.now();\n\n\t\t// Only backfill channels that already have a log.jsonl (mom has interacted with them before)\n\t\tconst channelsToBackfill: Array<[string, SlackChannel]> = [];\n\t\tfor (const [channelId, channel] of this.channels) {\n\t\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\t\tif (existsSync(logPath)) {\n\t\t\t\tchannelsToBackfill.push([channelId, channel]);\n\t\t\t}\n\t\t}\n\n\t\tlog.logBackfillStart(channelsToBackfill.length);\n\n\t\tlet totalMessages = 0;\n\t\tfor (const [channelId, channel] of channelsToBackfill) {\n\t\t\ttry {\n\t\t\t\tconst count = await this.backfillChannel(channelId);\n\t\t\t\tif (count > 0) log.logBackfillChannel(channel.name, count);\n\t\t\t\ttotalMessages += count;\n\t\t\t} catch (error) {\n\t\t\t\tlog.logWarning(`Failed to backfill #${channel.name}`, String(error));\n\t\t\t}\n\t\t}\n\n\t\tconst durationMs = Date.now() - startTime;\n\t\tlog.logBackfillComplete(totalMessages, durationMs);\n\t}\n\n\t// ==========================================================================\n\t// Private - Fetch Users/Channels\n\t// ==========================================================================\n\n\tprivate async fetchUsers(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.users.list({ limit: 200, cursor });\n\t\t\tconst members = result.members as\n\t\t\t\t| Array<{ id?: string; name?: string; real_name?: string; deleted?: boolean }>\n\t\t\t\t| undefined;\n\t\t\tif (members) {\n\t\t\t\tfor (const u of members) {\n\t\t\t\t\tif (u.id && u.name && !u.deleted) {\n\t\t\t\t\t\tthis.users.set(u.id, { id: u.id, userName: u.name, displayName: u.real_name || u.name });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n\n\tprivate async fetchChannels(): Promise<void> {\n\t\t// Fetch public/private channels\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"public_channel,private_channel\",\n\t\t\t\texclude_archived: true,\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst channels = result.channels as Array<{ id?: string; name?: string; is_member?: boolean }> | undefined;\n\t\t\tif (channels) {\n\t\t\t\tfor (const c of channels) {\n\t\t\t\t\tif (c.id && c.name && c.is_member) {\n\t\t\t\t\t\tthis.channels.set(c.id, { id: c.id, name: c.name });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\n\t\t// Also fetch DM channels (IMs)\n\t\tcursor = undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"im\",\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst ims = result.channels as Array<{ id?: string; user?: string }> | undefined;\n\t\t\tif (ims) {\n\t\t\t\tfor (const im of ims) {\n\t\t\t\t\tif (im.id) {\n\t\t\t\t\t\t// Use user's name as channel name for DMs\n\t\t\t\t\t\tconst user = im.user ? this.users.get(im.user) : undefined;\n\t\t\t\t\t\tconst name = user ? `DM:${user.userName}` : `DM:${im.id}`;\n\t\t\t\t\t\tthis.channels.set(im.id, { id: im.id, name });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n}\n"]}
1
+ {"version":3,"file":"slack.d.ts","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 { appendFileSync, existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport * as log from \"./log.js\";\nimport type { Attachment, ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface SlackEvent {\n\ttype: \"mention\" | \"dm\";\n\tchannel: string;\n\tts: string;\n\tuser: string;\n\ttext: string;\n\tfiles?: Array<{ name?: string; url_private_download?: string; url_private?: string }>;\n\t/** Processed attachments with local paths (populated after logUserMessage) */\n\tattachments?: Attachment[];\n}\n\nexport interface SlackUser {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\nexport interface SlackChannel {\n\tid: string;\n\tname: string;\n}\n\n// Types used by agent.ts\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 interface SlackContext {\n\tmessage: {\n\t\ttext: string;\n\t\trawText: string;\n\t\tuser: string;\n\t\tuserName?: string;\n\t\tchannel: string;\n\t\tts: string;\n\t\tattachments: Array<{ local: string }>;\n\t};\n\tchannelName?: string;\n\tchannels: ChannelInfo[];\n\tusers: UserInfo[];\n\trespond: (text: string, shouldLog?: boolean) => Promise<void>;\n\treplaceMessage: (text: string) => Promise<void>;\n\trespondInThread: (text: string) => Promise<void>;\n\tsetTyping: (isTyping: boolean) => Promise<void>;\n\tuploadFile: (filePath: string, title?: string) => Promise<void>;\n\tsetWorking: (working: boolean) => Promise<void>;\n}\n\nexport interface MomHandler {\n\t/**\n\t * Check if channel is currently running (SYNC)\n\t */\n\tisRunning(channelId: string): boolean;\n\n\t/**\n\t * Handle an event that triggers mom (ASYNC)\n\t * Called only when isRunning() returned false\n\t */\n\thandleEvent(event: SlackEvent, slack: SlackBot): Promise<void>;\n\n\t/**\n\t * Handle stop command (ASYNC)\n\t * Called when user says \"stop\" while mom is running\n\t */\n\thandleStop(channelId: string, slack: SlackBot): Promise<void>;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n\tprivate queue: QueuedWork[] = [];\n\tprivate processing = false;\n\n\tenqueue(work: QueuedWork): void {\n\t\tthis.queue.push(work);\n\t\tthis.processNext();\n\t}\n\n\tprivate async processNext(): Promise<void> {\n\t\tif (this.processing || this.queue.length === 0) return;\n\t\tthis.processing = true;\n\t\tconst work = this.queue.shift()!;\n\t\ttry {\n\t\t\tawait work();\n\t\t} catch (err) {\n\t\t\tlog.logWarning(\"Queue error\", err instanceof Error ? err.message : String(err));\n\t\t}\n\t\tthis.processing = false;\n\t\tthis.processNext();\n\t}\n}\n\n// ============================================================================\n// SlackBot\n// ============================================================================\n\nexport class SlackBot {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate handler: MomHandler;\n\tprivate workingDir: string;\n\tprivate store: ChannelStore;\n\tprivate botUserId: string | null = null;\n\tprivate startupTs: string | null = null; // Messages older than this are just logged, not processed\n\n\tprivate users = new Map<string, SlackUser>();\n\tprivate channels = new Map<string, SlackChannel>();\n\tprivate queues = new Map<string, ChannelQueue>();\n\n\tconstructor(\n\t\thandler: MomHandler,\n\t\tconfig: { appToken: string; botToken: string; workingDir: string; store: ChannelStore },\n\t) {\n\t\tthis.handler = handler;\n\t\tthis.workingDir = config.workingDir;\n\t\tthis.store = config.store;\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t}\n\n\t// ==========================================================================\n\t// Public API\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\tawait Promise.all([this.fetchUsers(), this.fetchChannels()]);\n\t\tlog.logInfo(`Loaded ${this.channels.size} channels, ${this.users.size} users`);\n\n\t\tawait this.backfillAllChannels();\n\n\t\tthis.setupEventHandlers();\n\t\tawait this.socketClient.start();\n\n\t\t// Record startup time - messages older than this are just logged, not processed\n\t\tthis.startupTs = (Date.now() / 1000).toFixed(6);\n\n\t\tlog.logConnected();\n\t}\n\n\tgetUser(userId: string): SlackUser | undefined {\n\t\treturn this.users.get(userId);\n\t}\n\n\tgetChannel(channelId: string): SlackChannel | undefined {\n\t\treturn this.channels.get(channelId);\n\t}\n\n\tgetAllUsers(): SlackUser[] {\n\t\treturn Array.from(this.users.values());\n\t}\n\n\tgetAllChannels(): SlackChannel[] {\n\t\treturn Array.from(this.channels.values());\n\t}\n\n\tasync postMessage(channel: string, text: string): Promise<string> {\n\t\tconst result = await this.webClient.chat.postMessage({ channel, text });\n\t\treturn result.ts as string;\n\t}\n\n\tasync updateMessage(channel: string, ts: string, text: string): Promise<void> {\n\t\tawait this.webClient.chat.update({ channel, ts, text });\n\t}\n\n\tasync postInThread(channel: string, threadTs: string, text: string): Promise<void> {\n\t\tawait this.webClient.chat.postMessage({ channel, thread_ts: threadTs, text });\n\t}\n\n\tasync uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n\t\tconst fileName = title || basename(filePath);\n\t\tconst fileContent = readFileSync(filePath);\n\t\tawait this.webClient.files.uploadV2({\n\t\t\tchannel_id: channel,\n\t\t\tfile: fileContent,\n\t\t\tfilename: fileName,\n\t\t\ttitle: fileName,\n\t\t});\n\t}\n\n\t/**\n\t * Log a message to log.jsonl (SYNC)\n\t * This is the ONLY place messages are written to log.jsonl\n\t */\n\tlogToFile(channel: string, entry: object): void {\n\t\tconst dir = join(this.workingDir, channel);\n\t\tif (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\t\tappendFileSync(join(dir, \"log.jsonl\"), JSON.stringify(entry) + \"\\n\");\n\t}\n\n\t/**\n\t * Log a bot response to log.jsonl\n\t */\n\tlogBotResponse(channel: string, text: string, ts: string): void {\n\t\tthis.logToFile(channel, {\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// Private - Event Handlers\n\t// ==========================================================================\n\n\tprivate getQueue(channelId: string): ChannelQueue {\n\t\tlet queue = this.queues.get(channelId);\n\t\tif (!queue) {\n\t\t\tqueue = new ChannelQueue();\n\t\t\tthis.queues.set(channelId, queue);\n\t\t}\n\t\treturn queue;\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\t// Channel @mentions\n\t\tthis.socketClient.on(\"app_mention\", ({ event, ack }) => {\n\t\t\tconst e = 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// Skip DMs (handled by message event)\n\t\t\tif (e.channel.startsWith(\"D\")) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst slackEvent: SlackEvent = {\n\t\t\t\ttype: \"mention\",\n\t\t\t\tchannel: e.channel,\n\t\t\t\tts: e.ts,\n\t\t\t\tuser: e.user,\n\t\t\t\ttext: e.text.replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t};\n\n\t\t\t// SYNC: Log to log.jsonl (ALWAYS, even for old messages)\n\t\t\t// Also downloads attachments in background and stores local paths\n\t\t\tslackEvent.attachments = this.logUserMessage(slackEvent);\n\n\t\t\t// Only trigger processing for messages AFTER startup (not replayed old messages)\n\t\t\tif (this.startupTs && e.ts < this.startupTs) {\n\t\t\t\tlog.logInfo(\n\t\t\t\t\t`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`,\n\t\t\t\t);\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for stop command - execute immediately, don't queue!\n\t\t\tif (slackEvent.text.toLowerCase().trim() === \"stop\") {\n\t\t\t\tif (this.handler.isRunning(e.channel)) {\n\t\t\t\t\tthis.handler.handleStop(e.channel, this); // Don't await, don't queue\n\t\t\t\t} else {\n\t\t\t\t\tthis.postMessage(e.channel, \"_Nothing running_\");\n\t\t\t\t}\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// SYNC: Check if busy\n\t\t\tif (this.handler.isRunning(e.channel)) {\n\t\t\t\tthis.postMessage(e.channel, \"_Already working. Say `@mom stop` to cancel._\");\n\t\t\t} else {\n\t\t\t\tthis.getQueue(e.channel).enqueue(() => this.handler.handleEvent(slackEvent, this));\n\t\t\t}\n\n\t\t\tack();\n\t\t});\n\n\t\t// All messages (for logging) + DMs (for triggering)\n\t\tthis.socketClient.on(\"message\", ({ event, ack }) => {\n\t\t\tconst e = 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// Skip bot messages, edits, etc.\n\t\t\tif (e.bot_id || !e.user || e.user === this.botUserId) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (e.subtype !== undefined && e.subtype !== \"file_share\") {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!e.text && (!e.files || e.files.length === 0)) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst isDM = e.channel_type === \"im\";\n\t\t\tconst isBotMention = e.text?.includes(`<@${this.botUserId}>`);\n\n\t\t\t// Skip channel @mentions - already handled by app_mention event\n\t\t\tif (!isDM && isBotMention) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst slackEvent: SlackEvent = {\n\t\t\t\ttype: isDM ? \"dm\" : \"mention\",\n\t\t\t\tchannel: e.channel,\n\t\t\t\tts: e.ts,\n\t\t\t\tuser: e.user,\n\t\t\t\ttext: (e.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t};\n\n\t\t\t// SYNC: Log to log.jsonl (ALL messages - channel chatter and DMs)\n\t\t\t// Also downloads attachments in background and stores local paths\n\t\t\tslackEvent.attachments = this.logUserMessage(slackEvent);\n\n\t\t\t// Only trigger processing for messages AFTER startup (not replayed old messages)\n\t\t\tif (this.startupTs && e.ts < this.startupTs) {\n\t\t\t\tlog.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Only trigger handler for DMs\n\t\t\tif (isDM) {\n\t\t\t\t// Check for stop command - execute immediately, don't queue!\n\t\t\t\tif (slackEvent.text.toLowerCase().trim() === \"stop\") {\n\t\t\t\t\tif (this.handler.isRunning(e.channel)) {\n\t\t\t\t\t\tthis.handler.handleStop(e.channel, this); // Don't await, don't queue\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.postMessage(e.channel, \"_Nothing running_\");\n\t\t\t\t\t}\n\t\t\t\t\tack();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (this.handler.isRunning(e.channel)) {\n\t\t\t\t\tthis.postMessage(e.channel, \"_Already working. Say `stop` to cancel._\");\n\t\t\t\t} else {\n\t\t\t\t\tthis.getQueue(e.channel).enqueue(() => this.handler.handleEvent(slackEvent, this));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tack();\n\t\t});\n\t}\n\n\t/**\n\t * Log a user message to log.jsonl (SYNC)\n\t * Downloads attachments in background via store\n\t */\n\tprivate logUserMessage(event: SlackEvent): Attachment[] {\n\t\tconst user = this.users.get(event.user);\n\t\t// Process attachments - queues downloads in background\n\t\tconst attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];\n\t\tthis.logToFile(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: user?.userName,\n\t\t\tdisplayName: user?.displayName,\n\t\t\ttext: event.text,\n\t\t\tattachments,\n\t\t\tisBot: false,\n\t\t});\n\t\treturn attachments;\n\t}\n\n\t// ==========================================================================\n\t// Private - Backfill\n\t// ==========================================================================\n\n\tprivate getExistingTimestamps(channelId: string): Set<string> {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tconst timestamps = new Set<string>();\n\t\tif (!existsSync(logPath)) return timestamps;\n\n\t\tconst content = readFileSync(logPath, \"utf-8\");\n\t\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\t\tfor (const line of lines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.ts) timestamps.add(entry.ts);\n\t\t\t} catch {}\n\t\t}\n\t\treturn timestamps;\n\t}\n\n\tprivate async backfillChannel(channelId: string): Promise<number> {\n\t\tconst existingTs = this.getExistingTimestamps(channelId);\n\n\t\t// Find the biggest ts in log.jsonl\n\t\tlet latestTs: string | undefined;\n\t\tfor (const ts of existingTs) {\n\t\t\tif (!latestTs || parseFloat(ts) > parseFloat(latestTs)) latestTs = ts;\n\t\t}\n\n\t\ttype Message = {\n\t\t\tuser?: string;\n\t\t\tbot_id?: string;\n\t\t\ttext?: string;\n\t\t\tts?: string;\n\t\t\tsubtype?: string;\n\t\t\tfiles?: Array<{ name: string }>;\n\t\t};\n\t\tconst allMessages: Message[] = [];\n\n\t\tlet cursor: string | undefined;\n\t\tlet pageCount = 0;\n\t\tconst maxPages = 3;\n\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.history({\n\t\t\t\tchannel: channelId,\n\t\t\t\toldest: latestTs, // Only fetch messages newer than what we have\n\t\t\t\tinclusive: false,\n\t\t\t\tlimit: 1000,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tif (result.messages) {\n\t\t\t\tallMessages.push(...(result.messages as Message[]));\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t\tpageCount++;\n\t\t} while (cursor && pageCount < maxPages);\n\n\t\t// Filter: include mom's messages, exclude other bots, skip already logged\n\t\tconst relevantMessages = allMessages.filter((msg) => {\n\t\t\tif (!msg.ts || existingTs.has(msg.ts)) return false; // Skip duplicates\n\t\t\tif (msg.user === this.botUserId) return true;\n\t\t\tif (msg.bot_id) return false;\n\t\t\tif (msg.subtype !== undefined && msg.subtype !== \"file_share\") return false;\n\t\t\tif (!msg.user) return false;\n\t\t\tif (!msg.text && (!msg.files || msg.files.length === 0)) return false;\n\t\t\treturn true;\n\t\t});\n\n\t\t// Reverse to chronological order\n\t\trelevantMessages.reverse();\n\n\t\t// Log each message to log.jsonl\n\t\tfor (const msg of relevantMessages) {\n\t\t\tconst isMomMessage = msg.user === this.botUserId;\n\t\t\tconst user = this.users.get(msg.user!);\n\t\t\t// Strip @mentions from text (same as live messages)\n\t\t\tconst text = (msg.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim();\n\t\t\t// Process attachments - queues downloads in background\n\t\t\tconst attachments = msg.files ? this.store.processAttachments(channelId, msg.files, msg.ts!) : [];\n\n\t\t\tthis.logToFile(channelId, {\n\t\t\t\tdate: new Date(parseFloat(msg.ts!) * 1000).toISOString(),\n\t\t\t\tts: msg.ts!,\n\t\t\t\tuser: isMomMessage ? \"bot\" : msg.user!,\n\t\t\t\tuserName: isMomMessage ? undefined : user?.userName,\n\t\t\t\tdisplayName: isMomMessage ? undefined : user?.displayName,\n\t\t\t\ttext,\n\t\t\t\tattachments,\n\t\t\t\tisBot: isMomMessage,\n\t\t\t});\n\t\t}\n\n\t\treturn relevantMessages.length;\n\t}\n\n\tprivate async backfillAllChannels(): Promise<void> {\n\t\tconst startTime = Date.now();\n\n\t\t// Only backfill channels that already have a log.jsonl (mom has interacted with them before)\n\t\tconst channelsToBackfill: Array<[string, SlackChannel]> = [];\n\t\tfor (const [channelId, channel] of this.channels) {\n\t\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\t\tif (existsSync(logPath)) {\n\t\t\t\tchannelsToBackfill.push([channelId, channel]);\n\t\t\t}\n\t\t}\n\n\t\tlog.logBackfillStart(channelsToBackfill.length);\n\n\t\tlet totalMessages = 0;\n\t\tfor (const [channelId, channel] of channelsToBackfill) {\n\t\t\ttry {\n\t\t\t\tconst count = await this.backfillChannel(channelId);\n\t\t\t\tif (count > 0) log.logBackfillChannel(channel.name, count);\n\t\t\t\ttotalMessages += count;\n\t\t\t} catch (error) {\n\t\t\t\tlog.logWarning(`Failed to backfill #${channel.name}`, String(error));\n\t\t\t}\n\t\t}\n\n\t\tconst durationMs = Date.now() - startTime;\n\t\tlog.logBackfillComplete(totalMessages, durationMs);\n\t}\n\n\t// ==========================================================================\n\t// Private - Fetch Users/Channels\n\t// ==========================================================================\n\n\tprivate async fetchUsers(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.users.list({ limit: 200, cursor });\n\t\t\tconst members = result.members as\n\t\t\t\t| Array<{ id?: string; name?: string; real_name?: string; deleted?: boolean }>\n\t\t\t\t| undefined;\n\t\t\tif (members) {\n\t\t\t\tfor (const u of members) {\n\t\t\t\t\tif (u.id && u.name && !u.deleted) {\n\t\t\t\t\t\tthis.users.set(u.id, { id: u.id, userName: u.name, displayName: u.real_name || u.name });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n\n\tprivate async fetchChannels(): Promise<void> {\n\t\t// Fetch public/private channels\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"public_channel,private_channel\",\n\t\t\t\texclude_archived: true,\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst channels = result.channels as Array<{ id?: string; name?: string; is_member?: boolean }> | undefined;\n\t\t\tif (channels) {\n\t\t\t\tfor (const c of channels) {\n\t\t\t\t\tif (c.id && c.name && c.is_member) {\n\t\t\t\t\t\tthis.channels.set(c.id, { id: c.id, name: c.name });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\n\t\t// Also fetch DM channels (IMs)\n\t\tcursor = undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"im\",\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst ims = result.channels as Array<{ id?: string; user?: string }> | undefined;\n\t\t\tif (ims) {\n\t\t\t\tfor (const im of ims) {\n\t\t\t\t\tif (im.id) {\n\t\t\t\t\t\t// Use user's name as channel name for DMs\n\t\t\t\t\t\tconst user = im.user ? this.users.get(im.user) : undefined;\n\t\t\t\t\t\tconst name = user ? `DM:${user.userName}` : `DM:${im.id}`;\n\t\t\t\t\t\tthis.channels.set(im.id, { id: im.id, name });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n}\n"]}
package/dist/slack.js CHANGED
@@ -33,6 +33,7 @@ export class SlackBot {
33
33
  webClient;
34
34
  handler;
35
35
  workingDir;
36
+ store;
36
37
  botUserId = null;
37
38
  startupTs = null; // Messages older than this are just logged, not processed
38
39
  users = new Map();
@@ -41,6 +42,7 @@ export class SlackBot {
41
42
  constructor(handler, config) {
42
43
  this.handler = handler;
43
44
  this.workingDir = config.workingDir;
45
+ this.store = config.store;
44
46
  this.socketClient = new SocketModeClient({ appToken: config.appToken });
45
47
  this.webClient = new WebClient(config.botToken);
46
48
  }
@@ -143,7 +145,8 @@ export class SlackBot {
143
145
  files: e.files,
144
146
  };
145
147
  // SYNC: Log to log.jsonl (ALWAYS, even for old messages)
146
- this.logUserMessage(slackEvent);
148
+ // Also downloads attachments in background and stores local paths
149
+ slackEvent.attachments = this.logUserMessage(slackEvent);
147
150
  // Only trigger processing for messages AFTER startup (not replayed old messages)
148
151
  if (this.startupTs && e.ts < this.startupTs) {
149
152
  log.logInfo(`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`);
@@ -202,7 +205,8 @@ export class SlackBot {
202
205
  files: e.files,
203
206
  };
204
207
  // SYNC: Log to log.jsonl (ALL messages - channel chatter and DMs)
205
- this.logUserMessage(slackEvent);
208
+ // Also downloads attachments in background and stores local paths
209
+ slackEvent.attachments = this.logUserMessage(slackEvent);
206
210
  // Only trigger processing for messages AFTER startup (not replayed old messages)
207
211
  if (this.startupTs && e.ts < this.startupTs) {
208
212
  log.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);
@@ -234,9 +238,12 @@ export class SlackBot {
234
238
  }
235
239
  /**
236
240
  * Log a user message to log.jsonl (SYNC)
241
+ * Downloads attachments in background via store
237
242
  */
238
243
  logUserMessage(event) {
239
244
  const user = this.users.get(event.user);
245
+ // Process attachments - queues downloads in background
246
+ const attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];
240
247
  this.logToFile(event.channel, {
241
248
  date: new Date(parseFloat(event.ts) * 1000).toISOString(),
242
249
  ts: event.ts,
@@ -244,9 +251,10 @@ export class SlackBot {
244
251
  userName: user?.userName,
245
252
  displayName: user?.displayName,
246
253
  text: event.text,
247
- attachments: event.files?.map((f) => f.name) || [],
254
+ attachments,
248
255
  isBot: false,
249
256
  });
257
+ return attachments;
250
258
  }
251
259
  // ==========================================================================
252
260
  // Private - Backfill
@@ -318,6 +326,8 @@ export class SlackBot {
318
326
  const user = this.users.get(msg.user);
319
327
  // Strip @mentions from text (same as live messages)
320
328
  const text = (msg.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim();
329
+ // Process attachments - queues downloads in background
330
+ const attachments = msg.files ? this.store.processAttachments(channelId, msg.files, msg.ts) : [];
321
331
  this.logToFile(channelId, {
322
332
  date: new Date(parseFloat(msg.ts) * 1000).toISOString(),
323
333
  ts: msg.ts,
@@ -325,7 +335,7 @@ export class SlackBot {
325
335
  userName: isMomMessage ? undefined : user?.userName,
326
336
  displayName: isMomMessage ? undefined : user?.displayName,
327
337
  text,
328
- attachments: msg.files?.map((f) => f.name) || [],
338
+ attachments,
329
339
  isBot: isMomMessage,
330
340
  });
331
341
  }