nimji
v0.0.5
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 need three values from DevTools (Network tab, any StreamGenerate request):
| Variable | Where to find it |
| ---------- | ------------------------------------------------------------- |
| COOKIES | Cookie: request header — the full SID=…; HSID=…; … string |
| AT_TOKEN | at= field in the POST body |
| F_SID | f.sid= query param in the URL |
Set them as environment variables, in a .env file, or in a config.jsonc (see Configuration files).
export COOKIES="SID=g.a000…"
export AT_TOKEN="AOOh0PE…"
export F_SID="-7468331635213129869"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.
CLI
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 |
Library
Quick start
import { create } from "nimji";
const client = create({
COOKIES: process.env.COOKIES ?? "",
AT_TOKEN: process.env.AT_TOKEN ?? "",
F_SID: process.env.F_SID ?? "",
});
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: "…", AT_TOKEN: "…", F_SID: "…" });
// 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 ?? "",
AT_TOKEN: process.env.AT_TOKEN ?? "",
F_SID: process.env.F_SID ?? "",
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, 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=…",
"AT_TOKEN": "AOOh0PE…",
"F_SID": "-746833163…",
"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 |
| AT_TOKEN | Anti-CSRF token from the POST body |
| F_SID | Session identifier from the URL query |
Optional
| Variable | Default | Description |
| ------------------------ | ---------------- | ------------------------------------------------- |
| MODEL | auto | auto or a full boq_assistant-… build string |
| 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 | 147.0.7727.56 | 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 |
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"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 |
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
│ ├── session.ts # createSessionStore — session.json persistence
│ ├── transport.ts # buildPayload, buildStreamGeneratePath, parseStreamChunks, …
│ └── 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)
MIT © Mra1k3r0
