@geminixiang/mama 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +149 -9
  2. package/dist/adapter.d.ts +14 -1
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/context.d.ts.map +1 -1
  6. package/dist/adapters/discord/context.js +1 -0
  7. package/dist/adapters/discord/context.js.map +1 -1
  8. package/dist/adapters/slack/bot.d.ts +5 -1
  9. package/dist/adapters/slack/bot.d.ts.map +1 -1
  10. package/dist/adapters/slack/bot.js +137 -13
  11. package/dist/adapters/slack/bot.js.map +1 -1
  12. package/dist/adapters/slack/context.d.ts.map +1 -1
  13. package/dist/adapters/slack/context.js +50 -25
  14. package/dist/adapters/slack/context.js.map +1 -1
  15. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  16. package/dist/adapters/slack/tools/attach.js +4 -2
  17. package/dist/adapters/slack/tools/attach.js.map +1 -1
  18. package/dist/adapters/telegram/bot.d.ts +4 -3
  19. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  20. package/dist/adapters/telegram/bot.js +11 -23
  21. package/dist/adapters/telegram/bot.js.map +1 -1
  22. package/dist/adapters/telegram/context.d.ts +1 -1
  23. package/dist/adapters/telegram/context.d.ts.map +1 -1
  24. package/dist/adapters/telegram/context.js +23 -40
  25. package/dist/adapters/telegram/context.js.map +1 -1
  26. package/dist/agent.d.ts +5 -0
  27. package/dist/agent.d.ts.map +1 -1
  28. package/dist/agent.js +63 -17
  29. package/dist/agent.js.map +1 -1
  30. package/dist/context.d.ts +13 -1
  31. package/dist/context.d.ts.map +1 -1
  32. package/dist/context.js +20 -2
  33. package/dist/context.js.map +1 -1
  34. package/dist/events.d.ts +10 -5
  35. package/dist/events.d.ts.map +1 -1
  36. package/dist/events.js +44 -10
  37. package/dist/events.js.map +1 -1
  38. package/dist/log.d.ts.map +1 -1
  39. package/dist/log.js +1 -1
  40. package/dist/log.js.map +1 -1
  41. package/dist/main.d.ts.map +1 -1
  42. package/dist/main.js +113 -37
  43. package/dist/main.js.map +1 -1
  44. package/dist/sandbox.d.ts +7 -1
  45. package/dist/sandbox.d.ts.map +1 -1
  46. package/dist/sandbox.js +127 -27
  47. package/dist/sandbox.js.map +1 -1
  48. package/package.json +12 -12
