mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 22:03:45 +00:00
271 lines
8.9 KiB
TypeScript
271 lines
8.9 KiB
TypeScript
import chalk from "chalk";
|
|
|
|
export interface LogContext {
|
|
channelId: string;
|
|
userName?: string;
|
|
channelName?: string; // For display like #dev-team vs C16HET4EQ
|
|
}
|
|
|
|
function timestamp(): string {
|
|
const now = new Date();
|
|
const hh = String(now.getHours()).padStart(2, "0");
|
|
const mm = String(now.getMinutes()).padStart(2, "0");
|
|
const ss = String(now.getSeconds()).padStart(2, "0");
|
|
return `[${hh}:${mm}:${ss}]`;
|
|
}
|
|
|
|
function formatContext(ctx: LogContext): string {
|
|
// DMs: [DM:username]
|
|
// Channels: [#channel-name:username] or [C16HET4EQ:username] if no name
|
|
if (ctx.channelId.startsWith("D")) {
|
|
return `[DM:${ctx.userName || ctx.channelId}]`;
|
|
}
|
|
const channel = ctx.channelName || ctx.channelId;
|
|
const user = ctx.userName || "unknown";
|
|
return `[${channel.startsWith("#") ? channel : `#${channel}`}:${user}]`;
|
|
}
|
|
|
|
function truncate(text: string, maxLen: number): string {
|
|
if (text.length <= maxLen) return text;
|
|
return `${text.substring(0, maxLen)}\n(truncated at ${maxLen} chars)`;
|
|
}
|
|
|
|
function formatToolArgs(args: Record<string, unknown>): string {
|
|
const lines: string[] = [];
|
|
|
|
for (const [key, value] of Object.entries(args)) {
|
|
// Skip the label - it's already shown in the tool name
|
|
if (key === "label") continue;
|
|
|
|
// For read tool, format path with offset/limit
|
|
if (key === "path" && typeof value === "string") {
|
|
const offset = args.offset as number | undefined;
|
|
const limit = args.limit as number | undefined;
|
|
if (offset !== undefined && limit !== undefined) {
|
|
lines.push(`${value}:${offset}-${offset + limit}`);
|
|
} else {
|
|
lines.push(value);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Skip offset/limit since we already handled them
|
|
if (key === "offset" || key === "limit") continue;
|
|
|
|
// For other values, format them
|
|
if (typeof value === "string") {
|
|
// Multi-line strings get indented
|
|
if (value.includes("\n")) {
|
|
lines.push(value);
|
|
} else {
|
|
lines.push(value);
|
|
}
|
|
} else {
|
|
lines.push(JSON.stringify(value));
|
|
}
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
// User messages
|
|
export function logUserMessage(ctx: LogContext, text: string): void {
|
|
console.log(chalk.green(`${timestamp()} ${formatContext(ctx)} ${text}`));
|
|
}
|
|
|
|
// Tool execution
|
|
export function logToolStart(ctx: LogContext, toolName: string, label: string, args: Record<string, unknown>): void {
|
|
const formattedArgs = formatToolArgs(args);
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));
|
|
if (formattedArgs) {
|
|
// Indent the args
|
|
const indented = formattedArgs
|
|
.split("\n")
|
|
.map((line) => ` ${line}`)
|
|
.join("\n");
|
|
console.log(chalk.dim(indented));
|
|
}
|
|
}
|
|
|
|
export function logToolSuccess(ctx: LogContext, toolName: string, durationMs: number, result: string): void {
|
|
const duration = (durationMs / 1000).toFixed(1);
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));
|
|
|
|
const truncated = truncate(result, 1000);
|
|
if (truncated) {
|
|
const indented = truncated
|
|
.split("\n")
|
|
.map((line) => ` ${line}`)
|
|
.join("\n");
|
|
console.log(chalk.dim(indented));
|
|
}
|
|
}
|
|
|
|
export function logToolError(ctx: LogContext, toolName: string, durationMs: number, error: string): void {
|
|
const duration = (durationMs / 1000).toFixed(1);
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));
|
|
|
|
const truncated = truncate(error, 1000);
|
|
const indented = truncated
|
|
.split("\n")
|
|
.map((line) => ` ${line}`)
|
|
.join("\n");
|
|
console.log(chalk.dim(indented));
|
|
}
|
|
|
|
// Response streaming
|
|
export function logResponseStart(ctx: LogContext): void {
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));
|
|
}
|
|
|
|
export function logThinking(ctx: LogContext, thinking: string): void {
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💭 Thinking`));
|
|
const truncated = truncate(thinking, 1000);
|
|
const indented = truncated
|
|
.split("\n")
|
|
.map((line) => ` ${line}`)
|
|
.join("\n");
|
|
console.log(chalk.dim(indented));
|
|
}
|
|
|
|
export function logResponse(ctx: LogContext, text: string): void {
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💬 Response`));
|
|
const truncated = truncate(text, 1000);
|
|
const indented = truncated
|
|
.split("\n")
|
|
.map((line) => ` ${line}`)
|
|
.join("\n");
|
|
console.log(chalk.dim(indented));
|
|
}
|
|
|
|
// Attachments
|
|
export function logDownloadStart(ctx: LogContext, filename: string, localPath: string): void {
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↓ Downloading attachment`));
|
|
console.log(chalk.dim(` ${filename} → ${localPath}`));
|
|
}
|
|
|
|
export function logDownloadSuccess(ctx: LogContext, sizeKB: number): void {
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Downloaded (${sizeKB.toLocaleString()} KB)`));
|
|
}
|
|
|
|
export function logDownloadError(ctx: LogContext, filename: string, error: string): void {
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ Download failed`));
|
|
console.log(chalk.dim(` ${filename}: ${error}`));
|
|
}
|
|
|
|
// Control
|
|
export function logStopRequest(ctx: LogContext): void {
|
|
console.log(chalk.green(`${timestamp()} ${formatContext(ctx)} stop`));
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));
|
|
}
|
|
|
|
// System
|
|
export function logInfo(message: string): void {
|
|
console.log(chalk.blue(`${timestamp()} [system] ${message}`));
|
|
}
|
|
|
|
export function logWarning(message: string, details?: string): void {
|
|
console.log(chalk.yellow(`${timestamp()} [system] ⚠ ${message}`));
|
|
if (details) {
|
|
const indented = details
|
|
.split("\n")
|
|
.map((line) => ` ${line}`)
|
|
.join("\n");
|
|
console.log(chalk.dim(indented));
|
|
}
|
|
}
|
|
|
|
export function logAgentError(ctx: LogContext | "system", error: string): void {
|
|
const context = ctx === "system" ? "[system]" : formatContext(ctx);
|
|
console.log(chalk.yellow(`${timestamp()} ${context} ✗ Agent error`));
|
|
const indented = error
|
|
.split("\n")
|
|
.map((line) => ` ${line}`)
|
|
.join("\n");
|
|
console.log(chalk.dim(indented));
|
|
}
|
|
|
|
// Usage summary
|
|
export function logUsageSummary(
|
|
ctx: LogContext,
|
|
usage: {
|
|
input: number;
|
|
output: number;
|
|
cacheRead: number;
|
|
cacheWrite: number;
|
|
cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
|
|
},
|
|
contextTokens?: number,
|
|
contextWindow?: number,
|
|
): string {
|
|
const formatTokens = (count: number): string => {
|
|
if (count < 1000) return count.toString();
|
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
return `${(count / 1000000).toFixed(1)}M`;
|
|
};
|
|
|
|
const lines: string[] = [];
|
|
lines.push("*Usage Summary*");
|
|
lines.push(`Tokens: ${usage.input.toLocaleString()} in, ${usage.output.toLocaleString()} out`);
|
|
if (usage.cacheRead > 0 || usage.cacheWrite > 0) {
|
|
lines.push(`Cache: ${usage.cacheRead.toLocaleString()} read, ${usage.cacheWrite.toLocaleString()} write`);
|
|
}
|
|
if (contextTokens && contextWindow) {
|
|
const contextPercent = ((contextTokens / contextWindow) * 100).toFixed(1);
|
|
lines.push(`Context: ${formatTokens(contextTokens)} / ${formatTokens(contextWindow)} (${contextPercent}%)`);
|
|
}
|
|
lines.push(
|
|
`Cost: $${usage.cost.input.toFixed(4)} in, $${usage.cost.output.toFixed(4)} out` +
|
|
(usage.cacheRead > 0 || usage.cacheWrite > 0
|
|
? `, $${usage.cost.cacheRead.toFixed(4)} cache read, $${usage.cost.cacheWrite.toFixed(4)} cache write`
|
|
: ""),
|
|
);
|
|
lines.push(`*Total: $${usage.cost.total.toFixed(4)}*`);
|
|
|
|
const summary = lines.join("\n");
|
|
|
|
// Log to console
|
|
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💰 Usage`));
|
|
console.log(
|
|
chalk.dim(
|
|
` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +
|
|
(usage.cacheRead > 0 || usage.cacheWrite > 0
|
|
? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`
|
|
: "") +
|
|
` = $${usage.cost.total.toFixed(4)}`,
|
|
),
|
|
);
|
|
|
|
return summary;
|
|
}
|
|
|
|
// Startup (no context needed)
|
|
export function logStartup(workingDir: string, sandbox: string): void {
|
|
console.log("Starting mom bot...");
|
|
console.log(` Working directory: ${workingDir}`);
|
|
console.log(` Sandbox: ${sandbox}`);
|
|
}
|
|
|
|
export function logConnected(): void {
|
|
console.log("⚡️ Mom bot connected and listening!");
|
|
console.log("");
|
|
}
|
|
|
|
export function logDisconnected(): void {
|
|
console.log("Mom bot disconnected.");
|
|
}
|
|
|
|
// Backfill
|
|
export function logBackfillStart(channelCount: number): void {
|
|
console.log(chalk.blue(`${timestamp()} [system] Backfilling ${channelCount} channels...`));
|
|
}
|
|
|
|
export function logBackfillChannel(channelName: string, messageCount: number): void {
|
|
console.log(chalk.blue(`${timestamp()} [system] #${channelName}: ${messageCount} messages`));
|
|
}
|
|
|
|
export function logBackfillComplete(totalMessages: number, durationMs: number): void {
|
|
const duration = (durationMs / 1000).toFixed(1);
|
|
console.log(chalk.blue(`${timestamp()} [system] Backfill complete: ${totalMessages} messages in ${duration}s`));
|
|
}
|