What gemini-cli is

gemini-cli is a command-line tool written in Go for working with Google's Gemini models. It takes its cue from the Unix tradition of small textual utilities that compose through pipes, and it targets quick prototyping of LLM analysis pipelines. Installation follows the usual Go route:

$ go install github.com/eliben/gemini-cli@latest

An API key is required, supplied either through the GEMINI_API_KEY environment variable or the --key flag; free keys are available from https://ai.google.dev/.

The author had wanted a "related posts" feature for a blog. The obvious approach is to embed posts into vector space and rank candidates by vector similarity — the same territory covered by earlier RAG work. Rather than commit to code first, the goal was to prototype the pipeline from the shell, in the spirit of chaining classical text-processing commands. gemini-cli is built for exactly this stage.

Embedding a document corpus

Input for the experiment was a directory of blog sources in reStructuredText and HTML. Any text collection works, including programming language source code.

A first count establishes the size of the corpus:

$ pss -f --rst content/|wc -l
279
$ pss -f --html content/|wc -l
1064

Just over 1300 files in total. Embedding the reST files is a matter of pointing the embed db subcommand at them and naming the SQLite output file, blogemb.db:

$ export GEMINI_API_KEY=...
$ gemini-cli embed db blogemb.db --files content/,"*.rst"
Found 279 values to embed
Splitting to 9 batches
Embedding batch #1 / 9, size=32
Embedding batch #2 / 9, size=32
Embedding batch #3 / 9, size=32
Embedding batch #4 / 9, size=32
Embedding batch #5 / 9, size=32
Embedding batch #6 / 9, size=32
Embedding batch #7 / 9, size=32
Embedding batch #8 / 9, size=32
Embedding batch #9 / 9, size=23
Collected 279 embeddings; inserting into table embeddings

Inspecting the result with the sqlite3 command-line tool:

$ sqlite3 blogemb.db
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.

sqlite> .tables
embeddings

sqlite> .schema
CREATE TABLE embeddings (
id TEXT PRIMARY KEY,
embedding BLOB
);

sqlite> select count(*) from embeddings;
279

sqlite> select id, length(embedding) from embeddings limit 10;
content/2014/blogging-setup-with-pelican.rst|3072
content/2014/c++-perfect-forwarding-and-universal-references.rst|3072
content/2014/derivation-normal-equation-linear-regression.rst|3072
content/2014/goodbye-wordpress.rst|3072
content/2014/highlight-tab-gnome-terminal.rst|3072
content/2014/meshgrids-and-disambiguating-rows-and-columns-from-cartesian-coordinates.rst|3072
content/2014/samples-for-llvm-clang-library.rst|3072
content/2014/sfinae-and-enable-if.rst|3072
content/2014/summary-of-reading-july-september-2014.rst|3072
content/2014/summary-of-reading-october-december-2014.rst|3072

The embeddings table holds 279 rows. Each row's id is the path of the source file, and embedding stores the vector as a blob, produced from the model's array of 32-bit floats:

// encodeEmbedding encodes an embedding into a byte buffer, e.g. for DB
// storage as a blob.
func encodeEmbedding(emb []float32) []byte {
  buf := new(bytes.Buffer)
  for _, f := range emb {
    err := binary.Write(buf, binary.LittleEndian, f)
    if err != nil {
      panic(err)
    }
  }
  return buf.Bytes()
}

A float32 takes 4 bytes, so blobs of 3072 bytes imply vectors of 768 elements: the model projects text into 768-dimensional space. Under the hood the tool uses Gemini's batch-embedding API, which keeps large corpora efficient; batch size is controllable via a flag, demonstrated here on the much larger HTML set:

