@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.
- package/CHANGELOG.md +20 -0
- package/README.md +6 -2
- package/dist/agent.d.ts +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +34 -16
- package/dist/agent.js.map +1 -1
- package/dist/download.d.ts +2 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +89 -0
- package/dist/download.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +32 -5
- package/dist/main.js.map +1 -1
- package/dist/slack.d.ts +7 -1
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +14 -4
- package/dist/slack.js.map +1 -1
- package/package.json +4 -4
|
@@ -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"]}
|
package/dist/main.d.ts.map
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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 } =
|
|
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":";AAEA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAoB,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,eAAe,EAAsB,eAAe,EAAE,MAAM,cAAc,CAAC;AACpF,OAAO,EAAkC,QAAQ,IAAI,aAAa,EAAmB,MAAM,YAAY,CAAC;AACxG,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,+EAA+E;AAC/E,SAAS;AACT,+EAA+E;AAE/E,MAAM,mBAAmB,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;AAC5D,MAAM,mBAAmB,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;AAC5D,MAAM,iBAAiB,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;AACxD,MAAM,qBAAqB,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;AAQhE,SAAS,SAAS,GAAe;IAChC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,OAAO,GAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC9C,IAAI,UAA8B,CAAC;IACnC,IAAI,iBAAqC,CAAC;IAE1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAClC,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;QAC3D,CAAC;aAAM,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;YAChC,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC5C,CAAC;aAAM,IAAI,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;YAC1C,iBAAiB,GAAG,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACrD,CAAC;aAAM,IAAI,GAAG,KAAK,YAAY,EAAE,CAAC;YACjC,iBAAiB,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/B,CAAC;aAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,UAAU,GAAG,GAAG,CAAC;QAClB,CAAC;IACF,CAAC;IAED,OAAO;QACN,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS;QACxD,OAAO;QACP,eAAe,EAAE,iBAAiB;KAClC,CAAC;AAAA,CACF;AAED,MAAM,UAAU,GAAG,SAAS,EAAE,CAAC;AAE/B,yBAAyB;AACzB,IAAI,UAAU,CAAC,eAAe,EAAE,CAAC;IAChC,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;QAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IACD,MAAM,eAAe,CAAC,UAAU,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC;IACvE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC;AAED,wCAAwC;AACxC,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC;IAC5B,OAAO,CAAC,KAAK,CAAC,+DAA+D,CAAC,CAAC;IAC/E,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC;AAED,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC;AAEnG,IAAI,CAAC,mBAAmB,IAAI,CAAC,mBAAmB,IAAI,CAAC,CAAC,iBAAiB,IAAI,CAAC,qBAAqB,CAAC,EAAE,CAAC;IACpG,OAAO,CAAC,KAAK,CAAC,mGAAmG,CAAC,CAAC;IACnH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC;AAED,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;AAc/B,MAAM,aAAa,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEtD,SAAS,QAAQ,CAAC,SAAiB,EAAgB;IAClD,IAAI,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACzC,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAC/C,KAAK,GAAG;YACP,OAAO,EAAE,KAAK;YACd,MAAM,EAAE,iBAAiB,CAAC,OAAO,EAAE,SAAS,EAAE,UAAU,CAAC;YACzD,KAAK,EAAE,IAAI,YAAY,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,mBAAoB,EAAE,CAAC;YACvE,aAAa,EAAE,KAAK;SACpB,CAAC;QACF,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED,+EAA+E;AAC/E,8BAA8B;AAC9B,+EAA+E;AAE/E,SAAS,kBAAkB,CAAC,KAAiB,EAAE,KAAe,EAAE,KAAmB,EAAE;IACpF,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,IAAI,eAAe,GAAG,EAAE,CAAC;IACzB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrB,MAAM,gBAAgB,GAAG,MAAM,CAAC;IAChC,IAAI,aAAa,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAEtC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEvC,OAAO;QACN,OAAO,EAAE;YACR,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,IAAI;YACnB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,IAAI,EAAE,QAAQ;YACxB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,WAAW,EAAE,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;SACvE;QACD,WAAW,EAAE,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI;QAClD,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,QAAQ,EAAE,KAAK,CAAC,cAAc,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACzE,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;QAEvG,OAAO,EAAE,KAAK,EAAE,IAAY,EAAE,SAAS,GAAG,IAAI,EAAE,EAAE,CAAC;YAClD,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC9C,eAAe,GAAG,eAAe,CAAC,CAAC,CAAC,eAAe,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;gBACzE,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC,eAAe,GAAG,gBAAgB,CAAC,CAAC,CAAC,eAAe,CAAC;gBAErF,IAAI,SAAS,EAAE,CAAC;oBACf,MAAM,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;gBAClE,CAAC;qBAAM,CAAC;oBACP,SAAS,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;gBACjE,CAAC;gBAED,IAAI,SAAS,IAAI,SAAS,EAAE,CAAC;oBAC5B,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;gBACtD,CAAC;YAAA,CACD,CAAC,CAAC;YACH,MAAM,aAAa,CAAC;QAAA,CACpB;QAED,cAAc,EAAE,KAAK,EAAE,IAAY,EAAE,EAAE,CAAC;YACvC,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC9C,eAAe,GAAG,IAAI,CAAC;gBACvB,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC,eAAe,GAAG,gBAAgB,CAAC,CAAC,CAAC,eAAe,CAAC;gBACrF,IAAI,SAAS,EAAE,CAAC;oBACf,MAAM,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;gBAClE,CAAC;qBAAM,CAAC;oBACP,SAAS,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;gBACjE,CAAC;YAAA,CACD,CAAC,CAAC;YACH,MAAM,aAAa,CAAC;QAAA,CACpB;QAED,eAAe,EAAE,KAAK,EAAE,IAAY,EAAE,EAAE,CAAC;YACxC,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC9C,IAAI,SAAS,EAAE,CAAC;oBACf,MAAM,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;gBAC1D,CAAC;YAAA,CACD,CAAC,CAAC;YACH,MAAM,aAAa,CAAC;QAAA,CACpB;QAED,SAAS,EAAE,KAAK,EAAE,QAAiB,EAAE,EAAE,CAAC;YACvC,IAAI,QAAQ,IAAI,CAAC,SAAS,EAAE,CAAC;gBAC5B,eAAe,GAAG,YAAY,CAAC;gBAC/B,SAAS,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,gBAAgB,CAAC,CAAC;YACxF,CAAC;QAAA,CACD;QAED,UAAU,EAAE,KAAK,EAAE,QAAgB,EAAE,KAAc,EAAE,EAAE,CAAC;YACvD,MAAM,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;QAAA,CACvD;QAED,UAAU,EAAE,KAAK,EAAE,OAAgB,EAAE,EAAE,CAAC;YACvC,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC9C,SAAS,GAAG,OAAO,CAAC;gBACpB,IAAI,SAAS,EAAE,CAAC;oBACf,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC,eAAe,GAAG,gBAAgB,CAAC,CAAC,CAAC,eAAe,CAAC;oBACrF,MAAM,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;gBAClE,CAAC;YAAA,CACD,CAAC,CAAC;YACH,MAAM,aAAa,CAAC;QAAA,CACpB;KACD,CAAC;AAAA,CACF;AAED,+EAA+E;AAC/E,UAAU;AACV,+EAA+E;AAE/E,MAAM,OAAO,GAAe;IAC3B,SAAS,CAAC,SAAiB,EAAW;QACrC,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,OAAO,KAAK,EAAE,OAAO,IAAI,KAAK,CAAC;IAAA,CAC/B;IAED,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,KAAe,EAAiB;QACnE,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,KAAK,EAAE,OAAO,EAAE,CAAC;YACpB,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC;YAC3B,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;YAC/D,KAAK,CAAC,aAAa,GAAG,EAAE,CAAC,CAAC,0BAA0B;QACrD,CAAC;aAAM,CAAC;YACP,MAAM,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;QACzD,CAAC;IAAA,CACD;IAED,KAAK,CAAC,WAAW,CAAC,KAAiB,EAAE,KAAe,EAAiB;QACpE,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACtC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAEnD,YAAY;QACZ,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACrB,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC;QAE5B,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,mBAAmB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAE/E,IAAI,CAAC;YACJ,gDAAgD;YAChD,mEAAmE;YACnE,4DAA4D;YAC5D,MAAM,WAAW,GAAG,gBAAgB,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;YAC3D,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;gBACrB,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,YAAY,WAAW,+BAA+B,CAAC,CAAC;YACtF,CAAC;YAED,yBAAyB;YACzB,MAAM,GAAG,GAAG,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YAEpD,gBAAgB;YAChB,MAAM,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YAC1B,MAAM,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAC3B,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,GAAU,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;YAC/D,MAAM,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YAE5B,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;gBAC5D,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;oBACzB,MAAM,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;oBAC3E,KAAK,CAAC,aAAa,GAAG,SAAS,CAAC;gBACjC,CAAC;qBAAM,CAAC;oBACP,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;gBACrD,CAAC;YACF,CAAC;QACF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,GAAG,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,OAAO,aAAa,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAClG,CAAC;gBAAS,CAAC;YACV,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QACvB,CAAC;IAAA,CACD;CACD,CAAC;AAEF,+EAA+E;AAC/E,QAAQ;AACR,+EAA+E;AAE/E,GAAG,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;AAE7F,4EAA4E;AAC5E,MAAM,WAAW,GAAG,IAAI,YAAY,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,mBAAoB,EAAE,CAAC,CAAC;AAErF,MAAM,GAAG,GAAG,IAAI,aAAa,CAAC,OAAO,EAAE;IACtC,QAAQ,EAAE,mBAAmB;IAC7B,QAAQ,EAAE,mBAAmB;IAC7B,UAAU;IACV,KAAK,EAAE,WAAW;CAClB,CAAC,CAAC;AAEH,GAAG,CAAC,KAAK,EAAE,CAAC","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
|
|
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;
|
package/dist/slack.d.ts.map
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
338
|
+
attachments,
|
|
329
339
|
isBot: isMomMessage,
|
|
330
340
|
});
|
|
331
341
|
}
|