* Parses the "pretty-print" stdout format emitted by the Copilot CLI when * debug logs are written to a --log-dir directory (not captured on stdout). * This format uses ✗ for failed tool calls and ● or ✓ for successful ones. * @param {string} logContent - Raw log content as a string * @returns {
(logContent)
| 215 | * @returns {Array} Array of log entries in structured format, or empty array if not detected |
| 216 | */ |
| 217 | function parsePrettyPrintFormat(logContent) { |
| 218 | // Only attempt this format if the characteristic markers are present |
| 219 | if (!/^[✗●✓]/m.test(logContent)) { |
| 220 | return []; |
| 221 | } |
| 222 | |
| 223 | const INFRA_LINE_RE = AWF_INFRA_LINE_RE; |
| 224 | const FAILED_TOOL_RE = /^✗\s+(\S+)/; |
| 225 | const SUCCESS_TOOL_RE = /^(?:●|✓)\s+(\S+)/; |
| 226 | const CONTINUATION_RE = /^\s+[└│]/; |
| 227 | const DEEP_INDENT_RE = /^ {4,}/; |
| 228 | const MODEL_BREAKDOWN_RE = /^Breakdown by AI model:/; |
| 229 | const MODEL_LINE_RE = /^ +(\S+)\s+([\d.]+k?)\s+in,\s+([\d.]+k?)\s+out(?:,\s+([\d.]+k?)\s+cached)?/; |
| 230 | // Recognise both legacy ("Total usage est:" / "API time spent:" / …) and the |
| 231 | // newer Copilot CLI footer ("Changes +N -N", "Duration Ns", "Tokens ↑N ↓N (cached)"). |
| 232 | // The newer footer omits a colon and uses arrow glyphs, so we extend the regex rather than |
| 233 | // relying on the legacy "Total …:" prefix alone. |
| 234 | const USAGE_LINES_RE = /^(?:Total usage est:|API time spent:|Total session time:|Total code changes:|Changes\s+[+-]?\d|Duration\s+\d|Tokens\s+[↑↓])/; |
| 235 | |
| 236 | const parseTokenCount = s => { |
| 237 | const n = parseFloat(s); |
| 238 | return s.endsWith("k") ? Math.round(n * 1000) : n; |
| 239 | }; |
| 240 | |
| 241 | const lines = logContent.split("\n").filter(line => !INFRA_LINE_RE.test(line)); |
| 242 | |
| 243 | const toolEntries = []; |
| 244 | const agentTextLines = []; |
| 245 | let inputTokens = 0; |
| 246 | let outputTokens = 0; |
| 247 | let cacheReadTokens = 0; |
| 248 | let modelName = "unknown"; |
| 249 | let inModelBreakdown = false; |
| 250 | let i = 0; |
| 251 | |
| 252 | while (i < lines.length) { |
| 253 | const line = lines[i]; |
| 254 | const trimmed = line.trim(); |
| 255 | |
| 256 | if (!trimmed) { |
| 257 | inModelBreakdown = false; |
| 258 | i++; |
| 259 | continue; |
| 260 | } |
| 261 | |
| 262 | // Failed tool call: ✗ tool_name |
| 263 | const failedMatch = trimmed.match(FAILED_TOOL_RE); |
| 264 | if (failedMatch) { |
| 265 | inModelBreakdown = false; |
| 266 | const toolName = failedMatch[1]; |
| 267 | const outputLines = []; |
| 268 | i++; |
| 269 | while (i < lines.length && (CONTINUATION_RE.test(lines[i]) || DEEP_INDENT_RE.test(lines[i]))) { |
| 270 | outputLines.push(lines[i].replace(/^\s*[└│]\s?/, "").replace(/^ {4}/, "")); |
| 271 | i++; |
| 272 | } |
| 273 | toolEntries.push({ name: toolName, success: false, output: outputLines.join("\n").trim() }); |
| 274 | continue; |
no test coverage detected