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).
This commit is contained in:
iamtoruk 2026-05-02 20:59:24 -07:00
parent 292265bf47
commit 341aa46f78
7 changed files with 18 additions and 12 deletions

3
package-lock.json generated
View file

@ -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"

View file

@ -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",

View file

@ -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

View file

@ -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<string, string> = {
@ -93,8 +94,7 @@ function parseSession(data: GeminiSession, seenKeys: Set<string>): 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))
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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<string, string> = {
@ -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))
}
}
}

View file

@ -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<string, string> = {
@ -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))
}
}
}