BUILD 06 / OPEN SOURCE
Igris.
Igris
// an experiment in possibility
OPEN SOURCE / EXPERIMENT 06const idea = await
build("Igris");
ready for what's next
THE IDEA & THE BUILDFrom concept
From concept
to something real.
Igris – Developed a CLI-first AI assistant supporting both local and cloud-hosted LLMs with persistent chat sessions, conversation history, voice interaction, and seamless model switching. Designed with a modular architecture for offline and online AI workflows.
THE QUICK TOUR
A closer look, in three steps.
Preview the project, understand the idea, then explore the product or source.
FROM THE REPOSITORY / README.mdOpen the source notes
# Igris
**Repo:** [github.com/aravindpunyamantula/Igris](https://github.com/aravindpunyamantula/Igris)
A local-first coding & study assistant CLI. Chat with a model (fully offline via Ollama, or your own cloud API key), execute code, read documents, remember things about you across sessions, practice DSA problems, check if your deployed project is up — and, optionally, talk to it out loud through a small floating knight-themed window.
Everything lives in one place: `~/.deepseek-cli/` (sessions, prompts, config, memory, voice models). No database, no cloud account required.
---
## Getting started
**→ See [SETUP.md](./SETUP.md) for the full install walkthrough**, split into separate Linux/Windows/Mac guides that assume no prior setup at all.
Quick version, if you already know what you're doing:
```bash
git clone https://github.com/aravindpunyamantula/Igris.git igris && cd igris
npm install
npm run setup-voice # optional - downloads the offline voice models
node igris-cli.js
```
On first run, Igris creates `~/.deepseek-cli/`, seeds the default prompt library, and checks whether Ollama is reachable at `http://localhost:11434`. If you're using local models, run `ollama serve` first. If not, you can skip straight to cloud setup (`/model api`, see below).
---
## Sessions — how conversations are organized
A **session** is one conversation: its message history, any files added as context, and which prompt template (if any) is active. Sessions are stored as plain JSON at `~/.deepseek-cli/sessions/<id>.json`, saved automatically after every message.
| Command | What it does |
|---|---|
| `/new [name]` | Creates a new, empty session and makes it active. The `id` (shown in your prompt as `[id] You:`) is a timestamp; the name is just a label. |
| `/load <session-id>` | Switches to an existing session, restoring its full history. |
| `/sessions` | Lists every saved session — id, name, message count. Pinned ones show first, marked `*`. |
| `/pin [session-id]` | Toggles pinning a session to the top of `/sessions`. With no argument, pins/unpins whichever session is currently active. |
| `/delete <session-id>` | Permanently deletes a session's file. |
You need an active session before you can chat — if you type a message with none active, Igris tells you to run `/new` first.
**Also relevant to a session:**
- `/history` — reprint every message in the current session
- `/clear` — wipe this session's history *and* context (keeps the session itself)
- `/undo` — remove just the last user+reply pair, without resending anything
- `/retry` — resend your last message (drops the old exchange first, so no duplicate history) — useful after a cancel, an error, or just to reroll
- `/export [file]` — save the conversation as a readable Markdown file
---
## Chat — what actually happens when you send a message
Type a message with no leading `/` (once a session is active). Behind the scenes, Igris assembles one request out of several layers, in this order:
1. **A terminal-formatting instruction** — tells the model to avoid markdown headers/bold/tables (which would show as literal punctuation in a plain terminal) and to keep code in fenced blocks only. In voice mode this is swapped for the **Shadow Knight persona instruction** instead (see [Voice](#voice--talking-to-igris) below).
2. **Remembered facts** (from `/remember`) — applies across *every* session, not just this one.
3. **A prompt template** — either one you explicitly loaded (`/load-prompt`), or one auto-selected by scanning your message for keywords (mentioning "flutter" auto-applies the Flutter template, etc.)
4. **Any `/context` files** you've attached.
5. **Recent history** — up to the last 30 messages of this session.
6. **Your new message.**
The request goes to whichever provider/model is currently active (see [Models](#models--local-vs-cloud) below). While waiting, an animated spinner cycles through phrases ("Thinking...", "Reasoning...", etc.) — press **Ctrl+C** at any point to cancel the in-flight request without quitting the app.
The reply is cleaned up before display:
- Fenced code blocks get their own bordered box, set apart from prose
- Stray markdown that slips through anyway (headers, `**bold**`, horizontal rules) gets stripped
- `<think>...</think>` blocks (from reasoning models like DeepSeek-R1 or Qwen3-thinking) are removed entirely — and if a `<think>` is left unclosed (the model ran out of tokens mid-thought and never reached a real answer), you get a clear warning instead of raw reasoning text
Both your message and the reply are saved to the session, and a `* Cooked for X.Xs` line shows how long the request actually took.
---
## Prompts — specialized instructions for the model
A prompt is a Markdown file in `~/.deepseek-cli/prompts/` containing system instructions for a domain — e.g. "you are a Flutter expert...". Igris ships 11 defaults: **Flutter, React, NextJS, ReactNative, NodeJS, Express, Java, SpringBoot, Deployment, DSA, Doubts.**
| Command | What it does |
|---|---|
| `/prompts` | Lists every prompt file available (defaults + anything you add). |
| `/load-prompt <name>` | Shows the prompt and applies it to your session — every message from now on includes it, until `/clear` or a different one is loaded. |
**Automatic routing**: if you haven't explicitly loaded one, Igris scans each message for keywords (e.g. "riverpod", "spring boot", "leetcode") and silently applies the best-matching prompt just for that message.
### Adding your own custom prompt `.md` file
No code changes needed — just a text file:
1. Go to `~/.deepseek-cli/prompts/` (create it doesn't already exist — Igris creates it on first run).
2. Create a new file named `<Whatever>.md` — the filename (without `.md`) is what you'll type in `/load-prompt`. E.g. a file called `DBMS.md` is loaded with `/load-prompt DBMS`.
3. Write plain instructions inside it, the same way the shipped ones are written — e.g.:
```markdown
# DBMS Expert
You are a database systems expert.
Help with:
- Normalization and schema design
- SQL query optimization
- Transactions, indexing, ACID properties
Keep answers example-driven, with actual SQL where relevant.
```
4. Save it. That's it — no restart, no registration step. Run `/prompts` and you'll see it listed immediately; `/load-prompt DBMS` applies it to your current session.
**One current limitation, worth knowing**: automatic keyword-routing (the "mentions Flutter → auto-applies the Flutter prompt" behavior) only recognizes keyword lists that are hardcoded in `lib/promptRouter.js` — a custom prompt you add this way won't auto-route on its own, but `/load-prompt <name>` always works regardless. If you want your custom prompt to auto-route too, add a keyword entry for it in that file directly.
---
## Context — giving the model a file to look at
```
/context <file-path>
```
Reads a file and attaches its content to every future message in the session (as background context, not counted in your visible chat history). **Auto-detects the format:**
- Plain text/code (`.js`, `.py`, `.md`, `.txt`, `.csv`, `.json`, etc.) — read directly.
- **PDF, Word (`.docx`), Excel (`.xlsx`/`.xls`), PowerPoint (`.pptx`), OpenDocument (`.odt`/`.ods`/`.odp`)** — parsed to plain text first via the `officeparser` package, so the model can actually read them.
`/context-pdf`, `/context-excel`, `/context-docs`, `/context-ppt` are explicit aliases for the same thing (useful for discoverability); they warn — but still try — if the extension doesn't match what you named.
Context is saved with the session and persists across `/load`. There's no "un-context" — use `/clear` to drop everything, or `/new` for a clean slate.
---
## Memory — facts that apply across *every* session
Separate from session history entirely. Stored at `~/.deepseek-cli/memory.json`, and folded into *every* chat request, in *every* session, from the moment it's saved.
```
remember I'm a btech student
```
Works as **plain chat** — no slash needed. Igris detects a message starting with "remember" and stores the rest as a fact instead of sending it to the model.
| Command | What it does |
|---|---|
| `/remember <fact>` | Same thing, explicitly. |
| `/memory` | Lists every remembered fact, numbered. |
| `/forget <number>` | Removes one fact by its number. |
Use this for things that are true about you regardless of topic — background, preferred stack, constraints — not one-off task context (that's what `/context` is for).
---
## Code execution — `/run`
```
/run javascript <code>
/run python <code>
/run bash <command>
/run powershell <command>
/run java <code>
/run flutter <args>
```
Each run writes the code to a fresh temp directory, executes it with a hard **10-second timeout** (killed if it runs longer), prints stdout/stderr separately, and deletes the temp directory afterward. Java's public class name is detected automatically from the code.
**Cross-platform notes:**
- Python tries `python3` first (or `python` first on Windows), falling back to the other if the first isn't found.
- Bash works out of the box on Mac/Linux; on Windows it needs Git Bash or WSL on `PATH` — use `/run powershell` instead if you don't have either.
- Java/Flutter need their SDKs installed regardless of OS.
- If an interpreter isn't found, Igris tells you what's missing instead of failing silently.
`/explain-error` — run this right after a failed `/run`. It sends your exact code and the exact stderr/exit info from that run to the model, asking specifically what went wrong and how to fix it — no re-pasting.
---
## Models — local vs. cloud
Igris talks to one **active provider** at a time: your local Ollama install, or a cloud API (Groq, OpenRouter, Google Gemini, AWS Bedrock).
```
/model Show active provider/model + status of all configured providers
/model list List models (numbered) for the active provider
/model use <name|number> Set the model for the active provider
/model switch Quick-pick a provider + model using already-saved credentials
/model provider <name> Switch active provider (ollama, groq, openrouter, gemini, bedrock)
/model api Interactive wizard: add/update a provider's key, URL, and model
/model pull <name> Pull a new Ollama model
```
**Local (Ollama)** — fully offline. Default model is `deepseek-coder:13b`, but if that exact tag isn't installed, Igris auto-detects at startup and switches to whatever you *do* have, saving the choice so it doesn't ask again.
**Adding a cloud provider**: `/model api` walks you through picking a provider, entering its key (or `accessKeyId:secretAccessKey` for Bedrock), confirming the base URL (tolerant of pasting the wrong endpoint — it normalizes known suffixes like `/models` off automatically), and picking a model from a live-fetched list.
**Switching later**: `/model switch` reuses already-saved credentials — no re-entering keys. `/model provider ollama` goes back to fully offline any time.
Response quality is a property of whichever model is active — a small local model (e.g. `deepseek-coder:latest`, ~700MB) will produce noticeably rougher answers than Groq/OpenRouter with a real hosted model. Match the model to the task.
---
## Study tools
Built for actual coursework, not just Q&A — all of these are really just pre-written prompts reusing the normal chat pipeline (same spinner, Ctrl+C to cancel, same history/memory).
| Command | What it does |
|---|---|
| `/quiz <topic>` | Generates a 5-question quiz (questions only, no answers yet). |
| `/quiz-grade` | Grades your answers against the quiz it just gave you, question by question. |
| `/dsa-practice [easy\|medium\|hard]` | Random problem from a built-in set (arrays, linked lists, trees, graphs, DP, etc.). Solve with `/run` or paste your solution as a message — the problem statement is saved into session history automatically so "review my solution" has full context. |
| `/summarize [file]` | Concise bullet-point revision note of the session so far. With a file argument, appends it (dated) to a running notes file instead of just printing it. |
---
## Project status — `/status`
```
/status <url> [url2] [url3...]
```
Plain HTTP GET (10s timeout, follows redirects) against any URL — your deployed frontend, a backend API, a specific route. Reports status code, response time, and where it ended up if redirected. With an active session, the raw results are also handed to the model for a plain-English read on whether things look healthy and what to check if not.
This is a live network check, not a code review — pair it with `/context` (attach `server.js`) and a follow-up question for that.
---
## Settings — view and wipe stored state
```
/settings Overview: session count, remembered facts, configured providers
/settings name <name> Rename the voice assistant + wake word (e.g. Jarvis)
/settings clear sessions Delete every saved session
/settings clear apis Wipe all cloud provider keys/URLs/models, revert to ollama
/settings clear memory Forget everything remembered
/settings clear cache Remove any leftover /run temp files (normally self-cleaning)
/settings reset All of the above at once - back to a fresh install
```
Every destructive one asks for `yes/no` confirmation first. Prompt templates in `~/.deepseek-cli/prompts/` are never touched by any of this — those are your files to edit.
---
## Voice — talking to Igris
Two layers: an offline voice pipeline (`lib/voice/`), and a small floating window (`voice-ui/`) that uses it.
### The voice pipeline
- **Text-to-speech**: `sherpa-onnx-node` + a Piper VITS voice model (`en_US-joe-medium`). Fully offline, no API key. `/say <text>` speaks arbitrary text as a raw test of the pipeline.
- **Speech-to-text**: a quantized Moonshine "base" English model (~106MB, also via `sherpa-onnx-node`), fully offline. `/listen [seconds]` records from your mic and transcribes it — with a session active, it's sent through chat like a typed message.
- **Voice + chat combined**: `/speak <question>` sends your text through the normal chat pipeline *and* speaks the reply aloud, in a dedicated persona (see below).
Both model files live in `~/.deepseek-cli/voice-models/` (`tts/` and `stt/`), separate from the rest of the app's data.
### The persona — customizable, and kept private by default
When a reply is going to be spoken (`voice: true`), the terminal-formatting system instruction is swapped for a **persona** instruction instead — a separate character/tone for voice mode. Two things are configurable independently:
- **The name** — `/settings name <name>` (e.g. `/settings name Jarvis`) changes both the wake word and how the assistant refers to itself. Everything (wake-word matching, the persona text, the window's title/status text) picks this up automatically; nothing else needs to be touched.
- **The personality itself** — lives in `~/.deepseek-cli/persona.md`, a plain text file seeded on first run with a short, generic default. Since that directory is outside the git repo entirely, **whatever you write there stays local** — write it as elaborate a character as you want (the maintainer's own setup uses a "loyal Shadow Knight" persona, for instance) without it ending up in a public repo if you ever fork/share this project. Any `{{name}}` in the file is substituted with whatever `/settings name` is currently set to, so a persona you write stays correct even after renaming.
The default persona is tuned to avoid a common failure mode: bookending every reply with "Hey there, [Name]!" style address, which reads as robotic. It's an instruction about *tone*, not a script — it shouldn't get in the way of a direct, useful answer.
### Writing your own persona
1. Open `~/.deepseek-cli/persona.md` in any text editor (it's created automatically the first time you run Igris).
2. Replace the contents with whatever character/tone you want. Use the literal text `{{name}}` anywhere you want the assistant's current name (whatever `/settings name` is set to) to appear — it gets substituted in automatically every time. For example, a "Jarvis"-style butler persona:
```markdown
You are {{name}}, a impeccably polite AI butler. This reply will be read
aloud, not shown as text, so:
- Speak in full, natural sentences - no markdown, no code, no lists.
- Address the user as "sir" or "madam" occasionally, not every reply.
- Be efficient and precise - a butler doesn't ramble.
- If the question needs code or something better shown as text, say so
briefly ("I've prepared that for you in text") rather than reading it.
```
3. Save the file. No restart needed for the terminal (`/speak`, `/listen`); for the floating window, close and reopen it with `/voice` to pick up the change.
4. Rename to match, if you like: `/settings name Jarvis`.
Because `~/.deepseek-cli/` lives outside this git repository entirely, this file is never committed — write as personal or elaborate a persona as you want without worrying about it ending up in a shared/public copy of the project.
### The floating window — `/voice`
```
/voice
```
Launches `voice-ui/`, a small always-on-top Electron window with an animated shield emblem. **Wake-word only** — no button. Say **"`<assistant name>`, ..."** followed by your question (default name is "Igris"; change it with `/settings name`); it detects the wake word, extracts your command, sends it through chat, and speaks + displays the reply.
**Why a separate process, and why wake-word detection is fuzzy rather than exact:**
- `sherpa-onnx-node` (the native STT/TTS addon) doesn't load reliably inside Electron's own bundled V8 — confirmed empirically, it fails even under `ELECTRON_RUN_AS_NODE`. So all the actual voice logic runs in `voice-ui/voice-host.js`, spawned as a genuine **system Node.js** child process; `voice-ui/main.js` just creates the window and relays that child's output over stdout-as-JSON.
- An uncommon/invented wake word (like the default "Igris") gets transcribed phonetically rather than correctly by STT — observed mis-transcriptions include "Agress", "Acre is", "I agree", "I crease", "Reese". Exact string matching would basically never fire, so wake-word detection uses a **Soundex-style consonant code** (vowels dropped, consonants mapped to digit groups, e.g. "igris" → `262`) computed from whichever name is currently configured, and checks for substring containment between codes — this correctly treats all of the above as the same word. A common real word (like "Jarvis") transcribes correctly on its own and doesn't need the fuzziness, but the same matching logic still applies harmlessly.
**Text synced to speech**: the reply is split into sentences; each sentence is synthesized *first* (so its exact audio duration is known), then its text is revealed character-by-character at a pace matched to that duration, while the audio plays concurrently — text and voice finish each sentence at the same moment, rather than text streaming in fast and voice lagging behind.
**Setup**: `cd voice-ui && npm install` once (pulls in Electron, kept as a separate dependency from the core CLI). `/voice` passes `--no-sandbox` automatically — Electron's `chrome-sandbox` helper needs root ownership + setuid to work at all on Linux, a common npm-install gotcha; for a small local window with no untrusted web content, skipping that isolation is a reasonable tradeoff.
---
## File structure
```
igris-cli.js Entry point: REPL loop, prompt bar, Ctrl+C handling
lib/
config.js Paths, defaults, per-provider settings
storage.js Sessions, prompts, memory, cache — all file I/O
chat.js Builds the request, dispatches to a provider, persists the result
commands.js Every /command handler
ui.js Terminal colors, banner, spinner, help text
ollama.js, providers.js Ollama + Groq/OpenRouter/Gemini/Bedrock integrations
executor.js /run sandboxing
fileReader.js /context format auto-detection (text vs. office docs)
promptRouter.js, defaultPrompts.js, dsaProblems.js Static data + auto-routing logic
persona.js Voice persona template loading + {{name}} substitution
voice/
tts.js Offline text-to-speech
stt.js Offline speech-to-text + mic recording
voice-ui/ The floating window - separate package.json (Electron is opt-in)
main.js Electron main process - window + child process relay
voice-host.js The actual wake-word loop, STT/TTS/chat orchestration
preload.cjs, index.html, style.css, renderer.js The window's UI
scripts/
setup-voice-models.sh Downloads the TTS/STT model files (see SETUP.md)
README.md This file - how everything works
SETUP.md How to install and run it
~/.deepseek-cli/ All user data (not in the repo)
sessions/*.json One file per session
prompts/*.md Prompt templates
persona.md Voice persona - user-editable, never committed
config.json Active provider/model, per-provider credentials, assistant name
memory.json Remembered facts
voice-models/tts/, stt/ Downloaded model files
```
---
## Platform support
Igris itself (sessions, config, prompts, chat, memory) is plain Node.js and runs identically on Windows, macOS, and Linux. `/run` and voice depend on what's actually installed:
- **Windows**: `/run bash` needs Git Bash or WSL; `/run powershell` works natively. Voice mic recording needs SoX on `PATH`.
- **macOS/Linux**: `/run bash` and Python work out of the box; `/run powershell` needs PowerShell Core (`pwsh`) installed separately.
- Everything prints a clear "X isn't installed" message rather than a bare crash when a required external tool is missing.
Continue on GitHub