From 341aa46f78a6f3d135e9893cb5e66672330c417c Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Sat, 2 May 2026 20:59:24 -0700 Subject: [PATCH] Strip ANSI escapes from bash commands across all providers Use strip-ansi (already in dep tree via Ink) in extractBashCommands to prevent terminal escape codes from leaking into dashboard bash breakdown keys. Route goose, gemini, qwen, and openclaw through extractBashCommands instead of inline split, which also gives them multi-command extraction (matching claude/codex/droid behavior). --- package-lock.json | 3 ++- package.json | 3 ++- src/bash-utils.ts | 6 ++++-- src/providers/gemini.ts | 4 ++-- src/providers/goose.ts | 6 ++++-- src/providers/openclaw.ts | 4 ++-- src/providers/qwen.ts | 4 ++-- 7 files changed, 18 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9163725..fc1cf6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "chalk": "^5.4.1", "commander": "^13.1.0", "ink": "^7.0.0", - "react": "^19.2.5" + "react": "^19.2.5", + "strip-ansi": "^7.2.0" }, "bin": { "codeburn": "dist/cli.js" diff --git a/package.json b/package.json index fec34af..718d763 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "chalk": "^5.4.1", "commander": "^13.1.0", "ink": "^7.0.0", - "react": "^19.2.5" + "react": "^19.2.5", + "strip-ansi": "^7.2.0" }, "devDependencies": { "@types/node": "^22.19.17", diff --git a/src/bash-utils.ts b/src/bash-utils.ts index c578972..2e5fe0d 100644 --- a/src/bash-utils.ts +++ b/src/bash-utils.ts @@ -1,12 +1,14 @@ import { basename } from 'path' +import stripAnsi from 'strip-ansi' function stripQuotedStrings(command: string): string { return command.replace(/"[^"]*"|'[^']*'/g, match => ' '.repeat(match.length)) } -export function extractBashCommands(command: string): string[] { - if (!command || !command.trim()) return [] +export function extractBashCommands(rawCommand: string): string[] { + if (!rawCommand || !rawCommand.trim()) return [] + const command = stripAnsi(rawCommand) const stripped = stripQuotedStrings(command) const separatorRegex = /\s*(?:&&|;|\|)\s*/g diff --git a/src/providers/gemini.ts b/src/providers/gemini.ts index 48b3107..d00f0dc 100644 --- a/src/providers/gemini.ts +++ b/src/providers/gemini.ts @@ -3,6 +3,7 @@ import { join } from 'path' import { homedir } from 'os' import { calculateCost } from '../models.js' +import { extractBashCommands } from '../bash-utils.js' import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js' const toolNameMap: Record = { @@ -93,8 +94,7 @@ function parseSession(data: GeminiSession, seenKeys: Set): ParsedProvide const mapped = toolNameMap[tc.displayName ?? ''] ?? toolNameMap[tc.name] ?? tc.displayName ?? tc.name allTools.push(mapped) if (mapped === 'Bash' && tc.args && typeof tc.args.command === 'string') { - const cmd = tc.args.command.split(/\s+/)[0] ?? '' - if (cmd) bashCommands.push(cmd) + bashCommands.push(...extractBashCommands(tc.args.command)) } } } diff --git a/src/providers/goose.ts b/src/providers/goose.ts index b46fa13..27f0c03 100644 --- a/src/providers/goose.ts +++ b/src/providers/goose.ts @@ -2,6 +2,7 @@ import { join } from 'path' import { homedir, platform } from 'os' import { calculateCost, getShortModelName } from '../models.js' +import { extractBashCommands } from '../bash-utils.js' import { isSqliteAvailable, getSqliteLoadError, openDatabase, type SqliteDatabase } from '../sqlite.js' import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js' @@ -109,8 +110,9 @@ function extractToolsFromMessages(db: SqliteDatabase, sessionId: string): { tool if (mapped === 'Bash') { const cmd = item.toolCall?.value?.arguments?.command if (typeof cmd === 'string') { - const first = cmd.split(/\s+/)[0] ?? '' - if (first && !bashCommands.includes(first)) bashCommands.push(first) + for (const c of extractBashCommands(cmd)) { + if (!bashCommands.includes(c)) bashCommands.push(c) + } } } } diff --git a/src/providers/openclaw.ts b/src/providers/openclaw.ts index 14575df..bc6da53 100644 --- a/src/providers/openclaw.ts +++ b/src/providers/openclaw.ts @@ -4,6 +4,7 @@ import { homedir } from 'os' import { readSessionFile } from '../fs-utils.js' import { calculateCost } from '../models.js' +import { extractBashCommands } from '../bash-utils.js' import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js' const toolNameMap: Record = { @@ -78,8 +79,7 @@ function extractTools(content: Array<{ type?: string; name?: string; arguments?: const mapped = toolNameMap[block.name] ?? block.name tools.push(mapped) if (mapped === 'Bash' && block.arguments && typeof block.arguments.command === 'string') { - const cmd = block.arguments.command.split(/\s+/)[0] ?? '' - if (cmd) bashCommands.push(cmd) + bashCommands.push(...extractBashCommands(block.arguments.command)) } } } diff --git a/src/providers/qwen.ts b/src/providers/qwen.ts index 3b61ce4..427b5fd 100644 --- a/src/providers/qwen.ts +++ b/src/providers/qwen.ts @@ -4,6 +4,7 @@ import { homedir } from 'os' import { readSessionFile } from '../fs-utils.js' import { calculateCost } from '../models.js' +import { extractBashCommands } from '../bash-utils.js' import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js' const toolNameMap: Record = { @@ -66,8 +67,7 @@ function extractTools(parts: QwenPart[]): { tools: string[]; bashCommands: strin const mapped = toolNameMap[part.functionCall.name] ?? part.functionCall.name tools.push(mapped) if (mapped === 'Bash' && part.functionCall.args && typeof part.functionCall.args['command'] === 'string') { - const cmd = (part.functionCall.args['command'] as string).split(/\s+/)[0] ?? '' - if (cmd) bashCommands.push(cmd) + bashCommands.push(...extractBashCommands(part.functionCall.args['command'] as string)) } } }