diff --git a/geni-util-cmd/Client.hs b/geni-util-cmd/Client.hs
new file mode 100644
--- /dev/null
+++ b/geni-util-cmd/Client.hs
@@ -0,0 +1,58 @@
+module Client where
+
+import           Control.Monad          (when, unless, forM)
+import qualified Data.ByteString.Char8  as BC
+import           Data.Monoid
+import qualified Data.Text              as T
+import           Data.Version
+import           Data.Word              (Word16)
+import           System.Environment
+import           System.FilePath
+import           System.IO
+
+import           NLP.GenI
+import           NLP.GenI.Configuration (emptyParams, setFlag, hasFlag,
+                                         processInstructions,
+                                         TestSuiteFlg(..), BatchDirFlg(..))
+import           NLP.GenI.Console       (writeResults, RunAs(PartOfSuite),
+                                         getBatchDir)
+import           NLP.GenI.TestSuite
+import qualified Text.JSON              as Json
+
+import           Config
+import           NLP.GenI.Client       (realise)
+
+main :: GeniUtil -> IO ()
+main opts@Client{} = do
+    when (null (suite opts)) $
+        fail "Need a suite"
+    let server_ = BC.pack (server opts)
+        port_   = port opts
+    params_ <- processInstructions
+        $ setFlag TestSuiteFlg (suite opts)
+        $ emptyParams
+    let params = if null (batchDir opts)
+                    then params_
+                    else setFlag BatchDirFlg (batchDir opts) params_
+        pst = emptyProgState params
+    batchDir  <- getBatchDir params
+    wrangler  <- defaultCustomSem pst
+    fullsuite <- loadTestSuite pst wrangler
+    let subBatchDir = batchDir </> takeFileName (suite opts)
+    forM fullsuite $ \tc -> do
+        -- FIXME: we should figure out what to do about params
+        mres <- realise server_ port_ [] (tcSemString tc)
+        case mres of
+            Json.Ok res -> do
+                writeResults pst (PartOfSuite (tcName tc) subBatchDir)
+                    wrangler (tcSemString tc) (tcSem tc) res
+                putStr . T.unpack . T.unlines $
+                    concat [ grRealisations x | GSuccess x <- grResults res ]
+            Json.Error err ->
+                fail $ "JSON decode error:\n" ++ err
+    unless (hasFlag BatchDirFlg pst) $ hPutStr stderr . unlines $
+        [ ""
+        , "Results saved to directory " ++ batchDir
+        , "To save results in a different directory, use the --batchdir flag"
+        ]
+mainGeniClient _ = error "geni-util routing (client)"
diff --git a/geni-util-cmd/Config.hs b/geni-util-cmd/Config.hs
new file mode 100644
--- /dev/null
+++ b/geni-util-cmd/Config.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Config where
+
+import           Data.Version
+import           Data.Word (Word16)
+
+import           System.Console.CmdArgs
+
+import           Paths_geni_util        (version)
+
+data GeniUtil = Client
+    { server   :: String
+    , port     :: Word16
+    , suite    :: FilePath
+    , batchDir :: FilePath
+    }
+  | Report
+    { inputDir  :: FilePath
+    , outputDir :: FilePath
+    }
+  deriving (Typeable, Data, Eq, Show)
+
+geniUtil :: String -> GeniUtil
+geniUtil p = modes
+    [ Client
+          { server   = def  &= argPos 0 &= typ "URL"
+          , suite    = def  &= typFile
+          , port     = 4364 &= typ "PORT"
+          , batchDir = def  &= typDir
+          }
+    , Report
+          { inputDir  = def &= argPos 0 &= typDir
+          , outputDir = def &= argPos 1 &= typDir
+          }
+    ] &= program p
+      &= help (p ++ " " ++ showVersion version)
diff --git a/geni-util-cmd/Report.hs b/geni-util-cmd/Report.hs
new file mode 100644
--- /dev/null
+++ b/geni-util-cmd/Report.hs
@@ -0,0 +1,18 @@
+module Report where
+
+import           Control.Monad    (forM_)
+import           System.Directory (copyFile)
+import           System.FilePath
+
+import           Client
+import           Config
+import           NLP.GenI.Report
+import           Paths_geni_util  (getDataFileName)
+
+main :: GeniUtil -> IO ()
+main opts@Report{} = do
+    odir <- createReport (inputDir opts) (outputDir opts)
+    forM_ dataFiles $ \bn -> do
+        f <- getDataFileName bn
+        copyFile f (odir </> bn)
+main _ = error "geni-util routing: report"
diff --git a/geni-util.cabal b/geni-util.cabal
--- a/geni-util.cabal
+++ b/geni-util.cabal
@@ -2,7 +2,7 @@
 -- documentation, see http://haskell.org/cabal/users-guide/
 
 name:                geni-util
-version:             0.24
+version:             0.24.1
 synopsis:            Companion tools for use with the GenI surface realiser
 description:
   Tools included:
@@ -51,7 +51,9 @@
 executable geni-util
   main-is:             Main.hs
   hs-source-dirs:      geni-util-cmd
-  -- other-modules:       
+  other-modules:       Report
+               ,       Client
+               ,       Config
   -- other-extensions:    
   build-depends:       base
                ,       bytestring
