@codeyantram/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +434 -0
- package/dist/index.js +22760 -0
- package/dist/server.js +78123 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Harshal Limaye
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# CodeYantram
|
|
2
|
+
|
|
3
|
+
A terminal-based AI coding assistant. CodeYantram brings an LLM agent into your terminal — chat with the model, and let it read, search, edit, and execute against your project — all with full streaming, a built-in approval gate for anything that mutates, and a small local server that keeps the TUI and the model providers separated.
|
|
4
|
+
|
|
5
|
+
Built with **Bun**, **OpenTUI** (a React-based TUI renderer), **Hono**, and the **Vercel AI SDK**.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **In-terminal chat UI** — OpenTUI/React interface with streaming markdown rendering, live deltas, a "Thinking…" spinner, and tool-call status (`running…`, `done`, `needs approval`, `denied`).
|
|
10
|
+
- **Multi-provider support** — Anthropic, OpenAI, Google, and DeepSeek from one catalog, each with its own API key.
|
|
11
|
+
- **Reasoning effort control** — models that support it expose per-model effort levels (`none` → `max`), validated before a request is ever sent.
|
|
12
|
+
- **Two agents with graduated tool access**:
|
|
13
|
+
- **Talk** — read-only tools (`read_file`, `list_dir`, `glob`, `grep`, `git`) plus `web_fetch` to read a URL — the one Talk tool that still asks for approval, since it leaves the machine.
|
|
14
|
+
- **Build** — the full tool catalog, including mutating tools (`edit_file`, `write_file`, `bash`).
|
|
15
|
+
- **Project instructions** — an `AGENTS.md` (or `CLAUDE.md`) at the project root is loaded into the system prompt on every turn, so the project's own conventions travel with each request.
|
|
16
|
+
- **Tool approval gate** — every mutating tool pauses mid-turn and asks for explicit approval (`y`/`n`) before it runs.
|
|
17
|
+
- **Streaming SSE protocol** — one wire format for success *and* failure; cancel is just closing the connection.
|
|
18
|
+
- **Persistent chat sessions** — every message is autosaved as it happens (not just at the end of a turn), so `/sessions` can list, resume, rename, or delete past conversations across restarts; `--continue`/`--resume <id>` pick one up straight from launch.
|
|
19
|
+
- **Local persistence** — API keys (`auth.json`, `0600`), preferences (`preferences.json`), per-project prompt history (`prompt-history.json`), and chat sessions (`sessions.db`, SQLite) under `~/.codeyantram/` (`0700`).
|
|
20
|
+
- **12 hand-tuned themes** and tree-sitter syntax highlighting across 17+ languages.
|
|
21
|
+
- **Slash-command menu** with autocomplete (`/new`, `/agents`, `/models`, `/connect`, `/sessions`, `/themes`, `/exit`, …).
|
|
22
|
+
- **Read-only git without the shell** — a dedicated `git` tool runs ten inspection subcommands as argv (no shell, no pipes), so reading history, diffs, and blame costs no approval prompt and works in Talk too. Branches and tags are *arguments* here (`diff main...HEAD`, `show v1.2.0:src/config.ts`), not subcommands; every git command that writes still goes through `bash`.
|
|
23
|
+
- **Sandboxed tool paths** — every tool path is resolved against the project root and can never escape it.
|
|
24
|
+
- **Single-flight turns** — one response in flight at a time; escape/ctrl+c cancels cleanly.
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
CodeYantram is a **Bun workspace monorepo** with four packages:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
codeyantram/
|
|
32
|
+
├── packages/
|
|
33
|
+
│ ├── cli/ # The terminal UI (OpenTUI + React)
|
|
34
|
+
│ ├── server/ # Local Hono server: model routing, tool execution, session API
|
|
35
|
+
│ ├── shared/ # Zod schemas, catalogs, and pure logic shared by both
|
|
36
|
+
│ └── sessions/ # SQLite-backed chat session store (schema, migrations, retention)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The **CLI** never talks to a model directly. It POSTs a chat request to the local **server**, which resolves the provider/model/API key, streams the response back over **SSE**, and executes any tool calls against the project root. The **shared** package defines the contract between them — schemas, the model/tool/agent catalogs, and stream-folding logic — so both sides can't drift apart. The **server** also owns the **sessions** package's store: every message and tool-approval decision is saved as it happens, so a conversation survives quitting the CLI.
|
|
40
|
+
|
|
41
|
+
## Getting Started
|
|
42
|
+
|
|
43
|
+
### Prerequisites
|
|
44
|
+
|
|
45
|
+
- **Bun 1.3.0+** to develop, build, or run the CLI. The CLI's terminal renderer
|
|
46
|
+
([OpenTUI](https://opentui.com)) doesn't implement its native FFI backend for Node yet
|
|
47
|
+
(`@opentui/core@0.5.9` throws "OpenTUI native FFI is not available for this runtime yet"
|
|
48
|
+
under Node) — this is an upstream limitation, not something this repo's build can work
|
|
49
|
+
around, so the CLI needs Bun for now.
|
|
50
|
+
- **Node.js 22+** is fully supported for the **server** on its own (its dual-runtime HTTP
|
|
51
|
+
entrypoint, env loading, and every tool have no Bun-only calls left — see
|
|
52
|
+
[How It Works](#how-it-works)) — useful for self-hosting the server independent of the
|
|
53
|
+
CLI. The CLI itself still spawns it via whichever runtime launched the CLI process
|
|
54
|
+
(`process.execPath`), so a Bun-run CLI always gets a Bun-run server today.
|
|
55
|
+
- A terminal with a modern color/UTF-8 profile
|
|
56
|
+
|
|
57
|
+
### Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bun install
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Configure API keys
|
|
64
|
+
|
|
65
|
+
Keys can be provided either way (checked in this order):
|
|
66
|
+
|
|
67
|
+
1. **In-app** — run `/connect` in the CLI, pick a provider, and paste a key. Stored in `~/.codeyantram/auth.json` with `0600` permissions.
|
|
68
|
+
2. **Environment variables** — copy `.env.example` to `.env` and fill in keys:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# .env
|
|
72
|
+
API_URL=http://localhost:3001
|
|
73
|
+
ANTHROPIC_API_KEY=
|
|
74
|
+
OPENAI_API_KEY=
|
|
75
|
+
GOOGLE_GENERATIVE_AI_API_KEY=
|
|
76
|
+
DEEPSEEK_API_KEY=
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The server checks the `/connect` auth store first, then falls back to the env var — a key set either way works at chat time.
|
|
80
|
+
|
|
81
|
+
### Run
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bun run dev:cli
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
One terminal, one command: the CLI checks whether a server is already answering at
|
|
88
|
+
`API_URL` (default `http://localhost:3001`) and, if not, spawns one itself and shuts it
|
|
89
|
+
down again on exit. Running `bun run dev:server` separately first still works exactly as
|
|
90
|
+
before — the CLI detects it and reuses it rather than spawning a second one, which is
|
|
91
|
+
useful for watching the server's own logs directly instead of `~/.codeyantram/server.log`.
|
|
92
|
+
|
|
93
|
+
Run it in the project root you want the agent to work on — the server resolves every tool
|
|
94
|
+
path against the directory it was launched from.
|
|
95
|
+
|
|
96
|
+
### Build
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cd packages/cli && bun run build
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Produces `packages/cli/dist/index.js` (executable, `#!/usr/bin/env bun` shebang — the
|
|
103
|
+
package's `bin` entry) and `packages/cli/dist/server.js` (the bundled server, copied
|
|
104
|
+
alongside it, which `ensureServerRunning()` spawns automatically instead of falling back to
|
|
105
|
+
raw source). `@opentui/*`, `react`, `web-tree-sitter`, every `tree-sitter-*` grammar
|
|
106
|
+
package, `@libsql/client`, and `@vscode/ripgrep` stay external (real `node_modules`
|
|
107
|
+
dependencies, not bundled) — each does its own native-binary or `import.meta.resolve()`
|
|
108
|
+
asset lookup at runtime that only works against a real, installed copy.
|
|
109
|
+
|
|
110
|
+
The server binds to `127.0.0.1` only (never `0.0.0.0`), and every route requires a local auth token the server mints on first start and the CLI reads back automatically — see [Local server auth](#local-server-auth) below. Running the CLI against a server on another machine (or a manually-copied config directory) needs that token to travel too; `API_URL` alone isn't enough.
|
|
111
|
+
|
|
112
|
+
## Usage
|
|
113
|
+
|
|
114
|
+
### Keybindings
|
|
115
|
+
|
|
116
|
+
| Key | Action |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `enter` | Send message |
|
|
119
|
+
| `shift+enter` | Insert a newline in the input |
|
|
120
|
+
| `↑` / `↓` | Step back and forward through this run's submitted prompts (inside a multi-line prompt, only from its first/last line) |
|
|
121
|
+
| `tab` | Cycle agents (Talk ⇄ Build) |
|
|
122
|
+
| `escape` | Cancel an in-flight stream; otherwise clear the prompt |
|
|
123
|
+
| `ctrl+c` | Clear the prompt; quit when the prompt is empty |
|
|
124
|
+
| `↑` / `↓` / `enter` / `tab` / `escape` | Navigate menus and overlays (when one owns the keyboard) |
|
|
125
|
+
|
|
126
|
+
### Slash commands
|
|
127
|
+
|
|
128
|
+
Type `/` in the input bar for an autocompleting menu:
|
|
129
|
+
|
|
130
|
+
| Command | Description |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `/new` | Start a new session (the old one is already saved — see [Sessions](#sessions)) |
|
|
133
|
+
| `/agents` | Switch agent (Talk / Build) |
|
|
134
|
+
| `/models` | Switch model |
|
|
135
|
+
| `/connect` | Connect a provider with an API key (or clear one) |
|
|
136
|
+
| `/init` | Generate or refine this project's `AGENTS.md` (switches to Build) |
|
|
137
|
+
| `/instructions` | Toggle project instructions on/off for the session |
|
|
138
|
+
| `/sessions` | Browse, resume, rename, or delete saved sessions |
|
|
139
|
+
| `/themes` | Switch theme |
|
|
140
|
+
| `/upgrade` | Upgrade CodeYantram |
|
|
141
|
+
| `/support` | Get support |
|
|
142
|
+
| `/exit` | Exit the app |
|
|
143
|
+
|
|
144
|
+
### Agents
|
|
145
|
+
|
|
146
|
+
- **Talk** (default) — chat and *read* the project. Exposed tools are read-only, so it's safe for exploration.
|
|
147
|
+
- **Build** — full agent. Can edit files, write files, and run shell commands — each mutating call pauses for your approval first.
|
|
148
|
+
|
|
149
|
+
Switch between them with `tab` or `/agents`; the current agent is shown in the input bar.
|
|
150
|
+
|
|
151
|
+
### Models
|
|
152
|
+
|
|
153
|
+
The catalog is defined in `packages/shared/src/models.ts`:
|
|
154
|
+
|
|
155
|
+
| Provider | Models | Effort levels |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| Anthropic | `claude-sonnet-5`, `claude-opus-5`, `claude-haiku-4-5` | `low` → `max` (haiku: none) |
|
|
158
|
+
| OpenAI | `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano` | `none` → `xhigh` |
|
|
159
|
+
| Google | `gemini-3.5-flash` | `minimal` → `high` |
|
|
160
|
+
| DeepSeek | `deepseek-v4-flash`, `deepseek-v4-pro` | — |
|
|
161
|
+
|
|
162
|
+
Default: `claude-sonnet-5` with `high` effort. A model is only offered in `/models` if its provider has a key configured.
|
|
163
|
+
|
|
164
|
+
### Project instructions
|
|
165
|
+
|
|
166
|
+
If the project root has an [`AGENTS.md`](https://agents.md) (or, as a fallback, a `CLAUDE.md`), CodeYantram appends it to the system prompt for both agents — conventions, build/test commands, things to avoid. Nothing to enable and no restart needed: the file is re-read on every turn, so an edit applies to your next message.
|
|
167
|
+
|
|
168
|
+
| Behaviour | Detail |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| Location | `AGENTS.md`, then `CLAUDE.md`, at the project root (the `cwd` the CLI was started in) — first match wins, never both |
|
|
171
|
+
| Size cap | 32 KB, cut at a line boundary with a note saying it was cut |
|
|
172
|
+
| Missing / empty / binary | Treated as "no instructions" — never an error |
|
|
173
|
+
| Symlinks | Followed only while they stay inside the project root |
|
|
174
|
+
| Precedence | Below your messages: it shapes *how* work is done, and cannot approve a tool call, widen an agent's tool access, or override what you ask for in the conversation |
|
|
175
|
+
|
|
176
|
+
The contents are framed by `--- BEGIN/END PROJECT INSTRUCTIONS ---` markers, and a file that tries to write those markers itself has them stripped — so a repository you didn't author can't close the block early and speak as the system prompt.
|
|
177
|
+
|
|
178
|
+
Don't have one yet? Run `/init` to generate one — it surveys the project (README, manifests, build/test/lint config, CI) and writes a concise `AGENTS.md`, or refines an existing one in place. It switches you to the Build agent (it needs `write_file`), and the write itself still goes through the normal approval prompt like any other Build edit. A toast confirms once it's actually finished — including after any approval prompts along the way, not just the first response.
|
|
179
|
+
|
|
180
|
+
**Visibility and the off-switch.** Below a reply that loaded the project's instruction file, the usual token-count line also shows its filename and size (e.g. `1.2k in · 340 out · AGENTS.md 2.0KB`, `(cut)` appended if it hit the 32 KB cap) — so the cost is legible per turn, not just inferred. If it got cut off, you also get a one-time warning toast the first turn that happens, rather than silent truncation. Run `/instructions` to toggle instructions off or back on for the rest of the session, without renaming or deleting any file — useful for an A/B comparison of the model's behavior with and without them. The toggle covers every source below uniformly; there's no persistent status-bar indicator for the global or nested sources specifically, only the per-turn line above for the project file.
|
|
181
|
+
|
|
182
|
+
**A user-level file.** `~/.codeyantram/AGENTS.md` (or `CLAUDE.md`) applies across every project — conventions you want everywhere without repeating them into each repo's own file. It's composed above the project file in the same block, and the project's file wins wherever the two disagree. Together they're capped at 48 KB combined (each still capped at 32 KB on its own) — if both are large enough to exceed that, the user-level file gives way for that turn rather than either being cut down further.
|
|
183
|
+
|
|
184
|
+
**Nested instructions.** A subdirectory's own `AGENTS.md`/`CLAUDE.md` — say, `packages/server/AGENTS.md` in a monorepo — isn't loaded up front. It's picked up lazily: the first time `read_file` opens a file under that subtree, every ancestor instruction file between it and the project root (whose own file is already covered above) is appended to that tool call's result, framed the same way and once per directory per turn. Nothing elsewhere in the tree costs anything until the agent actually reads something under it.
|
|
185
|
+
|
|
186
|
+
**Prompt caching (Anthropic only).** The system prompt is sent as two separate blocks — the static per-agent prompt, and the instructions block — each with its own Anthropic `cache_control` breakpoint, so editing `AGENTS.md` only invalidates the smaller, instructions-specific cache entry rather than the whole system prompt. In practice CodeYantram's own static prompt (~500 tokens) sits under Anthropic's 1024-token minimum-cacheable-length for Sonnet/Opus, so it doesn't yet get its own independent cache hit — the split still costs nothing when that's true, and pays off automatically once either block grows past the threshold. The combined prompt still caches and gets reused turn-to-turn whenever the instructions are unchanged, which is the common case. Non-Anthropic providers never see the cache marker.
|
|
187
|
+
|
|
188
|
+
### Themes
|
|
189
|
+
|
|
190
|
+
Twelve built-in themes (Sahyadri, Kaapi, Thirai, Konkan, Sanganak, Aranya, Gulabi, Bazaar, Shishir, Ladakh, Oviya, Kaadu), switchable via `/themes`. Theme, model, and agent preferences persist across restarts.
|
|
191
|
+
|
|
192
|
+
### Sessions
|
|
193
|
+
|
|
194
|
+
Every message and tool-approval decision is saved to `~/.codeyantram/sessions.db` as it happens — not just at the end of a turn — so a conversation survives quitting the CLI. The Session screen shows the current conversation's title (server-derived from your first message, e.g. `fix the socket handshake`) in a small header above the transcript.
|
|
195
|
+
|
|
196
|
+
**The `/sessions` picker:**
|
|
197
|
+
|
|
198
|
+
| Key | Action |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| `↑` / `↓` | Move the selection |
|
|
201
|
+
| `enter` | Resume the highlighted session |
|
|
202
|
+
| `ctrl+r` | Rename it (enter submits, a blank or unchanged title cancels with no API call) |
|
|
203
|
+
| `ctrl+d` | Delete it immediately (no confirm step — it's a deliberate modifier chord) |
|
|
204
|
+
| `escape` | Close the picker |
|
|
205
|
+
|
|
206
|
+
Deleting the session you're currently in starts a new one automatically (the same as `/new`) and closes the picker, dropping you straight onto the fresh session — otherwise the next message would fail against a session that no longer exists. Deleting any other session just removes it from the list; the picker stays open. Renaming the session you're currently in updates the Session screen's header immediately.
|
|
207
|
+
|
|
208
|
+
**Resuming at launch**, instead of through the picker:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
bun run dev:cli --continue # resume this project's most recently updated session
|
|
212
|
+
bun run dev:cli --resume <id> # resume a specific session by id
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Both fall back to starting fresh (with a toast explaining why) if there's nothing to resume — no previous session for `--continue`, or an unknown/deleted id for `--resume`.
|
|
216
|
+
|
|
217
|
+
**Retention.** `sessions.db` has no size cap of its own, but it doesn't grow forever either — see [Session storage and retention](#session-storage-and-retention) below.
|
|
218
|
+
|
|
219
|
+
**Prompt history** (what `↑`/`↓` step through in the input bar, separate from saved chat sessions) is also persisted, per project, in `prompt-history.json` — a fresh `bun run dev:cli` in the same directory picks up where you left off.
|
|
220
|
+
|
|
221
|
+
## Tools
|
|
222
|
+
|
|
223
|
+
Defined in `packages/shared/src/tools.ts` and executed by the server against the project root:
|
|
224
|
+
|
|
225
|
+
| Tool | Description | Access |
|
|
226
|
+
| --- | --- | --- |
|
|
227
|
+
| `read_file` | Read a file's contents | Read-only (Talk + Build) |
|
|
228
|
+
| `list_dir` | List a directory's entries | Read-only |
|
|
229
|
+
| `glob` | Find files matching a glob | Read-only |
|
|
230
|
+
| `grep` | Regex-search file contents | Read-only |
|
|
231
|
+
| `git` | Read the repository — `status`, `log`, `diff`, `show`, `blame`, `describe`, `shortlog`, `rev-parse`, `ls-files`, `show-ref` | Read-only (Talk + Build) |
|
|
232
|
+
| `edit_file` | Replace one exact, unique snippet | **Requires approval** (Build) |
|
|
233
|
+
| `write_file` | Create/overwrite a file | **Requires approval** (Build) |
|
|
234
|
+
| `bash` | Run a shell command (30s timeout) | **Requires approval** (Build) |
|
|
235
|
+
| `web_fetch` | Fetch a URL and return its content as text | **Requires approval** (Talk + Build) |
|
|
236
|
+
|
|
237
|
+
### The approval flow
|
|
238
|
+
|
|
239
|
+
1. The model calls a mutating tool → the server sends a `tool-approval-request` event and **pauses the turn**.
|
|
240
|
+
2. The CLI shows an **Approve tool call** overlay with the tool name and arguments.
|
|
241
|
+
3. `y` / `enter` approves, `n` denies, `escape`/`ctrl+c` counts as a denial.
|
|
242
|
+
4. The CLI records the decision, then automatically starts a new turn that replays it — the server acts on it and streams back the tool result.
|
|
243
|
+
|
|
244
|
+
Read-only tools run immediately with no prompt. `web_fetch` always needs approval, even in Talk, since it's the one tool that leaves the machine. A tool loop is capped at 15 steps to guard against a confused model looping forever.
|
|
245
|
+
|
|
246
|
+
### Network access
|
|
247
|
+
|
|
248
|
+
`web_fetch` is the only tool that reaches outside the project — everything else in the catalog is filesystem/shell-only, sandboxed to the project root. Its policy (`packages/server/src/tools/url-policy.ts` and `web-fetch.ts`):
|
|
249
|
+
|
|
250
|
+
- **https only, GET only.** No other scheme or method; the model can't set headers, cookies, or credentials — anything behind a login is unreachable.
|
|
251
|
+
- **No private/internal targets.** A loopback, link-local, RFC 1918, or otherwise non-public address is refused, whether given directly or reached via DNS or a redirect. Every redirect hop (up to 5) is re-validated against the same policy, not just the original URL.
|
|
252
|
+
- **Bounded like every other tool.** 5 MB response cap (post-decompression), 30s timeout, output paged and line-numbered like `read_file`. A content type this tool doesn't handle (PDF, images, archives, …) is refused before its body is even read.
|
|
253
|
+
- **Untrusted by design.** Fetched content comes back wrapped in an explicit `BEGIN`/`END UNTRUSTED FETCHED CONTENT` frame with a warning — the system prompt tells the model never to treat it as instructions.
|
|
254
|
+
|
|
255
|
+
Two environment variables (see `.env.example`) let you deliberately loosen the private-address check for a controlled target, e.g. an internal docs server:
|
|
256
|
+
|
|
257
|
+
- `WEB_FETCH_ALLOW_PRIVATE=1` — allow fetching private/loopback/internal addresses. The https-only rule is unaffected — the target still needs a valid TLS certificate. Off by default.
|
|
258
|
+
- `WEB_FETCH_DENY_HOSTS` — a comma-separated hostname blocklist, checked before anything else and enforced even with `WEB_FETCH_ALLOW_PRIVATE` set.
|
|
259
|
+
|
|
260
|
+
## How It Works
|
|
261
|
+
|
|
262
|
+
### Chat request
|
|
263
|
+
|
|
264
|
+
The CLI POSTs a JSON `ChatRequest` to `POST /chat`:
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{
|
|
268
|
+
"model": "claude-sonnet-5",
|
|
269
|
+
"effort": "high",
|
|
270
|
+
"agent": "Build",
|
|
271
|
+
"cwd": "/path/to/project",
|
|
272
|
+
"messages": [
|
|
273
|
+
{ "id": "...", "role": "user", "parts": [{ "type": "text", "text": "fix the socket handshake" }] }
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Reasoning parts are stripped before replay (`toRequestMessage`) — Anthropic thinking blocks carry signatures this format doesn't keep, so replaying them would be worse than omitting them.
|
|
279
|
+
|
|
280
|
+
### SSE stream protocol
|
|
281
|
+
|
|
282
|
+
The server replies with an SSE stream of JSON events (one per `data:` line):
|
|
283
|
+
|
|
284
|
+
| Event | Purpose |
|
|
285
|
+
| --- | --- |
|
|
286
|
+
| `start` | Announces the assistant message id before any content |
|
|
287
|
+
| `text-delta` | Streaming text fragment |
|
|
288
|
+
| `reasoning-delta` | Streaming reasoning fragment |
|
|
289
|
+
| `tool-call` | A tool invocation (name, id, args) |
|
|
290
|
+
| `tool-result` | A tool's output, folded into its call |
|
|
291
|
+
| `tool-approval-request` | A mutating tool waiting on the user |
|
|
292
|
+
| `done` | Turn complete (`durationMs`) |
|
|
293
|
+
| `error` | Failure (`code` + `message`) |
|
|
294
|
+
|
|
295
|
+
A stream always opens with `start` and closes with `done` or `error`. A connection that closes with neither means the request was **aborted** — a normal outcome, not a failure. Errors (including a missing API key) are delivered as `error` events on the stream rather than HTTP error statuses, so the CLI has exactly one code path for success and failure.
|
|
296
|
+
|
|
297
|
+
### Streaming pipeline
|
|
298
|
+
|
|
299
|
+
- **Server** (`packages/server/src/lib/chat-stream.ts`) — resolves the model/key, calls `streamText` with the right tools for the agent, and forwards every part as an SSE event.
|
|
300
|
+
- **CLI** (`packages/cli/src/api/chat.ts`) — buffers the SSE stream, splits on record boundaries, validates each event against the schema (bad lines are skipped, not fatal), and yields parsed events.
|
|
301
|
+
- **Shared folding** (`packages/shared/src/stream.ts` — `applyStreamEvent`) — the single, tested place where deltas are reconstructed into discrete message parts. Interleaved reasoning → text → tool-call → text folds into four distinct parts instead of collapsing.
|
|
302
|
+
- **CLI state** (`packages/cli/src/providers/chat.tsx`) — folds events into React state in real time, renders them as they arrive, and wires up cancel, new-session, and the approval round-trip.
|
|
303
|
+
|
|
304
|
+
## Server API
|
|
305
|
+
|
|
306
|
+
Mounted at `http://127.0.0.1:3001` (override with `API_URL` / `PORT` — `PORT` doesn't change the bind address, only which local port it listens on):
|
|
307
|
+
|
|
308
|
+
| Route | Method | Description |
|
|
309
|
+
| --- | --- | --- |
|
|
310
|
+
| `/health` | GET | Liveness check |
|
|
311
|
+
| `/providers` | GET | Which providers have API keys configured |
|
|
312
|
+
| `/chat` | POST | Streams one chat turn as SSE |
|
|
313
|
+
| `/sessions` | GET | List sessions for a project (`?project=<cwd>`) |
|
|
314
|
+
| `/sessions` | POST | Create a session with its first message; returns `{ id, title }` |
|
|
315
|
+
| `/sessions/:id` | GET | Load a session, messages included |
|
|
316
|
+
| `/sessions/:id` | PATCH | Rename a session |
|
|
317
|
+
| `/sessions/:id` | DELETE | Delete a session |
|
|
318
|
+
| `/sessions/:id/messages` | POST | Append an already-finished message |
|
|
319
|
+
| `/sessions/:id/approvals` | POST | Resolve a pending tool-call approval |
|
|
320
|
+
|
|
321
|
+
### Local server auth
|
|
322
|
+
|
|
323
|
+
The server is bound to loopback only, but loopback isn't a private channel — any process already running on the machine can reach `127.0.0.1`, not just the CLI. Every route (health check included) requires a token, checked against `~/.codeyantram/server-token.json` (`0600`, like `auth.json`):
|
|
324
|
+
|
|
325
|
+
- The server mints one the first time it starts against a given config directory, and reuses it on every subsequent start — nothing to configure.
|
|
326
|
+
- The CLI reads the same file and sends it automatically on every request; there's no setting to connect the two by hand as long as both point at the same config directory (the default, or a shared `CODEYANTRAM_CONFIG_DIR`).
|
|
327
|
+
- A request without the right token gets refused: a plain `401` for `/health` and `/providers`, and — matching how every other `/chat`-time failure is delivered — a normal-looking SSE stream carrying a `start` then an `error` event, never a raw HTTP error status. The CLI's existing error handling covers it with no special case.
|
|
328
|
+
- Deleting `server-token.json` and restarting the server rotates it; the CLI picks up the new value on its very next request, no CLI restart required.
|
|
329
|
+
|
|
330
|
+
## Configuration & State
|
|
331
|
+
|
|
332
|
+
`~/.codeyantram/` (`0700`) holds everything CodeYantram persists — flat JSON files, plus a small SQLite database for chat sessions:
|
|
333
|
+
|
|
334
|
+
| File | Contents |
|
|
335
|
+
| --- | --- |
|
|
336
|
+
| `auth.json` | Provider API keys (`0600`), managed via `/connect` |
|
|
337
|
+
| `preferences.json` | Saved theme, model, and agent |
|
|
338
|
+
| `server-token.json` | Local server auth token (`0600`) — see [Local server auth](#local-server-auth) |
|
|
339
|
+
| `sessions.db` (+ `-wal`/`-shm`) | Chat session history (`0600` on all three files) — see below |
|
|
340
|
+
| `prompt-history.json` | Prompt history (what `↑`/`↓` step through in the input), keyed per project |
|
|
341
|
+
|
|
342
|
+
Tests run with `NODE_ENV=test`, which disables disk writes so test suites never touch real config.
|
|
343
|
+
|
|
344
|
+
### Session storage and retention
|
|
345
|
+
|
|
346
|
+
Every message and tool-approval decision is saved to `sessions.db` as it happens (not just at the end of a conversation), so `/sessions` can list and resume past conversations across restarts. It has no size cap of its own — check its size any time with `du -h ~/.codeyantram/sessions.db*` — but it doesn't grow unbounded either: on every server start, a background sweep prunes, per project, whatever falls outside **both** of two independent caps (a session survives only if it clears both):
|
|
347
|
+
|
|
348
|
+
| Cap | Default |
|
|
349
|
+
| --- | --- |
|
|
350
|
+
| Keep the N most recently updated sessions | 200 |
|
|
351
|
+
| Drop anything older than | 90 days |
|
|
352
|
+
|
|
353
|
+
A prune that actually deletes something runs `VACUUM` afterward to reclaim the freed space on disk. You can also delete a session directly from the `/sessions` picker with `ctrl+d`.
|
|
354
|
+
|
|
355
|
+
## Development
|
|
356
|
+
|
|
357
|
+
### Root scripts
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
bun install # install workspace deps
|
|
361
|
+
bun run dev:cli # run the TUI (watch mode)
|
|
362
|
+
bun run dev:server # run the local server (watch mode)
|
|
363
|
+
bun run test # run every package's tests
|
|
364
|
+
bun run test:cli # run just the CLI tests
|
|
365
|
+
bun run typecheck # typecheck every package
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Per-package
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
cd packages/cli && bun dev # or: bun run --watch src/index.tsx
|
|
372
|
+
cd packages/server && bun run dev # or: bun --env-file=../../.env run --watch src/index.ts
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Testing
|
|
376
|
+
|
|
377
|
+
Each package has a `__tests__/` suite covering schemas, stream folding, models, tools, keyboard layer stack, commands, and more. Run from the root with `bun run test`, or per package.
|
|
378
|
+
|
|
379
|
+
## Project Structure
|
|
380
|
+
|
|
381
|
+
```
|
|
382
|
+
packages/
|
|
383
|
+
├── shared/src/
|
|
384
|
+
│ ├── agents.ts # Agent catalog (Talk / Build) + tool-access rules
|
|
385
|
+
│ ├── models.ts # Provider + model catalog, effort levels
|
|
386
|
+
│ ├── tools.ts # Tool catalog + read-only classification
|
|
387
|
+
│ ├── schemas.ts # Zod schemas: requests, messages, stream events, session API
|
|
388
|
+
│ ├── stream.ts # applyStreamEvent — delta → part folding
|
|
389
|
+
│ ├── auth.ts # API key store (read/write/remove)
|
|
390
|
+
│ ├── local-store.ts # ~/.codeyantram JSON read/write + test guard
|
|
391
|
+
│ └── routes.ts # API route constants
|
|
392
|
+
├── sessions/src/
|
|
393
|
+
│ ├── store.ts # createSession/appendMessage/listSessions/pruneSessions/…
|
|
394
|
+
│ ├── db.ts # SQLite client (WAL, foreign keys, file permissions)
|
|
395
|
+
│ ├── migrations.ts # forward-only PRAGMA user_version migrations
|
|
396
|
+
│ └── title.ts # deriveTitle — session title from the first message
|
|
397
|
+
├── server/src/
|
|
398
|
+
│ ├── index.ts # Hono app, routes, port, startup prune sweep + shutdown
|
|
399
|
+
│ ├── lib/chat-stream.ts # streamText → SSE bridge, tool loops, approval
|
|
400
|
+
│ ├── lib/models.ts # model resolution + provider options
|
|
401
|
+
│ ├── lib/system-prompt.ts # per-agent prompts as cache-breakpointed system messages
|
|
402
|
+
│ ├── lib/project-instructions.ts # global/project/nested AGENTS.md discovery, caps, framing, sandboxing
|
|
403
|
+
│ ├── providers/ # one builder per provider (AI SDK)
|
|
404
|
+
│ ├── routers/ # /chat, /providers, and /sessions Hono routers
|
|
405
|
+
│ └── tools/ # one executor per tool + path sandboxing
|
|
406
|
+
└── cli/src/
|
|
407
|
+
├── index.tsx # entrypoint, renderer, screen switch, --continue/--resume
|
|
408
|
+
├── resume.ts # parses --continue/--resume from argv
|
|
409
|
+
├── layouts/root.tsx # provider composition
|
|
410
|
+
├── screens/ # Home (landing) ⇄ Session (transcript + title header)
|
|
411
|
+
├── components/ # input bar, message list, overlays, toasts, pickers
|
|
412
|
+
├── components/session-picker.tsx # /sessions: list, resume, rename, delete
|
|
413
|
+
├── components/resume-on-launch.tsx # --continue/--resume's actual load-and-resume
|
|
414
|
+
├── providers/ # chat, theme, model, agent, overlay, toast, keyboard, history
|
|
415
|
+
├── providers/session-autosave.ts # save-as-you-go, serialized through one promise chain
|
|
416
|
+
├── api/chat.ts # SSE client + schema validation
|
|
417
|
+
├── api/sessions.ts # session API client (create/list/load/rename/delete/…)
|
|
418
|
+
├── utils/prompt-history-store.ts # per-project prompt history persistence
|
|
419
|
+
├── commands.tsx # slash-command registry
|
|
420
|
+
├── keyboard.ts # layered keyboard ownership stack
|
|
421
|
+
├── theme.ts # 12 themes
|
|
422
|
+
└── syntax-theme.ts # tree-sitter style mapping
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Design Notes
|
|
426
|
+
|
|
427
|
+
- **Single path for success and failure** — the CLI consumes only parsed SSE events; a missing key, a network error, or a provider error all arrive as `error` events.
|
|
428
|
+
- **Shared contracts, single source of truth** — catalogs, schemas, and stream folding live in `shared` and are tested exactly once there.
|
|
429
|
+
- **Project instructions are input, not authority** — every source (`~/.codeyantram/AGENTS.md`, the project root's, a subdirectory's own) is folded in with explicit limits (no approval bypass, no tool-access widening) and its framing markers neutralized, on the assumption the repo may not be one the user wrote.
|
|
430
|
+
- **Nested instructions ride on the tool that touches them, not the prompt** — a subdirectory's `AGENTS.md` costs nothing until `read_file` actually opens something under it, then attaches to that call's own result instead of growing the system prompt for the whole session.
|
|
431
|
+
- **Cache breakpoints follow content that actually varies together** — the static per-agent prompt and the instructions block are separate system messages precisely so an `AGENTS.md` edit invalidates only the smaller, variable one, not the whole prompt.
|
|
432
|
+
- **Sandboxed by design** — every tool path resolves against `cwd` and rejects anything escaping the project root; mutating tools are gated behind user approval.
|
|
433
|
+
- **Keyboard ownership via a layer stack** — root, autocomplete, and overlay each claim the keyboard in turn, so only the topmost UI reacts to a keypress (and `ctrl+c` exits only when nothing else owns it).
|
|
434
|
+
- **Saved as it happens, never in the way** — every autosave call is fire-and-forget and serialized through one promise chain; a slow or failed save is logged and (once per failure streak) surfaced as a toast, but can never delay or break the live conversation that already succeeded by the time it runs.
|