@mariozechner/pi-mom 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -18,6 +18,9 @@
18
18
 
19
19
  ### Added
20
20
 
21
+ - Port truncation logic from coding-agent: bash and read tools now use consistent 2000 lines OR 50KB limits with actionable notices
22
+ - Remove redundant context history truncation (tools already provide truncation with actionable hints)
23
+
21
24
  - Message backfill on startup (#103)
22
25
  - Fetches missed messages from Slack using `conversations.history` API when mom restarts
23
26
  - Backfills up to 3 pages (3000 messages) per channel since last logged timestamp
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAe,YAAY,EAAY,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA4B/C,MAAM,WAAW,WAAW;IAC3B,GAAG,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjG,KAAK,IAAI,IAAI,CAAC;CACd;AA8WD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,aAAa,GAAG,WAAW,CAgV3E","sourcesContent":["import { Agent, type AgentEvent, ProviderTransport } from \"@mariozechner/pi-agent-core\";\nimport { getModel } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { ChannelInfo, SlackContext, UserInfo } from \"./slack.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { createMomTools, setUploadFunction } from \"./tools/index.js\";\n\n// Hardcoded model for now\nconst model = getModel(\"anthropic\", \"claude-sonnet-4-5\");\n\n/**\n * Convert Date.now() to Slack timestamp format (seconds.microseconds)\n * Uses a monotonic counter to ensure ordering even within the same millisecond\n */\nlet lastTsMs = 0;\nlet tsCounter = 0;\n\nfunction toSlackTs(): string {\n\tconst now = Date.now();\n\tif (now === lastTsMs) {\n\t\t// Same millisecond - increment counter for sub-ms ordering\n\t\ttsCounter++;\n\t} else {\n\t\t// New millisecond - reset counter\n\t\tlastTsMs = now;\n\t\ttsCounter = 0;\n\t}\n\tconst seconds = Math.floor(now / 1000);\n\tconst micros = (now % 1000) * 1000 + tsCounter; // ms to micros + counter\n\treturn `${seconds}.${micros.toString().padStart(6, \"0\")}`;\n}\n\nexport interface AgentRunner {\n\trun(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }>;\n\tabort(): void;\n}\n\nfunction getAnthropicApiKey(): string {\n\tconst key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\tif (!key) {\n\t\tthrow new Error(\"ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set\");\n\t}\n\treturn key;\n}\n\ninterface LogMessage {\n\tdate?: string;\n\tts?: string;\n\tuser?: string;\n\tuserName?: string;\n\ttext?: string;\n\tattachments?: Array<{ local: string }>;\n\tisBot?: boolean;\n}\n\nfunction getRecentMessages(channelDir: string, turnCount: number): string {\n\tconst logPath = join(channelDir, \"log.jsonl\");\n\tif (!existsSync(logPath)) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\tconst content = readFileSync(logPath, \"utf-8\");\n\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\n\tif (lines.length === 0) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\t// Parse all messages and sort by Slack timestamp\n\t// (attachment downloads can cause out-of-order logging)\n\tconst messages: LogMessage[] = [];\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tmessages.push(JSON.parse(line));\n\t\t} catch {}\n\t}\n\tmessages.sort((a, b) => {\n\t\tconst tsA = parseFloat(a.ts || \"0\");\n\t\tconst tsB = parseFloat(b.ts || \"0\");\n\t\treturn tsA - tsB;\n\t});\n\n\t// Group into \"turns\" - a turn is either:\n\t// - A single user message (isBot: false)\n\t// - A sequence of consecutive bot messages (isBot: true) coalesced into one turn\n\t// We walk backwards to get the last N turns\n\tconst turns: LogMessage[][] = [];\n\tlet currentTurn: LogMessage[] = [];\n\tlet lastWasBot: boolean | null = null;\n\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst msg = messages[i];\n\t\tconst isBot = msg.isBot === true;\n\n\t\tif (lastWasBot === null) {\n\t\t\t// First message\n\t\t\tcurrentTurn.unshift(msg);\n\t\t\tlastWasBot = isBot;\n\t\t} else if (isBot && lastWasBot) {\n\t\t\t// Consecutive bot messages - same turn\n\t\t\tcurrentTurn.unshift(msg);\n\t\t} else {\n\t\t\t// Transition - save current turn and start new one\n\t\t\tturns.unshift(currentTurn);\n\t\t\tcurrentTurn = [msg];\n\t\t\tlastWasBot = isBot;\n\n\t\t\t// Stop if we have enough turns\n\t\t\tif (turns.length >= turnCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Don't forget the last turn we were building\n\tif (currentTurn.length > 0 && turns.length < turnCount) {\n\t\tturns.unshift(currentTurn);\n\t}\n\n\t// Flatten turns back to messages and format as TSV\n\tconst formatted: string[] = [];\n\tfor (const turn of turns) {\n\t\tfor (const msg of turn) {\n\t\t\tconst date = (msg.date || \"\").substring(0, 19);\n\t\t\tconst user = msg.userName || msg.user || \"\";\n\t\t\tlet text = msg.text || \"\";\n\t\t\t// Truncate bot messages (tool results can be huge)\n\t\t\tif (msg.isBot) {\n\t\t\t\ttext = truncateForContext(text, 50000, 2000, msg.ts);\n\t\t\t}\n\t\t\tconst attachments = (msg.attachments || []).map((a) => a.local).join(\",\");\n\t\t\tformatted.push(`${date}\\t${user}\\t${text}\\t${attachments}`);\n\t\t}\n\t}\n\n\treturn formatted.join(\"\\n\");\n}\n\n/**\n * Truncate text to maxChars or maxLines, whichever comes first.\n * Adds a note with stats and instructions if truncation occurred.\n */\nfunction truncateForContext(text: string, maxChars: number, maxLines: number, ts?: string): string {\n\tconst lines = text.split(\"\\n\");\n\tconst originalLines = lines.length;\n\tconst originalChars = text.length;\n\tlet truncated = false;\n\tlet result = text;\n\n\t// Check line limit first\n\tif (lines.length > maxLines) {\n\t\tresult = lines.slice(0, maxLines).join(\"\\n\");\n\t\ttruncated = true;\n\t}\n\n\t// Check char limit\n\tif (result.length > maxChars) {\n\t\tresult = result.substring(0, maxChars);\n\t\ttruncated = true;\n\t}\n\n\tif (truncated) {\n\t\tconst remainingLines = originalLines - result.split(\"\\n\").length;\n\t\tconst remainingChars = originalChars - result.length;\n\t\tresult += `\\n[... truncated ${remainingLines} more lines, ${remainingChars} more chars. `;\n\t\tif (ts) {\n\t\t\tresult += `To get full content: jq -r 'select(.ts==\"${ts}\") | .text' log.jsonl > /tmp/msg.txt, then read /tmp/msg.txt in segments]`;\n\t\t} else {\n\t\t\tresult += `Search log.jsonl for full content]`;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nfunction getMemory(channelDir: string): string {\n\tconst parts: string[] = [];\n\n\t// Read workspace-level memory (shared across all channels)\n\tconst workspaceMemoryPath = join(channelDir, \"..\", \"MEMORY.md\");\n\tif (existsSync(workspaceMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(workspaceMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Global Workspace Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read workspace memory\", `${workspaceMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\t// Read channel-specific memory\n\tconst channelMemoryPath = join(channelDir, \"MEMORY.md\");\n\tif (existsSync(channelMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(channelMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Channel-Specific Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read channel memory\", `${channelMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\tif (parts.length === 0) {\n\t\treturn \"(no working memory yet)\";\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\tmemory: string,\n\tsandboxConfig: SandboxConfig,\n\tchannels: ChannelInfo[],\n\tusers: UserInfo[],\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\t// Format channel mappings\n\tconst channelMappings =\n\t\tchannels.length > 0 ? channels.map((c) => `${c.id}\\t#${c.name}`).join(\"\\n\") : \"(no channels loaded)\";\n\n\t// Format user mappings\n\tconst userMappings =\n\t\tusers.length > 0 ? users.map((u) => `${u.id}\\t@${u.userName}\\t${u.displayName}`).join(\"\\n\") : \"(no users loaded)\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Bash working directory: / (use cd or absolute paths)\n- Install tools with: apk add <package>\n- Your changes persist across sessions`\n\t\t: `You are running directly on the host machine.\n- Bash working directory: ${process.cwd()}\n- Be careful with system modifications`;\n\n\tconst currentDate = new Date().toISOString().split(\"T\")[0]; // YYYY-MM-DD\n\tconst currentDateTime = new Date().toISOString(); // Full ISO 8601\n\n\treturn `You are mom, a Slack bot assistant. Be concise. No emojis.\n\n## Context\n- Date: ${currentDate} (${currentDateTime})\n- You receive the last 50 conversation turns. If you need older context, search log.jsonl.\n\n## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).\n\n## Slack IDs\nChannels: ${channelMappings}\n\nUsers: ${userMappings}\n\nWhen mentioning users, use <@username> format (e.g., <@mario>).\n\n## Environment\n${envDescription}\n\n## Workspace Layout\n${workspacePath}/\n├── MEMORY.md # Global memory (all channels)\n├── skills/ # Global CLI tools you create\n└── ${channelId}/ # This channel\n ├── MEMORY.md # Channel-specific memory\n ├── log.jsonl # Full message history\n ├── attachments/ # User-shared files\n ├── scratch/ # Your working directory\n └── skills/ # Channel-specific tools\n\n## Skills (Custom CLI Tools)\nYou can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).\nStore in \\`${workspacePath}/skills/<name>/\\` or \\`${channelPath}/skills/<name>/\\`.\nEach skill needs a \\`SKILL.md\\` documenting usage. Read it before using a skill.\nList skills in global memory so you remember them.\n\n## Memory\nWrite to MEMORY.md files to persist context across conversations.\n- Global (${workspacePath}/MEMORY.md): skills, preferences, project info\n- Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work\nUpdate when you learn something important or when asked to remember something.\n\n### Current Memory\n${memory}\n\n## System Configuration Log\nMaintain ${workspacePath}/SYSTEM.md to log all environment modifications:\n- Installed packages (apk add, npm install, pip install)\n- Environment variables set\n- Config files modified (~/.gitconfig, cron jobs, etc.)\n- Skill dependencies installed\n\nUpdate this file whenever you modify the environment. On fresh container, read it first to restore your setup.\n\n## Log Queries (CRITICAL: limit output to avoid context overflow)\nFormat: \\`{\"date\":\"...\",\"ts\":\"...\",\"user\":\"...\",\"userName\":\"...\",\"text\":\"...\",\"isBot\":false}\\`\nThe log contains user messages AND your tool calls/results. Filter appropriately.\n${isDocker ? \"Install jq: apk add jq\" : \"\"}\n\n**Conversation only (excludes tool calls/results) - use for summaries:**\n\\`\\`\\`bash\n# Recent conversation (no [Tool] or [Tool Result] lines)\ngrep -v '\"text\":\"\\\\[Tool' log.jsonl | tail -30 | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Yesterday's conversation\ngrep '\"date\":\"2025-11-26' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Specific user's messages\ngrep '\"userName\":\"mario\"' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | tail -20 | jq -c '{date: .date[0:19], text}'\n\\`\\`\\`\n\n**Full details (includes tool calls) - use when you need technical context:**\n\\`\\`\\`bash\n# Raw recent entries\ntail -20 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Count all messages\nwc -l log.jsonl\n\\`\\`\\`\n\n## Tools\n- bash: Run shell commands (primary tool). Install packages as needed.\n- read: Read files\n- write: Create/overwrite files \n- edit: Surgical file edits\n- attach: Share files to Slack\n\nEach tool requires a \"label\" parameter (shown to user).\n`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen - 3) + \"...\";\n}\n\nfunction extractToolResultText(result: unknown): string {\n\t// If it's already a string, return it\n\tif (typeof result === \"string\") {\n\t\treturn result;\n\t}\n\n\t// If it's an object with content array (tool result format)\n\tif (\n\t\tresult &&\n\t\ttypeof result === \"object\" &&\n\t\t\"content\" in result &&\n\t\tArray.isArray((result as { content: unknown }).content)\n\t) {\n\t\tconst content = (result as { content: Array<{ type: string; text?: string }> }).content;\n\t\tconst textParts: string[] = [];\n\t\tfor (const part of content) {\n\t\t\tif (part.type === \"text\" && part.text) {\n\t\t\t\ttextParts.push(part.text);\n\t\t\t}\n\t\t}\n\t\tif (textParts.length > 0) {\n\t\t\treturn textParts.join(\"\\n\");\n\t\t}\n\t}\n\n\t// Fallback to JSON\n\treturn JSON.stringify(result);\n}\n\nfunction formatToolArgsForSlack(_toolName: string, args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\tlines.push(value);\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {\n\tlet agent: Agent | null = null;\n\tconst executor = createExecutor(sandboxConfig);\n\n\treturn {\n\t\tasync run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }> {\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(channelDir, { recursive: true });\n\n\t\t\tconst channelId = ctx.message.channel;\n\t\t\tconst workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, \"\"));\n\t\t\tconst recentMessages = getRecentMessages(channelDir, 50);\n\n\t\t\tconst memory = getMemory(channelDir);\n\t\t\tconst systemPrompt = buildSystemPrompt(\n\t\t\t\tworkspacePath,\n\t\t\t\tchannelId,\n\t\t\t\tmemory,\n\t\t\t\tsandboxConfig,\n\t\t\t\tctx.channels,\n\t\t\t\tctx.users,\n\t\t\t);\n\n\t\t\t// Debug: log context sizes\n\t\t\tlog.logInfo(\n\t\t\t\t`Context sizes - system: ${systemPrompt.length} chars, messages: ${recentMessages.length} chars, memory: ${memory.length} chars`,\n\t\t\t);\n\t\t\tlog.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);\n\n\t\t\t// Set up file upload function for the attach tool\n\t\t\t// For Docker, we need to translate paths back to host\n\t\t\tsetUploadFunction(async (filePath: string, title?: string) => {\n\t\t\t\tconst hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);\n\t\t\t\tawait ctx.uploadFile(hostPath, title);\n\t\t\t});\n\n\t\t\t// Create tools with executor\n\t\t\tconst tools = createMomTools(executor);\n\n\t\t\t// Create ephemeral agent\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\ttransport: new ProviderTransport({\n\t\t\t\t\tgetApiKey: async () => getAnthropicApiKey(),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Create logging context\n\t\t\tconst logCtx = {\n\t\t\t\tchannelId: ctx.message.channel,\n\t\t\t\tuserName: ctx.message.userName,\n\t\t\t\tchannelName: ctx.channelName,\n\t\t\t};\n\n\t\t\t// Track pending tool calls to pair args with results and timing\n\t\t\tconst pendingTools = new Map<string, { toolName: string; args: unknown; startTime: number }>();\n\n\t\t\t// Track usage across all assistant messages in this run\n\t\t\tconst totalUsage = {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\tcost: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotal: 0,\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Track stop reason\n\t\t\tlet stopReason = \"stop\";\n\n\t\t\t// Slack message limit is 40,000 characters - split into multiple messages if needed\n\t\t\tconst SLACK_MAX_LENGTH = 40000;\n\t\t\tconst splitForSlack = (text: string): string[] => {\n\t\t\t\tif (text.length <= SLACK_MAX_LENGTH) return [text];\n\t\t\t\tconst parts: string[] = [];\n\t\t\t\tlet remaining = text;\n\t\t\t\tlet partNum = 1;\n\t\t\t\twhile (remaining.length > 0) {\n\t\t\t\t\tconst chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tremaining = remaining.substring(SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tconst suffix = remaining.length > 0 ? `\\n_(continued ${partNum}...)_` : \"\";\n\t\t\t\t\tparts.push(chunk + suffix);\n\t\t\t\t\tpartNum++;\n\t\t\t\t}\n\t\t\t\treturn parts;\n\t\t\t};\n\n\t\t\t// Promise queue to ensure ctx.respond/respondInThread calls execute in order\n\t\t\t// Handles errors gracefully by posting to thread instead of crashing\n\t\t\tconst queue = {\n\t\t\t\tchain: Promise.resolve(),\n\t\t\t\tenqueue(fn: () => Promise<void>, errorContext: string): void {\n\t\t\t\t\tthis.chain = this.chain.then(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait fn();\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\t\tlog.logWarning(`Slack API error (${errorContext})`, errMsg);\n\t\t\t\t\t\t\t// Try to post error to thread, but don't crash if that fails too\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait ctx.respondInThread(`_Error: ${errMsg}_`);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Ignore - we tried our best\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t// Enqueue a message that may need splitting\n\t\t\t\tenqueueMessage(text: string, target: \"main\" | \"thread\", errorContext: string, log = true): void {\n\t\t\t\t\tconst parts = splitForSlack(text);\n\t\t\t\t\tfor (const part of parts) {\n\t\t\t\t\t\tthis.enqueue(\n\t\t\t\t\t\t\t() => (target === \"main\" ? ctx.respond(part, log) : ctx.respondInThread(part)),\n\t\t\t\t\t\t\terrorContext,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tflush(): Promise<void> {\n\t\t\t\t\treturn this.chain;\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Subscribe to events\n\t\t\tagent.subscribe(async (event: AgentEvent) => {\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t\t\tconst args = event.args as { label?: string };\n\t\t\t\t\t\tconst label = args.label || event.toolName;\n\n\t\t\t\t\t\t// Store args to pair with result later\n\t\t\t\t\t\tpendingTools.set(event.toolCallId, {\n\t\t\t\t\t\t\ttoolName: event.toolName,\n\t\t\t\t\t\t\targs: event.args,\n\t\t\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tlog.logToolStart(logCtx, event.toolName, label, event.args as Record<string, unknown>);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Show label in main message only\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_→ ${label}_`, false), \"tool label\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t\t\tconst resultStr = extractToolResultText(event.result);\n\t\t\t\t\t\tconst pending = pendingTools.get(event.toolCallId);\n\t\t\t\t\t\tpendingTools.delete(event.toolCallId);\n\n\t\t\t\t\t\tconst durationMs = pending ? Date.now() - pending.startTime : 0;\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tlog.logToolError(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlog.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool Result] ${event.toolName}: ${event.isError ? \"ERROR: \" : \"\"}${resultStr}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Post args + result together in thread\n\t\t\t\t\t\tconst label = pending?.args ? (pending.args as { label?: string }).label : undefined;\n\t\t\t\t\t\tconst argsFormatted = pending\n\t\t\t\t\t\t\t? formatToolArgsForSlack(event.toolName, pending.args as Record<string, unknown>)\n\t\t\t\t\t\t\t: \"(args not found)\";\n\t\t\t\t\t\tconst duration = (durationMs / 1000).toFixed(1);\n\t\t\t\t\t\tlet threadMessage = `*${event.isError ? \"✗\" : \"✓\"} ${event.toolName}*`;\n\t\t\t\t\t\tif (label) {\n\t\t\t\t\t\t\tthreadMessage += `: ${label}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthreadMessage += ` (${duration}s)\\n`;\n\n\t\t\t\t\t\tif (argsFormatted) {\n\t\t\t\t\t\t\tthreadMessage += \"```\\n\" + argsFormatted + \"\\n```\\n\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthreadMessage += \"*Result:*\\n```\\n\" + resultStr + \"\\n```\";\n\n\t\t\t\t\t\tqueue.enqueueMessage(threadMessage, \"thread\", \"tool result thread\", false);\n\n\t\t\t\t\t\t// Show brief error in main message if failed\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), \"tool error\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_update\": {\n\t\t\t\t\t\t// No longer stream to console - just track that we're streaming\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_start\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tlog.logResponseStart(logCtx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"message_end\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tconst assistantMsg = event.message as any; // AssistantMessage type\n\n\t\t\t\t\t\t\t// Track stop reason\n\t\t\t\t\t\t\tif (assistantMsg.stopReason) {\n\t\t\t\t\t\t\t\tstopReason = assistantMsg.stopReason;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Accumulate usage\n\t\t\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\t\t\ttotalUsage.input += assistantMsg.usage.input;\n\t\t\t\t\t\t\t\ttotalUsage.output += assistantMsg.usage.output;\n\t\t\t\t\t\t\t\ttotalUsage.cacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.input += assistantMsg.usage.cost.input;\n\t\t\t\t\t\t\t\ttotalUsage.cost.output += assistantMsg.usage.cost.output;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.total += assistantMsg.usage.cost.total;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Extract thinking and text from assistant message\n\t\t\t\t\t\t\tconst content = event.message.content;\n\t\t\t\t\t\t\tconst thinkingParts: string[] = [];\n\t\t\t\t\t\t\tconst textParts: string[] = [];\n\t\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\t\tif (part.type === \"thinking\") {\n\t\t\t\t\t\t\t\t\tthinkingParts.push(part.thinking);\n\t\t\t\t\t\t\t\t} else if (part.type === \"text\") {\n\t\t\t\t\t\t\t\t\ttextParts.push(part.text);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst text = textParts.join(\"\\n\");\n\n\t\t\t\t\t\t\t// Post thinking to main message and thread\n\t\t\t\t\t\t\tfor (const thinking of thinkingParts) {\n\t\t\t\t\t\t\t\tlog.logThinking(logCtx, thinking);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"main\", \"thinking main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"thread\", \"thinking thread\", false);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Post text to main message and thread\n\t\t\t\t\t\t\tif (text.trim()) {\n\t\t\t\t\t\t\t\tlog.logResponse(logCtx, text);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"main\", \"response main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"thread\", \"response thread\", false);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Run the agent with user's message\n\t\t\t// Prepend recent messages to the user prompt (not system prompt) for better caching\n\t\t\t// The current message is already the last entry in recentMessages\n\t\t\tconst userPrompt =\n\t\t\t\t`Conversation history (last 50 turns). Respond to the last message.\\n` +\n\t\t\t\t`Format: date TAB user TAB text TAB attachments\\n\\n` +\n\t\t\t\trecentMessages;\n\t\t\t// Debug: write full context to file\n\t\t\tconst toolDefs = tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));\n\t\t\tconst debugPrompt =\n\t\t\t\t`=== SYSTEM PROMPT (${systemPrompt.length} chars) ===\\n\\n${systemPrompt}\\n\\n` +\n\t\t\t\t`=== TOOL DEFINITIONS (${JSON.stringify(toolDefs).length} chars) ===\\n\\n${JSON.stringify(toolDefs, null, 2)}\\n\\n` +\n\t\t\t\t`=== USER PROMPT (${userPrompt.length} chars) ===\\n\\n${userPrompt}`;\n\t\t\tawait writeFile(join(channelDir, \"last_prompt.txt\"), debugPrompt, \"utf-8\");\n\n\t\t\tawait agent.prompt(userPrompt);\n\n\t\t\t// Wait for all queued respond calls to complete\n\t\t\tawait queue.flush();\n\n\t\t\t// Get final assistant message text from agent state and replace main message\n\t\t\tconst messages = agent.state.messages;\n\t\t\tconst lastAssistant = messages.filter((m) => m.role === \"assistant\").pop();\n\t\t\tconst finalText =\n\t\t\t\tlastAssistant?.content\n\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t.join(\"\\n\") || \"\";\n\t\t\tif (finalText.trim()) {\n\t\t\t\ttry {\n\t\t\t\t\t// For the main message, truncate if too long (full text is in thread)\n\t\t\t\t\tconst mainText =\n\t\t\t\t\t\tfinalText.length > SLACK_MAX_LENGTH\n\t\t\t\t\t\t\t? finalText.substring(0, SLACK_MAX_LENGTH - 50) + \"\\n\\n_(see thread for full response)_\"\n\t\t\t\t\t\t\t: finalText;\n\t\t\t\t\tawait ctx.replaceMessage(mainText);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tlog.logWarning(\"Failed to replace message with final text\", errMsg);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log usage summary if there was any usage\n\t\t\tif (totalUsage.cost.total > 0) {\n\t\t\t\tconst summary = log.logUsageSummary(logCtx, totalUsage);\n\t\t\t\tqueue.enqueue(() => ctx.respondInThread(summary), \"usage summary\");\n\t\t\t\tawait queue.flush();\n\t\t\t}\n\n\t\t\treturn { stopReason };\n\t\t},\n\n\t\tabort(): void {\n\t\t\tagent?.abort();\n\t\t},\n\t};\n}\n\n/**\n * Translate container path back to host path for file operations\n */\nfunction translateToHostPath(\n\tcontainerPath: string,\n\tchannelDir: string,\n\tworkspacePath: string,\n\tchannelId: string,\n): string {\n\tif (workspacePath === \"/workspace\") {\n\t\t// Docker mode - translate /workspace/channelId/... to host path\n\t\tconst prefix = `/workspace/${channelId}/`;\n\t\tif (containerPath.startsWith(prefix)) {\n\t\t\treturn join(channelDir, containerPath.slice(prefix.length));\n\t\t}\n\t\t// Maybe it's just /workspace/...\n\t\tif (containerPath.startsWith(\"/workspace/\")) {\n\t\t\treturn join(channelDir, \"..\", containerPath.slice(\"/workspace/\".length));\n\t\t}\n\t}\n\t// Host mode or already a host path\n\treturn containerPath;\n}\n"]}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAe,YAAY,EAAY,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA4B/C,MAAM,WAAW,WAAW;IAC3B,GAAG,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjG,KAAK,IAAI,IAAI,CAAC;CACd;AAqUD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,aAAa,GAAG,WAAW,CAgV3E","sourcesContent":["import { Agent, type AgentEvent, ProviderTransport } from \"@mariozechner/pi-agent-core\";\nimport { getModel } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { ChannelInfo, SlackContext, UserInfo } from \"./slack.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { createMomTools, setUploadFunction } from \"./tools/index.js\";\n\n// Hardcoded model for now\nconst model = getModel(\"anthropic\", \"claude-sonnet-4-5\");\n\n/**\n * Convert Date.now() to Slack timestamp format (seconds.microseconds)\n * Uses a monotonic counter to ensure ordering even within the same millisecond\n */\nlet lastTsMs = 0;\nlet tsCounter = 0;\n\nfunction toSlackTs(): string {\n\tconst now = Date.now();\n\tif (now === lastTsMs) {\n\t\t// Same millisecond - increment counter for sub-ms ordering\n\t\ttsCounter++;\n\t} else {\n\t\t// New millisecond - reset counter\n\t\tlastTsMs = now;\n\t\ttsCounter = 0;\n\t}\n\tconst seconds = Math.floor(now / 1000);\n\tconst micros = (now % 1000) * 1000 + tsCounter; // ms to micros + counter\n\treturn `${seconds}.${micros.toString().padStart(6, \"0\")}`;\n}\n\nexport interface AgentRunner {\n\trun(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }>;\n\tabort(): void;\n}\n\nfunction getAnthropicApiKey(): string {\n\tconst key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\tif (!key) {\n\t\tthrow new Error(\"ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set\");\n\t}\n\treturn key;\n}\n\ninterface LogMessage {\n\tdate?: string;\n\tts?: string;\n\tuser?: string;\n\tuserName?: string;\n\ttext?: string;\n\tattachments?: Array<{ local: string }>;\n\tisBot?: boolean;\n}\n\nfunction getRecentMessages(channelDir: string, turnCount: number): string {\n\tconst logPath = join(channelDir, \"log.jsonl\");\n\tif (!existsSync(logPath)) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\tconst content = readFileSync(logPath, \"utf-8\");\n\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\n\tif (lines.length === 0) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\t// Parse all messages and sort by Slack timestamp\n\t// (attachment downloads can cause out-of-order logging)\n\tconst messages: LogMessage[] = [];\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tmessages.push(JSON.parse(line));\n\t\t} catch {}\n\t}\n\tmessages.sort((a, b) => {\n\t\tconst tsA = parseFloat(a.ts || \"0\");\n\t\tconst tsB = parseFloat(b.ts || \"0\");\n\t\treturn tsA - tsB;\n\t});\n\n\t// Group into \"turns\" - a turn is either:\n\t// - A single user message (isBot: false)\n\t// - A sequence of consecutive bot messages (isBot: true) coalesced into one turn\n\t// We walk backwards to get the last N turns\n\tconst turns: LogMessage[][] = [];\n\tlet currentTurn: LogMessage[] = [];\n\tlet lastWasBot: boolean | null = null;\n\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst msg = messages[i];\n\t\tconst isBot = msg.isBot === true;\n\n\t\tif (lastWasBot === null) {\n\t\t\t// First message\n\t\t\tcurrentTurn.unshift(msg);\n\t\t\tlastWasBot = isBot;\n\t\t} else if (isBot && lastWasBot) {\n\t\t\t// Consecutive bot messages - same turn\n\t\t\tcurrentTurn.unshift(msg);\n\t\t} else {\n\t\t\t// Transition - save current turn and start new one\n\t\t\tturns.unshift(currentTurn);\n\t\t\tcurrentTurn = [msg];\n\t\t\tlastWasBot = isBot;\n\n\t\t\t// Stop if we have enough turns\n\t\t\tif (turns.length >= turnCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Don't forget the last turn we were building\n\tif (currentTurn.length > 0 && turns.length < turnCount) {\n\t\tturns.unshift(currentTurn);\n\t}\n\n\t// Flatten turns back to messages and format as TSV\n\tconst formatted: string[] = [];\n\tfor (const turn of turns) {\n\t\tfor (const msg of turn) {\n\t\t\tconst date = (msg.date || \"\").substring(0, 19);\n\t\t\tconst user = msg.userName || msg.user || \"\";\n\t\t\tconst text = msg.text || \"\";\n\t\t\tconst attachments = (msg.attachments || []).map((a) => a.local).join(\",\");\n\t\t\tformatted.push(`${date}\\t${user}\\t${text}\\t${attachments}`);\n\t\t}\n\t}\n\n\treturn formatted.join(\"\\n\");\n}\n\nfunction getMemory(channelDir: string): string {\n\tconst parts: string[] = [];\n\n\t// Read workspace-level memory (shared across all channels)\n\tconst workspaceMemoryPath = join(channelDir, \"..\", \"MEMORY.md\");\n\tif (existsSync(workspaceMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(workspaceMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Global Workspace Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read workspace memory\", `${workspaceMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\t// Read channel-specific memory\n\tconst channelMemoryPath = join(channelDir, \"MEMORY.md\");\n\tif (existsSync(channelMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(channelMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Channel-Specific Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read channel memory\", `${channelMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\tif (parts.length === 0) {\n\t\treturn \"(no working memory yet)\";\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\tmemory: string,\n\tsandboxConfig: SandboxConfig,\n\tchannels: ChannelInfo[],\n\tusers: UserInfo[],\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\t// Format channel mappings\n\tconst channelMappings =\n\t\tchannels.length > 0 ? channels.map((c) => `${c.id}\\t#${c.name}`).join(\"\\n\") : \"(no channels loaded)\";\n\n\t// Format user mappings\n\tconst userMappings =\n\t\tusers.length > 0 ? users.map((u) => `${u.id}\\t@${u.userName}\\t${u.displayName}`).join(\"\\n\") : \"(no users loaded)\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Bash working directory: / (use cd or absolute paths)\n- Install tools with: apk add <package>\n- Your changes persist across sessions`\n\t\t: `You are running directly on the host machine.\n- Bash working directory: ${process.cwd()}\n- Be careful with system modifications`;\n\n\tconst currentDate = new Date().toISOString().split(\"T\")[0]; // YYYY-MM-DD\n\tconst currentDateTime = new Date().toISOString(); // Full ISO 8601\n\n\treturn `You are mom, a Slack bot assistant. Be concise. No emojis.\n\n## Context\n- Date: ${currentDate} (${currentDateTime})\n- You receive the last 50 conversation turns. If you need older context, search log.jsonl.\n\n## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).\n\n## Slack IDs\nChannels: ${channelMappings}\n\nUsers: ${userMappings}\n\nWhen mentioning users, use <@username> format (e.g., <@mario>).\n\n## Environment\n${envDescription}\n\n## Workspace Layout\n${workspacePath}/\n├── MEMORY.md # Global memory (all channels)\n├── skills/ # Global CLI tools you create\n└── ${channelId}/ # This channel\n ├── MEMORY.md # Channel-specific memory\n ├── log.jsonl # Full message history\n ├── attachments/ # User-shared files\n ├── scratch/ # Your working directory\n └── skills/ # Channel-specific tools\n\n## Skills (Custom CLI Tools)\nYou can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).\nStore in \\`${workspacePath}/skills/<name>/\\` or \\`${channelPath}/skills/<name>/\\`.\nEach skill needs a \\`SKILL.md\\` documenting usage. Read it before using a skill.\nList skills in global memory so you remember them.\n\n## Memory\nWrite to MEMORY.md files to persist context across conversations.\n- Global (${workspacePath}/MEMORY.md): skills, preferences, project info\n- Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work\nUpdate when you learn something important or when asked to remember something.\n\n### Current Memory\n${memory}\n\n## System Configuration Log\nMaintain ${workspacePath}/SYSTEM.md to log all environment modifications:\n- Installed packages (apk add, npm install, pip install)\n- Environment variables set\n- Config files modified (~/.gitconfig, cron jobs, etc.)\n- Skill dependencies installed\n\nUpdate this file whenever you modify the environment. On fresh container, read it first to restore your setup.\n\n## Log Queries (CRITICAL: limit output to avoid context overflow)\nFormat: \\`{\"date\":\"...\",\"ts\":\"...\",\"user\":\"...\",\"userName\":\"...\",\"text\":\"...\",\"isBot\":false}\\`\nThe log contains user messages AND your tool calls/results. Filter appropriately.\n${isDocker ? \"Install jq: apk add jq\" : \"\"}\n\n**Conversation only (excludes tool calls/results) - use for summaries:**\n\\`\\`\\`bash\n# Recent conversation (no [Tool] or [Tool Result] lines)\ngrep -v '\"text\":\"\\\\[Tool' log.jsonl | tail -30 | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Yesterday's conversation\ngrep '\"date\":\"2025-11-26' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Specific user's messages\ngrep '\"userName\":\"mario\"' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | tail -20 | jq -c '{date: .date[0:19], text}'\n\\`\\`\\`\n\n**Full details (includes tool calls) - use when you need technical context:**\n\\`\\`\\`bash\n# Raw recent entries\ntail -20 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Count all messages\nwc -l log.jsonl\n\\`\\`\\`\n\n## Tools\n- bash: Run shell commands (primary tool). Install packages as needed.\n- read: Read files\n- write: Create/overwrite files \n- edit: Surgical file edits\n- attach: Share files to Slack\n\nEach tool requires a \"label\" parameter (shown to user).\n`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen - 3) + \"...\";\n}\n\nfunction extractToolResultText(result: unknown): string {\n\t// If it's already a string, return it\n\tif (typeof result === \"string\") {\n\t\treturn result;\n\t}\n\n\t// If it's an object with content array (tool result format)\n\tif (\n\t\tresult &&\n\t\ttypeof result === \"object\" &&\n\t\t\"content\" in result &&\n\t\tArray.isArray((result as { content: unknown }).content)\n\t) {\n\t\tconst content = (result as { content: Array<{ type: string; text?: string }> }).content;\n\t\tconst textParts: string[] = [];\n\t\tfor (const part of content) {\n\t\t\tif (part.type === \"text\" && part.text) {\n\t\t\t\ttextParts.push(part.text);\n\t\t\t}\n\t\t}\n\t\tif (textParts.length > 0) {\n\t\t\treturn textParts.join(\"\\n\");\n\t\t}\n\t}\n\n\t// Fallback to JSON\n\treturn JSON.stringify(result);\n}\n\nfunction formatToolArgsForSlack(_toolName: string, args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\tlines.push(value);\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {\n\tlet agent: Agent | null = null;\n\tconst executor = createExecutor(sandboxConfig);\n\n\treturn {\n\t\tasync run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }> {\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(channelDir, { recursive: true });\n\n\t\t\tconst channelId = ctx.message.channel;\n\t\t\tconst workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, \"\"));\n\t\t\tconst recentMessages = getRecentMessages(channelDir, 50);\n\n\t\t\tconst memory = getMemory(channelDir);\n\t\t\tconst systemPrompt = buildSystemPrompt(\n\t\t\t\tworkspacePath,\n\t\t\t\tchannelId,\n\t\t\t\tmemory,\n\t\t\t\tsandboxConfig,\n\t\t\t\tctx.channels,\n\t\t\t\tctx.users,\n\t\t\t);\n\n\t\t\t// Debug: log context sizes\n\t\t\tlog.logInfo(\n\t\t\t\t`Context sizes - system: ${systemPrompt.length} chars, messages: ${recentMessages.length} chars, memory: ${memory.length} chars`,\n\t\t\t);\n\t\t\tlog.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);\n\n\t\t\t// Set up file upload function for the attach tool\n\t\t\t// For Docker, we need to translate paths back to host\n\t\t\tsetUploadFunction(async (filePath: string, title?: string) => {\n\t\t\t\tconst hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);\n\t\t\t\tawait ctx.uploadFile(hostPath, title);\n\t\t\t});\n\n\t\t\t// Create tools with executor\n\t\t\tconst tools = createMomTools(executor);\n\n\t\t\t// Create ephemeral agent\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\ttransport: new ProviderTransport({\n\t\t\t\t\tgetApiKey: async () => getAnthropicApiKey(),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Create logging context\n\t\t\tconst logCtx = {\n\t\t\t\tchannelId: ctx.message.channel,\n\t\t\t\tuserName: ctx.message.userName,\n\t\t\t\tchannelName: ctx.channelName,\n\t\t\t};\n\n\t\t\t// Track pending tool calls to pair args with results and timing\n\t\t\tconst pendingTools = new Map<string, { toolName: string; args: unknown; startTime: number }>();\n\n\t\t\t// Track usage across all assistant messages in this run\n\t\t\tconst totalUsage = {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\tcost: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotal: 0,\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Track stop reason\n\t\t\tlet stopReason = \"stop\";\n\n\t\t\t// Slack message limit is 40,000 characters - split into multiple messages if needed\n\t\t\tconst SLACK_MAX_LENGTH = 40000;\n\t\t\tconst splitForSlack = (text: string): string[] => {\n\t\t\t\tif (text.length <= SLACK_MAX_LENGTH) return [text];\n\t\t\t\tconst parts: string[] = [];\n\t\t\t\tlet remaining = text;\n\t\t\t\tlet partNum = 1;\n\t\t\t\twhile (remaining.length > 0) {\n\t\t\t\t\tconst chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tremaining = remaining.substring(SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tconst suffix = remaining.length > 0 ? `\\n_(continued ${partNum}...)_` : \"\";\n\t\t\t\t\tparts.push(chunk + suffix);\n\t\t\t\t\tpartNum++;\n\t\t\t\t}\n\t\t\t\treturn parts;\n\t\t\t};\n\n\t\t\t// Promise queue to ensure ctx.respond/respondInThread calls execute in order\n\t\t\t// Handles errors gracefully by posting to thread instead of crashing\n\t\t\tconst queue = {\n\t\t\t\tchain: Promise.resolve(),\n\t\t\t\tenqueue(fn: () => Promise<void>, errorContext: string): void {\n\t\t\t\t\tthis.chain = this.chain.then(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait fn();\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\t\tlog.logWarning(`Slack API error (${errorContext})`, errMsg);\n\t\t\t\t\t\t\t// Try to post error to thread, but don't crash if that fails too\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait ctx.respondInThread(`_Error: ${errMsg}_`);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Ignore - we tried our best\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t// Enqueue a message that may need splitting\n\t\t\t\tenqueueMessage(text: string, target: \"main\" | \"thread\", errorContext: string, log = true): void {\n\t\t\t\t\tconst parts = splitForSlack(text);\n\t\t\t\t\tfor (const part of parts) {\n\t\t\t\t\t\tthis.enqueue(\n\t\t\t\t\t\t\t() => (target === \"main\" ? ctx.respond(part, log) : ctx.respondInThread(part)),\n\t\t\t\t\t\t\terrorContext,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tflush(): Promise<void> {\n\t\t\t\t\treturn this.chain;\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Subscribe to events\n\t\t\tagent.subscribe(async (event: AgentEvent) => {\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t\t\tconst args = event.args as { label?: string };\n\t\t\t\t\t\tconst label = args.label || event.toolName;\n\n\t\t\t\t\t\t// Store args to pair with result later\n\t\t\t\t\t\tpendingTools.set(event.toolCallId, {\n\t\t\t\t\t\t\ttoolName: event.toolName,\n\t\t\t\t\t\t\targs: event.args,\n\t\t\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tlog.logToolStart(logCtx, event.toolName, label, event.args as Record<string, unknown>);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Show label in main message only\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_→ ${label}_`, false), \"tool label\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t\t\tconst resultStr = extractToolResultText(event.result);\n\t\t\t\t\t\tconst pending = pendingTools.get(event.toolCallId);\n\t\t\t\t\t\tpendingTools.delete(event.toolCallId);\n\n\t\t\t\t\t\tconst durationMs = pending ? Date.now() - pending.startTime : 0;\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tlog.logToolError(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlog.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool Result] ${event.toolName}: ${event.isError ? \"ERROR: \" : \"\"}${resultStr}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Post args + result together in thread\n\t\t\t\t\t\tconst label = pending?.args ? (pending.args as { label?: string }).label : undefined;\n\t\t\t\t\t\tconst argsFormatted = pending\n\t\t\t\t\t\t\t? formatToolArgsForSlack(event.toolName, pending.args as Record<string, unknown>)\n\t\t\t\t\t\t\t: \"(args not found)\";\n\t\t\t\t\t\tconst duration = (durationMs / 1000).toFixed(1);\n\t\t\t\t\t\tlet threadMessage = `*${event.isError ? \"✗\" : \"✓\"} ${event.toolName}*`;\n\t\t\t\t\t\tif (label) {\n\t\t\t\t\t\t\tthreadMessage += `: ${label}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthreadMessage += ` (${duration}s)\\n`;\n\n\t\t\t\t\t\tif (argsFormatted) {\n\t\t\t\t\t\t\tthreadMessage += \"```\\n\" + argsFormatted + \"\\n```\\n\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthreadMessage += \"*Result:*\\n```\\n\" + resultStr + \"\\n```\";\n\n\t\t\t\t\t\tqueue.enqueueMessage(threadMessage, \"thread\", \"tool result thread\", false);\n\n\t\t\t\t\t\t// Show brief error in main message if failed\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), \"tool error\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_update\": {\n\t\t\t\t\t\t// No longer stream to console - just track that we're streaming\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_start\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tlog.logResponseStart(logCtx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"message_end\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tconst assistantMsg = event.message as any; // AssistantMessage type\n\n\t\t\t\t\t\t\t// Track stop reason\n\t\t\t\t\t\t\tif (assistantMsg.stopReason) {\n\t\t\t\t\t\t\t\tstopReason = assistantMsg.stopReason;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Accumulate usage\n\t\t\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\t\t\ttotalUsage.input += assistantMsg.usage.input;\n\t\t\t\t\t\t\t\ttotalUsage.output += assistantMsg.usage.output;\n\t\t\t\t\t\t\t\ttotalUsage.cacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.input += assistantMsg.usage.cost.input;\n\t\t\t\t\t\t\t\ttotalUsage.cost.output += assistantMsg.usage.cost.output;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.total += assistantMsg.usage.cost.total;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Extract thinking and text from assistant message\n\t\t\t\t\t\t\tconst content = event.message.content;\n\t\t\t\t\t\t\tconst thinkingParts: string[] = [];\n\t\t\t\t\t\t\tconst textParts: string[] = [];\n\t\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\t\tif (part.type === \"thinking\") {\n\t\t\t\t\t\t\t\t\tthinkingParts.push(part.thinking);\n\t\t\t\t\t\t\t\t} else if (part.type === \"text\") {\n\t\t\t\t\t\t\t\t\ttextParts.push(part.text);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst text = textParts.join(\"\\n\");\n\n\t\t\t\t\t\t\t// Post thinking to main message and thread\n\t\t\t\t\t\t\tfor (const thinking of thinkingParts) {\n\t\t\t\t\t\t\t\tlog.logThinking(logCtx, thinking);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"main\", \"thinking main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"thread\", \"thinking thread\", false);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Post text to main message and thread\n\t\t\t\t\t\t\tif (text.trim()) {\n\t\t\t\t\t\t\t\tlog.logResponse(logCtx, text);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"main\", \"response main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"thread\", \"response thread\", false);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Run the agent with user's message\n\t\t\t// Prepend recent messages to the user prompt (not system prompt) for better caching\n\t\t\t// The current message is already the last entry in recentMessages\n\t\t\tconst userPrompt =\n\t\t\t\t`Conversation history (last 50 turns). Respond to the last message.\\n` +\n\t\t\t\t`Format: date TAB user TAB text TAB attachments\\n\\n` +\n\t\t\t\trecentMessages;\n\t\t\t// Debug: write full context to file\n\t\t\tconst toolDefs = tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));\n\t\t\tconst debugPrompt =\n\t\t\t\t`=== SYSTEM PROMPT (${systemPrompt.length} chars) ===\\n\\n${systemPrompt}\\n\\n` +\n\t\t\t\t`=== TOOL DEFINITIONS (${JSON.stringify(toolDefs).length} chars) ===\\n\\n${JSON.stringify(toolDefs, null, 2)}\\n\\n` +\n\t\t\t\t`=== USER PROMPT (${userPrompt.length} chars) ===\\n\\n${userPrompt}`;\n\t\t\tawait writeFile(join(channelDir, \"last_prompt.txt\"), debugPrompt, \"utf-8\");\n\n\t\t\tawait agent.prompt(userPrompt);\n\n\t\t\t// Wait for all queued respond calls to complete\n\t\t\tawait queue.flush();\n\n\t\t\t// Get final assistant message text from agent state and replace main message\n\t\t\tconst messages = agent.state.messages;\n\t\t\tconst lastAssistant = messages.filter((m) => m.role === \"assistant\").pop();\n\t\t\tconst finalText =\n\t\t\t\tlastAssistant?.content\n\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t.join(\"\\n\") || \"\";\n\t\t\tif (finalText.trim()) {\n\t\t\t\ttry {\n\t\t\t\t\t// For the main message, truncate if too long (full text is in thread)\n\t\t\t\t\tconst mainText =\n\t\t\t\t\t\tfinalText.length > SLACK_MAX_LENGTH\n\t\t\t\t\t\t\t? finalText.substring(0, SLACK_MAX_LENGTH - 50) + \"\\n\\n_(see thread for full response)_\"\n\t\t\t\t\t\t\t: finalText;\n\t\t\t\t\tawait ctx.replaceMessage(mainText);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tlog.logWarning(\"Failed to replace message with final text\", errMsg);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log usage summary if there was any usage\n\t\t\tif (totalUsage.cost.total > 0) {\n\t\t\t\tconst summary = log.logUsageSummary(logCtx, totalUsage);\n\t\t\t\tqueue.enqueue(() => ctx.respondInThread(summary), \"usage summary\");\n\t\t\t\tawait queue.flush();\n\t\t\t}\n\n\t\t\treturn { stopReason };\n\t\t},\n\n\t\tabort(): void {\n\t\t\tagent?.abort();\n\t\t},\n\t};\n}\n\n/**\n * Translate container path back to host path for file operations\n */\nfunction translateToHostPath(\n\tcontainerPath: string,\n\tchannelDir: string,\n\tworkspacePath: string,\n\tchannelId: string,\n): string {\n\tif (workspacePath === \"/workspace\") {\n\t\t// Docker mode - translate /workspace/channelId/... to host path\n\t\tconst prefix = `/workspace/${channelId}/`;\n\t\tif (containerPath.startsWith(prefix)) {\n\t\t\treturn join(channelDir, containerPath.slice(prefix.length));\n\t\t}\n\t\t// Maybe it's just /workspace/...\n\t\tif (containerPath.startsWith(\"/workspace/\")) {\n\t\t\treturn join(channelDir, \"..\", containerPath.slice(\"/workspace/\".length));\n\t\t}\n\t}\n\t// Host mode or already a host path\n\treturn containerPath;\n}\n"]}
package/dist/agent.js CHANGED
@@ -100,50 +100,13 @@ function getRecentMessages(channelDir, turnCount) {
100
100
  for (const msg of turn) {
101
101
  const date = (msg.date || "").substring(0, 19);
102
102
  const user = msg.userName || msg.user || "";
103
- let text = msg.text || "";
104
- // Truncate bot messages (tool results can be huge)
105
- if (msg.isBot) {
106
- text = truncateForContext(text, 50000, 2000, msg.ts);
107
- }
103
+ const text = msg.text || "";
108
104
  const attachments = (msg.attachments || []).map((a) => a.local).join(",");
109
105
  formatted.push(`${date}\t${user}\t${text}\t${attachments}`);
110
106
  }
111
107
  }
112
108
  return formatted.join("\n");
113
109
  }
