@@ -3,6 +3,7 @@ package ssh
33import (
44 "context"
55 "fmt"
6+ "path"
67 "strings"
78
89 "go.kenn.io/agentsview/internal/parser"
@@ -13,6 +14,8 @@ import (
1314// is not a valid agent type, so parseResolvedDirs routes it separately.
1415const resolveFilePrefix = "@file"
1516
17+ const resolveRecordSep = "\x00 "
18+
1619func aiderSkipDirCasePattern () string {
1720 return strings .Join (parser .AiderDiscoverySkipDirNames (), "|" )
1821}
@@ -34,7 +37,7 @@ func buildAiderResolveSnippet(envVar string) string {
3437 "[ \" $av_aider_files\" -ge %d ] && return; " +
3538 "[ \" $av_aider_dirs\" -ge %d ] && return; " +
3639 "elif [ -f \" $av_entry\" ] && [ \" $av_base\" = '%s' ]; then " +
37- "echo \" %s:$av_entry\" ; " +
40+ "printf '%%s \\ 000' \" %s:$av_entry\" ; " +
3841 "av_aider_files=$((av_aider_files + 1)); " +
3942 "[ \" $av_aider_files\" -ge %d ] && return; " +
4043 "fi; " +
@@ -100,7 +103,8 @@ func buildResolveScript() string {
100103 dirExpr = fmt .Sprintf ("${%s:-%s}" , def .EnvVar , defaultDir )
101104 }
102105 fmt .Fprintf (& b ,
103- "dir=\" %s\" ; [ -d \" $dir\" ] && echo \" %s:$dir\" \n " ,
106+ "dir=\" %s\" ; [ -d \" $dir\" ] && " +
107+ "printf '%%s\\ 000' \" %s:$dir\" \n " ,
104108 dirExpr , string (def .Type ),
105109 )
106110 // Codex stores renameable session titles in
@@ -110,7 +114,8 @@ func buildResolveScript() string {
110114 if def .Type == parser .AgentCodex {
111115 fmt .Fprintf (& b ,
112116 "idx=\" ${dir%%/*}/%s\" ; " +
113- "[ -f \" $idx\" ] && echo \" %s:$idx\" \n " ,
117+ "[ -f \" $idx\" ] && " +
118+ "printf '%%s\\ 000' \" %s:$idx\" \n " ,
114119 parser .CodexSessionIndexFilename ,
115120 resolveFilePrefix ,
116121 )
@@ -124,23 +129,26 @@ func buildResolveScript() string {
124129}
125130
126131// parseResolvedDirs parses script output into a map of agent type to transfer
127- // target paths plus a deduplicated list of extra files (lines tagged with
128- // resolveFilePrefix). Most agent targets are directories; Aider targets are
129- // individual .aider.chat.history.md files. Skips empty lines and entries with
130- // empty values.
132+ // target paths plus a deduplicated list of extra files (records tagged with
133+ // resolveFilePrefix). Generated resolver output is NUL-delimited so remote
134+ // paths containing newlines cannot inject extra records; newline-delimited input
135+ // is accepted only for older tests and defensive compatibility. Most agent
136+ // targets are directories; Aider targets are individual .aider.chat.history.md
137+ // files. Skips empty records, empty values, and values containing record
138+ // separators.
131139func parseResolvedDirs (
132140 output string ,
133141) (map [parser.AgentType ][]string , []string ) {
134142 dirs := make (map [parser.AgentType ][]string )
135143 var extraFiles []string
136144 seenFile := make (map [string ]struct {})
137- for line := range strings . SplitSeq (output , " \n " ) {
138- line = strings .TrimSpace (line )
139- if line == "" {
145+ for _ , record := range resolveOutputRecords (output ) {
146+ record = strings .TrimSpace (record )
147+ if record == "" {
140148 continue
141149 }
142- key , value , ok := strings .Cut (line , ":" )
143- if ! ok || value == "" {
150+ key , value , ok := strings .Cut (record , ":" )
151+ if ! ok || invalidResolvedPath ( value ) {
144152 continue
145153 }
146154 if key == resolveFilePrefix {
@@ -152,11 +160,26 @@ func parseResolvedDirs(
152160 continue
153161 }
154162 at := parser .AgentType (key )
163+ if at == parser .AgentAider &&
164+ path .Base (value ) != parser .AiderHistoryFileName () {
165+ continue
166+ }
155167 dirs [at ] = append (dirs [at ], value )
156168 }
157169 return dirs , extraFiles
158170}
159171
172+ func resolveOutputRecords (output string ) []string {
173+ if strings .Contains (output , resolveRecordSep ) {
174+ return strings .Split (output , resolveRecordSep )
175+ }
176+ return strings .Split (output , "\n " )
177+ }
178+
179+ func invalidResolvedPath (value string ) bool {
180+ return value == "" || strings .ContainsAny (value , "\x00 \r \n " )
181+ }
182+
160183// resolveDirs runs the resolve script on the remote host via SSH and
161184// returns the discovered agent directories plus extra sibling files
162185// (such as Codex's session_index.jsonl) to include in the transfer.
0 commit comments