mom: add centralized logging, usage tracking, and improve prompt caching

Major improvements to mom's logging and cost reporting:

Centralized Logging System:
- Add src/log.ts with type-safe logging functions
- Colored console output (green=user, yellow=mom, dim=details)
- Consistent format: [HH:MM:SS] [context] message
- Replace scattered console.log/error calls throughout codebase

Usage Tracking & Cost Reporting:
- Track tokens (input, output, cache read/write) and costs per run
- Display summary at end of each run in console and Slack thread
- Example: 💰 Usage: 12,543 in + 847 out (5,234 cache read) = $0.0234

Prompt Caching Optimization:
- Move recent messages from system prompt to user message
- System prompt now mostly static (only changes with memory files)
- Enables effective use of Anthropic's prompt caching
- Significantly reduces costs on subsequent requests

Model & Cost Improvements:
- Switch from Claude Opus 4.5 to Sonnet 4.5 (~40% cost reduction)
- Fix Claude Opus 4.5 cache pricing in ai package (was 3x too expensive)
- Add manual override in generate-models.ts until upstream fix merges
- Submitted PR to models.dev: https://github.com/sst/models.dev/pull/439

UI/UX Improvements:
- Extract actual text from tool results instead of JSON wrapper
- Cleaner Slack thread formatting with duration and labels
- Tool args formatting shows paths with offset:limit notation
- Add chalk for colored terminal output

Dependencies:
- Add chalk package for terminal colors
This commit is contained in:
Mario Zechner 2025-11-26 18:04:16 +01:00
parent 82d4ac93e1
commit 213bc4df1c
11 changed files with 478 additions and 63 deletions

View file

@ -2,12 +2,14 @@ import { SocketModeClient } from "@slack/socket-mode";
import { WebClient } from "@slack/web-api";
import { readFileSync } from "fs";
import { basename } from "path";
import * as log from "./log.js";
import { type Attachment, ChannelStore } from "./store.js";
export interface SlackMessage {
text: string; // message content (mentions stripped)
rawText: string; // original text with mentions
user: string; // user ID
userName?: string; // user handle
channel: string; // channel ID
ts: string; // timestamp (for threading)
attachments: Attachment[]; // file attachments
@ -15,6 +17,7 @@ export interface SlackMessage {
export interface SlackContext {
message: SlackMessage;
channelName?: string; // channel name for logging (e.g., #dev-team)
store: ChannelStore;
/** Send/update the main message (accumulates text) */
respond(text: string): Promise<void>;
@ -92,7 +95,7 @@ export class MomBot {
// Log the mention (message event may not fire for app_mention)
await this.logMessage(slackEvent);
const ctx = this.createContext(slackEvent);
const ctx = await this.createContext(slackEvent);
await this.handler.onChannelMention(ctx);
});
@ -133,7 +136,7 @@ export class MomBot {
// Only trigger handler for DMs (channel mentions are handled by app_mention event)
if (slackEvent.channel_type === "im") {
const ctx = this.createContext({
const ctx = await this.createContext({
text: slackEvent.text || "",
channel: slackEvent.channel,
user: slackEvent.user,
@ -167,16 +170,30 @@ export class MomBot {
});
}
private createContext(event: {
private async createContext(event: {
text: string;
channel: string;
user: string;
ts: string;
files?: Array<{ name: string; url_private_download?: string; url_private?: string }>;
}): SlackContext {
}): Promise<SlackContext> {
const rawText = event.text;
const text = rawText.replace(/<@[A-Z0-9]+>/gi, "").trim();
// Get user info for logging
const { userName } = await this.getUserInfo(event.user);
// Get channel name for logging (best effort)
let channelName: string | undefined;
try {
if (event.channel.startsWith("C")) {
const result = await this.webClient.conversations.info({ channel: event.channel });
channelName = result.channel?.name ? `#${result.channel.name}` : undefined;
}
} catch {
// Ignore errors - we'll just use the channel ID
}
// Process attachments (for context, already logged by message handler)
const attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];
@ -191,10 +208,12 @@ export class MomBot {
text,
rawText,
user: event.user,
userName,
channel: event.channel,
ts: event.ts,
attachments,
},
channelName,
store: this.store,
respond: async (responseText: string) => {
// Queue updates to avoid race conditions
@ -276,11 +295,11 @@ export class MomBot {
const auth = await this.webClient.auth.test();
this.botUserId = auth.user_id as string;
await this.socketClient.start();
console.log("⚡️ Mom bot connected and listening!");
log.logConnected();
}
async stop(): Promise<void> {
await this.socketClient.disconnect();
console.log("Mom bot disconnected.");
log.logDisconnected();
}
}