114
- /**
115
- * Truncate text to maxChars or maxLines, whichever comes first.
116
- * Adds a note with stats and instructions if truncation occurred.
117
- */
118
- function truncateForContext(text, maxChars, maxLines, ts) {
119
- const lines = text.split("\n");
120
- const originalLines = lines.length;
121
- const originalChars = text.length;
122
- let truncated = false;
123
- let result = text;
124
- // Check line limit first
125
- if (lines.length > maxLines) {
126
- result = lines.slice(0, maxLines).join("\n");
127
- truncated = true;
128
- }
129
- // Check char limit
130
- if (result.length > maxChars) {
131
- result = result.substring(0, maxChars);
132
- truncated = true;
133
- }
134
- if (truncated) {
135
- const remainingLines = originalLines - result.split("\n").length;
136
- const remainingChars = originalChars - result.length;
137
- result += `\n[... truncated ${remainingLines} more lines, ${remainingChars} more chars. `;
138
- if (ts) {
139
- result += `To get full content: jq -r 'select(.ts=="${ts}") | .text' log.jsonl > /tmp/msg.txt, then read /tmp/msg.txt in segments]`;
140
- }
141
- else {
142
- result += `Search log.jsonl for full content]`;
143
- }
144
- }
145
- return result;
146
- }
147
110
  function getMemory(channelDir) {
148
111
  const parts = [];
149
112
  // Read workspace-level memory (shared across all channels)
package/dist/agent.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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","sourcesContent":["import { Agent, type AgentEvent, ProviderTransport } from \"@mariozechner/pi-agent-core\";\nimport { getModel } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { ChannelInfo, SlackContext, UserInfo } from \"./slack.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { createMomTools, setUploadFunction } from \"./tools/index.js\";\n\n// Hardcoded model for now\nconst model = getModel(\"anthropic\", \"claude-sonnet-4-5\");\n\n/**\n * Convert Date.now() to Slack timestamp format (seconds.microseconds)\n * Uses a monotonic counter to ensure ordering even within the same millisecond\n */\nlet lastTsMs = 0;\nlet tsCounter = 0;\n\nfunction toSlackTs(): string {\n\tconst now = Date.now();\n\tif (now === lastTsMs) {\n\t\t// Same millisecond - increment counter for sub-ms ordering\n\t\ttsCounter++;\n\t} else {\n\t\t// New millisecond - reset counter\n\t\tlastTsMs = now;\n\t\ttsCounter = 0;\n\t}\n\tconst seconds = Math.floor(now / 1000);\n\tconst micros = (now % 1000) * 1000 + tsCounter; // ms to micros + counter\n\treturn `${seconds}.${micros.toString().padStart(6, \"0\")}`;\n}\n\nexport interface AgentRunner {\n\trun(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }>;\n\tabort(): void;\n}\n\nfunction getAnthropicApiKey(): string {\n\tconst key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\tif (!key) {\n\t\tthrow new Error(\"ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set\");\n\t}\n\treturn key;\n}\n\ninterface LogMessage {\n\tdate?: string;\n\tts?: string;\n\tuser?: string;\n\tuserName?: string;\n\ttext?: string;\n\tattachments?: Array<{ local: string }>;\n\tisBot?: boolean;\n}\n\nfunction getRecentMessages(channelDir: string, turnCount: number): string {\n\tconst logPath = join(channelDir, \"log.jsonl\");\n\tif (!existsSync(logPath)) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\tconst content = readFileSync(logPath, \"utf-8\");\n\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\n\tif (lines.length === 0) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\t// Parse all messages and sort by Slack timestamp\n\t// (attachment downloads can cause out-of-order logging)\n\tconst messages: LogMessage[] = [];\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tmessages.push(JSON.parse(line));\n\t\t} catch {}\n\t}\n\tmessages.sort((a, b) => {\n\t\tconst tsA = parseFloat(a.ts || \"0\");\n\t\tconst tsB = parseFloat(b.ts || \"0\");\n\t\treturn tsA - tsB;\n\t});\n\n\t// Group into \"turns\" - a turn is either:\n\t// - A single user message (isBot: false)\n\t// - A sequence of consecutive bot messages (isBot: true) coalesced into one turn\n\t// We walk backwards to get the last N turns\n\tconst turns: LogMessage[][] = [];\n\tlet currentTurn: LogMessage[] = [];\n\tlet lastWasBot: boolean | null = null;\n\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst msg = messages[i];\n\t\tconst isBot = msg.isBot === true;\n\n\t\tif (lastWasBot === null) {\n\t\t\t// First message\n\t\t\tcurrentTurn.unshift(msg);\n\t\t\tlastWasBot = isBot;\n\t\t} else if (isBot && lastWasBot) {\n\t\t\t// Consecutive bot messages - same turn\n\t\t\tcurrentTurn.unshift(msg);\n\t\t} else {\n\t\t\t// Transition - save current turn and start new one\n\t\t\tturns.unshift(currentTurn);\n\t\t\tcurrentTurn = [msg];\n\t\t\tlastWasBot = isBot;\n\n\t\t\t// Stop if we have enough turns\n\t\t\tif (turns.length >= turnCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Don't forget the last turn we were building\n\tif (currentTurn.length > 0 && turns.length < turnCount) {\n\t\tturns.unshift(currentTurn);\n\t}\n\n\t// Flatten turns back to messages and format as TSV\n\tconst formatted: string[] = [];\n\tfor (const turn of turns) {\n\t\tfor (const msg of turn) {\n\t\t\tconst date = (msg.date || \"\").substring(0, 19);\n\t\t\tconst user = msg.userName || msg.user || \"\";\n\t\t\tlet text = msg.text || \"\";\n\t\t\t// Truncate bot messages (tool results can be huge)\n\t\t\tif (msg.isBot) {\n\t\t\t\ttext = truncateForContext(text, 50000, 2000, msg.ts);\n\t\t\t}\n\t\t\tconst attachments = (msg.attachments || []).map((a) => a.local).join(\",\");\n\t\t\tformatted.push(`${date}\\t${user}\\t${text}\\t${attachments}`);\n\t\t}\n\t}\n\n\treturn formatted.join(\"\\n\");\n}\n\n/**\n * Truncate text to maxChars or maxLines, whichever comes first.\n * Adds a note with stats and instructions if truncation occurred.\n */\nfunction truncateForContext(text: string, maxChars: number, maxLines: number, ts?: string): string {\n\tconst lines = text.split(\"\\n\");\n\tconst originalLines = lines.length;\n\tconst originalChars = text.length;\n\tlet truncated = false;\n\tlet result = text;\n\n\t// Check line limit first\n\tif (lines.length > maxLines) {\n\t\tresult = lines.slice(0, maxLines).join(\"\\n\");\n\t\ttruncated = true;\n\t}\n\n\t// Check char limit\n\tif (result.length > maxChars) {\n\t\tresult = result.substring(0, maxChars);\n\t\ttruncated = true;\n\t}\n\n\tif (truncated) {\n\t\tconst remainingLines = originalLines - result.split(\"\\n\").length;\n\t\tconst remainingChars = originalChars - result.length;\n\t\tresult += `\\n[... truncated ${remainingLines} more lines, ${remainingChars} more chars. `;\n\t\tif (ts) {\n\t\t\tresult += `To get full content: jq -r 'select(.ts==\"${ts}\") | .text' log.jsonl > /tmp/msg.txt, then read /tmp/msg.txt in segments]`;\n\t\t} else {\n\t\t\tresult += `Search log.jsonl for full content]`;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nfunction getMemory(channelDir: string): string {\n\tconst parts: string[] = [];\n\n\t// Read workspace-level memory (shared across all channels)\n\tconst workspaceMemoryPath = join(channelDir, \"..\", \"MEMORY.md\");\n\tif (existsSync(workspaceMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(workspaceMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Global Workspace Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read workspace memory\", `${workspaceMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\t// Read channel-specific memory\n\tconst channelMemoryPath = join(channelDir, \"MEMORY.md\");\n\tif (existsSync(channelMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(channelMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Channel-Specific Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read channel memory\", `${channelMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\tif (parts.length === 0) {\n\t\treturn \"(no working memory yet)\";\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\tmemory: string,\n\tsandboxConfig: SandboxConfig,\n\tchannels: ChannelInfo[],\n\tusers: UserInfo[],\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\t// Format channel mappings\n\tconst channelMappings =\n\t\tchannels.length > 0 ? channels.map((c) => `${c.id}\\t#${c.name}`).join(\"\\n\") : \"(no channels loaded)\";\n\n\t// Format user mappings\n\tconst userMappings =\n\t\tusers.length > 0 ? users.map((u) => `${u.id}\\t@${u.userName}\\t${u.displayName}`).join(\"\\n\") : \"(no users loaded)\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Bash working directory: / (use cd or absolute paths)\n- Install tools with: apk add <package>\n- Your changes persist across sessions`\n\t\t: `You are running directly on the host machine.\n- Bash working directory: ${process.cwd()}\n- Be careful with system modifications`;\n\n\tconst currentDate = new Date().toISOString().split(\"T\")[0]; // YYYY-MM-DD\n\tconst currentDateTime = new Date().toISOString(); // Full ISO 8601\n\n\treturn `You are mom, a Slack bot assistant. Be concise. No emojis.\n\n## Context\n- Date: ${currentDate} (${currentDateTime})\n- You receive the last 50 conversation turns. If you need older context, search log.jsonl.\n\n## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).\n\n## Slack IDs\nChannels: ${channelMappings}\n\nUsers: ${userMappings}\n\nWhen mentioning users, use <@username> format (e.g., <@mario>).\n\n## Environment\n${envDescription}\n\n## Workspace Layout\n${workspacePath}/\n├── MEMORY.md # Global memory (all channels)\n├── skills/ # Global CLI tools you create\n└── ${channelId}/ # This channel\n ├── MEMORY.md # Channel-specific memory\n ├── log.jsonl # Full message history\n ├── attachments/ # User-shared files\n ├── scratch/ # Your working directory\n └── skills/ # Channel-specific tools\n\n## Skills (Custom CLI Tools)\nYou can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).\nStore in \\`${workspacePath}/skills/<name>/\\` or \\`${channelPath}/skills/<name>/\\`.\nEach skill needs a \\`SKILL.md\\` documenting usage. Read it before using a skill.\nList skills in global memory so you remember them.\n\n## Memory\nWrite to MEMORY.md files to persist context across conversations.\n- Global (${workspacePath}/MEMORY.md): skills, preferences, project info\n- Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work\nUpdate when you learn something important or when asked to remember something.\n\n### Current Memory\n${memory}\n\n## System Configuration Log\nMaintain ${workspacePath}/SYSTEM.md to log all environment modifications:\n- Installed packages (apk add, npm install, pip install)\n- Environment variables set\n- Config files modified (~/.gitconfig, cron jobs, etc.)\n- Skill dependencies installed\n\nUpdate this file whenever you modify the environment. On fresh container, read it first to restore your setup.\n\n## Log Queries (CRITICAL: limit output to avoid context overflow)\nFormat: \\`{\"date\":\"...\",\"ts\":\"...\",\"user\":\"...\",\"userName\":\"...\",\"text\":\"...\",\"isBot\":false}\\`\nThe log contains user messages AND your tool calls/results. Filter appropriately.\n${isDocker ? \"Install jq: apk add jq\" : \"\"}\n\n**Conversation only (excludes tool calls/results) - use for summaries:**\n\\`\\`\\`bash\n# Recent conversation (no [Tool] or [Tool Result] lines)\ngrep -v '\"text\":\"\\\\[Tool' log.jsonl | tail -30 | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Yesterday's conversation\ngrep '\"date\":\"2025-11-26' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Specific user's messages\ngrep '\"userName\":\"mario\"' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | tail -20 | jq -c '{date: .date[0:19], text}'\n\\`\\`\\`\n\n**Full details (includes tool calls) - use when you need technical context:**\n\\`\\`\\`bash\n# Raw recent entries\ntail -20 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Count all messages\nwc -l log.jsonl\n\\`\\`\\`\n\n## Tools\n- bash: Run shell commands (primary tool). Install packages as needed.\n- read: Read files\n- write: Create/overwrite files \n- edit: Surgical file edits\n- attach: Share files to Slack\n\nEach tool requires a \"label\" parameter (shown to user).\n`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen - 3) + \"...\";\n}\n\nfunction extractToolResultText(result: unknown): string {\n\t// If it's already a string, return it\n\tif (typeof result === \"string\") {\n\t\treturn result;\n\t}\n\n\t// If it's an object with content array (tool result format)\n\tif (\n\t\tresult &&\n\t\ttypeof result === \"object\" &&\n\t\t\"content\" in result &&\n\t\tArray.isArray((result as { content: unknown }).content)\n\t) {\n\t\tconst content = (result as { content: Array<{ type: string; text?: string }> }).content;\n\t\tconst textParts: string[] = [];\n\t\tfor (const part of content) {\n\t\t\tif (part.type === \"text\" && part.text) {\n\t\t\t\ttextParts.push(part.text);\n\t\t\t}\n\t\t}\n\t\tif (textParts.length > 0) {\n\t\t\treturn textParts.join(\"\\n\");\n\t\t}\n\t}\n\n\t// Fallback to JSON\n\treturn JSON.stringify(result);\n}\n\nfunction formatToolArgsForSlack(_toolName: string, args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\tlines.push(value);\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {\n\tlet agent: Agent | null = null;\n\tconst executor = createExecutor(sandboxConfig);\n\n\treturn {\n\t\tasync run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }> {\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(channelDir, { recursive: true });\n\n\t\t\tconst channelId = ctx.message.channel;\n\t\t\tconst workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, \"\"));\n\t\t\tconst recentMessages = getRecentMessages(channelDir, 50);\n\n\t\t\tconst memory = getMemory(channelDir);\n\t\t\tconst systemPrompt = buildSystemPrompt(\n\t\t\t\tworkspacePath,\n\t\t\t\tchannelId,\n\t\t\t\tmemory,\n\t\t\t\tsandboxConfig,\n\t\t\t\tctx.channels,\n\t\t\t\tctx.users,\n\t\t\t);\n\n\t\t\t// Debug: log context sizes\n\t\t\tlog.logInfo(\n\t\t\t\t`Context sizes - system: ${systemPrompt.length} chars, messages: ${recentMessages.length} chars, memory: ${memory.length} chars`,\n\t\t\t);\n\t\t\tlog.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);\n\n\t\t\t// Set up file upload function for the attach tool\n\t\t\t// For Docker, we need to translate paths back to host\n\t\t\tsetUploadFunction(async (filePath: string, title?: string) => {\n\t\t\t\tconst hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);\n\t\t\t\tawait ctx.uploadFile(hostPath, title);\n\t\t\t});\n\n\t\t\t// Create tools with executor\n\t\t\tconst tools = createMomTools(executor);\n\n\t\t\t// Create ephemeral agent\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\ttransport: new ProviderTransport({\n\t\t\t\t\tgetApiKey: async () => getAnthropicApiKey(),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Create logging context\n\t\t\tconst logCtx = {\n\t\t\t\tchannelId: ctx.message.channel,\n\t\t\t\tuserName: ctx.message.userName,\n\t\t\t\tchannelName: ctx.channelName,\n\t\t\t};\n\n\t\t\t// Track pending tool calls to pair args with results and timing\n\t\t\tconst pendingTools = new Map<string, { toolName: string; args: unknown; startTime: number }>();\n\n\t\t\t// Track usage across all assistant messages in this run\n\t\t\tconst totalUsage = {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\tcost: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotal: 0,\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Track stop reason\n\t\t\tlet stopReason = \"stop\";\n\n\t\t\t// Slack message limit is 40,000 characters - split into multiple messages if needed\n\t\t\tconst SLACK_MAX_LENGTH = 40000;\n\t\t\tconst splitForSlack = (text: string): string[] => {\n\t\t\t\tif (text.length <= SLACK_MAX_LENGTH) return [text];\n\t\t\t\tconst parts: string[] = [];\n\t\t\t\tlet remaining = text;\n\t\t\t\tlet partNum = 1;\n\t\t\t\twhile (remaining.length > 0) {\n\t\t\t\t\tconst chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tremaining = remaining.substring(SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tconst suffix = remaining.length > 0 ? `\\n_(continued ${partNum}...)_` : \"\";\n\t\t\t\t\tparts.push(chunk + suffix);\n\t\t\t\t\tpartNum++;\n\t\t\t\t}\n\t\t\t\treturn parts;\n\t\t\t};\n\n\t\t\t// Promise queue to ensure ctx.respond/respondInThread calls execute in order\n\t\t\t// Handles errors gracefully by posting to thread instead of crashing\n\t\t\tconst queue = {\n\t\t\t\tchain: Promise.resolve(),\n\t\t\t\tenqueue(fn: () => Promise<void>, errorContext: string): void {\n\t\t\t\t\tthis.chain = this.chain.then(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait fn();\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\t\tlog.logWarning(`Slack API error (${errorContext})`, errMsg);\n\t\t\t\t\t\t\t// Try to post error to thread, but don't crash if that fails too\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait ctx.respondInThread(`_Error: ${errMsg}_`);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Ignore - we tried our best\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t// Enqueue a message that may need splitting\n\t\t\t\tenqueueMessage(text: string, target: \"main\" | \"thread\", errorContext: string, log = true): void {\n\t\t\t\t\tconst parts = splitForSlack(text);\n\t\t\t\t\tfor (const part of parts) {\n\t\t\t\t\t\tthis.enqueue(\n\t\t\t\t\t\t\t() => (target === \"main\" ? ctx.respond(part, log) : ctx.respondInThread(part)),\n\t\t\t\t\t\t\terrorContext,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tflush(): Promise<void> {\n\t\t\t\t\treturn this.chain;\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Subscribe to events\n\t\t\tagent.subscribe(async (event: AgentEvent) => {\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t\t\tconst args = event.args as { label?: string };\n\t\t\t\t\t\tconst label = args.label || event.toolName;\n\n\t\t\t\t\t\t// Store args to pair with result later\n\t\t\t\t\t\tpendingTools.set(event.toolCallId, {\n\t\t\t\t\t\t\ttoolName: event.toolName,\n\t\t\t\t\t\t\targs: event.args,\n\t\t\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tlog.logToolStart(logCtx, event.toolName, label, event.args as Record<string, unknown>);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Show label in main message only\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_→ ${label}_`, false), \"tool label\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t\t\tconst resultStr = extractToolResultText(event.result);\n\t\t\t\t\t\tconst pending = pendingTools.get(event.toolCallId);\n\t\t\t\t\t\tpendingTools.delete(event.toolCallId);\n\n\t\t\t\t\t\tconst durationMs = pending ? Date.now() - pending.startTime : 0;\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tlog.logToolError(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlog.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool Result] ${event.toolName}: ${event.isError ? \"ERROR: \" : \"\"}${resultStr}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Post args + result together in thread\n\t\t\t\t\t\tconst label = pending?.args ? (pending.args as { label?: string }).label : undefined;\n\t\t\t\t\t\tconst argsFormatted = pending\n\t\t\t\t\t\t\t? formatToolArgsForSlack(event.toolName, pending.args as Record<string, unknown>)\n\t\t\t\t\t\t\t: \"(args not found)\";\n\t\t\t\t\t\tconst duration = (durationMs / 1000).toFixed(1);\n\t\t\t\t\t\tlet threadMessage = `*${event.isError ? \"✗\" : \"✓\"} ${event.toolName}*`;\n\t\t\t\t\t\tif (label) {\n\t\t\t\t\t\t\tthreadMessage += `: ${label}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthreadMessage += ` (${duration}s)\\n`;\n\n\t\t\t\t\t\tif (argsFormatted) {\n\t\t\t\t\t\t\tthreadMessage += \"```\\n\" + argsFormatted + \"\\n```\\n\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthreadMessage += \"*Result:*\\n```\\n\" + resultStr + \"\\n```\";\n\n\t\t\t\t\t\tqueue.enqueueMessage(threadMessage, \"thread\", \"tool result thread\", false);\n\n\t\t\t\t\t\t// Show brief error in main message if failed\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), \"tool error\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_update\": {\n\t\t\t\t\t\t// No longer stream to console - just track that we're streaming\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_start\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tlog.logResponseStart(logCtx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"message_end\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tconst assistantMsg = event.message as any; // AssistantMessage type\n\n\t\t\t\t\t\t\t// Track stop reason\n\t\t\t\t\t\t\tif (assistantMsg.stopReason) {\n\t\t\t\t\t\t\t\tstopReason = assistantMsg.stopReason;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Accumulate usage\n\t\t\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\t\t\ttotalUsage.input += assistantMsg.usage.input;\n\t\t\t\t\t\t\t\ttotalUsage.output += assistantMsg.usage.output;\n\t\t\t\t\t\t\t\ttotalUsage.cacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.input += assistantMsg.usage.cost.input;\n\t\t\t\t\t\t\t\ttotalUsage.cost.output += assistantMsg.usage.cost.output;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.total += assistantMsg.usage.cost.total;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Extract thinking and text from assistant message\n\t\t\t\t\t\t\tconst content = event.message.content;\n\t\t\t\t\t\t\tconst thinkingParts: string[] = [];\n\t\t\t\t\t\t\tconst textParts: string[] = [];\n\t\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\t\tif (part.type === \"thinking\") {\n\t\t\t\t\t\t\t\t\tthinkingParts.push(part.thinking);\n\t\t\t\t\t\t\t\t} else if (part.type === \"text\") {\n\t\t\t\t\t\t\t\t\ttextParts.push(part.text);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst text = textParts.join(\"\\n\");\n\n\t\t\t\t\t\t\t// Post thinking to main message and thread\n\t\t\t\t\t\t\tfor (const thinking of thinkingParts) {\n\t\t\t\t\t\t\t\tlog.logThinking(logCtx, thinking);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"main\", \"thinking main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"thread\", \"thinking thread\", false);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Post text to main message and thread\n\t\t\t\t\t\t\tif (text.trim()) {\n\t\t\t\t\t\t\t\tlog.logResponse(logCtx, text);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"main\", \"response main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"thread\", \"response thread\", false);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Run the agent with user's message\n\t\t\t// Prepend recent messages to the user prompt (not system prompt) for better caching\n\t\t\t// The current message is already the last entry in recentMessages\n\t\t\tconst userPrompt =\n\t\t\t\t`Conversation history (last 50 turns). Respond to the last message.\\n` +\n\t\t\t\t`Format: date TAB user TAB text TAB attachments\\n\\n` +\n\t\t\t\trecentMessages;\n\t\t\t// Debug: write full context to file\n\t\t\tconst toolDefs = tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));\n\t\t\tconst debugPrompt =\n\t\t\t\t`=== SYSTEM PROMPT (${systemPrompt.length} chars) ===\\n\\n${systemPrompt}\\n\\n` +\n\t\t\t\t`=== TOOL DEFINITIONS (${JSON.stringify(toolDefs).length} chars) ===\\n\\n${JSON.stringify(toolDefs, null, 2)}\\n\\n` +\n\t\t\t\t`=== USER PROMPT (${userPrompt.length} chars) ===\\n\\n${userPrompt}`;\n\t\t\tawait writeFile(join(channelDir, \"last_prompt.txt\"), debugPrompt, \"utf-8\");\n\n\t\t\tawait agent.prompt(userPrompt);\n\n\t\t\t// Wait for all queued respond calls to complete\n\t\t\tawait queue.flush();\n\n\t\t\t// Get final assistant message text from agent state and replace main message\n\t\t\tconst messages = agent.state.messages;\n\t\t\tconst lastAssistant = messages.filter((m) => m.role === \"assistant\").pop();\n\t\t\tconst finalText =\n\t\t\t\tlastAssistant?.content\n\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t.join(\"\\n\") || \"\";\n\t\t\tif (finalText.trim()) {\n\t\t\t\ttry {\n\t\t\t\t\t// For the main message, truncate if too long (full text is in thread)\n\t\t\t\t\tconst mainText =\n\t\t\t\t\t\tfinalText.length > SLACK_MAX_LENGTH\n\t\t\t\t\t\t\t? finalText.substring(0, SLACK_MAX_LENGTH - 50) + \"\\n\\n_(see thread for full response)_\"\n\t\t\t\t\t\t\t: finalText;\n\t\t\t\t\tawait ctx.replaceMessage(mainText);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tlog.logWarning(\"Failed to replace message with final text\", errMsg);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log usage summary if there was any usage\n\t\t\tif (totalUsage.cost.total > 0) {\n\t\t\t\tconst summary = log.logUsageSummary(logCtx, totalUsage);\n\t\t\t\tqueue.enqueue(() => ctx.respondInThread(summary), \"usage summary\");\n\t\t\t\tawait queue.flush();\n\t\t\t}\n\n\t\t\treturn { stopReason };\n\t\t},\n\n\t\tabort(): void {\n\t\t\tagent?.abort();\n\t\t},\n\t};\n}\n\n/**\n * Translate container path back to host path for file operations\n */\nfunction translateToHostPath(\n\tcontainerPath: string,\n\tchannelDir: string,\n\tworkspacePath: string,\n\tchannelId: string,\n): string {\n\tif (workspacePath === \"/workspace\") {\n\t\t// Docker mode - translate /workspace/channelId/... to host path\n\t\tconst prefix = `/workspace/${channelId}/`;\n\t\tif (containerPath.startsWith(prefix)) {\n\t\t\treturn join(channelDir, containerPath.slice(prefix.length));\n\t\t}\n\t\t// Maybe it's just /workspace/...\n\t\tif (containerPath.startsWith(\"/workspace/\")) {\n\t\t\treturn join(channelDir, \"..\", containerPath.slice(\"/workspace/\".length));\n\t\t}\n\t}\n\t// Host mode or already a host path\n\treturn containerPath;\n}\n"]}
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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","sourcesContent":["import { Agent, type AgentEvent, ProviderTransport } from \"@mariozechner/pi-agent-core\";\nimport { getModel } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { ChannelInfo, SlackContext, UserInfo } from \"./slack.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { createMomTools, setUploadFunction } from \"./tools/index.js\";\n\n// Hardcoded model for now\nconst model = getModel(\"anthropic\", \"claude-sonnet-4-5\");\n\n/**\n * Convert Date.now() to Slack timestamp format (seconds.microseconds)\n * Uses a monotonic counter to ensure ordering even within the same millisecond\n */\nlet lastTsMs = 0;\nlet tsCounter = 0;\n\nfunction toSlackTs(): string {\n\tconst now = Date.now();\n\tif (now === lastTsMs) {\n\t\t// Same millisecond - increment counter for sub-ms ordering\n\t\ttsCounter++;\n\t} else {\n\t\t// New millisecond - reset counter\n\t\tlastTsMs = now;\n\t\ttsCounter = 0;\n\t}\n\tconst seconds = Math.floor(now / 1000);\n\tconst micros = (now % 1000) * 1000 + tsCounter; // ms to micros + counter\n\treturn `${seconds}.${micros.toString().padStart(6, \"0\")}`;\n}\n\nexport interface AgentRunner {\n\trun(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }>;\n\tabort(): void;\n}\n\nfunction getAnthropicApiKey(): string {\n\tconst key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\tif (!key) {\n\t\tthrow new Error(\"ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set\");\n\t}\n\treturn key;\n}\n\ninterface LogMessage {\n\tdate?: string;\n\tts?: string;\n\tuser?: string;\n\tuserName?: string;\n\ttext?: string;\n\tattachments?: Array<{ local: string }>;\n\tisBot?: boolean;\n}\n\nfunction getRecentMessages(channelDir: string, turnCount: number): string {\n\tconst logPath = join(channelDir, \"log.jsonl\");\n\tif (!existsSync(logPath)) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\tconst content = readFileSync(logPath, \"utf-8\");\n\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\n\tif (lines.length === 0) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\t// Parse all messages and sort by Slack timestamp\n\t// (attachment downloads can cause out-of-order logging)\n\tconst messages: LogMessage[] = [];\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tmessages.push(JSON.parse(line));\n\t\t} catch {}\n\t}\n\tmessages.sort((a, b) => {\n\t\tconst tsA = parseFloat(a.ts || \"0\");\n\t\tconst tsB = parseFloat(b.ts || \"0\");\n\t\treturn tsA - tsB;\n\t});\n\n\t// Group into \"turns\" - a turn is either:\n\t// - A single user message (isBot: false)\n\t// - A sequence of consecutive bot messages (isBot: true) coalesced into one turn\n\t// We walk backwards to get the last N turns\n\tconst turns: LogMessage[][] = [];\n\tlet currentTurn: LogMessage[] = [];\n\tlet lastWasBot: boolean | null = null;\n\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst msg = messages[i];\n\t\tconst isBot = msg.isBot === true;\n\n\t\tif (lastWasBot === null) {\n\t\t\t// First message\n\t\t\tcurrentTurn.unshift(msg);\n\t\t\tlastWasBot = isBot;\n\t\t} else if (isBot && lastWasBot) {\n\t\t\t// Consecutive bot messages - same turn\n\t\t\tcurrentTurn.unshift(msg);\n\t\t} else {\n\t\t\t// Transition - save current turn and start new one\n\t\t\tturns.unshift(currentTurn);\n\t\t\tcurrentTurn = [msg];\n\t\t\tlastWasBot = isBot;\n\n\t\t\t// Stop if we have enough turns\n\t\t\tif (turns.length >= turnCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Don't forget the last turn we were building\n\tif (currentTurn.length > 0 && turns.length < turnCount) {\n\t\tturns.unshift(currentTurn);\n\t}\n\n\t// Flatten turns back to messages and format as TSV\n\tconst formatted: string[] = [];\n\tfor (const turn of turns) {\n\t\tfor (const msg of turn) {\n\t\t\tconst date = (msg.date || \"\").substring(0, 19);\n\t\t\tconst user = msg.userName || msg.user || \"\";\n\t\t\tconst text = msg.text || \"\";\n\t\t\tconst attachments = (msg.attachments || []).map((a) => a.local).join(\",\");\n\t\t\tformatted.push(`${date}\\t${user}\\t${text}\\t${attachments}`);\n\t\t}\n\t}\n\n\treturn formatted.join(\"\\n\");\n}\n\nfunction getMemory(channelDir: string): string {\n\tconst parts: string[] = [];\n\n\t// Read workspace-level memory (shared across all channels)\n\tconst workspaceMemoryPath = join(channelDir, \"..\", \"MEMORY.md\");\n\tif (existsSync(workspaceMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(workspaceMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Global Workspace Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read workspace memory\", `${workspaceMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\t// Read channel-specific memory\n\tconst channelMemoryPath = join(channelDir, \"MEMORY.md\");\n\tif (existsSync(channelMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(channelMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Channel-Specific Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read channel memory\", `${channelMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\tif (parts.length === 0) {\n\t\treturn \"(no working memory yet)\";\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\tmemory: string,\n\tsandboxConfig: SandboxConfig,\n\tchannels: ChannelInfo[],\n\tusers: UserInfo[],\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\t// Format channel mappings\n\tconst channelMappings =\n\t\tchannels.length > 0 ? channels.map((c) => `${c.id}\\t#${c.name}`).join(\"\\n\") : \"(no channels loaded)\";\n\n\t// Format user mappings\n\tconst userMappings =\n\t\tusers.length > 0 ? users.map((u) => `${u.id}\\t@${u.userName}\\t${u.displayName}`).join(\"\\n\") : \"(no users loaded)\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Bash working directory: / (use cd or absolute paths)\n- Install tools with: apk add <package>\n- Your changes persist across sessions`\n\t\t: `You are running directly on the host machine.\n- Bash working directory: ${process.cwd()}\n- Be careful with system modifications`;\n\n\tconst currentDate = new Date().toISOString().split(\"T\")[0]; // YYYY-MM-DD\n\tconst currentDateTime = new Date().toISOString(); // Full ISO 8601\n\n\treturn `You are mom, a Slack bot assistant. Be concise. No emojis.\n\n## Context\n- Date: ${currentDate} (${currentDateTime})\n- You receive the last 50 conversation turns. If you need older context, search log.jsonl.\n\n## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).\n\n## Slack IDs\nChannels: ${channelMappings}\n\nUsers: ${userMappings}\n\nWhen mentioning users, use <@username> format (e.g., <@mario>).\n\n## Environment\n${envDescription}\n\n## Workspace Layout\n${workspacePath}/\n├── MEMORY.md # Global memory (all channels)\n├── skills/ # Global CLI tools you create\n└── ${channelId}/ # This channel\n ├── MEMORY.md # Channel-specific memory\n ├── log.jsonl # Full message history\n ├── attachments/ # User-shared files\n ├── scratch/ # Your working directory\n └── skills/ # Channel-specific tools\n\n## Skills (Custom CLI Tools)\nYou can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).\nStore in \\`${workspacePath}/skills/<name>/\\` or \\`${channelPath}/skills/<name>/\\`.\nEach skill needs a \\`SKILL.md\\` documenting usage. Read it before using a skill.\nList skills in global memory so you remember them.\n\n## Memory\nWrite to MEMORY.md files to persist context across conversations.\n- Global (${workspacePath}/MEMORY.md): skills, preferences, project info\n- Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work\nUpdate when you learn something important or when asked to remember something.\n\n### Current Memory\n${memory}\n\n## System Configuration Log\nMaintain ${workspacePath}/SYSTEM.md to log all environment modifications:\n- Installed packages (apk add, npm install, pip install)\n- Environment variables set\n- Config files modified (~/.gitconfig, cron jobs, etc.)\n- Skill dependencies installed\n\nUpdate this file whenever you modify the environment. On fresh container, read it first to restore your setup.\n\n## Log Queries (CRITICAL: limit output to avoid context overflow)\nFormat: \\`{\"date\":\"...\",\"ts\":\"...\",\"user\":\"...\",\"userName\":\"...\",\"text\":\"...\",\"isBot\":false}\\`\nThe log contains user messages AND your tool calls/results. Filter appropriately.\n${isDocker ? \"Install jq: apk add jq\" : \"\"}\n\n**Conversation only (excludes tool calls/results) - use for summaries:**\n\\`\\`\\`bash\n# Recent conversation (no [Tool] or [Tool Result] lines)\ngrep -v '\"text\":\"\\\\[Tool' log.jsonl | tail -30 | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Yesterday's conversation\ngrep '\"date\":\"2025-11-26' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Specific user's messages\ngrep '\"userName\":\"mario\"' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | tail -20 | jq -c '{date: .date[0:19], text}'\n\\`\\`\\`\n\n**Full details (includes tool calls) - use when you need technical context:**\n\\`\\`\\`bash\n# Raw recent entries\ntail -20 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Count all messages\nwc -l log.jsonl\n\\`\\`\\`\n\n## Tools\n- bash: Run shell commands (primary tool). Install packages as needed.\n- read: Read files\n- write: Create/overwrite files \n- edit: Surgical file edits\n- attach: Share files to Slack\n\nEach tool requires a \"label\" parameter (shown to user).\n`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen - 3) + \"...\";\n}\n\nfunction extractToolResultText(result: unknown): string {\n\t// If it's already a string, return it\n\tif (typeof result === \"string\") {\n\t\treturn result;\n\t}\n\n\t// If it's an object with content array (tool result format)\n\tif (\n\t\tresult &&\n\t\ttypeof result === \"object\" &&\n\t\t\"content\" in result &&\n\t\tArray.isArray((result as { content: unknown }).content)\n\t) {\n\t\tconst content = (result as { content: Array<{ type: string; text?: string }> }).content;\n\t\tconst textParts: string[] = [];\n\t\tfor (const part of content) {\n\t\t\tif (part.type === \"text\" && part.text) {\n\t\t\t\ttextParts.push(part.text);\n\t\t\t}\n\t\t}\n\t\tif (textParts.length > 0) {\n\t\t\treturn textParts.join(\"\\n\");\n\t\t}\n\t}\n\n\t// Fallback to JSON\n\treturn JSON.stringify(result);\n}\n\nfunction formatToolArgsForSlack(_toolName: string, args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\tlines.push(value);\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {\n\tlet agent: Agent | null = null;\n\tconst executor = createExecutor(sandboxConfig);\n\n\treturn {\n\t\tasync run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }> {\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(channelDir, { recursive: true });\n\n\t\t\tconst channelId = ctx.message.channel;\n\t\t\tconst workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, \"\"));\n\t\t\tconst recentMessages = getRecentMessages(channelDir, 50);\n\n\t\t\tconst memory = getMemory(channelDir);\n\t\t\tconst systemPrompt = buildSystemPrompt(\n\t\t\t\tworkspacePath,\n\t\t\t\tchannelId,\n\t\t\t\tmemory,\n\t\t\t\tsandboxConfig,\n\t\t\t\tctx.channels,\n\t\t\t\tctx.users,\n\t\t\t);\n\n\t\t\t// Debug: log context sizes\n\t\t\tlog.logInfo(\n\t\t\t\t`Context sizes - system: ${systemPrompt.length} chars, messages: ${recentMessages.length} chars, memory: ${memory.length} chars`,\n\t\t\t);\n\t\t\tlog.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);\n\n\t\t\t// Set up file upload function for the attach tool\n\t\t\t// For Docker, we need to translate paths back to host\n\t\t\tsetUploadFunction(async (filePath: string, title?: string) => {\n\t\t\t\tconst hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);\n\t\t\t\tawait ctx.uploadFile(hostPath, title);\n\t\t\t});\n\n\t\t\t// Create tools with executor\n\t\t\tconst tools = createMomTools(executor);\n\n\t\t\t// Create ephemeral agent\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\ttransport: new ProviderTransport({\n\t\t\t\t\tgetApiKey: async () => getAnthropicApiKey(),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Create logging context\n\t\t\tconst logCtx = {\n\t\t\t\tchannelId: ctx.message.channel,\n\t\t\t\tuserName: ctx.message.userName,\n\t\t\t\tchannelName: ctx.channelName,\n\t\t\t};\n\n\t\t\t// Track pending tool calls to pair args with results and timing\n\t\t\tconst pendingTools = new Map<string, { toolName: string; args: unknown; startTime: number }>();\n\n\t\t\t// Track usage across all assistant messages in this run\n\t\t\tconst totalUsage = {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\tcost: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotal: 0,\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Track stop reason\n\t\t\tlet stopReason = \"stop\";\n\n\t\t\t// Slack message limit is 40,000 characters - split into multiple messages if needed\n\t\t\tconst SLACK_MAX_LENGTH = 40000;\n\t\t\tconst splitForSlack = (text: string): string[] => {\n\t\t\t\tif (text.length <= SLACK_MAX_LENGTH) return [text];\n\t\t\t\tconst parts: string[] = [];\n\t\t\t\tlet remaining = text;\n\t\t\t\tlet partNum = 1;\n\t\t\t\twhile (remaining.length > 0) {\n\t\t\t\t\tconst chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tremaining = remaining.substring(SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tconst suffix = remaining.length > 0 ? `\\n_(continued ${partNum}...)_` : \"\";\n\t\t\t\t\tparts.push(chunk + suffix);\n\t\t\t\t\tpartNum++;\n\t\t\t\t}\n\t\t\t\treturn parts;\n\t\t\t};\n\n\t\t\t// Promise queue to ensure ctx.respond/respondInThread calls execute in order\n\t\t\t// Handles errors gracefully by posting to thread instead of crashing\n\t\t\tconst queue = {\n\t\t\t\tchain: Promise.resolve(),\n\t\t\t\tenqueue(fn: () => Promise<void>, errorContext: string): void {\n\t\t\t\t\tthis.chain = this.chain.then(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait fn();\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\t\tlog.logWarning(`Slack API error (${errorContext})`, errMsg);\n\t\t\t\t\t\t\t// Try to post error to thread, but don't crash if that fails too\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait ctx.respondInThread(`_Error: ${errMsg}_`);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Ignore - we tried our best\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t// Enqueue a message that may need splitting\n\t\t\t\tenqueueMessage(text: string, target: \"main\" | \"thread\", errorContext: string, log = true): void {\n\t\t\t\t\tconst parts = splitForSlack(text);\n\t\t\t\t\tfor (const part of parts) {\n\t\t\t\t\t\tthis.enqueue(\n\t\t\t\t\t\t\t() => (target === \"main\" ? ctx.respond(part, log) : ctx.respondInThread(part)),\n\t\t\t\t\t\t\terrorContext,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tflush(): Promise<void> {\n\t\t\t\t\treturn this.chain;\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Subscribe to events\n\t\t\tagent.subscribe(async (event: AgentEvent) => {\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t\t\tconst args = event.args as { label?: string };\n\t\t\t\t\t\tconst label = args.label || event.toolName;\n\n\t\t\t\t\t\t// Store args to pair with result later\n\t\t\t\t\t\tpendingTools.set(event.toolCallId, {\n\t\t\t\t\t\t\ttoolName: event.toolName,\n\t\t\t\t\t\t\targs: event.args,\n\t\t\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tlog.logToolStart(logCtx, event.toolName, label, event.args as Record<string, unknown>);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Show label in main message only\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_→ ${label}_`, false), \"tool label\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t\t\tconst resultStr = extractToolResultText(event.result);\n\t\t\t\t\t\tconst pending = pendingTools.get(event.toolCallId);\n\t\t\t\t\t\tpendingTools.delete(event.toolCallId);\n\n\t\t\t\t\t\tconst durationMs = pending ? Date.now() - pending.startTime : 0;\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tlog.logToolError(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlog.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool Result] ${event.toolName}: ${event.isError ? \"ERROR: \" : \"\"}${resultStr}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Post args + result together in thread\n\t\t\t\t\t\tconst label = pending?.args ? (pending.args as { label?: string }).label : undefined;\n\t\t\t\t\t\tconst argsFormatted = pending\n\t\t\t\t\t\t\t? formatToolArgsForSlack(event.toolName, pending.args as Record<string, unknown>)\n\t\t\t\t\t\t\t: \"(args not found)\";\n\t\t\t\t\t\tconst duration = (durationMs / 1000).toFixed(1);\n\t\t\t\t\t\tlet threadMessage = `*${event.isError ? \"✗\" : \"✓\"} ${event.toolName}*`;\n\t\t\t\t\t\tif (label) {\n\t\t\t\t\t\t\tthreadMessage += `: ${label}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthreadMessage += ` (${duration}s)\\n`;\n\n\t\t\t\t\t\tif (argsFormatted) {\n\t\t\t\t\t\t\tthreadMessage += \"```\\n\" + argsFormatted + \"\\n```\\n\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthreadMessage += \"*Result:*\\n```\\n\" + resultStr + \"\\n```\";\n\n\t\t\t\t\t\tqueue.enqueueMessage(threadMessage, \"thread\", \"tool result thread\", false);\n\n\t\t\t\t\t\t// Show brief error in main message if failed\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), \"tool error\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_update\": {\n\t\t\t\t\t\t// No longer stream to console - just track that we're streaming\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_start\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tlog.logResponseStart(logCtx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"message_end\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tconst assistantMsg = event.message as any; // AssistantMessage type\n\n\t\t\t\t\t\t\t// Track stop reason\n\t\t\t\t\t\t\tif (assistantMsg.stopReason) {\n\t\t\t\t\t\t\t\tstopReason = assistantMsg.stopReason;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Accumulate usage\n\t\t\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\t\t\ttotalUsage.input += assistantMsg.usage.input;\n\t\t\t\t\t\t\t\ttotalUsage.output += assistantMsg.usage.output;\n\t\t\t\t\t\t\t\ttotalUsage.cacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.input += assistantMsg.usage.cost.input;\n\t\t\t\t\t\t\t\ttotalUsage.cost.output += assistantMsg.usage.cost.output;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.total += assistantMsg.usage.cost.total;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Extract thinking and text from assistant message\n\t\t\t\t\t\t\tconst content = event.message.content;\n\t\t\t\t\t\t\tconst thinkingParts: string[] = [];\n\t\t\t\t\t\t\tconst textParts: string[] = [];\n\t\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\t\tif (part.type === \"thinking\") {\n\t\t\t\t\t\t\t\t\tthinkingParts.push(part.thinking);\n\t\t\t\t\t\t\t\t} else if (part.type === \"text\") {\n\t\t\t\t\t\t\t\t\ttextParts.push(part.text);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst text = textParts.join(\"\\n\");\n\n\t\t\t\t\t\t\t// Post thinking to main message and thread\n\t\t\t\t\t\t\tfor (const thinking of thinkingParts) {\n\t\t\t\t\t\t\t\tlog.logThinking(logCtx, thinking);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"main\", \"thinking main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"thread\", \"thinking thread\", false);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Post text to main message and thread\n\t\t\t\t\t\t\tif (text.trim()) {\n\t\t\t\t\t\t\t\tlog.logResponse(logCtx, text);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"main\", \"response main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"thread\", \"response thread\", false);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Run the agent with user's message\n\t\t\t// Prepend recent messages to the user prompt (not system prompt) for better caching\n\t\t\t// The current message is already the last entry in recentMessages\n\t\t\tconst userPrompt =\n\t\t\t\t`Conversation history (last 50 turns). Respond to the last message.\\n` +\n\t\t\t\t`Format: date TAB user TAB text TAB attachments\\n\\n` +\n\t\t\t\trecentMessages;\n\t\t\t// Debug: write full context to file\n\t\t\tconst toolDefs = tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));\n\t\t\tconst debugPrompt =\n\t\t\t\t`=== SYSTEM PROMPT (${systemPrompt.length} chars) ===\\n\\n${systemPrompt}\\n\\n` +\n\t\t\t\t`=== TOOL DEFINITIONS (${JSON.stringify(toolDefs).length} chars) ===\\n\\n${JSON.stringify(toolDefs, null, 2)}\\n\\n` +\n\t\t\t\t`=== USER PROMPT (${userPrompt.length} chars) ===\\n\\n${userPrompt}`;\n\t\t\tawait writeFile(join(channelDir, \"last_prompt.txt\"), debugPrompt, \"utf-8\");\n\n\t\t\tawait agent.prompt(userPrompt);\n\n\t\t\t// Wait for all queued respond calls to complete\n\t\t\tawait queue.flush();\n\n\t\t\t// Get final assistant message text from agent state and replace main message\n\t\t\tconst messages = agent.state.messages;\n\t\t\tconst lastAssistant = messages.filter((m) => m.role === \"assistant\").pop();\n\t\t\tconst finalText =\n\t\t\t\tlastAssistant?.content\n\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t.join(\"\\n\") || \"\";\n\t\t\tif (finalText.trim()) {\n\t\t\t\ttry {\n\t\t\t\t\t// For the main message, truncate if too long (full text is in thread)\n\t\t\t\t\tconst mainText =\n\t\t\t\t\t\tfinalText.length > SLACK_MAX_LENGTH\n\t\t\t\t\t\t\t? finalText.substring(0, SLACK_MAX_LENGTH - 50) + \"\\n\\n_(see thread for full response)_\"\n\t\t\t\t\t\t\t: finalText;\n\t\t\t\t\tawait ctx.replaceMessage(mainText);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tlog.logWarning(\"Failed to replace message with final text\", errMsg);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log usage summary if there was any usage\n\t\t\tif (totalUsage.cost.total > 0) {\n\t\t\t\tconst summary = log.logUsageSummary(logCtx, totalUsage);\n\t\t\t\tqueue.enqueue(() => ctx.respondInThread(summary), \"usage summary\");\n\t\t\t\tawait queue.flush();\n\t\t\t}\n\n\t\t\treturn { stopReason };\n\t\t},\n\n\t\tabort(): void {\n\t\t\tagent?.abort();\n\t\t},\n\t};\n}\n\n/**\n * Translate container path back to host path for file operations\n */\nfunction translateToHostPath(\n\tcontainerPath: string,\n\tchannelDir: string,\n\tworkspacePath: string,\n\tchannelId: string,\n): string {\n\tif (workspacePath === \"/workspace\") {\n\t\t// Docker mode - translate /workspace/channelId/... to host path\n\t\tconst prefix = `/workspace/${channelId}/`;\n\t\tif (containerPath.startsWith(prefix)) {\n\t\t\treturn join(channelDir, containerPath.slice(prefix.length));\n\t\t}\n\t\t// Maybe it's just /workspace/...\n\t\tif (containerPath.startsWith(\"/workspace/\")) {\n\t\t\treturn join(channelDir, \"..\", containerPath.slice(\"/workspace/\".length));\n\t\t}\n\t}\n\t// Host mode or already a host path\n\treturn containerPath;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAEH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CA2B/E","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { Executor } from \"../sandbox.js\";\n\nconst bashSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what this command does (shown to user)\" }),\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport function createBashTool(executor: Executor): AgentTool<typeof bashSchema> {\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription:\n\t\t\t\"Execute a bash command in the current working directory. Returns stdout and stderr. Optionally provide a timeout in seconds.\",\n\t\tparameters: bashSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ command, timeout }: { label: string; command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst result = await executor.exec(command, { timeout, signal });\n\t\t\tlet output = \"\";\n\t\t\tif (result.stdout) output += result.stdout;\n\t\t\tif (result.stderr) {\n\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\toutput += result.stderr;\n\t\t\t}\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(`${output}\\n\\nCommand exited with code ${result.code}`.trim());\n\t\t\t}\n\n\t\t\treturn { content: [{ type: \"text\", text: output || \"(no output)\" }], details: undefined };\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAW9C,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAOH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAoE/E","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { Executor } from \"../sandbox.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from \"./truncate.js\";\n\n/**\n * Generate a unique temp file path for bash output\n */\nfunction getTempFilePath(): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `mom-bash-${id}.log`);\n}\n\nconst bashSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what this command does (shown to user)\" }),\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\ninterface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\nexport function createBashTool(executor: Executor): AgentTool<typeof bashSchema> {\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tparameters: bashSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ command, timeout }: { label: string; command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\t// Track output for potential temp file writing\n\t\t\tlet tempFilePath: string | undefined;\n\t\t\tlet tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\n\t\t\tconst result = await executor.exec(command, { timeout, signal });\n\t\t\tlet output = \"\";\n\t\t\tif (result.stdout) output += result.stdout;\n\t\t\tif (result.stderr) {\n\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\toutput += result.stderr;\n\t\t\t}\n\n\t\t\tconst totalBytes = Buffer.byteLength(output, \"utf-8\");\n\n\t\t\t// Write to temp file if output exceeds limit\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\t\ttempFilePath = getTempFilePath();\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\ttempFileStream.write(output);\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Apply tail truncation\n\t\t\tconst truncation = truncateTail(output);\n\t\t\tlet outputText = truncation.content || \"(no output)\";\n\n\t\t\t// Build details with truncation info\n\t\t\tlet details: BashToolDetails | undefined;\n\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails = {\n\t\t\t\t\ttruncation,\n\t\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\t};\n\n\t\t\t\t// Build actionable notice\n\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\tconst endLine = truncation.totalLines;\n\n\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t// Edge case: last line alone > 50KB\n\t\t\t\t\tconst lastLineSize = formatSize(Buffer.byteLength(output.split(\"\\n\").pop() || \"\", \"utf-8\"));\n\t\t\t\t\toutputText += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;\n\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;\n\t\t\t\t} else {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(`${outputText}\\n\\nCommand exited with code ${result.code}`.trim());\n\t\t\t}\n\n\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t},\n\t};\n}\n"]}
@@ -1,4 +1,16 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createWriteStream } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
1
5
  import { Type } from "@sinclair/typebox";
