diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,8 @@
+# Revision history for cabal-file
+
+## 0.1.0 -- 2020-05-24
+
+- initial release
+- cblfile commands: diff (second), list, latest, files, get, date, show,
+                    metadata, preferred, depends (oldest code)
+- Hackage.Index library module
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2020, Jens Petersen
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,70 @@
+# cabal-file
+
+cabal-file is a library on top of the hackage-security library for accessing
+cabal files and versions, and also a small tool for getting and comparing
+.cabal files.
+
+Since it accesses package and .cabal file data directly from
+the local Hackage repo cache index tar archive under ~/.cabal/ it is quite fast.
+
+## Usage
+List versions of a package:
+```
+$ cblfile list pandoc
+0.4
+:
+:
+2.9.2.1
+```
+
+Diff .cabal files of package versions
+```
+$ cblfile diff pandoc 2.8 2.9.2.1
+--- pandoc-2.8.cabal    2020-05-24 19:47:58.487921450 +0800
++++ pandoc-2.9.2.1.cabal        2020-05-24 19:47:58.487921450 +0800
+@@ -1,10 +1,10 @@
+ name:            pandoc
+-version:         2.8
++version:         2.9.2.1
+ cabal-version:   2.0
+:
+:
+```
+
+Latest version of a package:
+```
+$ cblfile latest extra
+1.7.1
+```
+
+Timestamp for the latest .cabal file (or for a specific version):
+```
+$ cblfile date dhall
+2020-05-19 02:18:01 UTC
+```
+
+Save a .cabal file (latest or specific version)
+```
+$ cblfile get purescript
+$ ls purescript.cabal
+purescript.cabal
+```
+
+Check package source metadata:
+```
+$ cblfile metadata tls-1.5.4
+Size: 150171
+SHA256 Hash "ce42bfa581a50f62776581da4b86408ebb1a51bc6cb2f95ad0a2ac7caa19a437"
+```
+
+## Depends output
+`cblfile depends` outputs quite verbose package dependency lists.
+
+## Library
+For library documentation see the [Hackage.Index](https://hackage.haskell.org/package/cabal-file/docs/Hackage-Index.html) documentation.
+
+For example usage see `app/Cmds.hs`.
+
+## Related
+If you want full diffs of Hackage sources you can try out cabal-diff from
+[cabal-extras](https://github.com/phadej/cabal-extras).
diff --git a/app/Cmds.hs b/app/Cmds.hs
new file mode 100644
--- /dev/null
+++ b/app/Cmds.hs
@@ -0,0 +1,86 @@
+module Cmds (
+  diffCmd,
+  saveCabal,
+  showCabal,
+  listPkg,
+  latestPkg,
+  listFiles,
+  preferCmd,
+  showMetadata,
+  dateCabal
+  ) where
+
+import Control.Monad
+import Control.Monad.Extra
+import qualified Data.ByteString.Lazy.Char8 as BL
+import Data.List
+import Distribution.Version (Version)
+import SimpleCabal
+import SimpleCmd
+import System.Directory
+import System.FilePath
+import System.IO.Extra (withTempDir)
+
+import Hackage.Index
+
+-- FIXME structural diff of PackageDescription
+-- FIXME revisions?
+diffCmd :: String -> Version -> Version -> IO ()
+diffCmd pkg v1 v2 =
+  withTempDir $ \ tmpdir -> do
+    setCurrentDirectory tmpdir
+    let pkgid1 = PackageIdentifier (mkPackageName pkg) v1
+        pkgid2 = PackageIdentifier (mkPackageName pkg) v2
+    saveCabals pkgid1 pkgid2
+    void $ cmdBool "diff" ["-u", showPkgId pkgid1 <.> "cabal", showPkgId pkgid2 <.> "cabal"]
+
+--  pkgdesc <- finalPackageDescription [] cabal
+  where
+    saveCabals :: PackageIdentifier -> PackageIdentifier -> IO ()
+    saveCabals pkgid1 pkgid2 = do
+      (bs1,bs2) <- getCabals pkgid1 pkgid2
+      BL.writeFile (showPkgId pkgid1 <.> "cabal") bs1
+      BL.writeFile (showPkgId pkgid2 <.> "cabal") bs2
+
+listPkg :: Maybe PackageName -> IO ()
+listPkg (Just pkgname) = do
+  versions <- packageVersions pkgname
+  mapM_ (putStrLn . showVersion) versions
+listPkg Nothing = fmap sort listPackages >>= mapM_ putStrLn
+
+saveCabal :: PackageIdentifier -> IO ()
+saveCabal pkgid = do
+  pkgver <- packageIdOrLatest pkgid
+  getCabal pkgver >>= BL.writeFile (showPkgId pkgid <.> "cabal")
+
+showCabal :: PackageIdentifier -> IO ()
+showCabal pkgid = do
+  pkgver <- packageIdOrLatest pkgid
+  getCabal pkgver >>= BL.putStrLn
+
+showMetadata :: PackageIdentifier -> IO ()
+showMetadata pkgid = do
+  pkgver <- packageIdOrLatest pkgid
+  finfo <- getFileInfo pkgver
+  let bytes = show . fileLength . fileInfoLength
+  putStrLn $ "Size: " ++ bytes finfo
+  whenJust (fileInfoSHA256 finfo) $ \ hash ->
+    putStrLn $ "SHA256 " ++ show hash
+
+latestPkg :: PackageName -> IO ()
+latestPkg pkgname =
+  whenJustM (latestVersion pkgname) (putStrLn . showVersion)
+
+listFiles :: IO ()
+listFiles = do
+  files <- indexFiles
+  mapM_ putStrLn files
+
+preferCmd :: PackageName -> IO ()
+preferCmd pkgname =
+  whenJustM (preferredVersions pkgname) BL.putStrLn
+
+dateCabal :: PackageIdentifier -> IO ()
+dateCabal pkgid = do
+  pkgver <- packageIdOrLatest pkgid
+  whenJustM (getTimestamp pkgver) print
diff --git a/app/Depends.hs b/app/Depends.hs
new file mode 100644
--- /dev/null
+++ b/app/Depends.hs
@@ -0,0 +1,139 @@
+module Depends (
+  Deps(..),
+  Details(..),
+  dependsCmd)
+where
+
+import Control.Monad (unless, when)
+import Data.List (nub, (\\))
+import Data.Maybe
+import System.Directory
+import System.FilePath
+
+import Distribution.PackageDescription (Library(..), exeName, setupDepends)
+import Distribution.Pretty
+import Distribution.Simple.BuildToolDepends (getAllToolDependencies)
+import Distribution.Text (simpleParse)
+import Distribution.Types.ExeDependency
+
+import SimpleCabal (allBuildInfo, buildDepends, buildDependencies, buildTools,
+                    exeDepName, extraLibs, findCabalFile,
+                    finalPackageDescription,
+                    mkPackageName, package, PackageDescription(..), PackageName,
+                    pkgcfgDepName, pkgconfigDepends, pkgName,
+                    setupBuildInfo, setupDependencies)
+
+import Hackage.Index (withCabalFile)
+
+data Deps = Build | Setup | Tool | Legacy | CLib | PkgConfig | NotBuild
+  deriving (Eq, Show)
+
+data Details = Normal | Unique | Detail
+  deriving Eq
+
+
+dependsCmd :: Bool -> Details -> Maybe Deps -> Maybe FilePath -> IO ()
+dependsCmd quiet details opt mfile =
+  case mfile of
+    Nothing -> findCabalFile >>= displayDepends
+    Just path -> do
+      dir <- doesDirectoryExist path
+      if dir
+        then setCurrentDirectory path >> findCabalFile >>= displayDepends
+        else
+        if "cabal" `isExtensionOf` path
+        then do
+          isfile <- doesFileExist path
+          if isfile then displayDepends path
+            else error $ path ++ " does not exist"
+        else
+          case simpleParse path of
+            Nothing -> error $ path ++ " not a package identifier"
+            Just pkgid -> withCabalFile pkgid displayDepends
+  where
+    displayDepends :: FilePath -> IO ()
+    displayDepends cabal = do
+      pkgdesc <- finalPackageDescription [] cabal
+      work (details == Detail) (details == Unique) (pkgName (package pkgdesc)) pkgdesc
+      where
+        work :: Bool -> Bool -> PackageName -> PackageDescription -> IO ()
+        work detail unique self pkgdesc = do
+          printDeps Build
+          printDeps Setup
+          printDeps Tool
+          printDeps Legacy
+          printDeps CLib
+          printDeps PkgConfig
+          printLib
+          printExe
+            where
+              printDeps :: Deps -> IO ()
+              printDeps kind =
+                unless (opt == Just NotBuild && kind == Build) $ do
+                  let deps = nub $ lens kind
+                  unless (null deps) $
+                    when (isNothing opt || opt `elem` [Just kind, Just NotBuild]) $ do
+                      when (not quiet && (opt /= Just kind || opt == Just NotBuild)) $
+                        title $ show kind ++ " depends"
+                      mapM_ putStrLn deps
+
+              lens :: Deps -> [String]
+              lens Build =
+                if detail
+                then map prettyShow $ buildDepends pkgdesc
+                else map prettyShow $ buildDependencies pkgdesc \\ [self | unique]
+              lens Setup =
+                if detail
+                then map prettyShow $ maybe [] setupDepends (setupBuildInfo pkgdesc)
+                else map prettyShow $ setupDependencies pkgdesc \\ (mkPackageName "Cabal" : if unique then buildDependencies pkgdesc else [])
+              lens Tool =
+                let bts = concatMap (getAllToolDependencies pkgdesc) $
+                          allBuildInfo pkgdesc
+                in if detail then map prettyShow bts
+                   else map (prettyShow . exeDepPkgName) bts \\ ([prettyShow self | unique] ++ ["hsc2hs" | unique])
+              lens Legacy =
+                let legacy = concatMap buildTools $ allBuildInfo pkgdesc
+                in (if detail then map prettyShow legacy
+                    else map exeDepName legacy) \\ if unique then "hsc2hs" : prettyShow self : lens Tool else []
+              lens CLib = (concatMap extraLibs .  allBuildInfo) pkgdesc
+              lens PkgConfig =
+                let ds = (concatMap pkgconfigDepends . allBuildInfo) pkgdesc
+                in (if detail then map prettyShow
+                    else map pkgcfgDepName) ds
+              lens NotBuild = error "No lens for NotBuild!"
+
+              printLib :: IO ()
+              printLib =
+                case library pkgdesc of
+                  Nothing -> return ()
+                  Just lib -> do
+                    title "Library"
+                    when detail $ do
+                      subtitle "Exposed Modules"
+                      mapM_ prettyPut $ exposedModules lib
+                      let export = reexportedModules lib
+                      unless (null export) $ do
+                        subtitle "Re-exported Modules"
+                        mapM_ prettyPut export
+
+              printExe :: IO ()
+              printExe = do
+                let exes = executables pkgdesc
+                unless (null exes) $ do
+                  title $ "Executable" ++ ['s' | length exes > 1]
+                  when detail $
+                    mapM_ (prettyPut . exeName) exes
+
+title :: String -> IO ()
+title ts =
+  putStrLn $ "# " ++ ts
+
+subtitle :: String -> IO ()
+subtitle ts =
+  putStrLn $ "- " ++ ts
+
+prettyPut :: Pretty a => a -> IO ()
+prettyPut = putStrLn . prettyShow
+
+exeDepPkgName :: ExeDependency -> PackageName
+exeDepPkgName (ExeDependency n _ _) = n
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,70 @@
+{-# LANGUAGE CPP #-}
+
+import Data.Version.Extra (readVersion)
+import Distribution.Package
+import Distribution.Text (simpleParse)
+import Distribution.Version
+#if MIN_VERSION_simple_cmd_args(0,1,7)
+#else
+import Options.Applicative
+#endif
+import SimpleCmdArgs
+
+import Cmds
+import Depends
+
+main :: IO ()
+main =
+  simpleCmdArgs Nothing "Cabal file tool"
+    "cabal-file can access and compare .cabal files from the cache" $
+    subcommands
+    [ Subcommand "diff" "Diff .cabal files of package versions" $
+      diffCmd <$> strArg "PKG" <*> versionArg <*> versionArg
+    , Subcommand "list" "List package versions" $
+      listPkg <$> optional pkgArg
+    , Subcommand "latest" "Latest package version" $
+      latestPkg <$> pkgArg
+    , Subcommand "files" "List all index files" $
+      pure listFiles
+    , Subcommand "get" "Get .cabal file for package version" $
+      saveCabal <$> pkgIdArg
+    , Subcommand "date" "Timestamp for package version (revision)" $
+      dateCabal <$> pkgIdArg
+    , Subcommand "show" "Show .cabal file for package version" $
+      showCabal <$> pkgIdArg
+    , Subcommand "metadata" "Show metadata for package version" $
+      showMetadata <$> pkgIdArg
+    , Subcommand "preferred" "Show preferred versions for package" $
+      preferCmd <$> pkgArg
+    , Subcommand "depends" "Print package dependencies" $
+      dependsCmd <$> switchWith 'q' "quiet" "No depends section headers"
+      <*> detailOpt
+      <*> optional depsOpt
+      <*> optional (strArg "DIRorCABAL")
+    ]
+  where
+    versionArg :: Parser Version
+    versionArg = argumentWith versionM "VERSION"
+
+    versionM = maybeReader (Just . mkVersion' . readVersion)
+
+    pkgIdArg :: Parser PackageIdentifier
+    pkgIdArg = argumentWith (maybeReader simpleParse) "PKG[VER]"
+
+    pkgArg :: Parser PackageName
+    pkgArg = argumentWith (maybeReader simpleParse) "PKG"
+
+    detailOpt :: Parser Details
+    detailOpt =
+      flagWith' Detail 'd' "detail" "Show version ranges" <|>
+      flagWith Normal Unique 'u' "unique" "Remove self and build-depends from other depends"
+
+    depsOpt :: Parser Deps
+    depsOpt =
+      flagWith' Build 'b' "build" "Only build-depends" <|>
+      flagWith' Setup 's' "setup" "Only setup-depends" <|>
+      flagWith' Tool 't' "tool" "Only tool-build-depends" <|>
+      flagWith' Legacy 'l' "legacy" "Only build-tools" <|>
+      flagWith' CLib 'c' "clibs" "Only extra-libraries" <|>
+      flagWith' PkgConfig 'p' "pkgconfig" "Only extra-libraries" <|>
+      flagWith' NotBuild 'B' "not-build" "Hide build-depends"
diff --git a/cabal-file.cabal b/cabal-file.cabal
new file mode 100644
--- /dev/null
+++ b/cabal-file.cabal
@@ -0,0 +1,83 @@
+name:                cabal-file
+version:             0.1.0
+synopsis:            Cabal file access
+description:
+        cabal-file is a small library on top of the 'hackage-security' library
+        for accessing the local Hackage repo index cache, and a command-line tool
+        'cblfile' which can diff .cabal versions, list package versions,
+        save a cabal file, and show metadata from the index. It uses
+        simple-cabal to display package dependency lists.
+license:             BSD3
+license-file:        LICENSE
+author:              Jens Petersen
+maintainer:          juhpetersen@gmail.com
+copyright:           2019-2020  Jens Petersen
+category:            Distribution
+build-type:          Simple
+extra-doc-files:     README.md
+                   , ChangeLog.md
+cabal-version:       1.18
+
+source-repository head
+  type:                git
+  location:            https://github.com/juhp/cabal-file.git
+
+executable cblfile
+  main-is:             Main.hs
+  other-modules:       Cmds
+                       Depends
+  hs-source-dirs:      app
+  build-depends:       base < 5
+                     , bytestring
+                     , Cabal > 2.2
+                     , cabal-file
+                     , directory
+                     , extra
+                     , filepath
+                     , optparse-applicative
+                     , simple-cabal > 0.1
+                     , simple-cmd
+                     , simple-cmd-args >= 0.1.2
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+  if impl(ghc >= 8.0)
+    ghc-options:       -Wcompat
+                       -Widentities
+                       -Wincomplete-uni-patterns
+                       -Wincomplete-record-updates
+                       -Wredundant-constraints
+  if impl(ghc >= 8.2)
+    ghc-options:       -fhide-source-paths
+  if impl(ghc >= 8.4)
+    ghc-options:       -Wmissing-export-lists
+                       -Wpartial-fields
+
+library
+  build-depends:       base < 5
+                     , bytestring
+                     , Cabal > 2.2
+                     , directory
+                     , extra
+                     , filepath
+                     , hackage-security
+                     , optparse-applicative
+                     , simple-cabal > 0.1
+                     , simple-cmd
+                     , time
+  exposed-modules:     Hackage.Index
+  hs-source-dirs:      src
+
+  ghc-options:         -Wall
+  if impl(ghc >= 8.0)
+    ghc-options:       -Wcompat
+                       -Widentities
+                       -Wincomplete-uni-patterns
+                       -Wincomplete-record-updates
+                       -Wredundant-constraints
+  if impl(ghc >= 8.2)
+    ghc-options:       -fhide-source-paths
+  if impl(ghc >= 8.4)
+    ghc-options:       -Wmissing-export-lists
+                       -Wpartial-fields
+
+  default-language:    Haskell2010
diff --git a/src/Hackage/Index.hs b/src/Hackage/Index.hs
new file mode 100644
--- /dev/null
+++ b/src/Hackage/Index.hs
@@ -0,0 +1,196 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE GADTs #-}
+
+module Hackage.Index (
+  getCabal,
+  getCabals,
+  withCabalFile,
+  listPackages,
+  packageVersions,
+  latestVersion,
+  preferredVersions,
+  getTimestamp,
+  indexFiles,
+  getPackageDescription,
+  getPackageDescription',
+  packageIdOrLatest,
+  getFileInfo,
+  -- * Re-exports from hackage-security
+  FileInfo(..),
+  FileLength(..),
+  fileInfoSHA256
+  ) where
+
+import qualified Data.ByteString.Lazy.Char8 as BL
+import Data.List
+import Data.Maybe
+import Data.Time.Clock
+import Data.Time.Clock.POSIX
+import Data.Version.Extra (readVersion)
+import System.Directory
+import System.FilePath
+import System.IO.Extra (withTempDir)
+
+import Distribution.Version
+#if MIN_VERSION_Cabal(3,0,0)
+#else
+  hiding (showVersion)
+#endif
+import Hackage.Security.Client
+import qualified Hackage.Security.Client.Repository.Local as Local
+import qualified Hackage.Security.Util.Path as Path
+--import Hackage.Security.Util.Some
+import qualified Hackage.Security.Client.Repository.Cache as Cache
+import Hackage.Security.Util.Pretty
+
+import SimpleCabal
+
+-- | Get the contents of the .cabal file for package version
+getCabal  :: PackageIdentifier -> IO BL.ByteString
+getCabal pkgid =
+  withLocalRepo $ \rep -> uncheckClientErrors $
+    withIndex rep $ \ IndexCallbacks{..} ->
+    trusted <$> indexLookupCabal pkgid
+
+-- | Pass a temporary copy of .cabal file to some action
+withCabalFile :: PackageIdentifier -> (FilePath -> IO a) -> IO a
+withCabalFile pkgid act =
+  withTempDir $ \ tmpdir -> do
+    bs <- getCabal pkgid
+    let filepath = tmpdir </> showPkgId pkgid <.> "cabal"
+    BL.writeFile filepath bs
+    act filepath
+
+-- | Get two .cabal files at once!
+getCabals  :: PackageIdentifier -> PackageIdentifier
+           -> IO (BL.ByteString, BL.ByteString)
+getCabals pkgid1 pkgid2 =
+  withLocalRepo $ \rep -> uncheckClientErrors $
+    withIndex rep $ \ IndexCallbacks{..} -> do
+    bs1 <- trusted <$> indexLookupCabal pkgid1
+    bs2 <- trusted <$> indexLookupCabal pkgid2
+    return (bs1,bs2)
+
+-- | Get FileInfo metadata for package version source
+getFileInfo :: PackageIdentifier -> IO FileInfo
+getFileInfo pkgid =
+  withLocalRepo $ \rep -> uncheckClientErrors $
+      withIndex rep $ \ IndexCallbacks{..} ->
+        trusted <$> indexLookupFileInfo pkgid
+
+-- | Get and try to parse the PackageDescription of a package version
+getPackageDescription :: PackageIdentifier -> IO (Maybe PackageDescription)
+getPackageDescription pkgid =
+#if (defined(MIN_VERSION_simple_cabal) && MIN_VERSION_simple_cabal(0,1,2))
+  do
+  cabal <- getCabal pkgid
+  parseFinalPackageDescription [] $ BL.toStrict cabal
+#else
+  Just <$> withCabalFile pkgid (finalPackageDescription [])
+#endif
+
+-- | Get and parse PackageDescription of package version
+--
+-- Raises an error on failure.
+getPackageDescription' :: PackageIdentifier -> IO PackageDescription
+getPackageDescription' pkgid = do
+  mfpd <- getPackageDescription pkgid
+  maybe (error "Failed to parse cabal file") return mfpd
+
+-- | Easy access to the local Hackage repo
+withLocalRepo :: (Repository Local.LocalFile -> IO a) -> IO a
+withLocalRepo action = do
+  home <- getHomeDirectory
+  localrepo <- (Path.makeAbsolute . Path.fromFilePath) (home </> ".cabal")
+  localcache <- (Path.makeAbsolute . Path.fromFilePath) (home </> ".cabal/packages/hackage.haskell.org")
+  Local.withRepository localrepo (cache localcache) hackageRepoLayout hackageIndexLayout logTUF action
+  where
+    -- FIXME could also support 00-index
+    cache localcache = Cache.Cache {
+        Cache.cacheRoot   = localcache
+      , Cache.cacheLayout = cabalCacheLayout
+        { cacheLayoutIndexTar   = Path.rootPath $ Path.fragment "01-index.tar"
+        , cacheLayoutIndexIdx   = Path.rootPath $ Path.fragment "01-index.tar.idx"
+        , cacheLayoutIndexTarGz = Path.rootPath $ Path.fragment "01-index.tar.gz"}
+    }
+
+    logTUF msg = putStrLn $ "# " ++ pretty msg
+
+-- | Get all versions of a package in the index
+packageVersions :: PackageName -> IO [Version]
+packageVersions pkgname =
+  withLocalRepo $ \rep -> uncheckClientErrors $ do
+    dir <- getDirectory rep
+    let pkg = unPackageName pkgname
+    return $ sort . mapMaybe (extractPkgVersion pkg . second) $ directoryEntries dir
+  where
+    second (_,b,_) = b
+
+    extractPkgVersion :: String -> IndexPath -> Maybe Version
+    extractPkgVersion pkg path =
+      if Path.takeExtension path == ".cabal" then
+        let namever = (Path.toUnrootedFilePath . Path.unrootPath . Path.takeDirectory) path
+        in if takeDirectory namever == pkg
+           then Just $ mkVersion' . readVersion $ takeFileName namever
+           else Nothing
+      else Nothing
+
+-- | Get the preferred-versions metadata for package
+preferredVersions :: PackageName -> IO (Maybe BL.ByteString)
+preferredVersions pkgname =
+  withLocalRepo $ \rep -> uncheckClientErrors $
+    withIndex rep $ \ IndexCallbacks{..} ->
+    fmap indexEntryContent <$> indexLookupFile (IndexPkgPrefs pkgname)
+
+-- | List all files in the Hackage index
+-- (.cabal files, metadata .json files, preferred-versions files)
+indexFiles :: IO [String]
+indexFiles =
+  withLocalRepo $ \rep -> uncheckClientErrors $ do
+    dir <- getDirectory rep
+    return $ map dirEntryPath (directoryEntries dir)
+  where
+    second (_,b,_) = b
+
+    dirEntryPath = Path.toUnrootedFilePath . Path.unrootPath . second
+
+-- | Get the latest version of package from the index
+--
+-- Note: does not take preferred-versions into account
+latestVersion :: PackageName -> IO (Maybe Version)
+latestVersion pkgname = do
+  versions <- packageVersions pkgname
+  if null versions then return Nothing
+    else return $ Just $ last versions
+
+-- | Get the index timestamp for (the latest revision of) pkgid
+getTimestamp :: PackageIdentifier -> IO (Maybe UTCTime)
+getTimestamp pkgid =
+  withLocalRepo $ \rep -> uncheckClientErrors $
+    withIndex rep $ \ IndexCallbacks{..} ->
+    fmap (posixSecondsToUTCTime . realToFrac . indexEntryTime) <$>
+    indexLookupFile (IndexPkgCabal pkgid)
+
+-- | Convert a PackageID if unversioned to latest package version
+packageIdOrLatest :: PackageIdentifier -> IO PackageIdentifier
+packageIdOrLatest pkgid = do
+  let name = pkgName pkgid
+  if pkgVersion pkgid == nullVersion then do
+    mlatest <- latestVersion name
+    return $ maybe pkgid (PackageIdentifier name) mlatest
+    else return pkgid
+
+-- | List all packages in the index (unsorted for performance)
+listPackages :: IO [String]
+listPackages =
+  withLocalRepo $ \rep -> uncheckClientErrors $ do
+    dir <- getDirectory rep
+    return $ nub $ mapMaybe (extractPkg . second) (directoryEntries dir)
+  where
+    extractPkg path =
+      if Path.takeExtension path == ".cabal" then
+        (Just . takeWhile (/= '/') . Path.toUnrootedFilePath . Path.unrootPath) path
+        else Nothing
+
+    second (_,b,_) = b