$ gemini-cli embed db blogemb.db --batch-size=64 --files content/,"*.html"
Found 1064 values to embed
Splitting to 17 batches
Embedding batch #1 / 17, size=64
Embedding batch #2 / 17, size=64
Embedding batch #3 / 17, size=64
Embedding batch #4 / 17, size=64
Embedding batch #5 / 17, size=64
Embedding batch #6 / 17, size=64
Embedding batch #7 / 17, size=64
Embedding batch #8 / 17, size=64
Embedding batch #9 / 17, size=64
Embedding batch #10 / 17, size=64
Embedding batch #11 / 17, size=64
Embedding batch #12 / 17, size=64
Embedding batch #13 / 17, size=64
Embedding batch #14 / 17, size=64
Embedding batch #15 / 17, size=64
Embedding batch #16 / 17, size=64
Embedding batch #17 / 17, size=40
Collected 1064 embeddings; inserting into table embeddings

At batch size 64 the run completed in 17 seconds for over a thousand documents. Further gains are planned through more concurrency and adaptive batch sizing. The combined database:

$ stat -c %s blogemb.db
5627904
$ echo "select count(*) from embeddings" | sqlite3 blogemb.db
1343

All 1343 entries are present, with the database a little over 5 MiB in size.

Querying for similar posts

Ranking is handled by the embed similar subcommand, which takes a SQLite database of embeddings plus a string of content to compare. Passing - as the content reads the query from standard input:

$ gemini-cli embed similar blogemb.db - < content/2023/better-http-server-routing-in-go-122.rst
{"id":"content/2023/better-http-server-routing-in-go-122.rst","score":"1.0000001"}
{"id":"content/2021/rest-servers-in-go-part-2-using-a-router-package.rst","score":"0.8904768"}
{"id":"content/2021/life-of-an-http-request-in-a-go-server.rst","score":"0.83037585"}
{"id":"content/2021/rest-servers-in-go-part-5-middleware.rst","score":"0.8136583"}
{"id":"content/2022/serving-static-files-and-web-apps-in-go.rst","score":"0.7732284"}

Output is JSON Lines, printing the ID and similarity score — cosine similarity — ordered from most to least similar. As expected, the top hit for a post is the post itself, at a perfect 1.0. The remaining matches are genuinely relevant. To widen the candidate list, the topk flag increases how many results are printed; the example below queries with a book review:

$ gemini-cli embed similar blogemb.db --topk=10 - < content/2011/book-review-the-voyage-of-the-beagle-by-charles-darwin.html
{"id":"content/2011/book-review-the-voyage-of-the-beagle-by-charles-darwin.html","score":"1"}
{"id":"content/2008/book-review-the-origin-of-species-by-charles-darwin.html","score":"0.80570847"}
{"id":"content/2006/book-review-the-selfish-gene-by-richard-dawkins.html","score":"0.7845073"}
{"id":"content/2011/summary-of-reading-april-june-2011.html","score":"0.7939675"}
{"id":"content/2004/book-review-a-short-history-of-nearly-by-bill-bryson.html","score":"0.7784306"}
{"id":"content/2005/book-review-around-the-world-in-80-days-by-jules-verne.html","score":"0.7792236"}
{"id":"content/2005/book-review-the-double-helix-by-james-watson.html","score":"0.7658307"}
{"id":"content/2008/book-review-after-tamerlane-by-john-darwin.html","score":"0.7641713"}
{"id":"content/2005/book-review-mysterious-island-by-jules-verne.html","score":"0.7605505"}
{"id":"content/2008/book-review-the-adventures-of-tom-sawyer-by-mark-twain.html","score":"0.75610566"}

Scope and availability

With this pipeline in place, all the information needed to implement related posts was on hand, and the experimenting took under five minutes. SQLite as the storage and retrieval layer adds interoperability for free, and the pipe-friendly CLI design fits the prototyping workflow it was meant for.

Only the embed * subcommands have been shown here. The tool also supports chatting with a model through the terminal and querying the API for model details, with additional flags for configuration. It is open-source on GitHub; the README provides extensive documentation, and gemini-cli help covers the rest.