nimji
v0.2.1
Published
Promise-first TypeScript client and nimji CLI for Gemini web chat: multi-turn sessions, configurable retries and keepalive, auth via env or JSON config, and a polished terminal UI.
Maintainers
Readme
nimji
TypeScript client and CLI for Gemini web StreamGenerate
Multi-turn sessions · Image input & generation · Keepalive · Configurable retries · Polished terminal UI
Contents
- Install
- Credentials
- CLI
- Library
- Configuration files
- Environment variables
- Image pipeline
- Session & keepalive
- Development
Install
# Global CLI
npm install -g nimji
# Project library
npm install nimjiCredentials
nimji talks directly to the Gemini web StreamGenerate endpoint using your browser session. You only need one thing:
| Variable | Where to find it |
| --------- | ------------------------------------------------------------- |
| COOKIES | Cookie: request header — the full SID=…; HSID=…; … string |
Set it as an environment variable, in a .env file, or in a config.jsonc (see Configuration files).
export COOKIES="SID=g.a000…"That's it. AT_TOKEN and F_SID are extracted automatically via the bard-utils REST API when missing.
Legacy — nimji used to require
AT_TOKENandF_SIDto be manually copy-pasted from DevTools. imagine doing all that work when the API just does it for you 💀 remove them from your.envand touch grass
Note — credentials are tied to your browser session and rotate when you sign out or Chrome rotates them. Re-capture from DevTools when requests start failing.
Supported Models
Set the MODEL env var to choose which Gemini model to use:
| Model | MODEL value | Speed | Best for |
| --------------------- | ------------- | ------ | -------------------------------------- |
| Flash-Lite | flash-lite | ⚡⚡⚡ | Quick answers, default mode |
| Flash | flash | ⚡⚡ | Balanced speed and quality |
| Pro | pro | ⚡ | Advanced math, code, reasoning |
| Extended Thinking | extended | 🐢 | Complex problem solving, deep analysis |
# Use Pro for complex tasks
MODEL=pro nimji "explain the time complexity of quicksort with edge cases"
# Use Flash for everyday queries
MODEL=flash nimji "summarize this file"
# Default (Flash-Lite) — fastest
nimji "what's 2+2"You can also set MODEL in your .env or config.jsonc:
{ "MODEL": "flash" }Pro users — Extended Thinking shares the same backend as Pro but enables deeper reasoning chains. Use it when you need step-by-step analysis rather than fast answers.
One-shot
Sends a single prompt, prints the response, and exits.
nimji "explain async/await in JavaScript"
nimji --prompt "summarize this repo's README"Interactive chat
Starts a readline REPL with full multi-turn memory via session.json.
nimji --chat
nimji --chat --keep-alive # ping Gemini in-process to stay warmType /exit or /quit to leave. Session state is saved automatically to ~/.nimji/session.json between runs.
Image prompts
Attach a local image to your prompt. nimji uploads it to Gemini's upload endpoint first, then sends the contribution token inside StreamGenerate.
# Explicit flag
nimji --input-image ./screenshot.png "what's wrong in this code?"
# Auto-detected — image extension at end of args
nimji "describe this photo" ./sunset.jpg
# Auto-detected — image extension at start of args
nimji ./diagram.png "explain this architecture"Supported formats: png jpg / jpeg webp gif svg bmp tiff
Image generation
Use --image to signal image-generation prompts. Enables extended stream timeouts, extra retries, and optional local saving.
# Generate and print CDN URL
nimji --image "a neon-lit cyberpunk alleyway at night"
# Generate and save to ./output-images/ (requires IMAGE_PIPELINE_ENABLED=1)
IMAGE_PIPELINE_ENABLED=1 nimji --image --save-images "a minimalist logo for a dev tool"
# Generate, save, and upload to ImgBB
IMAGE_PIPELINE_ENABLED=1 nimji --image --save-images --upload "a watercolor cat"Flags reference
| Flag | Description |
| -------------------------------- | ------------------------------------------------------------ |
| --prompt "text" | Explicit prompt (alternative to positional arg) |
| --chat | Interactive multi-turn REPL |
| --image | Image-generation mode — extended timeouts + extra retries |
| --input-image <path> | Attach local image file to the prompt |
| --save-images | Save generated images to ./output-images/ |
| --no-save-images | Skip disk saving even in --image mode |
| --upload / --upload-images | Upload saved images to ImgBB (requires IMG_BB_API_KEY) |
| --show-source-image-urls | Print raw CDN URLs even when files are saved |
| --reset-session | Clear session.json before running |
| --no-session | Do not load or save session state |
| --keepalive / --keep-alive | Enable keepalive (in-process for --chat, daemon otherwise) |
| --keepalive-minutes N | Keepalive ping interval in minutes (default 10) |
| --no-retry | Disable automatic retries on partial/empty responses |
| --density compact\|comfortable | Terminal output density (default comfortable) |
| --answer-style plain\|boxed | Answer rendering style (default boxed) |
| --version / -v | Print version and exit |
| --help / -h | Print help and exit |
Quick start
import { create } from "nimji";
const client = create({
COOKIES: process.env.COOKIES ?? "",
});
const res = await client.generate({ prompt: "hello" });
if (res.ok) console.log(res.value.text);
client.stopKeepalive();Image attachment
import { create, uploadImageToGemini, inferMimeTypeFromPath } from "nimji";
const client = create({ COOKIES: "…" });
// 1. Upload the image → get a contribution token
const attachment = await uploadImageToGemini(client.getConversation(), "./photo.png");
// attachment = { tokenPath: "/contrib_service/ttl_1d/…", mimeType: "image/png", fileName: "photo.png" }
// 2. Pass the attachment alongside the prompt
const res = await client.generate({
prompt: "what is in this image?",
imageAttachment: attachment,
});
if (res.ok) console.log(res.value.text);Full config
import { createClient, loadConfigFromEnv } from "nimji";
const client = createClient(
loadConfigFromEnv({
overrides: {
COOKIES: process.env.COOKIES ?? "",
MODEL: "auto", // or paste a boq_assistant-… bl string
LANGUAGE: "en",
DEBUG_CANDIDATES: "1", // log raw text candidate scores to stderr
IMAGE_PIPELINE_ENABLED: "1", // enable image download/save/upload
},
}),
{
hooks: {
onCandidates: async (candidates) => {
console.error("top:", candidates[0]);
},
},
keepalive: { enabled: true, intervalMs: 300_000 },
},
);API reference
create(input, hooksOrOptions?, options?) → GemaiClient
Convenience factory. Accepts flat env-style keys (COOKIES, optional AT_TOKEN, …) directly. Also accepts keepalive: true | { intervalMs, prompt } inline.
createClient(config, hooksOrOptions?) → GemaiClient
Low-level factory taking a full GemaiConfig object.
createClientFromEnv(hooksOrOptions?, options?) → GemaiClient
Builds config entirely from process.env + config files, then calls createClient.
client.generate(options) → Promise<Result<GenerateResult>>
| Option | Type | Description |
| ----------------- | ------------------ | --------------------------------------------------- |
| prompt | string | The user message |
| includeImages | boolean? | Surface image URLs in the result (default true) |
| saveImages | boolean? | Download and save images to imageOutputDir |
| uploadImages | boolean? | Upload saved images to ImgBB |
| imageOutputDir | string? | Save directory (default ./output-images) |
| imageAttachment | ImageAttachment? | Pre-uploaded image token from uploadImageToGemini |
GenerateResult
type GenerateResult = {
text: string | null; // assistant reply
imageUrls: readonly string[]; // rd-gg / gg-dl CDN links
savedImagePaths: readonly string[]; // local files (if saved)
uploadedImageUrls: readonly string[]; // ImgBB URLs (if uploaded)
conversation: ConversationState; // ids for next turn
meta: StreamMeta; // statusCode, rawSize, chunkCount, latency
};ImageAttachment
type ImageAttachment = {
tokenPath: string; // "/contrib_service/ttl_1d/<token>"
mimeType: string; // "image/png" | "image/jpeg" | …
fileName: string; // original filename
};uploadImageToGemini(config, filePath) → Promise<ImageAttachment>
Uploads a local file via Gemini's two-step resumable-upload endpoint and returns the contribution token to embed in generate().
inferMimeTypeFromPath(filePath) → string
Maps a file extension to its MIME type. Returns "application/octet-stream" for unknown extensions.
Configuration files
nimji merges config from the first file found (env always wins):
- Path in
NIMJI_CONFIGenv var (absolute or relative to cwd) ./config.jsoncor./config.jsonin the current working directory~/.nimji/config.jsoncor~/.nimji/config.json
Keys use the same names as environment variables. See config.jsonc in this repo for the full annotated shape with all nested groups (chat, runtime, keepalive, browser, upload).
{
"COOKIES": "SID=…",
"MODEL": "auto",
"chat": {
"UI_DENSITY": "comfortable",
"UI_ANSWER_STYLE": "boxed",
},
"runtime": {
"STREAM_IDLE_TIMEOUT_MS": "30000",
"IMAGE_STREAM_IDLE_TIMEOUT_MS": "120000",
},
}Environment variables
Required
| Variable | Description |
| --------- | ---------------------------------- |
| COOKIES | Full browser session cookie string |
Optional
| Variable | Default | Description |
| ------------------------ | ---------------- | -------------------------------------------------------------------------------- |
| MODEL | auto | Model selector: flash, pro, flash-lite, extended, or auto (Flash-Lite) |
| BL_PARAM | — | Overrides MODEL when set; direct bl= value |
| USER_AGENT | Chrome UA | Custom User-Agent header |
| LANGUAGE | en | Gemini request language |
| ACCEPT_LANGUAGE | en-US,en;q=0.9 | Accept-Language header |
| CHROME_FULL_VERSION | 151.0.7922.138 | Chrome version for client-hint headers |
| IMAGE_PIPELINE_ENABLED | — | Set to 1 to enable image download/save/upload |
| IMG_BB_API_KEY | — | ImgBB API key for --upload |
| IMG_BB_EXPIRATION_SEC | 0 (permanent) | ImgBB link TTL |
| NIMJI_HOME | ~/.nimji | Override the persistent state directory |
| NIMJI_CONFIG | — | Explicit config file path |
| UI_DENSITY | comfortable | compact or comfortable |
| UI_ANSWER_STYLE | boxed | boxed or plain |
| DEBUG_CANDIDATES | 0 | Set to 1 to log raw text candidate scores |
| NO_COLOR | — | Set to 1 to disable ANSI color output |
| FORCE_COLOR | — | Set to 1 to force color in non-TTY environments |
| ENC_KEY | — | Passphrase to encrypt rotated cookies (AES-256-GCM) |
Stream timeouts
| Variable | Default | Description |
| ------------------------------ | -------- | ---------------------------------------- |
| STREAM_IDLE_TIMEOUT_MS | 30000 | Idle gap before finalizing a text stream |
| STREAM_MAX_DURATION_MS | 180000 | Hard wall-clock cap for text streams |
| IMAGE_STREAM_IDLE_TIMEOUT_MS | 120000 | Idle gap for image-generation streams |
| IMAGE_STREAM_MAX_DURATION_MS | 600000 | Hard cap for image-generation streams |
Image pipeline
By default the image download pipeline is disabled. URLs are still surfaced from the stream (imageUrls in GenerateResult) but bytes are not fetched.
Enable it with IMAGE_PIPELINE_ENABLED=1:
# Save generated images to ./output-images/
IMAGE_PIPELINE_ENABLED=1 nimji --image --save-images "a mountain at sunset"
# Or in config.jsonc
{ "IMAGE_PIPELINE_ENABLED": "1" }Why images expire — Gemini CDN URLs (lh3.googleusercontent.com) are session-scoped signed tokens. gg-dl/ links expire in minutes; rd-gg/ links last hours. The pipeline chases gg-dl → rd-gg redirects first, then downloads while the token is fresh. The saved files on disk never expire.
For ImgBB hosting:
IMAGE_PIPELINE_ENABLED=1 IMG_BB_API_KEY=your_key nimji --image --upload "generate art"Session & keepalive
Conversation state (conversationId, responseId, choiceId) is persisted to ~/.nimji/session.json after each run so multi-turn context survives across shell invocations.
nimji "what's 2+2"
nimji "why?" # continues the same conversation
nimji --reset-session # start fresh
nimji --no-session # skip load/save entirelyKeepalive pings Gemini to prevent session expiry during long pauses:
# In-process (chat mode)
nimji --chat --keep-alive --keepalive-minutes 5
# Detached background daemon (one-shot mode)
nimji --keepalive --keepalive-minutes 10 "hi"Cookie rotation — nimji automatically rotates Google's freshness cookies (__Secure-1PSIDTS) via the internal /RotateCookies endpoint, extending your session indefinitely. The CLI rotates on startup; the daemon rotates every 8 minutes.
Encrypted cookie storage — set ENC_KEY to any passphrase to encrypt rotated cookies at rest in session.json (AES-256-GCM):
ENC_KEY="my-secret-passphrase" nimji "hello"Development
# Install dependencies
npm install
# Build TypeScript → dist/
npm run build
# Run tests (node:test, no extra deps)
npm test
# Watch tests
npm run test:watch
# Run from source (tsx)
npm run dev -- "hello"
# Lint
npm run lint
# Format
npm run formatTests
Tests live in tests/ and use Node's built-in node:test + node:assert — no external test framework needed.
| File | Coverage |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| transport.test.ts | buildPayload (text + image attachment), buildStreamGeneratePath, parseStreamChunks, readStreamWithTimeouts, client-hint headers, keepalive body |
| parser.test.ts | extractResponse (text, image URLs, conversation state, noise filtering, image-gen scenarios), sortStableGoogleImageUrls |
| config.test.ts | makePick, loadConfigFromEnv, validateConfig, mergeProjectConfigIntoEnv (file loading, env precedence, idempotency) |
| session.test.ts | createSessionStore — load, save, clear, field normalization, size caps, round-trip |
| images.test.ts | inferMimeTypeFromPath, IMAGE_PIPELINE_DISABLED env flag, disabled-path fast-returns |
| client.test.ts | createClient validation, conversation get/set/reset, keepalive lifecycle, create() / createClientFromEnv() factories |
| result.test.ts | Result type — ok, err, tryCatch, tryAsync, unwrap, match |
| paths.test.ts | resolveAppHomeDir |
npm testProject structure
nimji/
├── src/
│ ├── cli.ts # CLI entry point (one-shot + --chat)
│ ├── client.ts # GemaiClient — generate(), keepalive, conversation state
│ ├── config.ts # loadConfigFromEnv, mergeProjectConfigIntoEnv, validateConfig
│ ├── images.ts # uploadImageToGemini, downloadImages, inferMimeTypeFromPath
│ ├── index.ts # Public library surface
│ ├── parser.ts # extractResponse — text candidates, image URLs, conversation IDs
│ ├── paths.ts # resolveAppHomeDir
│ ├── result.ts # Custom Result<T,E> type (Rust-style, zero deps)
│ ├── session.ts # createSessionStore — session.json persistence
│ ├── transport.ts # buildPayload, buildStreamGeneratePath, parseStreamChunks, …
│ ├── types.ts # Type definitions
│ └── runtime/
│ └── keepalive.ts # Detached keepalive daemon
├── tests/
│ ├── client.test.ts
│ ├── config.test.ts
│ ├── images.test.ts
│ ├── parser.test.ts
│ ├── paths.test.ts
│ └── transport.test.ts
├── dist/ # Compiled output (ESM)
├── config.jsonc # Annotated config template
└── package.jsonRequirements
- Node.js ≥ 22.19 (uses
node:test, native fetch viaundici,--env-file) - Active Gemini web session (free or paid tier)