6
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
7
+ /**
8
+ * Generate a unique temp file path for bash output
9
+ */
10
+ function getTempFilePath() {
11
+ const id = randomBytes(8).toString("hex");
12
+ return join(tmpdir(), `mom-bash-${id}.log`);
13
+ }
2
14
  const bashSchema = Type.Object({
3
15
  label: Type.String({ description: "Brief description of what this command does (shown to user)" }),
4
16
  command: Type.String({ description: "Bash command to execute" }),
@@ -8,9 +20,12 @@ export function createBashTool(executor) {
8
20
  return {
9
21
  name: "bash",
10
22
  label: "bash",
11
- description: "Execute a bash command in the current working directory. Returns stdout and stderr. Optionally provide a timeout in seconds.",
23
+ description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
12
24
  parameters: bashSchema,
13
25
  execute: async (_toolCallId, { command, timeout }, signal) => {
26
+ // Track output for potential temp file writing
27
+ let tempFilePath;
28
+ let tempFileStream;
14
29
  const result = await executor.exec(command, { timeout, signal });
15
30
  let output = "";
16
31
  if (result.stdout)
@@ -20,10 +35,43 @@ export function createBashTool(executor) {
20
35
  output += "\n";
21
36
  output += result.stderr;
22
37
  }
38
+ const totalBytes = Buffer.byteLength(output, "utf-8");
39
+ // Write to temp file if output exceeds limit
40
+ if (totalBytes > DEFAULT_MAX_BYTES) {
41
+ tempFilePath = getTempFilePath();
42
+ tempFileStream = createWriteStream(tempFilePath);
43
+ tempFileStream.write(output);
44
+ tempFileStream.end();
45
+ }
46
+ // Apply tail truncation
47
+ const truncation = truncateTail(output);
48
+ let outputText = truncation.content || "(no output)";
49
+ // Build details with truncation info
50
+ let details;
51
+ if (truncation.truncated) {
52
+ details = {
53
+ truncation,
54
+ fullOutputPath: tempFilePath,
55
+ };
56
+ // Build actionable notice
57
+ const startLine = truncation.totalLines - truncation.outputLines + 1;
58
+ const endLine = truncation.totalLines;
59
+ if (truncation.lastLinePartial) {
60
+ // Edge case: last line alone > 50KB
61
+ const lastLineSize = formatSize(Buffer.byteLength(output.split("\n").pop() || "", "utf-8"));
62
+ outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
63
+ }
64
+ else if (truncation.truncatedBy === "lines") {
65
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
66
+ }
67
+ else {
68
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
69
+ }
70
+ }
23
71
  if (result.code !== 0) {
24
- throw new Error(`${output}\n\nCommand exited with code ${result.code}`.trim());
72
+ throw new Error(`${outputText}\n\nCommand exited with code ${result.code}`.trim());
25
73
  }
26
- return { content: [{ type: "text", text: output || "(no output)" }], details: undefined };
74
+ return { content: [{ type: "text", text: outputText }], details };
27
75
  },
28
76
  };
29
77
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bash.js","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAGzC,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;IAC9B,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC;IAClG,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,yBAAyB,EAAE,CAAC;IAChE,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC,CAAC;CACzG,CAAC,CAAC;AAEH,MAAM,UAAU,cAAc,CAAC,QAAkB,EAAgC;IAChF,OAAO;QACN,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,MAAM;QACb,WAAW,EACV,8HAA8H;QAC/H,UAAU,EAAE,UAAU;QACtB,OAAO,EAAE,KAAK,EACb,WAAmB,EACnB,EAAE,OAAO,EAAE,OAAO,EAAwD,EAC1E,MAAoB,EACnB,EAAE,CAAC;YACJ,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;YACjE,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,IAAI,MAAM,CAAC,MAAM;gBAAE,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC;YAC3C,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBACnB,IAAI,MAAM;oBAAE,MAAM,IAAI,IAAI,CAAC;gBAC3B,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC;YACzB,CAAC;YAED,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBACvB,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,gCAAgC,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;YAChF,CAAC;YAED,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,IAAI,aAAa,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;QAAA,CAC1F;KACD,CAAC;AAAA,CACF","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { Executor } from \"../sandbox.js\";\n\nconst bashSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what this command does (shown to user)\" }),\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport function createBashTool(executor: Executor): AgentTool<typeof bashSchema> {\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription:\n\t\t\t\"Execute a bash command in the current working directory. Returns stdout and stderr. Optionally provide a timeout in seconds.\",\n\t\tparameters: bashSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ command, timeout }: { label: string; command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst result = await executor.exec(command, { timeout, signal });\n\t\t\tlet output = \"\";\n\t\t\tif (result.stdout) output += result.stdout;\n\t\t\tif (result.stderr) {\n\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\toutput += result.stderr;\n\t\t\t}\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(`${output}\\n\\nCommand exited with code ${result.code}`.trim());\n\t\t\t}\n\n\t\t\treturn { content: [{ type: \"text\", text: output || \"(no output)\" }], details: undefined };\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"bash.js","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEzC,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,UAAU,EAAyB,YAAY,EAAE,MAAM,eAAe,CAAC;AAEtH;;GAEG;AACH,SAAS,eAAe,GAAW;IAClC,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC1C,OAAO,IAAI,CAAC,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC;AAAA,CAC5C;AAED,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;IAC9B,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC;IAClG,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,yBAAyB,EAAE,CAAC;IAChE,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC,CAAC;CACzG,CAAC,CAAC;AAOH,MAAM,UAAU,cAAc,CAAC,QAAkB,EAAgC;IAChF,OAAO;QACN,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,MAAM;QACb,WAAW,EAAE,mHAAmH,iBAAiB,aAAa,iBAAiB,GAAG,IAAI,0HAA0H;QAChT,UAAU,EAAE,UAAU;QACtB,OAAO,EAAE,KAAK,EACb,WAAmB,EACnB,EAAE,OAAO,EAAE,OAAO,EAAwD,EAC1E,MAAoB,EACnB,EAAE,CAAC;YACJ,+CAA+C;YAC/C,IAAI,YAAgC,CAAC;YACrC,IAAI,cAAgE,CAAC;YAErE,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;YACjE,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,IAAI,MAAM,CAAC,MAAM;gBAAE,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC;YAC3C,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBACnB,IAAI,MAAM;oBAAE,MAAM,IAAI,IAAI,CAAC;gBAC3B,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC;YACzB,CAAC;YAED,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAEtD,6CAA6C;YAC7C,IAAI,UAAU,GAAG,iBAAiB,EAAE,CAAC;gBACpC,YAAY,GAAG,eAAe,EAAE,CAAC;gBACjC,cAAc,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;gBACjD,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;gBAC7B,cAAc,CAAC,GAAG,EAAE,CAAC;YACtB,CAAC;YAED,wBAAwB;YACxB,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;YACxC,IAAI,UAAU,GAAG,UAAU,CAAC,OAAO,IAAI,aAAa,CAAC;YAErD,qCAAqC;YACrC,IAAI,OAAoC,CAAC;YAEzC,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;gBAC1B,OAAO,GAAG;oBACT,UAAU;oBACV,cAAc,EAAE,YAAY;iBAC5B,CAAC;gBAEF,0BAA0B;gBAC1B,MAAM,SAAS,GAAG,UAAU,CAAC,UAAU,GAAG,UAAU,CAAC,WAAW,GAAG,CAAC,CAAC;gBACrE,MAAM,OAAO,GAAG,UAAU,CAAC,UAAU,CAAC;gBAEtC,IAAI,UAAU,CAAC,eAAe,EAAE,CAAC;oBAChC,oCAAoC;oBACpC,MAAM,YAAY,GAAG,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;oBAC5F,UAAU,IAAI,qBAAqB,UAAU,CAAC,UAAU,CAAC,WAAW,CAAC,YAAY,OAAO,aAAa,YAAY,mBAAmB,YAAY,GAAG,CAAC;gBACrJ,CAAC;qBAAM,IAAI,UAAU,CAAC,WAAW,KAAK,OAAO,EAAE,CAAC;oBAC/C,UAAU,IAAI,sBAAsB,SAAS,IAAI,OAAO,OAAO,UAAU,CAAC,UAAU,kBAAkB,YAAY,GAAG,CAAC;gBACvH,CAAC;qBAAM,CAAC;oBACP,UAAU,IAAI,sBAAsB,SAAS,IAAI,OAAO,OAAO,UAAU,CAAC,UAAU,KAAK,UAAU,CAAC,iBAAiB,CAAC,yBAAyB,YAAY,GAAG,CAAC;gBAChK,CAAC;YACF,CAAC;YAED,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBACvB,MAAM,IAAI,KAAK,CAAC,GAAG,UAAU,gCAAgC,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;YACpF,CAAC;YAED,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC;QAAA,CAClE;KACD,CAAC;AAAA,CACF","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { Executor } from \"../sandbox.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from \"./truncate.js\";\n\n/**\n * Generate a unique temp file path for bash output\n */\nfunction getTempFilePath(): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `mom-bash-${id}.log`);\n}\n\nconst bashSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what this command does (shown to user)\" }),\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\ninterface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\nexport function createBashTool(executor: Executor): AgentTool<typeof bashSchema> {\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tparameters: bashSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ command, timeout }: { label: string; command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\t// Track output for potential temp file writing\n\t\t\tlet tempFilePath: string | undefined;\n\t\t\tlet tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\n\t\t\tconst result = await executor.exec(command, { timeout, signal });\n\t\t\tlet output = \"\";\n\t\t\tif (result.stdout) output += result.stdout;\n\t\t\tif (result.stderr) {\n\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\toutput += result.stderr;\n\t\t\t}\n\n\t\t\tconst totalBytes = Buffer.byteLength(output, \"utf-8\");\n\n\t\t\t// Write to temp file if output exceeds limit\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\t\ttempFilePath = getTempFilePath();\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\ttempFileStream.write(output);\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Apply tail truncation\n\t\t\tconst truncation = truncateTail(output);\n\t\t\tlet outputText = truncation.content || \"(no output)\";\n\n\t\t\t// Build details with truncation info\n\t\t\tlet details: BashToolDetails | undefined;\n\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails = {\n\t\t\t\t\ttruncation,\n\t\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\t};\n\n\t\t\t\t// Build actionable notice\n\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\tconst endLine = truncation.totalLines;\n\n\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t// Edge case: last line alone > 50KB\n\t\t\t\t\tconst lastLineSize = formatSize(Buffer.byteLength(output.split(\"\\n\").pop() || \"\", \"utf-8\"));\n\t\t\t\t\toutputText += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;\n\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;\n\t\t\t\t} else {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(`${outputText}\\n\\nCommand exited with code ${result.code}`.trim());\n\t\t\t}\n\n\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t},\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAA6B,MAAM,qBAAqB,CAAC;AAGhF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAqB9C,QAAA,MAAM,UAAU;;;;;EAKd,CAAC;AAKH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAwF/E","sourcesContent":["import type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { extname } from \"path\";\nimport type { Executor } from \"../sandbox.js\";\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".gif\": \"image/gif\",\n\t\".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what you're reading and why (shown to user)\" }),\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nconst MAX_LINES = 2000;\nconst MAX_LINE_LENGTH = 2000;\n\nexport function createReadTool(executor: Executor): AgentTool<typeof readSchema> {\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription:\n\t\t\t\"Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.\",\n\t\tparameters: readSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, offset, limit }: { label: string; path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst mimeType = isImageFile(path);\n\n\t\t\tif (mimeType) {\n\t\t\t\t// Read as image (binary) - use base64\n\t\t\t\tconst result = await executor.exec(`base64 < ${shellEscape(path)}`, { signal });\n\t\t\t\tif (result.code !== 0) {\n\t\t\t\t\tthrow new Error(result.stderr || `Failed to read file: ${path}`);\n\t\t\t\t}\n\t\t\t\tconst base64 = result.stdout.replace(/\\s/g, \"\"); // Remove whitespace from base64\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t] as (TextContent | ImageContent)[],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Read as text using cat with offset/limit via sed/head/tail\n\t\t\tlet cmd: string;\n\t\t\tconst startLine = offset ? Math.max(1, offset) : 1;\n\t\t\tconst maxLines = limit || MAX_LINES;\n\n\t\t\tif (startLine === 1) {\n\t\t\t\tcmd = `head -n ${maxLines} ${shellEscape(path)}`;\n\t\t\t} else {\n\t\t\t\tcmd = `sed -n '${startLine},${startLine + maxLines - 1}p' ${shellEscape(path)}`;\n\t\t\t}\n\n\t\t\t// Also get total line count\n\t\t\tconst countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });\n\t\t\tconst totalLines = Number.parseInt(countResult.stdout.trim(), 10) || 0;\n\n\t\t\tconst result = await executor.exec(cmd, { signal });\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(result.stderr || `Failed to read file: ${path}`);\n\t\t\t}\n\n\t\t\tconst lines = result.stdout.split(\"\\n\");\n\n\t\t\t// Truncate long lines\n\t\t\tlet hadTruncatedLines = false;\n\t\t\tconst formattedLines = lines.map((line) => {\n\t\t\t\tif (line.length > MAX_LINE_LENGTH) {\n\t\t\t\t\thadTruncatedLines = true;\n\t\t\t\t\treturn line.slice(0, MAX_LINE_LENGTH);\n\t\t\t\t}\n\t\t\t\treturn line;\n\t\t\t});\n\n\t\t\tlet outputText = formattedLines.join(\"\\n\");\n\n\t\t\t// Add notices\n\t\t\tconst notices: string[] = [];\n\t\t\tconst endLine = startLine + lines.length - 1;\n\n\t\t\tif (hadTruncatedLines) {\n\t\t\t\tnotices.push(`Some lines were truncated to ${MAX_LINE_LENGTH} characters for display`);\n\t\t\t}\n\n\t\t\tif (endLine < totalLines) {\n\t\t\t\tconst remaining = totalLines - endLine;\n\t\t\t\tnotices.push(`${remaining} more lines not shown. Use offset=${endLine + 1} to continue reading`);\n\t\t\t}\n\n\t\t\tif (notices.length > 0) {\n\t\t\t\toutputText += `\\n\\n... (${notices.join(\". \")})`;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: outputText }] as (TextContent | ImageContent)[],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction shellEscape(s: string): string {\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAA6B,MAAM,qBAAqB,CAAC;AAGhF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAsB9C,QAAA,MAAM,UAAU;;;;;EAKd,CAAC;AAMH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAqH/E","sourcesContent":["import type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { extname } from \"path\";\nimport type { Executor } from \"../sandbox.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".gif\": \"image/gif\",\n\t\".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what you're reading and why (shown to user)\" }),\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\ninterface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\nexport function createReadTool(executor: Executor): AgentTool<typeof readSchema> {\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n\t\tparameters: readSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, offset, limit }: { label: string; path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t): Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }> => {\n\t\t\tconst mimeType = isImageFile(path);\n\n\t\t\tif (mimeType) {\n\t\t\t\t// Read as image (binary) - use base64\n\t\t\t\tconst result = await executor.exec(`base64 < ${shellEscape(path)}`, { signal });\n\t\t\t\tif (result.code !== 0) {\n\t\t\t\t\tthrow new Error(result.stderr || `Failed to read file: ${path}`);\n\t\t\t\t}\n\t\t\t\tconst base64 = result.stdout.replace(/\\s/g, \"\"); // Remove whitespace from base64\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Get total line count first\n\t\t\tconst countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });\n\t\t\tif (countResult.code !== 0) {\n\t\t\t\tthrow new Error(countResult.stderr || `Failed to read file: ${path}`);\n\t\t\t}\n\t\t\tconst totalFileLines = Number.parseInt(countResult.stdout.trim(), 10) + 1; // wc -l counts newlines, not lines\n\n\t\t\t// Apply offset if specified (1-indexed)\n\t\t\tconst startLine = offset ? Math.max(1, offset) : 1;\n\t\t\tconst startLineDisplay = startLine;\n\n\t\t\t// Check if offset is out of bounds\n\t\t\tif (startLine > totalFileLines) {\n\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);\n\t\t\t}\n\n\t\t\t// Read content with offset\n\t\t\tlet cmd: string;\n\t\t\tif (startLine === 1) {\n\t\t\t\tcmd = `cat ${shellEscape(path)}`;\n\t\t\t} else {\n\t\t\t\tcmd = `tail -n +${startLine} ${shellEscape(path)}`;\n\t\t\t}\n\n\t\t\tconst result = await executor.exec(cmd, { signal });\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(result.stderr || `Failed to read file: ${path}`);\n\t\t\t}\n\n\t\t\tlet selectedContent = result.stdout;\n\t\t\tlet userLimitedLines: number | undefined;\n\n\t\t\t// Apply user limit if specified\n\t\t\tif (limit !== undefined) {\n\t\t\t\tconst lines = selectedContent.split(\"\\n\");\n\t\t\t\tconst endLine = Math.min(limit, lines.length);\n\t\t\t\tselectedContent = lines.slice(0, endLine).join(\"\\n\");\n\t\t\t\tuserLimitedLines = endLine;\n\t\t\t}\n\n\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\tlet outputText: string;\n\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t// First line at offset exceeds 50KB - tell model to use bash\n\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(selectedContent.split(\"\\n\")[0], \"utf-8\"));\n\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\tdetails = { truncation };\n\t\t\t} else if (truncation.truncated) {\n\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\toutputText = truncation.content;\n\n\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t} else {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t}\n\t\t\t\tdetails = { truncation };\n\t\t\t} else if (userLimitedLines !== undefined) {\n\t\t\t\t// User specified limit, check if there's more content\n\t\t\t\tconst linesFromStart = startLine - 1 + userLimitedLines;\n\t\t\t\tif (linesFromStart < totalFileLines) {\n\t\t\t\t\tconst remaining = totalFileLines - linesFromStart;\n\t\t\t\t\tconst nextOffset = startLine + userLimitedLines;\n\n\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t} else {\n\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\toutputText = truncation.content;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: outputText }],\n\t\t\t\tdetails,\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction shellEscape(s: string): string {\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { extname } from "path";
3
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
3
4
  /**
4
5
  * Map of file extensions to MIME types for common image formats
5
6
  */
@@ -23,13 +24,11 @@ const readSchema = Type.Object({
23
24
  offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
24
25
  limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
25
26
  });
26
- const MAX_LINES = 2000;
27
- const MAX_LINE_LENGTH = 2000;
28
27
  export function createReadTool(executor) {
29
28
  return {
30
29
  name: "read",
31
30
  label: "read",
32
- description: "Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.",
31
+ description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,
33
32
  parameters: readSchema,
34
33
  execute: async (_toolCallId, { path, offset, limit }, signal) => {
35
34
  const mimeType = isImageFile(path);
@@ -48,50 +47,83 @@ export function createReadTool(executor) {
48
47
  details: undefined,
49
48
  };
50
49
  }
51
- // Read as text using cat with offset/limit via sed/head/tail
52
- let cmd;
50
+ // Get total line count first
51
+ const countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });
52
+ if (countResult.code !== 0) {
53
+ throw new Error(countResult.stderr || `Failed to read file: ${path}`);
54
+ }
55
+ const totalFileLines = Number.parseInt(countResult.stdout.trim(), 10) + 1; // wc -l counts newlines, not lines
56
+ // Apply offset if specified (1-indexed)
53
57
  const startLine = offset ? Math.max(1, offset) : 1;
54
- const maxLines = limit || MAX_LINES;
58
+ const startLineDisplay = startLine;
59
+ // Check if offset is out of bounds
60
+ if (startLine > totalFileLines) {
61
+ throw new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);
62
+ }
63
+ // Read content with offset
64
+ let cmd;
55
65
  if (startLine === 1) {
56
- cmd = `head -n ${maxLines} ${shellEscape(path)}`;
66
+ cmd = `cat ${shellEscape(path)}`;
57
67
  }
58
68
  else {
59
- cmd = `sed -n '${startLine},${startLine + maxLines - 1}p' ${shellEscape(path)}`;
69
+ cmd = `tail -n +${startLine} ${shellEscape(path)}`;
60
70
  }
61
- // Also get total line count
62
- const countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });
63
- const totalLines = Number.parseInt(countResult.stdout.trim(), 10) || 0;
64
71
  const result = await executor.exec(cmd, { signal });
65
72
  if (result.code !== 0) {
66
73
  throw new Error(result.stderr || `Failed to read file: ${path}`);
67
74
  }
68
- const lines = result.stdout.split("\n");
69
- // Truncate long lines
70
- let hadTruncatedLines = false;
71
- const formattedLines = lines.map((line) => {
72
- if (line.length > MAX_LINE_LENGTH) {
73
- hadTruncatedLines = true;
74
- return line.slice(0, MAX_LINE_LENGTH);
75
+ let selectedContent = result.stdout;
76
+ let userLimitedLines;
77
+ // Apply user limit if specified
78
+ if (limit !== undefined) {
79
+ const lines = selectedContent.split("\n");
80
+ const endLine = Math.min(limit, lines.length);
81
+ selectedContent = lines.slice(0, endLine).join("\n");
82
+ userLimitedLines = endLine;
83
+ }
84
+ // Apply truncation (respects both line and byte limits)
85
+ const truncation = truncateHead(selectedContent);
86
+ let outputText;
87
+ let details;
88
+ if (truncation.firstLineExceedsLimit) {
89
+ // First line at offset exceeds 50KB - tell model to use bash
90
+ const firstLineSize = formatSize(Buffer.byteLength(selectedContent.split("\n")[0], "utf-8"));
91
+ outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
92
+ details = { truncation };
93
+ }
94
+ else if (truncation.truncated) {
95
+ // Truncation occurred - build actionable notice
96
+ const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
97
+ const nextOffset = endLineDisplay + 1;
98
+ outputText = truncation.content;
99
+ if (truncation.truncatedBy === "lines") {
100
+ outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
101
+ }
102
+ else {
103
+ outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;
75
104
  }
76
- return line;
77
- });
78
- let outputText = formattedLines.join("\n");
79
- // Add notices
80
- const notices = [];
81
- const endLine = startLine + lines.length - 1;
82
- if (hadTruncatedLines) {
83
- notices.push(`Some lines were truncated to ${MAX_LINE_LENGTH} characters for display`);
105
+ details = { truncation };
84
106
  }
85
- if (endLine < totalLines) {
86
- const remaining = totalLines - endLine;
87
- notices.push(`${remaining} more lines not shown. Use offset=${endLine + 1} to continue reading`);
107
+ else if (userLimitedLines !== undefined) {
108
+ // User specified limit, check if there's more content
109
+ const linesFromStart = startLine - 1 + userLimitedLines;
110
+ if (linesFromStart < totalFileLines) {
111
+ const remaining = totalFileLines - linesFromStart;
112
+ const nextOffset = startLine + userLimitedLines;
113
+ outputText = truncation.content;
114
+ outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;
115
+ }
116
+ else {
117
+ outputText = truncation.content;
118
+ }
88
119
  }
89
- if (notices.length > 0) {
90
- outputText += `\n\n... (${notices.join(". ")})`;
120
+ else {
121
+ // No truncation, no user limit exceeded
122
+ outputText = truncation.content;
91
123
  }
92
124
  return {
93
125
  content: [{ type: "text", text: outputText }],
94
- details: undefined,
126
+ details,
95
127
  };
96
128
  },
97
129
  };
@@ -1 +1 @@
1
- {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { extname } from \"path\";\nimport type { Executor } from \"../sandbox.js\";\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".gif\": \"image/gif\",\n\t\".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what you're reading and why (shown to user)\" }),\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nconst MAX_LINES = 2000;\nconst MAX_LINE_LENGTH = 2000;\n\nexport function createReadTool(executor: Executor): AgentTool<typeof readSchema> {\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription:\n\t\t\t\"Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.\",\n\t\tparameters: readSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, offset, limit }: { label: string; path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tconst mimeType = isImageFile(path);\n\n\t\t\tif (mimeType) {\n\t\t\t\t// Read as image (binary) - use base64\n\t\t\t\tconst result = await executor.exec(`base64 < ${shellEscape(path)}`, { signal });\n\t\t\t\tif (result.code !== 0) {\n\t\t\t\t\tthrow new Error(result.stderr || `Failed to read file: ${path}`);\n\t\t\t\t}\n\t\t\t\tconst base64 = result.stdout.replace(/\\s/g, \"\"); // Remove whitespace from base64\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t] as (TextContent | ImageContent)[],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Read as text using cat with offset/limit via sed/head/tail\n\t\t\tlet cmd: string;\n\t\t\tconst startLine = offset ? Math.max(1, offset) : 1;\n\t\t\tconst maxLines = limit || MAX_LINES;\n\n\t\t\tif (startLine === 1) {\n\t\t\t\tcmd = `head -n ${maxLines} ${shellEscape(path)}`;\n\t\t\t} else {\n\t\t\t\tcmd = `sed -n '${startLine},${startLine + maxLines - 1}p' ${shellEscape(path)}`;\n\t\t\t}\n\n\t\t\t// Also get total line count\n\t\t\tconst countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });\n\t\t\tconst totalLines = Number.parseInt(countResult.stdout.trim(), 10) || 0;\n\n\t\t\tconst result = await executor.exec(cmd, { signal });\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(result.stderr || `Failed to read file: ${path}`);\n\t\t\t}\n\n\t\t\tconst lines = result.stdout.split(\"\\n\");\n\n\t\t\t// Truncate long lines\n\t\t\tlet hadTruncatedLines = false;\n\t\t\tconst formattedLines = lines.map((line) => {\n\t\t\t\tif (line.length > MAX_LINE_LENGTH) {\n\t\t\t\t\thadTruncatedLines = true;\n\t\t\t\t\treturn line.slice(0, MAX_LINE_LENGTH);\n\t\t\t\t}\n\t\t\t\treturn line;\n\t\t\t});\n\n\t\t\tlet outputText = formattedLines.join(\"\\n\");\n\n\t\t\t// Add notices\n\t\t\tconst notices: string[] = [];\n\t\t\tconst endLine = startLine + lines.length - 1;\n\n\t\t\tif (hadTruncatedLines) {\n\t\t\t\tnotices.push(`Some lines were truncated to ${MAX_LINE_LENGTH} characters for display`);\n\t\t\t}\n\n\t\t\tif (endLine < totalLines) {\n\t\t\t\tconst remaining = totalLines - endLine;\n\t\t\t\tnotices.push(`${remaining} more lines not shown. Use offset=${endLine + 1} to continue reading`);\n\t\t\t}\n\n\t\t\tif (notices.length > 0) {\n\t\t\t\toutputText += `\\n\\n... (${notices.join(\". \")})`;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: outputText }] as (TextContent | ImageContent)[],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction shellEscape(s: string): string {\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { extname } from \"path\";\nimport type { Executor } from \"../sandbox.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".gif\": \"image/gif\",\n\t\".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what you're reading and why (shown to user)\" }),\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\ninterface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\nexport function createReadTool(executor: Executor): AgentTool<typeof readSchema> {\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n\t\tparameters: readSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, offset, limit }: { label: string; path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t): Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }> => {\n\t\t\tconst mimeType = isImageFile(path);\n\n\t\t\tif (mimeType) {\n\t\t\t\t// Read as image (binary) - use base64\n\t\t\t\tconst result = await executor.exec(`base64 < ${shellEscape(path)}`, { signal });\n\t\t\t\tif (result.code !== 0) {\n\t\t\t\t\tthrow new Error(result.stderr || `Failed to read file: ${path}`);\n\t\t\t\t}\n\t\t\t\tconst base64 = result.stdout.replace(/\\s/g, \"\"); // Remove whitespace from base64\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Get total line count first\n\t\t\tconst countResult = await executor.exec(`wc -l < ${shellEscape(path)}`, { signal });\n\t\t\tif (countResult.code !== 0) {\n\t\t\t\tthrow new Error(countResult.stderr || `Failed to read file: ${path}`);\n\t\t\t}\n\t\t\tconst totalFileLines = Number.parseInt(countResult.stdout.trim(), 10) + 1; // wc -l counts newlines, not lines\n\n\t\t\t// Apply offset if specified (1-indexed)\n\t\t\tconst startLine = offset ? Math.max(1, offset) : 1;\n\t\t\tconst startLineDisplay = startLine;\n\n\t\t\t// Check if offset is out of bounds\n\t\t\tif (startLine > totalFileLines) {\n\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);\n\t\t\t}\n\n\t\t\t// Read content with offset\n\t\t\tlet cmd: string;\n\t\t\tif (startLine === 1) {\n\t\t\t\tcmd = `cat ${shellEscape(path)}`;\n\t\t\t} else {\n\t\t\t\tcmd = `tail -n +${startLine} ${shellEscape(path)}`;\n\t\t\t}\n\n\t\t\tconst result = await executor.exec(cmd, { signal });\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(result.stderr || `Failed to read file: ${path}`);\n\t\t\t}\n\n\t\t\tlet selectedContent = result.stdout;\n\t\t\tlet userLimitedLines: number | undefined;\n\n\t\t\t// Apply user limit if specified\n\t\t\tif (limit !== undefined) {\n\t\t\t\tconst lines = selectedContent.split(\"\\n\");\n\t\t\t\tconst endLine = Math.min(limit, lines.length);\n\t\t\t\tselectedContent = lines.slice(0, endLine).join(\"\\n\");\n\t\t\t\tuserLimitedLines = endLine;\n\t\t\t}\n\n\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\tlet outputText: string;\n\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t// First line at offset exceeds 50KB - tell model to use bash\n\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(selectedContent.split(\"\\n\")[0], \"utf-8\"));\n\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\tdetails = { truncation };\n\t\t\t} else if (truncation.truncated) {\n\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\toutputText = truncation.content;\n\n\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t} else {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t}\n\t\t\t\tdetails = { truncation };\n\t\t\t} else if (userLimitedLines !== undefined) {\n\t\t\t\t// User specified limit, check if there's more content\n\t\t\t\tconst linesFromStart = startLine - 1 + userLimitedLines;\n\t\t\t\tif (linesFromStart < totalFileLines) {\n\t\t\t\t\tconst remaining = totalFileLines - linesFromStart;\n\t\t\t\t\tconst nextOffset = startLine + userLimitedLines;\n\n\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t} else {\n\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\toutputText = truncation.content;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: outputText }],\n\t\t\t\tdetails,\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction shellEscape(s: string): string {\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared truncation utilities for tool outputs.
3
+ *
4
+ * Truncation is based on two independent limits - whichever is hit first wins:
5
+ * - Line limit (default: 2000 lines)
6
+ * - Byte limit (default: 50KB)
7
+ *
8
+ * Never returns partial lines (except bash tail truncation edge case).
9
+ */
10
+ export declare const DEFAULT_MAX_LINES = 2000;
11
+ export declare const DEFAULT_MAX_BYTES: number;
12
+ export interface TruncationResult {
13
+ /** The truncated content */
14
+ content: string;
15
+ /** Whether truncation occurred */
16
+ truncated: boolean;
17
+ /** Which limit was hit: "lines", "bytes", or null if not truncated */
18
+ truncatedBy: "lines" | "bytes" | null;
19
+ /** Total number of lines in the original content */
20
+ totalLines: number;
21
+ /** Total number of bytes in the original content */
22
+ totalBytes: number;
23
+ /** Number of complete lines in the truncated output */
24
+ outputLines: number;
25
+ /** Number of bytes in the truncated output */
26
+ outputBytes: number;
27
+ /** Whether the last line was partially truncated (only for tail truncation edge case) */
28
+ lastLinePartial: boolean;
29
+ /** Whether the first line exceeded the byte limit (for head truncation) */
30
+ firstLineExceedsLimit: boolean;
31
+ }
32
+ export interface TruncationOptions {
33
+ /** Maximum number of lines (default: 2000) */
34
+ maxLines?: number;
35
+ /** Maximum number of bytes (default: 50KB) */
36
+ maxBytes?: number;
37
+ }
38
+ /**
39
+ * Format bytes as human-readable size.
40
+ */
41
+ export declare function formatSize(bytes: number): string;
42
+ /**
43
+ * Truncate content from the head (keep first N lines/bytes).
44
+ * Suitable for file reads where you want to see the beginning.
45
+ *
46
+ * Never returns partial lines. If first line exceeds byte limit,
47
+ * returns empty content with firstLineExceedsLimit=true.
48
+ */
49
+ export declare function truncateHead(content: string, options?: TruncationOptions): TruncationResult;
50
+ /**
51
+ * Truncate content from the tail (keep last N lines/bytes).
52
+ * Suitable for bash output where you want to see the end (errors, final results).
53
+ *
54
+ * May return partial first line if the last line of original content exceeds byte limit.
55
+ */
56
+ export declare function truncateTail(content: string, options?: TruncationOptions): TruncationResult;
57
+ //# sourceMappingURL=truncate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/tools/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAChC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IACjC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CA4E/F;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAqE/F","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines (except bash tail truncation edge case).\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\n\nexport interface TruncationResult {\n\t/** The truncated content */\n\tcontent: string;\n\t/** Whether truncation occurred */\n\ttruncated: boolean;\n\t/** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\t/** Total number of lines in the original content */\n\ttotalLines: number;\n\t/** Total number of bytes in the original content */\n\ttotalBytes: number;\n\t/** Number of complete lines in the truncated output */\n\toutputLines: number;\n\t/** Number of bytes in the truncated output */\n\toutputBytes: number;\n\t/** Whether the last line was partially truncated (only for tail truncation edge case) */\n\tlastLinePartial: boolean;\n\t/** Whether the first line exceeded the byte limit (for head truncation) */\n\tfirstLineExceedsLimit: boolean;\n}\n\nexport interface TruncationOptions {\n\t/** Maximum number of lines (default: 2000) */\n\tmaxLines?: number;\n\t/** Maximum number of bytes (default: 50KB) */\n\tmaxBytes?: number;\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n\tif (bytes < 1024) {\n\t\treturn `${bytes}B`;\n\t} else if (bytes < 1024 * 1024) {\n\t\treturn `${(bytes / 1024).toFixed(1)}KB`;\n\t} else {\n\t\treturn `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\t}\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = content.split(\"\\n\");\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t};\n\t}\n\n\t// Check if first line alone exceeds byte limit\n\tconst firstLineBytes = Buffer.byteLength(lines[0], \"utf-8\");\n\tif (firstLineBytes > maxBytes) {\n\t\treturn {\n\t\t\tcontent: \"\",\n\t\t\ttruncated: true,\n\t\t\ttruncatedBy: \"bytes\",\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: 0,\n\t\t\toutputBytes: 0,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: true,\n\t\t};\n\t}\n\n\t// Collect complete lines that fit\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n\tfor (let i = 0; i < lines.length && i < maxLines; i++) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (i > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.push(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial: false,\n\t\tfirstLineExceedsLimit: false,\n\t};\n}\n\n/**\n * Truncate content from the tail (keep last N lines/bytes).\n * Suitable for bash output where you want to see the end (errors, final results).\n *\n * May return partial first line if the last line of original content exceeds byte limit.\n */\nexport function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = content.split(\"\\n\");\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t};\n\t}\n\n\t// Work backwards from the end\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\tlet lastLinePartial = false;\n\n\tfor (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\t// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,\n\t\t\t// take the end of the line (partial)\n\t\t\tif (outputLinesArr.length === 0) {\n\t\t\t\tconst truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);\n\t\t\t\toutputLinesArr.unshift(truncatedLine);\n\t\t\t\toutputBytesCount = Buffer.byteLength(truncatedLine, \"utf-8\");\n\t\t\t\tlastLinePartial = true;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.unshift(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial,\n\t\tfirstLineExceedsLimit: false,\n\t};\n}\n\n/**\n * Truncate a string to fit within a byte limit (from the end).\n * Handles multi-byte UTF-8 characters correctly.\n */\nfunction truncateStringToBytesFromEnd(str: string, maxBytes: number): string {\n\tconst buf = Buffer.from(str, \"utf-8\");\n\tif (buf.length <= maxBytes) {\n\t\treturn str;\n\t}\n\n\t// Start from the end, skip maxBytes back\n\tlet start = buf.length - maxBytes;\n\n\t// Find a valid UTF-8 boundary (start of a character)\n\twhile (start < buf.length && (buf[start] & 0xc0) === 0x80) {\n\t\tstart++;\n\t}\n\n\treturn buf.slice(start).toString(\"utf-8\");\n}\n"]}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Shared truncation utilities for tool outputs.
3
+ *
4
+ * Truncation is based on two independent limits - whichever is hit first wins:
5
+ * - Line limit (default: 2000 lines)
6
+ * - Byte limit (default: 50KB)
7
+ *
8
+ * Never returns partial lines (except bash tail truncation edge case).
9
+ */
10
+ export const DEFAULT_MAX_LINES = 2000;
11
+ export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
12
+ /**
13
+ * Format bytes as human-readable size.
14
+ */
15
+ export function formatSize(bytes) {
16
+ if (bytes < 1024) {
17
+ return `${bytes}B`;
18
+ }
19
+ else if (bytes < 1024 * 1024) {
20
+ return `${(bytes / 1024).toFixed(1)}KB`;
21
+ }
22
+ else {
23
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
24
+ }
25
+ }
26
+ /**
27
+ * Truncate content from the head (keep first N lines/bytes).
28
+ * Suitable for file reads where you want to see the beginning.
29
+ *
30
+ * Never returns partial lines. If first line exceeds byte limit,
31
+ * returns empty content with firstLineExceedsLimit=true.
32
+ */
33
+ export function truncateHead(content, options = {}) {
34
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
35
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
36
+ const totalBytes = Buffer.byteLength(content, "utf-8");
37
+ const lines = content.split("\n");
38
+ const totalLines = lines.length;
39
+ // Check if no truncation needed
40
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
41
+ return {
42
+ content,
43
+ truncated: false,
44
+ truncatedBy: null,
45
+ totalLines,
46
+ totalBytes,
47
+ outputLines: totalLines,
48
+ outputBytes: totalBytes,
49
+ lastLinePartial: false,
50
+ firstLineExceedsLimit: false,
51
+ };
52
+ }
53
+ // Check if first line alone exceeds byte limit
54
+ const firstLineBytes = Buffer.byteLength(lines[0], "utf-8");
55
+ if (firstLineBytes > maxBytes) {
56
+ return {
57
+ content: "",
58
+ truncated: true,
59
+ truncatedBy: "bytes",
60
+ totalLines,
61
+ totalBytes,
62
+ outputLines: 0,
63
+ outputBytes: 0,
64
+ lastLinePartial: false,
65
+ firstLineExceedsLimit: true,
66
+ };
67
+ }
68
+ // Collect complete lines that fit
69
+ const outputLinesArr = [];
70
+ let outputBytesCount = 0;
71
+ let truncatedBy = "lines";
72
+ for (let i = 0; i < lines.length && i < maxLines; i++) {
73
+ const line = lines[i];
74
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0); // +1 for newline
75
+ if (outputBytesCount + lineBytes > maxBytes) {
76
+ truncatedBy = "bytes";
77
+ break;
78
+ }
79
+ outputLinesArr.push(line);
80
+ outputBytesCount += lineBytes;
81
+ }
82
+ // If we exited due to line limit
83
+ if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
84
+ truncatedBy = "lines";
85
+ }
86
+ const outputContent = outputLinesArr.join("\n");
87
+ const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
88
+ return {
89
+ content: outputContent,
90
+ truncated: true,
91
+ truncatedBy,
92
+ totalLines,
93
+ totalBytes,
94
+ outputLines: outputLinesArr.length,
95
+ outputBytes: finalOutputBytes,
96
+ lastLinePartial: false,
97
+ firstLineExceedsLimit: false,
98
+ };
99
+ }
100
+ /**
101
+ * Truncate content from the tail (keep last N lines/bytes).
102
+ * Suitable for bash output where you want to see the end (errors, final results).
103
+ *
104
+ * May return partial first line if the last line of original content exceeds byte limit.
105
+ */
106
+ export function truncateTail(content, options = {}) {
107
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
108
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
109
+ const totalBytes = Buffer.byteLength(content, "utf-8");
110
+ const lines = content.split("\n");
111
+ const totalLines = lines.length;
112
+ // Check if no truncation needed
113
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
114
+ return {
115
+ content,
116
+ truncated: false,
117
+ truncatedBy: null,
118
+ totalLines,
119
+ totalBytes,
120
+ outputLines: totalLines,
121
+ outputBytes: totalBytes,
122
+ lastLinePartial: false,
123
+ firstLineExceedsLimit: false,
124
+ };
125
+ }
126
+ // Work backwards from the end
127
+ const outputLinesArr = [];
128
+ let outputBytesCount = 0;
129
+ let truncatedBy = "lines";
130
+ let lastLinePartial = false;
131
+ for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
132
+ const line = lines[i];
133
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline
134
+ if (outputBytesCount + lineBytes > maxBytes) {
135
+ truncatedBy = "bytes";
136
+ // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,
137
+ // take the end of the line (partial)
138
+ if (outputLinesArr.length === 0) {
139
+ const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
140
+ outputLinesArr.unshift(truncatedLine);
141
+ outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
142
+ lastLinePartial = true;
143
+ }
144
+ break;
145
+ }
146
+ outputLinesArr.unshift(line);
147
+ outputBytesCount += lineBytes;
148
+ }
149
+ // If we exited due to line limit
150
+ if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
151
+ truncatedBy = "lines";
152
+ }
153
+ const outputContent = outputLinesArr.join("\n");
154
+ const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
155
+ return {
156
+ content: outputContent,
157
+ truncated: true,
158
+ truncatedBy,
159
+ totalLines,
160
+ totalBytes,
161
+ outputLines: outputLinesArr.length,
162
+ outputBytes: finalOutputBytes,
163
+ lastLinePartial,
164
+ firstLineExceedsLimit: false,
165
+ };
166
+ }
167
+ /**
168
+ * Truncate a string to fit within a byte limit (from the end).
169
+ * Handles multi-byte UTF-8 characters correctly.
170
+ */
171
+ function truncateStringToBytesFromEnd(str, maxBytes) {
172
+ const buf = Buffer.from(str, "utf-8");
173
+ if (buf.length <= maxBytes) {
174
+ return str;
175
+ }
176
+ // Start from the end, skip maxBytes back
177
+ let start = buf.length - maxBytes;
178
+ // Find a valid UTF-8 boundary (start of a character)
179
+ while (start < buf.length && (buf[start] & 0xc0) === 0x80) {
180
+ start++;
181
+ }
182
+ return buf.slice(start).toString("utf-8");
183
+ }
184
+ //# sourceMappingURL=truncate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../src/tools/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AACtC,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,OAAO;AA8BnD;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa,EAAU;IACjD,IAAI,KAAK,GAAG,IAAI,EAAE,CAAC;QAClB,OAAO,GAAG,KAAK,GAAG,CAAC;IACpB,CAAC;SAAM,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;QAChC,OAAO,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IACzC,CAAC;SAAM,CAAC;QACP,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IAClD,CAAC;AAAA,CACD;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,OAAO,GAAsB,EAAE,EAAoB;IAChG,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IACvD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IAEvD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC;IAEhC,gCAAgC;IAChC,IAAI,UAAU,IAAI,QAAQ,IAAI,UAAU,IAAI,QAAQ,EAAE,CAAC;QACtD,OAAO;YACN,OAAO;YACP,SAAS,EAAE,KAAK;YAChB,WAAW,EAAE,IAAI;YACjB,UAAU;YACV,UAAU;YACV,WAAW,EAAE,UAAU;YACvB,WAAW,EAAE,UAAU;YACvB,eAAe,EAAE,KAAK;YACtB,qBAAqB,EAAE,KAAK;SAC5B,CAAC;IACH,CAAC;IAED,+CAA+C;IAC/C,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC5D,IAAI,cAAc,GAAG,QAAQ,EAAE,CAAC;QAC/B,OAAO;YACN,OAAO,EAAE,EAAE;YACX,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,OAAO;YACpB,UAAU;YACV,UAAU;YACV,WAAW,EAAE,CAAC;YACd,WAAW,EAAE,CAAC;YACd,eAAe,EAAE,KAAK;YACtB,qBAAqB,EAAE,IAAI;SAC3B,CAAC;IACH,CAAC;IAED,kCAAkC;IAClC,MAAM,cAAc,GAAa,EAAE,CAAC;IACpC,IAAI,gBAAgB,GAAG,CAAC,CAAC;IACzB,IAAI,WAAW,GAAsB,OAAO,CAAC;IAE7C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QACvD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB;QAEvF,IAAI,gBAAgB,GAAG,SAAS,GAAG,QAAQ,EAAE,CAAC;YAC7C,WAAW,GAAG,OAAO,CAAC;YACtB,MAAM;QACP,CAAC;QAED,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1B,gBAAgB,IAAI,SAAS,CAAC;IAC/B,CAAC;IAED,iCAAiC;IACjC,IAAI,cAAc,CAAC,MAAM,IAAI,QAAQ,IAAI,gBAAgB,IAAI,QAAQ,EAAE,CAAC;QACvE,WAAW,GAAG,OAAO,CAAC;IACvB,CAAC;IAED,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,gBAAgB,GAAG,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;IAEnE,OAAO;QACN,OAAO,EAAE,aAAa;QACtB,SAAS,EAAE,IAAI;QACf,WAAW;QACX,UAAU;QACV,UAAU;QACV,WAAW,EAAE,cAAc,CAAC,MAAM;QAClC,WAAW,EAAE,gBAAgB;QAC7B,eAAe,EAAE,KAAK;QACtB,qBAAqB,EAAE,KAAK;KAC5B,CAAC;AAAA,CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,OAAO,GAAsB,EAAE,EAAoB;IAChG,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IACvD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IAEvD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC;IAEhC,gCAAgC;IAChC,IAAI,UAAU,IAAI,QAAQ,IAAI,UAAU,IAAI,QAAQ,EAAE,CAAC;QACtD,OAAO;YACN,OAAO;YACP,SAAS,EAAE,KAAK;YAChB,WAAW,EAAE,IAAI;YACjB,UAAU;YACV,UAAU;YACV,WAAW,EAAE,UAAU;YACvB,WAAW,EAAE,UAAU;YACvB,eAAe,EAAE,KAAK;YACtB,qBAAqB,EAAE,KAAK;SAC5B,CAAC;IACH,CAAC;IAED,8BAA8B;IAC9B,MAAM,cAAc,GAAa,EAAE,CAAC;IACpC,IAAI,gBAAgB,GAAG,CAAC,CAAC;IACzB,IAAI,WAAW,GAAsB,OAAO,CAAC;IAC7C,IAAI,eAAe,GAAG,KAAK,CAAC;IAE5B,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,cAAc,CAAC,MAAM,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QAChF,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB;QAE3G,IAAI,gBAAgB,GAAG,SAAS,GAAG,QAAQ,EAAE,CAAC;YAC7C,WAAW,GAAG,OAAO,CAAC;YACtB,+EAA+E;YAC/E,qCAAqC;YACrC,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACjC,MAAM,aAAa,GAAG,4BAA4B,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;gBACnE,cAAc,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;gBACtC,gBAAgB,GAAG,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;gBAC7D,eAAe,GAAG,IAAI,CAAC;YACxB,CAAC;YACD,MAAM;QACP,CAAC;QAED,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7B,gBAAgB,IAAI,SAAS,CAAC;IAC/B,CAAC;IAED,iCAAiC;IACjC,IAAI,cAAc,CAAC,MAAM,IAAI,QAAQ,IAAI,gBAAgB,IAAI,QAAQ,EAAE,CAAC;QACvE,WAAW,GAAG,OAAO,CAAC;IACvB,CAAC;IAED,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,gBAAgB,GAAG,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;IAEnE,OAAO;QACN,OAAO,EAAE,aAAa;QACtB,SAAS,EAAE,IAAI;QACf,WAAW;QACX,UAAU;QACV,UAAU;QACV,WAAW,EAAE,cAAc,CAAC,MAAM;QAClC,WAAW,EAAE,gBAAgB;QAC7B,eAAe;QACf,qBAAqB,EAAE,KAAK;KAC5B,CAAC;AAAA,CACF;AAED;;;GAGG;AACH,SAAS,4BAA4B,CAAC,GAAW,EAAE,QAAgB,EAAU;IAC5E,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACtC,IAAI,GAAG,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC;IACZ,CAAC;IAED,yCAAyC;IACzC,IAAI,KAAK,GAAG,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC;IAElC,qDAAqD;IACrD,OAAO,KAAK,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC3D,KAAK,EAAE,CAAC;IACT,CAAC;IAED,OAAO,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAAA,CAC1C","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines (except bash tail truncation edge case).\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\n\nexport interface TruncationResult {\n\t/** The truncated content */\n\tcontent: string;\n\t/** Whether truncation occurred */\n\ttruncated: boolean;\n\t/** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\t/** Total number of lines in the original content */\n\ttotalLines: number;\n\t/** Total number of bytes in the original content */\n\ttotalBytes: number;\n\t/** Number of complete lines in the truncated output */\n\toutputLines: number;\n\t/** Number of bytes in the truncated output */\n\toutputBytes: number;\n\t/** Whether the last line was partially truncated (only for tail truncation edge case) */\n\tlastLinePartial: boolean;\n\t/** Whether the first line exceeded the byte limit (for head truncation) */\n\tfirstLineExceedsLimit: boolean;\n}\n\nexport interface TruncationOptions {\n\t/** Maximum number of lines (default: 2000) */\n\tmaxLines?: number;\n\t/** Maximum number of bytes (default: 50KB) */\n\tmaxBytes?: number;\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n\tif (bytes < 1024) {\n\t\treturn `${bytes}B`;\n\t} else if (bytes < 1024 * 1024) {\n\t\treturn `${(bytes / 1024).toFixed(1)}KB`;\n\t} else {\n\t\treturn `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\t}\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = content.split(\"\\n\");\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t};\n\t}\n\n\t// Check if first line alone exceeds byte limit\n\tconst firstLineBytes = Buffer.byteLength(lines[0], \"utf-8\");\n\tif (firstLineBytes > maxBytes) {\n\t\treturn {\n\t\t\tcontent: \"\",\n\t\t\ttruncated: true,\n\t\t\ttruncatedBy: \"bytes\",\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: 0,\n\t\t\toutputBytes: 0,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: true,\n\t\t};\n\t}\n\n\t// Collect complete lines that fit\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n\tfor (let i = 0; i < lines.length && i < maxLines; i++) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (i > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.push(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial: false,\n\t\tfirstLineExceedsLimit: false,\n\t};\n}\n\n/**\n * Truncate content from the tail (keep last N lines/bytes).\n * Suitable for bash output where you want to see the end (errors, final results).\n *\n * May return partial first line if the last line of original content exceeds byte limit.\n */\nexport function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = Buffer.byteLength(content, \"utf-8\");\n\tconst lines = content.split(\"\\n\");\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t};\n\t}\n\n\t// Work backwards from the end\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\tlet lastLinePartial = false;\n\n\tfor (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = Buffer.byteLength(line, \"utf-8\") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\t// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,\n\t\t\t// take the end of the line (partial)\n\t\t\tif (outputLinesArr.length === 0) {\n\t\t\t\tconst truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);\n\t\t\t\toutputLinesArr.unshift(truncatedLine);\n\t\t\t\toutputBytesCount = Buffer.byteLength(truncatedLine, \"utf-8\");\n\t\t\t\tlastLinePartial = true;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.unshift(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// If we exited due to line limit\n\tif (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {\n\t\ttruncatedBy = \"lines\";\n\t}\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = Buffer.byteLength(outputContent, \"utf-8\");\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial,\n\t\tfirstLineExceedsLimit: false,\n\t};\n}\n\n/**\n * Truncate a string to fit within a byte limit (from the end).\n * Handles multi-byte UTF-8 characters correctly.\n */\nfunction truncateStringToBytesFromEnd(str: string, maxBytes: number): string {\n\tconst buf = Buffer.from(str, \"utf-8\");\n\tif (buf.length <= maxBytes) {\n\t\treturn str;\n\t}\n\n\t// Start from the end, skip maxBytes back\n\tlet start = buf.length - maxBytes;\n\n\t// Find a valid UTF-8 boundary (start of a character)\n\twhile (start < buf.length && (buf[start] & 0xc0) === 0x80) {\n\t\tstart++;\n\t}\n\n\treturn buf.slice(start).toString(\"utf-8\");\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-mom",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Slack bot that delegates messages to the pi coding agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@anthropic-ai/sandbox-runtime": "^0.0.16",
24
- "@mariozechner/pi-agent-core": "^0.16.0",
25
- "@mariozechner/pi-ai": "^0.16.0",
24
+ "@mariozechner/pi-agent-core": "^0.18.0",
25
+ "@mariozechner/pi-ai": "^0.18.0",
26
26
  "@sinclair/typebox": "^0.34.0",
27
27
  "@slack/socket-mode": "^2.0.0",
28
28
  "@slack/web-api": "^7.0.0",