@@ -12,6 +12,7 @@ import (
1212 "os"
1313 "os/exec"
1414 "regexp"
15+ "strconv"
1516 "strings"
1617 "syscall"
1718 "time"
@@ -255,16 +256,249 @@ func parseOutput(format string, data []byte) (map[string]any, error) {
255256 return nil , fmt .Errorf ("parsing JSON output: %w" , err )
256257 }
257258 return result , nil
259+
260+ case "jsonl" :
261+ lines := strings .Split (strings .TrimSpace (string (data )), "\n " )
262+ var parsed []any
263+ for _ , line := range lines {
264+ line = strings .TrimSpace (line )
265+ if line == "" {
266+ continue
267+ }
268+ var obj any
269+ if err := json .Unmarshal ([]byte (line ), & obj ); err != nil {
270+ return nil , fmt .Errorf ("parsing JSONL line: %w" , err )
271+ }
272+ parsed = append (parsed , obj )
273+ }
274+ return map [string ]any {"parsed_output" : parsed }, nil
275+
276+ case "csv" :
277+ return parseCsvOutput (data ), nil
278+
279+ case "xml" :
280+ return parseXmlOutput (data ), nil
281+
258282 case "text" , "" :
259- return map [string ]any {
260- "raw_output" : string (data ),
261- }, nil
283+ return map [string ]any {"raw_output" : string (data )}, nil
284+
262285 default :
263- // For xml, csv, jsonl -- fall back to raw text in the reference impl.
264- return map [string ]any {
265- "raw_output" : string (data ),
266- }, nil
286+ return map [string ]any {"raw_output" : string (data )}, nil
287+ }
288+ }
289+
290+ // parseCsvOutput parses CSV data with auto-delimiter detection and type inference.
291+ func parseCsvOutput (data []byte ) map [string ]any {
292+ raw := strings .TrimSpace (string (data ))
293+ if raw == "" {
294+ return map [string ]any {"parsed_output" : []any {}}
295+ }
296+
297+ lines := strings .Split (raw , "\n " )
298+ if len (lines ) == 0 {
299+ return map [string ]any {"parsed_output" : []any {}}
300+ }
301+
302+ // Auto-detect delimiter
303+ firstLine := lines [0 ]
304+ delimiter := ','
305+ if strings .Contains (firstLine , "\t " ) {
306+ delimiter = '\t'
307+ } else if strings .Contains (firstLine , "|" ) && ! strings .Contains (firstLine , "," ) {
308+ delimiter = '|'
309+ }
310+
311+ headers := splitCsvLine (firstLine , delimiter )
312+ for i := range headers {
313+ headers [i ] = strings .TrimSpace (headers [i ])
314+ }
315+
316+ var rows []any
317+ for _ , line := range lines [1 :] {
318+ line = strings .TrimSpace (line )
319+ if line == "" {
320+ continue
321+ }
322+ fields := splitCsvLine (line , delimiter )
323+ row := make (map [string ]any )
324+ for i , h := range headers {
325+ val := ""
326+ if i < len (fields ) {
327+ val = strings .TrimSpace (fields [i ])
328+ }
329+ // Type inference
330+ if strings .ToLower (val ) == "true" || strings .ToLower (val ) == "false" {
331+ row [h ] = strings .ToLower (val ) == "true"
332+ } else if n , err := strconv .Atoi (val ); err == nil {
333+ row [h ] = n
334+ } else if f , err := strconv .ParseFloat (val , 64 ); err == nil {
335+ row [h ] = f
336+ } else {
337+ row [h ] = val
338+ }
339+ }
340+ rows = append (rows , row )
341+ }
342+
343+ return map [string ]any {"parsed_output" : rows }
344+ }
345+
346+ // splitCsvLine splits a CSV line respecting quoted fields and escaped quotes.
347+ func splitCsvLine (line string , delimiter rune ) []string {
348+ var fields []string
349+ var current strings.Builder
350+ inQuotes := false
351+ runes := []rune (line )
352+
353+ for i := 0 ; i < len (runes ); i ++ {
354+ c := runes [i ]
355+ if c == '"' {
356+ if inQuotes && i + 1 < len (runes ) && runes [i + 1 ] == '"' {
357+ current .WriteRune ('"' )
358+ i ++ // skip escaped quote
359+ } else {
360+ inQuotes = ! inQuotes
361+ }
362+ } else if c == delimiter && ! inQuotes {
363+ fields = append (fields , current .String ())
364+ current .Reset ()
365+ } else {
366+ current .WriteRune (c )
367+ }
368+ }
369+ fields = append (fields , current .String ())
370+ return fields
371+ }
372+
373+ // parseXmlOutput parses XML data into a nested map structure.
374+ func parseXmlOutput (data []byte ) map [string ]any {
375+ raw := strings .TrimSpace (string (data ))
376+ if raw == "" {
377+ return map [string ]any {"raw_output" : "" }
378+ }
379+
380+ // Strip XML declaration
381+ if strings .HasPrefix (raw , "<?xml" ) {
382+ if idx := strings .Index (raw , "?>" ); idx != - 1 {
383+ raw = strings .TrimSpace (raw [idx + 2 :])
384+ }
385+ }
386+
387+ type stackEntry struct {
388+ name string
389+ obj map [string ]any
390+ }
391+
392+ var stack []stackEntry
393+ currentName := ""
394+ currentObj := make (map [string ]any )
395+ var textBuf strings.Builder
396+ pos := 0
397+
398+ attrRe := regexp .MustCompile (`(\w+)\s*=\s*["']([^"']*)["']` )
399+
400+ for pos < len (raw ) {
401+ if raw [pos ] == '<' {
402+ // Flush text
403+ text := strings .TrimSpace (textBuf .String ())
404+ if text != "" && currentName != "" {
405+ currentObj ["#text" ] = text
406+ }
407+ textBuf .Reset ()
408+ pos ++
409+ if pos >= len (raw ) {
410+ break
411+ }
412+
413+ if raw [pos ] == '/' {
414+ // Closing tag
415+ pos ++
416+ tagEnd := strings .Index (raw [pos :], ">" )
417+ if tagEnd == - 1 {
418+ break
419+ }
420+ pos += tagEnd + 1
421+
422+ finishedObj := currentObj
423+ if len (stack ) > 0 {
424+ parent := stack [len (stack )- 1 ]
425+ stack = stack [:len (stack )- 1 ]
426+
427+ if existing , ok := parent .obj [currentName ]; ok {
428+ if arr , ok := existing .([]any ); ok {
429+ parent .obj [currentName ] = append (arr , finishedObj )
430+ } else {
431+ parent .obj [currentName ] = []any {existing , finishedObj }
432+ }
433+ } else {
434+ parent .obj [currentName ] = finishedObj
435+ }
436+ currentName = parent .name
437+ currentObj = parent .obj
438+ }
439+ } else if raw [pos ] == '!' || raw [pos ] == '?' {
440+ // Comment or PI — skip
441+ tagEnd := strings .Index (raw [pos :], ">" )
442+ if tagEnd == - 1 {
443+ break
444+ }
445+ pos += tagEnd + 1
446+ } else {
447+ // Opening tag
448+ tagEnd := strings .Index (raw [pos :], ">" )
449+ if tagEnd == - 1 {
450+ break
451+ }
452+ tagContent := raw [pos : pos + tagEnd ]
453+ selfClosing := strings .HasSuffix (tagContent , "/" )
454+ if selfClosing {
455+ tagContent = tagContent [:len (tagContent )- 1 ]
456+ }
457+
458+ parts := strings .Fields (tagContent )
459+ tagName := ""
460+ if len (parts ) > 0 {
461+ tagName = parts [0 ]
462+ }
463+
464+ attrs := make (map [string ]any )
465+ attrStr := ""
466+ if len (tagContent ) > len (tagName ) {
467+ attrStr = tagContent [len (tagName ):]
468+ }
469+ for _ , match := range attrRe .FindAllStringSubmatch (attrStr , - 1 ) {
470+ attrs ["@" + match [1 ]] = match [2 ]
471+ }
472+
473+ if selfClosing {
474+ if existing , ok := currentObj [tagName ]; ok {
475+ if arr , ok := existing .([]any ); ok {
476+ currentObj [tagName ] = append (arr , attrs )
477+ } else {
478+ currentObj [tagName ] = []any {existing , attrs }
479+ }
480+ } else {
481+ currentObj [tagName ] = attrs
482+ }
483+ } else {
484+ stack = append (stack , stackEntry {name : currentName , obj : currentObj })
485+ currentName = tagName
486+ currentObj = attrs
487+ }
488+ pos += tagEnd + 1
489+ }
490+ } else {
491+ textBuf .WriteByte (raw [pos ])
492+ pos ++
493+ }
494+ }
495+
496+ if currentName != "" {
497+ result := make (map [string ]any )
498+ result [currentName ] = currentObj
499+ return result
267500 }
501+ return currentObj
268502}
269503
270504// secretPattern matches {_secret:name} placeholders in templates.
0 commit comments