@@ -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 { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.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 MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\n\ninterface ParsedArgs {\n workingDir?: string;\n sandbox: SandboxConfig;\n downloadChannel?: string;\n}\n\nfunction parseArgs(): ParsedArgs {\n const args = process.argv.slice(2);\n let sandbox: SandboxConfig = { type: \"host\" };\n let workingDir: string | undefined;\n let downloadChannelId: string | undefined;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg.startsWith(\"--sandbox=\")) {\n sandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n } else if (arg === \"--sandbox\") {\n sandbox = parseSandboxArg(args[++i] || \"\");\n } else if (arg.startsWith(\"--download=\")) {\n downloadChannelId = arg.slice(\"--download=\".length);\n } else if (arg === \"--download\") {\n downloadChannelId = args[++i];\n } else if (!arg.startsWith(\"-\")) {\n workingDir = arg;\n }\n }\n\n return {\n workingDir: workingDir ? resolve(workingDir) : undefined,\n sandbox,\n downloadChannel: downloadChannelId,\n };\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n if (!MOM_SLACK_BOT_TOKEN) {\n console.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n process.exit(1);\n }\n await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n process.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n console.error(\"Usage: mama [--sandbox=host|docker:<name>] <working-directory>\");\n console.error(\" mama --download <channel-id>\");\n process.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n console.error(\n \"No platform tokens found. Set one of:\\n\" +\n \" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n \" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n \" Discord: MOM_DISCORD_BOT_TOKEN\",\n );\n process.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n\nasync function getState(channelId: string, sessionKey?: string): Promise<ChannelState> {\n const key = sessionKey ?? channelId;\n let state = channelStates.get(key);\n if (!state) {\n const channelDir = join(workingDir, channelId);\n state = {\n running: false,\n runner: await createRunner(sandbox, key, channelId, channelDir, workingDir),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n channelStates.set(key, state);\n } else {\n state.lastAccessedAt = Date.now();\n }\n return state;\n}\n\n/**\n * Evict idle sessions from channelStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of channelStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n channelStates.delete(key);\n }\n }\n\n if (channelStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of channelStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = channelStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n channelStates.delete(idleSessions[i].key);\n }\n }\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n isRunning(sessionKey: string): boolean {\n const state = channelStates.get(sessionKey);\n return state?.running ?? false;\n },\n\n getRunningSessions() {\n const sessions: import(\"./adapter.js\").RunningSession[] = [];\n for (const [sessionKey, state] of channelStates) {\n if (state.running && state.startedAt) {\n sessions.push({ sessionKey, startedAt: state.startedAt });\n }\n }\n return sessions;\n },\n\n async handleStop(sessionKey: string, channelId: string, bot: Bot): Promise<void> {\n const state = channelStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(channelId, \"_Stopping..._\");\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(channelId, \"_Nothing running_\");\n }\n },\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n _isEvent?: boolean,\n ): Promise<void> {\n // Don't accept new events during shutdown\n if (isShuttingDown) {\n log.logInfo(\n `[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;\n const state = await getState(event.channel, sessionKey);\n\n // Start run\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n\n log.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n // Wrap in-flight run tracking\n const runPromise = (async () => {\n try {\n const { message, responseCtx, platform } = adapters;\n\n // Run the agent\n await responseCtx.setTyping(true);\n await responseCtx.setWorking(true);\n const result = await state.runner.run(message, responseCtx, platform);\n await responseCtx.setWorking(false);\n\n if (result.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(event.channel, \"_Stopped_\");\n }\n }\n } catch (err) {\n log.logWarning(\n `[${event.channel}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n evictIdleSessions();\n }\n })();\n\n inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n inFlightRuns.delete(runPromise);\n }\n },\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\n// Create the appropriate platform bot\nlet bot: Bot;\n\nif (hasSlack) {\n const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n bot = new SlackBotClass(handler, {\n appToken: MOM_SLACK_APP_TOKEN!,\n botToken: MOM_SLACK_BOT_TOKEN!,\n workingDir,\n store: sharedStore,\n });\n log.logInfo(\"Platform: Slack\");\n} else if (hasTelegram) {\n bot = new TelegramBot(handler, {\n token: MOM_TELEGRAM_BOT_TOKEN!,\n workingDir,\n });\n log.logInfo(\"Platform: Telegram\");\n} else {\n bot = new DiscordBot(handler, {\n token: MOM_DISCORD_BOT_TOKEN!,\n workingDir,\n });\n log.logInfo(\"Platform: Discord\");\n}\n\n// Start events watcher\nconst eventsWatcher = createEventsWatcher(workingDir, bot);\nif (hasSlack) {\n (bot as SlackBotClass).setEventsWatcher(eventsWatcher);\n}\neventsWatcher.start();\n\n// Handle shutdown\nprocess.on(\"SIGINT\", async () => {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n process.exit(0);\n});\n\nprocess.on(\"SIGTERM\", async () => {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n process.exit(0);\n});\n\nbot.start().catch((err) => {\n log.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\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 { readFileSync } from \"fs\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join as pathJoin } from \"path\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\n// Get version from package.json\nfunction getVersion(): string {\n // Try to find package.json in the dist directory or parent\n const possiblePaths = [\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"package.json\"),\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\"),\n pathJoin(process.cwd(), \"package.json\"),\n ];\n\n for (const pkgPath of possiblePaths) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n if (pkg.version) return pkg.version;\n } catch {\n // Continue to next path\n }\n }\n return \"unknown\";\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 MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\n\ninterface ParsedArgs {\n workingDir?: string;\n sandbox: SandboxConfig;\n downloadChannel?: string;\n showVersion?: boolean;\n}\n\nfunction parseArgs(): ParsedArgs {\n const args = process.argv.slice(2);\n let sandbox: SandboxConfig = { type: \"host\" };\n let workingDir: string | undefined;\n let downloadChannelId: string | undefined;\n let showVersion = false;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\") {\n showVersion = true;\n } else if (arg.startsWith(\"--sandbox=\")) {\n sandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n } else if (arg === \"--sandbox\") {\n sandbox = parseSandboxArg(args[++i] || \"\");\n } else if (arg.startsWith(\"--download=\")) {\n downloadChannelId = arg.slice(\"--download=\".length);\n } else if (arg === \"--download\") {\n downloadChannelId = args[++i];\n } else if (!arg.startsWith(\"-\")) {\n workingDir = arg;\n }\n }\n\n return {\n workingDir: workingDir ? resolve(workingDir) : undefined,\n sandbox,\n downloadChannel: downloadChannelId,\n showVersion,\n };\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --version\nif (parsedArgs.showVersion) {\n console.log(getVersion());\n process.exit(0);\n}\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n if (!MOM_SLACK_BOT_TOKEN) {\n console.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n process.exit(1);\n }\n await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n process.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n console.error(\n \"Usage: mama [--sandbox=host|docker:<name>|firecracker:<vm-id>:<host-path>] <working-directory>\",\n );\n console.error(\" mama --download <channel-id>\");\n process.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n console.error(\n \"No platform tokens found. Set one of:\\n\" +\n \" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n \" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n \" Discord: MOM_DISCORD_BOT_TOKEN\",\n );\n process.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\n/**\n * Maps \"channel:botReplyTs\" → sessionKey.\n * When the bot posts a top-level reply, the Slack thread anchors to that ts.\n * Users replying in that thread will have thread_ts = botReplyTs, which differs\n * from the original sessionKey (channel:userMessageTs). This alias map lets\n * stop commands resolve the correct session even when the ts doesn't match.\n */\nconst threadAliases = new Map<string, string>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n\nasync function getState(channelId: string, sessionKey?: string): Promise<ChannelState> {\n const key = sessionKey ?? channelId;\n let state = channelStates.get(key);\n if (!state) {\n const channelDir = join(workingDir, channelId);\n state = {\n running: false,\n runner: await createRunner(sandbox, key, channelId, channelDir, workingDir),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n channelStates.set(key, state);\n } else {\n state.lastAccessedAt = Date.now();\n }\n return state;\n}\n\n/**\n * Evict idle sessions from channelStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of channelStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n channelStates.delete(key);\n // Clean up aliases pointing to this session\n for (const [alias, target] of threadAliases) {\n if (target === key) threadAliases.delete(alias);\n }\n }\n }\n\n if (channelStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of channelStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = channelStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n const evictedKey = idleSessions[i].key;\n channelStates.delete(evictedKey);\n for (const [alias, target] of threadAliases) {\n if (target === evictedKey) threadAliases.delete(alias);\n }\n }\n }\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n isRunning(sessionKey: string): boolean {\n const state = channelStates.get(sessionKey);\n return !!state?.running;\n },\n\n getRunningSessions() {\n const sessions: import(\"./adapter.js\").RunningSession[] = [];\n for (const [sessionKey, state] of channelStates) {\n if (state.running && state.startedAt) {\n // Get current step from runner\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n },\n\n async handleStop(sessionKey: string, channelId: string, bot: Bot): Promise<void> {\n const state = channelStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(channelId, \"_Stopping..._\");\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(channelId, \"_Nothing running_\");\n }\n },\n\n forceStop(sessionKey: string): void {\n const state = channelStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n },\n\n resolveSessionKey(rawKey: string): string {\n return threadAliases.get(rawKey) ?? rawKey;\n },\n\n registerThreadAlias(aliasKey: string, sessionKey: string): void {\n threadAliases.set(aliasKey, sessionKey);\n },\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n _isEvent?: boolean,\n ): Promise<void> {\n // Don't accept new events during shutdown\n if (isShuttingDown) {\n log.logInfo(\n `[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const rawSessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;\n const sessionKey = this.resolveSessionKey(rawSessionKey);\n const state = await getState(event.channel, sessionKey);\n\n // Start run\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n // Wrap in-flight run tracking\n const runPromise = (async () => {\n try {\n const { message, responseCtx, platform } = adapters;\n\n // Run the agent\n await responseCtx.setTyping(true);\n await responseCtx.setWorking(true);\n const result = await state.runner.run(message, responseCtx, platform);\n await responseCtx.setWorking(false);\n\n if (result.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(event.channel, \"_Stopped_\");\n }\n }\n } catch (err) {\n log.logWarning(\n `[${event.channel}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n evictIdleSessions();\n }\n })();\n\n inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n inFlightRuns.delete(runPromise);\n }\n },\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nconst sandboxDesc =\n sandbox.type === \"host\"\n ? \"host\"\n : sandbox.type === \"docker\"\n ? `docker:${sandbox.container}`\n : `firecracker:${sandbox.vmId}`;\nlog.logStartup(workingDir, sandboxDesc);\n\n// Create platform bots\nconst bots: Bot[] = [];\nconst botsByPlatform: Record<string, Bot> = {};\n\nif (hasSlack) {\n const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n const slackBot = new SlackBotClass(handler, {\n appToken: MOM_SLACK_APP_TOKEN!,\n botToken: MOM_SLACK_BOT_TOKEN!,\n workingDir,\n store: sharedStore,\n });\n bots.push(slackBot);\n botsByPlatform.slack = slackBot;\n log.logInfo(\"Platform: Slack\");\n}\nif (hasTelegram) {\n const telegramBot = new TelegramBot(handler, {\n token: MOM_TELEGRAM_BOT_TOKEN!,\n workingDir,\n });\n bots.push(telegramBot);\n botsByPlatform.telegram = telegramBot;\n log.logInfo(\"Platform: Telegram\");\n}\nif (hasDiscord) {\n const discordBot = new DiscordBot(handler, {\n token: MOM_DISCORD_BOT_TOKEN!,\n workingDir,\n });\n bots.push(discordBot);\n botsByPlatform.discord = discordBot;\n log.logInfo(\"Platform: Discord\");\n}\n\n// Start events watcher with explicit platform routing\nconst eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);\nconst slackBot = botsByPlatform.slack as SlackBotClass | undefined;\nif (slackBot) {\n slackBot.setEventsWatcher(eventsWatcher);\n}\neventsWatcher.start();\n\n// Handle shutdown\nasync function shutdown(): Promise<void> {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n process.exit(0);\n}\n\nprocess.on(\"SIGINT\", shutdown);\nprocess.on(\"SIGTERM\", shutdown);\n\n// Start all bots\nawait Promise.all(\n bots.map((bot) =>\n bot.start().catch((err) => {\n log.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n process.exit(1);\n }),\n ),\n);\n"]}
package/dist/main.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { join, resolve } from "path";
3
+ import { readFileSync } from "fs";
4
+ import { fileURLToPath } from "url";
5
+ import { dirname, join as pathJoin } from "path";
3
6
  import { DiscordBot } from "./adapters/discord/index.js";
4
7
  import { TelegramBot } from "./adapters/telegram/index.js";
5
8
  import { SlackBot as SlackBotClass } from "./adapters/slack/index.js";
@@ -12,6 +15,26 @@ import { ChannelStore } from "./store.js";
12
15
  // ============================================================================
13
16
  // Config
14
17
  // ============================================================================
18
+ // Get version from package.json
19
+ function getVersion() {
20
+ // Try to find package.json in the dist directory or parent
21
+ const possiblePaths = [
22
+ pathJoin(dirname(fileURLToPath(import.meta.url)), "package.json"),
23
+ pathJoin(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
24
+ pathJoin(process.cwd(), "package.json"),
25
+ ];
26
+ for (const pkgPath of possiblePaths) {
27
+ try {
28
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
29
+ if (pkg.version)
30
+ return pkg.version;
31
+ }
32
+ catch {
33
+ // Continue to next path
34
+ }
35
+ }
36
+ return "unknown";
37
+ }
15
38
  const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
16
39
  const MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;
17
40
  const MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;
@@ -21,9 +44,13 @@ function parseArgs() {
21
44
  let sandbox = { type: "host" };
22
45
  let workingDir;
23
46
  let downloadChannelId;
47
+ let showVersion = false;
24
48
  for (let i = 0; i < args.length; i++) {
25
49
  const arg = args[i];
26
- if (arg.startsWith("--sandbox=")) {
50
+ if (arg === "--version" || arg === "-v" || arg === "-V") {
51
+ showVersion = true;
52
+ }
53
+ else if (arg.startsWith("--sandbox=")) {
27
54
  sandbox = parseSandboxArg(arg.slice("--sandbox=".length));
28
55
  }
29
56
  else if (arg === "--sandbox") {
@@ -43,9 +70,15 @@ function parseArgs() {
43
70
  workingDir: workingDir ? resolve(workingDir) : undefined,
44
71
  sandbox,
45
72
  downloadChannel: downloadChannelId,
73
+ showVersion,
46
74
  };
47
75
  }
48
76
  const parsedArgs = parseArgs();
77
+ // Handle --version
78
+ if (parsedArgs.showVersion) {
79
+ console.log(getVersion());
80
+ process.exit(0);
81
+ }
49
82
  // Handle --download mode (Slack only)
50
83
  if (parsedArgs.downloadChannel) {
51
84
  if (!MOM_SLACK_BOT_TOKEN) {
@@ -57,7 +90,7 @@ if (parsedArgs.downloadChannel) {
57
90
  }
58
91
  // Normal bot mode - require working dir
59
92
  if (!parsedArgs.workingDir) {
60
- console.error("Usage: mama [--sandbox=host|docker:<name>] <working-directory>");
93
+ console.error("Usage: mama [--sandbox=host|docker:<name>|firecracker:<vm-id>:<host-path>] <working-directory>");
61
94
  console.error(" mama --download <channel-id>");
62
95
  process.exit(1);
63
96
  }
@@ -75,6 +108,14 @@ if (!hasSlack && !hasTelegram && !hasDiscord) {
75
108
  }
76
109
  await validateSandbox(sandbox);
77
110
  const channelStates = new Map();
111
+ /**
112
+ * Maps "channel:botReplyTs" → sessionKey.
113
+ * When the bot posts a top-level reply, the Slack thread anchors to that ts.
114
+ * Users replying in that thread will have thread_ts = botReplyTs, which differs
115
+ * from the original sessionKey (channel:userMessageTs). This alias map lets
116
+ * stop commands resolve the correct session even when the ts doesn't match.
117
+ */
118
+ const threadAliases = new Map();
78
119
  /** Track in-flight runs for graceful shutdown */
79
120
  const inFlightRuns = new Set();
80
121
  /** Flag to stop accepting new events during shutdown */
@@ -110,6 +151,11 @@ function evictIdleSessions() {
110
151
  for (const [key, state] of channelStates) {
111
152
  if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {
112
153
  channelStates.delete(key);
154
+ // Clean up aliases pointing to this session
155
+ for (const [alias, target] of threadAliases) {
156
+ if (target === key)
157
+ threadAliases.delete(alias);
158
+ }
113
159
  }
114
160
  }
115
161
  if (channelStates.size > MAX_SESSIONS) {
@@ -122,7 +168,12 @@ function evictIdleSessions() {
122
168
  idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
123
169
  const toEvict = channelStates.size - MAX_SESSIONS;
124
170
  for (let i = 0; i < toEvict && i < idleSessions.length; i++) {
125
- channelStates.delete(idleSessions[i].key);
171
+ const evictedKey = idleSessions[i].key;
172
+ channelStates.delete(evictedKey);
173
+ for (const [alias, target] of threadAliases) {
174
+ if (target === evictedKey)
175
+ threadAliases.delete(alias);
176
+ }
126
177
  }
127
178
  }
128
179
  }
@@ -132,13 +183,20 @@ function evictIdleSessions() {
132
183
  const handler = {
133
184
  isRunning(sessionKey) {
134
185
  const state = channelStates.get(sessionKey);
135
- return state?.running ?? false;
186
+ return !!state?.running;
136
187
  },
137
188
  getRunningSessions() {
138
189
  const sessions = [];
139
190
  for (const [sessionKey, state] of channelStates) {
140
191
  if (state.running && state.startedAt) {
141
- sessions.push({ sessionKey, startedAt: state.startedAt });
192
+ // Get current step from runner
193
+ const currentStep = state.runner.getCurrentStep();
194
+ sessions.push({
195
+ sessionKey,
196
+ startedAt: state.startedAt,
197
+ lastActivityAt: state.lastActivityAt,
198
+ currentTool: currentStep?.label || currentStep?.toolName,
199
+ });
142
200
  }
143
201
  }
144
202
  return sessions;
@@ -155,18 +213,35 @@ const handler = {
155
213
  await bot.postMessage(channelId, "_Nothing running_");
156
214
  }
157
215
  },
216
+ forceStop(sessionKey) {
217
+ const state = channelStates.get(sessionKey);
218
+ if (state?.running) {
219
+ log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);
220
+ state.stopRequested = true;
221
+ state.runner.abort();
222
+ state.running = false;
223
+ }
224
+ },
225
+ resolveSessionKey(rawKey) {
226
+ return threadAliases.get(rawKey) ?? rawKey;
227
+ },
228
+ registerThreadAlias(aliasKey, sessionKey) {
229
+ threadAliases.set(aliasKey, sessionKey);
230
+ },
158
231
  async handleEvent(event, bot, adapters, _isEvent) {
159
232
  // Don't accept new events during shutdown
160
233
  if (isShuttingDown) {
161
234
  log.logInfo(`[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);
162
235
  return;
163
236
  }
164
- const sessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;
237
+ const rawSessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;
238
+ const sessionKey = this.resolveSessionKey(rawSessionKey);
165
239
  const state = await getState(event.channel, sessionKey);
166
240
  // Start run
167
241
  state.running = true;
168
242
  state.stopRequested = false;
169
243
  state.startedAt = Date.now();
244
+ state.lastActivityAt = Date.now();
170
245
  log.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);
171
246
  // Wrap in-flight run tracking
172
247
  const runPromise = (async () => {
@@ -208,41 +283,54 @@ const handler = {
208
283
  // ============================================================================
209
284
  // Start
210
285
  // ============================================================================
211
- log.logStartup(workingDir, sandbox.type === "host" ? "host" : `docker:${sandbox.container}`);
212
- // Create the appropriate platform bot
213
- let bot;
286
+ const sandboxDesc = sandbox.type === "host"
287
+ ? "host"
288
+ : sandbox.type === "docker"
289
+ ? `docker:${sandbox.container}`
290
+ : `firecracker:${sandbox.vmId}`;
291
+ log.logStartup(workingDir, sandboxDesc);
292
+ // Create platform bots
293
+ const bots = [];
294
+ const botsByPlatform = {};
214
295
  if (hasSlack) {
215
296
  const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN });
216
- bot = new SlackBotClass(handler, {
297
+ const slackBot = new SlackBotClass(handler, {
217
298
  appToken: MOM_SLACK_APP_TOKEN,
218
299
  botToken: MOM_SLACK_BOT_TOKEN,
219
300
  workingDir,
220
301
  store: sharedStore,
221
302
  });
303
+ bots.push(slackBot);
304
+ botsByPlatform.slack = slackBot;
222
305
  log.logInfo("Platform: Slack");
223
306
  }
224
- else if (hasTelegram) {
225
- bot = new TelegramBot(handler, {
307
+ if (hasTelegram) {
308
+ const telegramBot = new TelegramBot(handler, {
226
309
  token: MOM_TELEGRAM_BOT_TOKEN,
227
310
  workingDir,
228
311
  });
312
+ bots.push(telegramBot);
313
+ botsByPlatform.telegram = telegramBot;
229
314
  log.logInfo("Platform: Telegram");
230
315
  }
231
- else {
232
- bot = new DiscordBot(handler, {
316
+ if (hasDiscord) {
317
+ const discordBot = new DiscordBot(handler, {
233
318
  token: MOM_DISCORD_BOT_TOKEN,
234
319
  workingDir,
235
320
  });
321
+ bots.push(discordBot);
322
+ botsByPlatform.discord = discordBot;
236
323
  log.logInfo("Platform: Discord");
237
324
  }
238
- // Start events watcher
239
- const eventsWatcher = createEventsWatcher(workingDir, bot);
240
- if (hasSlack) {
241
- bot.setEventsWatcher(eventsWatcher);
325
+ // Start events watcher with explicit platform routing
326
+ const eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);
327
+ const slackBot = botsByPlatform.slack;
328
+ if (slackBot) {
329
+ slackBot.setEventsWatcher(eventsWatcher);
242
330
  }
243
331
  eventsWatcher.start();
244
332
  // Handle shutdown
245
- process.on("SIGINT", async () => {
333
+ async function shutdown() {
246
334
  if (isShuttingDown)
247
335
  return;
248
336
  isShuttingDown = true;
@@ -256,24 +344,12 @@ process.on("SIGINT", async () => {
256
344
  }
257
345
  eventsWatcher.stop();
258
346
  process.exit(0);
259
- });
260
- process.on("SIGTERM", async () => {
261
- if (isShuttingDown)
262
- return;
263
- isShuttingDown = true;
264
- log.logInfo("Shutting down gracefully...");
265
- const timeout = Date.now() + 30000;
266
- while (inFlightRuns.size > 0 && Date.now() < timeout) {
267
- await new Promise((resolve) => setTimeout(resolve, 500));
268
- }
269
- if (inFlightRuns.size > 0) {
270
- log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);
271
- }
272
- eventsWatcher.stop();
273
- process.exit(0);
274
- });
275
- bot.start().catch((err) => {
347
+ }
348
+ process.on("SIGINT", shutdown);
349
+ process.on("SIGTERM", shutdown);
350
+ // Start all bots
351
+ await Promise.all(bots.map((bot) => bot.start().catch((err) => {
276
352
  log.logWarning("Failed to start bot", err instanceof Error ? err.message : String(err));
277
353
  process.exit(1);
278
- });
354
+ })));
279
355
  //# 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":";AAEA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,QAAQ,IAAI,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAoB,YAAY,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,eAAe,EAAsB,eAAe,EAAE,MAAM,cAAc,CAAC;AACpF,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,sBAAsB,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;AAClE,MAAM,qBAAqB,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;AAQhE,SAAS,SAAS;IAChB,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;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YACjC,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;QAC5D,CAAC;aAAM,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;YAC/B,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7C,CAAC;aAAM,IAAI,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;YACzC,iBAAiB,GAAG,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACtD,CAAC;aAAM,IAAI,GAAG,KAAK,YAAY,EAAE,CAAC;YAChC,iBAAiB,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QAChC,CAAC;aAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAChC,UAAU,GAAG,GAAG,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO;QACL,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS;QACxD,OAAO;QACP,eAAe,EAAE,iBAAiB;KACnC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,GAAG,SAAS,EAAE,CAAC;AAE/B,sCAAsC;AACtC,IAAI,UAAU,CAAC,eAAe,EAAE,CAAC;IAC/B,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;QAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,eAAe,CAAC,UAAU,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC;IACvE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,wCAAwC;AACxC,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC;IAC3B,OAAO,CAAC,KAAK,CAAC,gEAAgE,CAAC,CAAC;IAChF,OAAO,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,CAAC;AAEnG,2BAA2B;AAC3B,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,mBAAmB,IAAI,mBAAmB,CAAC,CAAC;AAChE,MAAM,WAAW,GAAG,CAAC,CAAC,sBAAsB,CAAC;AAC7C,MAAM,UAAU,GAAG,CAAC,CAAC,qBAAqB,CAAC;AAE3C,IAAI,CAAC,QAAQ,IAAI,CAAC,WAAW,IAAI,CAAC,UAAU,EAAE,CAAC;IAC7C,OAAO,CAAC,KAAK,CACX,yCAAyC;QACvC,yDAAyD;QACzD,sCAAsC;QACtC,mCAAmC,CACtC,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;AAe/B,MAAM,aAAa,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEtD,iDAAiD;AACjD,MAAM,YAAY,GAAG,IAAI,GAAG,EAAiB,CAAC;AAE9C,wDAAwD;AACxD,IAAI,cAAc,GAAG,KAAK,CAAC;AAE3B,wCAAwC;AACxC,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,wEAAwE;AACxE,MAAM,eAAe,GAAG,OAAO,CAAC;AAEhC,KAAK,UAAU,QAAQ,CAAC,SAAiB,EAAE,UAAmB;IAC5D,MAAM,GAAG,GAAG,UAAU,IAAI,SAAS,CAAC;IACpC,IAAI,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAC/C,KAAK,GAAG;YACN,OAAO,EAAE,KAAK;YACd,MAAM,EAAE,MAAM,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC;YAC3E,aAAa,EAAE,KAAK;YACpB,cAAc,EAAE,IAAI,CAAC,GAAG,EAAE;SAC3B,CAAC;QACF,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACpC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB;IACxB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEvB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,aAAa,EAAE,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,GAAG,GAAG,KAAK,CAAC,cAAc,GAAG,eAAe,EAAE,CAAC;YACnE,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,IAAI,aAAa,CAAC,IAAI,GAAG,YAAY,EAAE,CAAC;QACtC,MAAM,YAAY,GAAmD,EAAE,CAAC;QACxE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,aAAa,EAAE,CAAC;YACzC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;gBACnB,YAAY,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC;YACnE,CAAC;QACH,CAAC;QAED,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,GAAG,CAAC,CAAC,cAAc,CAAC,CAAC;QAEjE,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,GAAG,YAAY,CAAC;QAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5D,aAAa,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,UAAU;AACV,+EAA+E;AAE/E,MAAM,OAAO,GAAe;IAC1B,SAAS,CAAC,UAAkB;QAC1B,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC5C,OAAO,KAAK,EAAE,OAAO,IAAI,KAAK,CAAC;IACjC,CAAC;IAED,kBAAkB;QAChB,MAAM,QAAQ,GAA4C,EAAE,CAAC;QAC7D,KAAK,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,aAAa,EAAE,CAAC;YAChD,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;gBACrC,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,UAAkB,EAAE,SAAiB,EAAE,GAAQ;QAC9D,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC5C,IAAI,KAAK,EAAE,OAAO,EAAE,CAAC;YACnB,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC;YAC3B,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,WAAW,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;YAC7D,KAAK,CAAC,aAAa,GAAG,EAAE,CAAC;QAC3B,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,CAAC,WAAW,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CACf,KAAe,EACf,GAAQ,EACR,QAAqB,EACrB,QAAkB;QAElB,0CAA0C;QAC1C,IAAI,cAAc,EAAE,CAAC;YACnB,GAAG,CAAC,OAAO,CACT,IAAI,KAAK,CAAC,OAAO,qCAAqC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CACpF,CAAC;YACF,OAAO;QACT,CAAC;QAED,MAAM,UAAU,GAAG,GAAG,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,EAAE,EAAE,CAAC;QACrE,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAExD,YAAY;QACZ,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACrB,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC;QAC5B,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAE7B,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,mBAAmB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;QAE/E,8BAA8B;QAC9B,MAAM,UAAU,GAAG,CAAC,KAAK,IAAI,EAAE;YAC7B,IAAI,CAAC;gBACH,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC;gBAEpD,gBAAgB;gBAChB,MAAM,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;gBAClC,MAAM,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;gBACnC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;gBACtE,MAAM,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;gBAEpC,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;oBAC3D,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;wBACxB,MAAM,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;wBACzE,KAAK,CAAC,aAAa,GAAG,SAAS,CAAC;oBAClC,CAAC;yBAAM,CAAC;wBACN,MAAM,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;oBACpD,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,UAAU,CACZ,IAAI,KAAK,CAAC,OAAO,aAAa,EAC9B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CACjD,CAAC;YACJ,CAAC;oBAAS,CAAC;gBACT,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;gBACtB,KAAK,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAClC,iBAAiB,EAAE,CAAC;YACtB,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QAEL,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,UAAU,CAAC;QACnB,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;CACF,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,sCAAsC;AACtC,IAAI,GAAQ,CAAC;AAEb,IAAI,QAAQ,EAAE,CAAC;IACb,MAAM,WAAW,GAAG,IAAI,YAAY,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,mBAAoB,EAAE,CAAC,CAAC;IACrF,GAAG,GAAG,IAAI,aAAa,CAAC,OAAO,EAAE;QAC/B,QAAQ,EAAE,mBAAoB;QAC9B,QAAQ,EAAE,mBAAoB;QAC9B,UAAU;QACV,KAAK,EAAE,WAAW;KACnB,CAAC,CAAC;IACH,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;AACjC,CAAC;KAAM,IAAI,WAAW,EAAE,CAAC;IACvB,GAAG,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE;QAC7B,KAAK,EAAE,sBAAuB;QAC9B,UAAU;KACX,CAAC,CAAC;IACH,GAAG,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;AACpC,CAAC;KAAM,CAAC;IACN,GAAG,GAAG,IAAI,UAAU,CAAC,OAAO,EAAE;QAC5B,KAAK,EAAE,qBAAsB;QAC7B,UAAU;KACX,CAAC,CAAC;IACH,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;AACnC,CAAC;AAED,uBAAuB;AACvB,MAAM,aAAa,GAAG,mBAAmB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;AAC3D,IAAI,QAAQ,EAAE,CAAC;IACZ,GAAqB,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC;AACzD,CAAC;AACD,aAAa,CAAC,KAAK,EAAE,CAAC;AAEtB,kBAAkB;AAClB,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;IAC9B,IAAI,cAAc;QAAE,OAAO;IAC3B,cAAc,GAAG,IAAI,CAAC;IACtB,GAAG,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAC;IAE3C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;IACnC,OAAO,YAAY,CAAC,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC;QACrD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,YAAY,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QAC1B,GAAG,CAAC,UAAU,CAAC,qBAAqB,YAAY,CAAC,IAAI,yBAAyB,CAAC,CAAC;IAClF,CAAC;IAED,aAAa,CAAC,IAAI,EAAE,CAAC;IACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE;IAC/B,IAAI,cAAc;QAAE,OAAO;IAC3B,cAAc,GAAG,IAAI,CAAC;IACtB,GAAG,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAC;IAE3C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;IACnC,OAAO,YAAY,CAAC,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC;QACrD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,YAAY,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QAC1B,GAAG,CAAC,UAAU,CAAC,qBAAqB,YAAY,CAAC,IAAI,yBAAyB,CAAC,CAAC;IAClF,CAAC;IAED,aAAa,CAAC,IAAI,EAAE,CAAC;IACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACxB,GAAG,CAAC,UAAU,CAAC,qBAAqB,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACxF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.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 MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\n\ninterface ParsedArgs {\n workingDir?: string;\n sandbox: SandboxConfig;\n downloadChannel?: string;\n}\n\nfunction parseArgs(): ParsedArgs {\n const args = process.argv.slice(2);\n let sandbox: SandboxConfig = { type: \"host\" };\n let workingDir: string | undefined;\n let downloadChannelId: string | undefined;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg.startsWith(\"--sandbox=\")) {\n sandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n } else if (arg === \"--sandbox\") {\n sandbox = parseSandboxArg(args[++i] || \"\");\n } else if (arg.startsWith(\"--download=\")) {\n downloadChannelId = arg.slice(\"--download=\".length);\n } else if (arg === \"--download\") {\n downloadChannelId = args[++i];\n } else if (!arg.startsWith(\"-\")) {\n workingDir = arg;\n }\n }\n\n return {\n workingDir: workingDir ? resolve(workingDir) : undefined,\n sandbox,\n downloadChannel: downloadChannelId,\n };\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n if (!MOM_SLACK_BOT_TOKEN) {\n console.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n process.exit(1);\n }\n await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n process.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n console.error(\"Usage: mama [--sandbox=host|docker:<name>] <working-directory>\");\n console.error(\" mama --download <channel-id>\");\n process.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n console.error(\n \"No platform tokens found. Set one of:\\n\" +\n \" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n \" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n \" Discord: MOM_DISCORD_BOT_TOKEN\",\n );\n process.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n\nasync function getState(channelId: string, sessionKey?: string): Promise<ChannelState> {\n const key = sessionKey ?? channelId;\n let state = channelStates.get(key);\n if (!state) {\n const channelDir = join(workingDir, channelId);\n state = {\n running: false,\n runner: await createRunner(sandbox, key, channelId, channelDir, workingDir),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n channelStates.set(key, state);\n } else {\n state.lastAccessedAt = Date.now();\n }\n return state;\n}\n\n/**\n * Evict idle sessions from channelStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of channelStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n channelStates.delete(key);\n }\n }\n\n if (channelStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of channelStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = channelStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n channelStates.delete(idleSessions[i].key);\n }\n }\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n isRunning(sessionKey: string): boolean {\n const state = channelStates.get(sessionKey);\n return state?.running ?? false;\n },\n\n getRunningSessions() {\n const sessions: import(\"./adapter.js\").RunningSession[] = [];\n for (const [sessionKey, state] of channelStates) {\n if (state.running && state.startedAt) {\n sessions.push({ sessionKey, startedAt: state.startedAt });\n }\n }\n return sessions;\n },\n\n async handleStop(sessionKey: string, channelId: string, bot: Bot): Promise<void> {\n const state = channelStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(channelId, \"_Stopping..._\");\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(channelId, \"_Nothing running_\");\n }\n },\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n _isEvent?: boolean,\n ): Promise<void> {\n // Don't accept new events during shutdown\n if (isShuttingDown) {\n log.logInfo(\n `[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;\n const state = await getState(event.channel, sessionKey);\n\n // Start run\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n\n log.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n // Wrap in-flight run tracking\n const runPromise = (async () => {\n try {\n const { message, responseCtx, platform } = adapters;\n\n // Run the agent\n await responseCtx.setTyping(true);\n await responseCtx.setWorking(true);\n const result = await state.runner.run(message, responseCtx, platform);\n await responseCtx.setWorking(false);\n\n if (result.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(event.channel, \"_Stopped_\");\n }\n }\n } catch (err) {\n log.logWarning(\n `[${event.channel}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n evictIdleSessions();\n }\n })();\n\n inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n inFlightRuns.delete(runPromise);\n }\n },\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\n// Create the appropriate platform bot\nlet bot: Bot;\n\nif (hasSlack) {\n const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n bot = new SlackBotClass(handler, {\n appToken: MOM_SLACK_APP_TOKEN!,\n botToken: MOM_SLACK_BOT_TOKEN!,\n workingDir,\n store: sharedStore,\n });\n log.logInfo(\"Platform: Slack\");\n} else if (hasTelegram) {\n bot = new TelegramBot(handler, {\n token: MOM_TELEGRAM_BOT_TOKEN!,\n workingDir,\n });\n log.logInfo(\"Platform: Telegram\");\n} else {\n bot = new DiscordBot(handler, {\n token: MOM_DISCORD_BOT_TOKEN!,\n workingDir,\n });\n log.logInfo(\"Platform: Discord\");\n}\n\n// Start events watcher\nconst eventsWatcher = createEventsWatcher(workingDir, bot);\nif (hasSlack) {\n (bot as SlackBotClass).setEventsWatcher(eventsWatcher);\n}\neventsWatcher.start();\n\n// Handle shutdown\nprocess.on(\"SIGINT\", async () => {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n process.exit(0);\n});\n\nprocess.on(\"SIGTERM\", async () => {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n process.exit(0);\n});\n\nbot.start().catch((err) => {\n log.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n process.exit(1);\n});\n"]}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport { readFileSync } from \"fs\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join as pathJoin } from \"path\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\n// Get version from package.json\nfunction getVersion(): string {\n // Try to find package.json in the dist directory or parent\n const possiblePaths = [\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"package.json\"),\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\"),\n pathJoin(process.cwd(), \"package.json\"),\n ];\n\n for (const pkgPath of possiblePaths) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n if (pkg.version) return pkg.version;\n } catch {\n // Continue to next path\n }\n }\n return \"unknown\";\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 MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\n\ninterface ParsedArgs {\n workingDir?: string;\n sandbox: SandboxConfig;\n downloadChannel?: string;\n showVersion?: boolean;\n}\n\nfunction parseArgs(): ParsedArgs {\n const args = process.argv.slice(2);\n let sandbox: SandboxConfig = { type: \"host\" };\n let workingDir: string | undefined;\n let downloadChannelId: string | undefined;\n let showVersion = false;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\") {\n showVersion = true;\n } else if (arg.startsWith(\"--sandbox=\")) {\n sandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n } else if (arg === \"--sandbox\") {\n sandbox = parseSandboxArg(args[++i] || \"\");\n } else if (arg.startsWith(\"--download=\")) {\n downloadChannelId = arg.slice(\"--download=\".length);\n } else if (arg === \"--download\") {\n downloadChannelId = args[++i];\n } else if (!arg.startsWith(\"-\")) {\n workingDir = arg;\n }\n }\n\n return {\n workingDir: workingDir ? resolve(workingDir) : undefined,\n sandbox,\n downloadChannel: downloadChannelId,\n showVersion,\n };\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --version\nif (parsedArgs.showVersion) {\n console.log(getVersion());\n process.exit(0);\n}\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n if (!MOM_SLACK_BOT_TOKEN) {\n console.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n process.exit(1);\n }\n await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n process.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n console.error(\n \"Usage: mama [--sandbox=host|docker:<name>|firecracker:<vm-id>:<host-path>] <working-directory>\",\n );\n console.error(\" mama --download <channel-id>\");\n process.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n console.error(\n \"No platform tokens found. Set one of:\\n\" +\n \" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n \" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n \" Discord: MOM_DISCORD_BOT_TOKEN\",\n );\n process.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\n/**\n * Maps \"channel:botReplyTs\" → sessionKey.\n * When the bot posts a top-level reply, the Slack thread anchors to that ts.\n * Users replying in that thread will have thread_ts = botReplyTs, which differs\n * from the original sessionKey (channel:userMessageTs). This alias map lets\n * stop commands resolve the correct session even when the ts doesn't match.\n */\nconst threadAliases = new Map<string, string>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n\nasync function getState(channelId: string, sessionKey?: string): Promise<ChannelState> {\n const key = sessionKey ?? channelId;\n let state = channelStates.get(key);\n if (!state) {\n const channelDir = join(workingDir, channelId);\n state = {\n running: false,\n runner: await createRunner(sandbox, key, channelId, channelDir, workingDir),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n channelStates.set(key, state);\n } else {\n state.lastAccessedAt = Date.now();\n }\n return state;\n}\n\n/**\n * Evict idle sessions from channelStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of channelStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n channelStates.delete(key);\n // Clean up aliases pointing to this session\n for (const [alias, target] of threadAliases) {\n if (target === key) threadAliases.delete(alias);\n }\n }\n }\n\n if (channelStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of channelStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = channelStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n const evictedKey = idleSessions[i].key;\n channelStates.delete(evictedKey);\n for (const [alias, target] of threadAliases) {\n if (target === evictedKey) threadAliases.delete(alias);\n }\n }\n }\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n isRunning(sessionKey: string): boolean {\n const state = channelStates.get(sessionKey);\n return !!state?.running;\n },\n\n getRunningSessions() {\n const sessions: import(\"./adapter.js\").RunningSession[] = [];\n for (const [sessionKey, state] of channelStates) {\n if (state.running && state.startedAt) {\n // Get current step from runner\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n },\n\n async handleStop(sessionKey: string, channelId: string, bot: Bot): Promise<void> {\n const state = channelStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(channelId, \"_Stopping..._\");\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(channelId, \"_Nothing running_\");\n }\n },\n\n forceStop(sessionKey: string): void {\n const state = channelStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n },\n\n resolveSessionKey(rawKey: string): string {\n return threadAliases.get(rawKey) ?? rawKey;\n },\n\n registerThreadAlias(aliasKey: string, sessionKey: string): void {\n threadAliases.set(aliasKey, sessionKey);\n },\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n _isEvent?: boolean,\n ): Promise<void> {\n // Don't accept new events during shutdown\n if (isShuttingDown) {\n log.logInfo(\n `[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const rawSessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;\n const sessionKey = this.resolveSessionKey(rawSessionKey);\n const state = await getState(event.channel, sessionKey);\n\n // Start run\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n // Wrap in-flight run tracking\n const runPromise = (async () => {\n try {\n const { message, responseCtx, platform } = adapters;\n\n // Run the agent\n await responseCtx.setTyping(true);\n await responseCtx.setWorking(true);\n const result = await state.runner.run(message, responseCtx, platform);\n await responseCtx.setWorking(false);\n\n if (result.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(event.channel, \"_Stopped_\");\n }\n }\n } catch (err) {\n log.logWarning(\n `[${event.channel}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n evictIdleSessions();\n }\n })();\n\n inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n inFlightRuns.delete(runPromise);\n }\n },\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nconst sandboxDesc =\n sandbox.type === \"host\"\n ? \"host\"\n : sandbox.type === \"docker\"\n ? `docker:${sandbox.container}`\n : `firecracker:${sandbox.vmId}`;\nlog.logStartup(workingDir, sandboxDesc);\n\n// Create platform bots\nconst bots: Bot[] = [];\nconst botsByPlatform: Record<string, Bot> = {};\n\nif (hasSlack) {\n const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n const slackBot = new SlackBotClass(handler, {\n appToken: MOM_SLACK_APP_TOKEN!,\n botToken: MOM_SLACK_BOT_TOKEN!,\n workingDir,\n store: sharedStore,\n });\n bots.push(slackBot);\n botsByPlatform.slack = slackBot;\n log.logInfo(\"Platform: Slack\");\n}\nif (hasTelegram) {\n const telegramBot = new TelegramBot(handler, {\n token: MOM_TELEGRAM_BOT_TOKEN!,\n workingDir,\n });\n bots.push(telegramBot);\n botsByPlatform.telegram = telegramBot;\n log.logInfo(\"Platform: Telegram\");\n}\nif (hasDiscord) {\n const discordBot = new DiscordBot(handler, {\n token: MOM_DISCORD_BOT_TOKEN!,\n workingDir,\n });\n bots.push(discordBot);\n botsByPlatform.discord = discordBot;\n log.logInfo(\"Platform: Discord\");\n}\n\n// Start events watcher with explicit platform routing\nconst eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);\nconst slackBot = botsByPlatform.slack as SlackBotClass | undefined;\nif (slackBot) {\n slackBot.setEventsWatcher(eventsWatcher);\n}\neventsWatcher.start();\n\n// Handle shutdown\nasync function shutdown(): Promise<void> {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n process.exit(0);\n}\n\nprocess.on(\"SIGINT\", shutdown);\nprocess.on(\"SIGTERM\", shutdown);\n\n// Start all bots\nawait Promise.all(\n bots.map((bot) =>\n bot.start().catch((err) => {\n log.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n process.exit(1);\n }),\n ),\n);\n"]}
package/dist/sandbox.d.ts CHANGED
@@ -3,11 +3,17 @@ export type SandboxConfig = {
3
3
  } | {
4
4
  type: "docker";
5
5
  container: string;
6
+ } | {
7
+ type: "firecracker";
8
+ vmId: string;
9
+ hostPath: string;
10
+ sshUser?: string;
11
+ sshPort?: number;
6
12
  };
7
13
  export declare function parseSandboxArg(value: string): SandboxConfig;
8
14
  export declare function validateSandbox(config: SandboxConfig): Promise<void>;
9
15
  /**
10
- * Create an executor that runs commands either on host or in Docker container
16
+ * Create an executor that runs commands either on host, in Docker container, or in Firecracker VM
11
17
  */
12
18
  export declare function createExecutor(config: SandboxConfig): Executor;
13
19
  export interface Executor {
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAErF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAc5D;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAiC1E;AAoBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,QAAQ,CAK9D;AAED,MAAM,WAAW,QAAQ;IACvB;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAElE;;;;OAIG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5C;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd","sourcesContent":["import { spawn } from \"child_process\";\n\nexport type SandboxConfig = { type: \"host\" } | { type: \"docker\"; container: string };\n\nexport function parseSandboxArg(value: string): SandboxConfig {\n if (value === \"host\") {\n return { type: \"host\" };\n }\n if (value.startsWith(\"docker:\")) {\n const container = value.slice(\"docker:\".length);\n if (!container) {\n console.error(\"Error: docker sandbox requires container name (e.g., docker:mama-sandbox)\");\n process.exit(1);\n }\n return { type: \"docker\", container };\n }\n console.error(`Error: Invalid sandbox type '${value}'. Use 'host' or 'docker:<container-name>'`);\n process.exit(1);\n}\n\nexport async function validateSandbox(config: SandboxConfig): Promise<void> {\n if (config.type === \"host\") {\n return;\n }\n\n // Check if Docker is available\n try {\n await execSimple(\"docker\", [\"--version\"]);\n } catch {\n console.error(\"Error: Docker is not installed or not in PATH\");\n process.exit(1);\n }\n\n // Check if container exists and is running\n try {\n const result = await execSimple(\"docker\", [\n \"inspect\",\n \"-f\",\n \"{{.State.Running}}\",\n config.container,\n ]);\n if (result.trim() !== \"true\") {\n console.error(`Error: Container '${config.container}' is not running.`);\n console.error(`Start it with: docker start ${config.container}`);\n process.exit(1);\n }\n } catch {\n console.error(`Error: Container '${config.container}' does not exist.`);\n console.error(\"Create it with: ./docker.sh create <data-dir>\");\n process.exit(1);\n }\n\n console.log(` Docker container '${config.container}' is running.`);\n}\n\nfunction execSimple(cmd: string, args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n let stdout = \"\";\n let stderr = \"\";\n child.stdout?.on(\"data\", (d) => {\n stdout += d;\n });\n child.stderr?.on(\"data\", (d) => {\n stderr += d;\n });\n child.on(\"close\", (code) => {\n if (code === 0) resolve(stdout);\n else reject(new Error(stderr || `Exit code ${code}`));\n });\n });\n}\n\n/**\n * Create an executor that runs commands either on host or in Docker container\n */\nexport function createExecutor(config: SandboxConfig): Executor {\n if (config.type === \"host\") {\n return new HostExecutor();\n }\n return new DockerExecutor(config.container);\n}\n\nexport interface Executor {\n /**\n * Execute a bash command\n */\n exec(command: string, options?: ExecOptions): Promise<ExecResult>;\n\n /**\n * Get the workspace path prefix for this executor\n * Host: returns the actual path\n * Docker: returns /workspace\n */\n getWorkspacePath(hostPath: string): string;\n}\n\nexport interface ExecOptions {\n timeout?: number;\n signal?: AbortSignal;\n}\n\nexport interface ExecResult {\n stdout: string;\n stderr: string;\n code: number;\n}\n\nclass HostExecutor implements Executor {\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n return new Promise((resolve, reject) => {\n const shell = process.platform === \"win32\" ? \"cmd\" : \"sh\";\n const shellArgs = process.platform === \"win32\" ? [\"/c\"] : [\"-c\"];\n\n const child = spawn(shell, [...shellArgs, command], {\n detached: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n\n const timeoutHandle =\n options?.timeout && options.timeout > 0\n ? setTimeout(() => {\n timedOut = true;\n killProcessTree(child.pid!);\n }, options.timeout * 1000)\n : undefined;\n\n const onAbort = () => {\n if (child.pid) killProcessTree(child.pid);\n };\n\n if (options?.signal) {\n if (options.signal.aborted) {\n onAbort();\n } else {\n options.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n child.stdout?.on(\"data\", (data) => {\n stdout += data.toString();\n if (stdout.length > 10 * 1024 * 1024) {\n stdout = stdout.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.stderr?.on(\"data\", (data) => {\n stderr += data.toString();\n if (stderr.length > 10 * 1024 * 1024) {\n stderr = stderr.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.on(\"close\", (code) => {\n if (timeoutHandle) clearTimeout(timeoutHandle);\n if (options?.signal) {\n options.signal.removeEventListener(\"abort\", onAbort);\n }\n\n if (options?.signal?.aborted) {\n reject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n return;\n }\n\n if (timedOut) {\n reject(\n new Error(\n `${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim(),\n ),\n );\n return;\n }\n\n resolve({ stdout, stderr, code: code ?? 0 });\n });\n });\n }\n\n getWorkspacePath(hostPath: string): string {\n return hostPath;\n }\n}\n\nclass DockerExecutor implements Executor {\n constructor(private container: string) {}\n\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n // Wrap command for docker exec\n const dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;\n const hostExecutor = new HostExecutor();\n return hostExecutor.exec(dockerCmd, options);\n }\n\n getWorkspacePath(_hostPath: string): string {\n // Docker container sees /workspace\n return \"/workspace\";\n }\n}\n\nfunction killProcessTree(pid: number): void {\n if (process.platform === \"win32\") {\n try {\n spawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n detached: true,\n });\n } catch {\n // Ignore errors\n }\n } else {\n try {\n process.kill(-pid, \"SIGKILL\");\n } catch {\n try {\n process.kill(pid, \"SIGKILL\");\n } catch {\n // Process already dead\n }\n }\n }\n}\n\nfunction shellEscape(s: string): string {\n // Escape for passing to sh -c\n return `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhG,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CA8C5D;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAmF1E;AAoBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,QAAQ,CAQ9D;AAED,MAAM,WAAW,QAAQ;IACvB;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAElE;;;;OAIG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5C;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd","sourcesContent":["import { spawn } from \"child_process\";\n\nexport type SandboxConfig =\n | { type: \"host\" }\n | { type: \"docker\"; container: string }\n | { type: \"firecracker\"; vmId: string; hostPath: string; sshUser?: string; sshPort?: number };\n\nexport function parseSandboxArg(value: string): SandboxConfig {\n if (value === \"host\") {\n return { type: \"host\" };\n }\n if (value.startsWith(\"docker:\")) {\n const container = value.slice(\"docker:\".length);\n if (!container) {\n console.error(\"Error: docker sandbox requires container name (e.g., docker:mama-sandbox)\");\n process.exit(1);\n }\n return { type: \"docker\", container };\n }\n if (value.startsWith(\"firecracker:\")) {\n const arg = value.slice(\"firecracker:\".length);\n // Format: firecracker:<vm-id>:<host-path>[:<ssh-user>[:<ssh-port>]]\n // Example: firecracker:vm1:/home/user/workspace\n // firecracker:vm1:/home/user/workspace:root\n // firecracker:vm1:/home/user/workspace:root:22\n const parts = arg.split(\":\");\n if (parts.length < 2) {\n console.error(\n \"Error: firecracker sandbox requires vm-id and host-path\\n\" +\n \"Usage: firecracker:<vm-id>:<host-path>[:<ssh-user>[:<ssh-port>]]\\n\" +\n \"Example: firecracker:vm1:/home/user/workspace\",\n );\n process.exit(1);\n }\n const vmId = parts[0];\n const hostPath = parts[1];\n const sshUser = parts[2] || \"root\";\n const sshPort = parts[3] ? parseInt(parts[3], 10) : 22;\n\n if (!vmId || !hostPath) {\n console.error(\"Error: firecracker sandbox requires vm-id and host-path\");\n process.exit(1);\n }\n if (isNaN(sshPort) || sshPort <= 0 || sshPort > 65535) {\n console.error(\"Error: invalid SSH port\");\n process.exit(1);\n }\n return { type: \"firecracker\", vmId, hostPath, sshUser, sshPort };\n }\n console.error(\n `Error: Invalid sandbox type '${value}'. Use 'host', 'docker:<container-name>', or 'firecracker:<vm-id>:<host-path>'`,\n );\n process.exit(1);\n}\n\nexport async function validateSandbox(config: SandboxConfig): Promise<void> {\n if (config.type === \"host\") {\n return;\n }\n\n if (config.type === \"docker\") {\n // Check if Docker is available\n try {\n await execSimple(\"docker\", [\"--version\"]);\n } catch {\n console.error(\"Error: Docker is not installed or not in PATH\");\n process.exit(1);\n }\n\n // Check if container exists and is running\n try {\n const result = await execSimple(\"docker\", [\n \"inspect\",\n \"-f\",\n \"{{.State.Running}}\",\n config.container,\n ]);\n if (result.trim() !== \"true\") {\n console.error(`Error: Container '${config.container}' is not running.`);\n console.error(`Start it with: docker start ${config.container}`);\n process.exit(1);\n }\n } catch {\n console.error(`Error: Container '${config.container}' does not exist.`);\n console.error(\"Create it with: ./docker.sh create <data-dir>\");\n process.exit(1);\n }\n\n console.log(` Docker container '${config.container}' is running.`);\n return;\n }\n\n if (config.type === \"firecracker\") {\n // Check if fc-agent or firecracker CLI is available\n try {\n await execSimple(\"fc-agent\", [\"--version\"]);\n } catch {\n // Try alternative: firecracker\n try {\n await execSimple(\"firecracker\", [\"--version\"]);\n } catch {\n console.error(\"Error: Firecracker tools (fc-agent or firecracker) not found in PATH\");\n console.error(\"Install firecracker: https://github.com/firecracker-microvm/firecracker\");\n process.exit(1);\n }\n }\n\n // Check if VM is running using fc-agent\n try {\n const result = await execSimple(\"fc-agent\", [\"status\", config.vmId]);\n if (!result.includes(\"running\") && !result.includes(\"Running\")) {\n console.error(`Error: Firecracker VM '${config.vmId}' is not running.`);\n console.error(`Start it with: fc-agent start ${config.vmId}`);\n process.exit(1);\n }\n } catch {\n // Try alternative: firecracker-ctl or direct check\n try {\n await execSimple(\"firecracker-ctl\", [\"status\", config.vmId]);\n } catch {\n console.error(`Warning: Could not verify if VM '${config.vmId}' is running.`);\n console.error(\"Make sure the VM is started before running mama.\");\n }\n }\n\n // Verify host path exists\n try {\n await execSimple(\"ls\", [\"-d\", config.hostPath]);\n } catch {\n console.error(`Error: Host path '${config.hostPath}' does not exist.`);\n process.exit(1);\n }\n\n console.log(\n ` Firecracker VM '${config.vmId}' configured with workspace '${config.hostPath}'.`,\n );\n return;\n }\n}\n\nfunction execSimple(cmd: string, args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n let stdout = \"\";\n let stderr = \"\";\n child.stdout?.on(\"data\", (d) => {\n stdout += d;\n });\n child.stderr?.on(\"data\", (d) => {\n stderr += d;\n });\n child.on(\"close\", (code) => {\n if (code === 0) resolve(stdout);\n else reject(new Error(stderr || `Exit code ${code}`));\n });\n });\n}\n\n/**\n * Create an executor that runs commands either on host, in Docker container, or in Firecracker VM\n */\nexport function createExecutor(config: SandboxConfig): Executor {\n if (config.type === \"host\") {\n return new HostExecutor();\n }\n if (config.type === \"docker\") {\n return new DockerExecutor(config.container);\n }\n return new FirecrackerExecutor(config.vmId, config.hostPath, config.sshUser, config.sshPort);\n}\n\nexport interface Executor {\n /**\n * Execute a bash command\n */\n exec(command: string, options?: ExecOptions): Promise<ExecResult>;\n\n /**\n * Get the workspace path prefix for this executor\n * Host: returns the actual path\n * Docker: returns /workspace\n */\n getWorkspacePath(hostPath: string): string;\n}\n\nexport interface ExecOptions {\n timeout?: number;\n signal?: AbortSignal;\n}\n\nexport interface ExecResult {\n stdout: string;\n stderr: string;\n code: number;\n}\n\nclass HostExecutor implements Executor {\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n return new Promise((resolve, reject) => {\n const shell = process.platform === \"win32\" ? \"cmd\" : \"sh\";\n const shellArgs = process.platform === \"win32\" ? [\"/c\"] : [\"-c\"];\n\n const child = spawn(shell, [...shellArgs, command], {\n detached: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n\n const timeoutHandle =\n options?.timeout && options.timeout > 0\n ? setTimeout(() => {\n timedOut = true;\n killProcessTree(child.pid!);\n }, options.timeout * 1000)\n : undefined;\n\n const onAbort = () => {\n if (child.pid) killProcessTree(child.pid);\n };\n\n if (options?.signal) {\n if (options.signal.aborted) {\n onAbort();\n } else {\n options.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n child.stdout?.on(\"data\", (data) => {\n stdout += data.toString();\n if (stdout.length > 10 * 1024 * 1024) {\n stdout = stdout.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.stderr?.on(\"data\", (data) => {\n stderr += data.toString();\n if (stderr.length > 10 * 1024 * 1024) {\n stderr = stderr.slice(0, 10 * 1024 * 1024);\n }\n });\n\n child.on(\"close\", (code) => {\n if (timeoutHandle) clearTimeout(timeoutHandle);\n if (options?.signal) {\n options.signal.removeEventListener(\"abort\", onAbort);\n }\n\n if (options?.signal?.aborted) {\n reject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n return;\n }\n\n if (timedOut) {\n reject(\n new Error(\n `${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim(),\n ),\n );\n return;\n }\n\n resolve({ stdout, stderr, code: code ?? 0 });\n });\n });\n }\n\n getWorkspacePath(hostPath: string): string {\n return hostPath;\n }\n}\n\nclass DockerExecutor implements Executor {\n constructor(private container: string) {}\n\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n // Wrap command for docker exec\n const dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;\n const hostExecutor = new HostExecutor();\n return hostExecutor.exec(dockerCmd, options);\n }\n\n getWorkspacePath(_hostPath: string): string {\n // Docker container sees /workspace\n return \"/workspace\";\n }\n}\n\nclass FirecrackerExecutor implements Executor {\n constructor(\n private vmId: string,\n private hostPath: string,\n private sshUser: string = \"root\",\n private sshPort: number = 22,\n ) {}\n\n async exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n // Use direct SSH to execute command in the Firecracker VM\n // The workspace inside the VM is expected to be mounted at /workspace\n const sshCmd =\n this.sshPort === 22\n ? `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${this.sshUser}@${this.vmId} sh -c ${shellEscape(command)}`\n : `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -p ${this.sshPort} ${this.sshUser}@${this.vmId} sh -c ${shellEscape(command)}`;\n const hostExecutor = new HostExecutor();\n return hostExecutor.exec(sshCmd, options);\n }\n\n getWorkspacePath(_hostPath: string): string {\n // Firecracker VM sees /workspace (assumes hostPath is mounted there)\n return \"/workspace\";\n }\n}\n\nfunction killProcessTree(pid: number): void {\n if (process.platform === \"win32\") {\n try {\n spawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n stdio: \"ignore\",\n detached: true,\n });\n } catch {\n // Ignore errors\n }\n } else {\n try {\n process.kill(-pid, \"SIGKILL\");\n } catch {\n try {\n process.kill(pid, \"SIGKILL\");\n } catch {\n // Process already dead\n }\n }\n }\n}\n\nfunction shellEscape(s: string): string {\n // Escape for passing to sh -c\n return `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}