chivgent 1.0.0 → 1.0.1

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.
Files changed (177) hide show
  1. package/README.md +136 -907
  2. package/README.zh-CN.md +25 -2
  3. package/dist/agent.d.ts +14 -1
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +195 -28
  6. package/dist/agent.js.map +1 -1
  7. package/dist/cli-options.d.ts +2 -0
  8. package/dist/cli-options.d.ts.map +1 -1
  9. package/dist/cli-options.js +12 -2
  10. package/dist/cli-options.js.map +1 -1
  11. package/dist/cli-runtime.d.ts +24 -0
  12. package/dist/cli-runtime.d.ts.map +1 -0
  13. package/dist/cli-runtime.js +64 -0
  14. package/dist/cli-runtime.js.map +1 -0
  15. package/dist/cli.js +20 -72
  16. package/dist/cli.js.map +1 -1
  17. package/dist/context/compaction.d.ts +5 -2
  18. package/dist/context/compaction.d.ts.map +1 -1
  19. package/dist/context/compaction.js +58 -32
  20. package/dist/context/compaction.js.map +1 -1
  21. package/dist/context/context-manager.d.ts +2 -0
  22. package/dist/context/context-manager.d.ts.map +1 -1
  23. package/dist/context/context-manager.js +4 -3
  24. package/dist/context/context-manager.js.map +1 -1
  25. package/dist/context/project-instructions.d.ts +17 -0
  26. package/dist/context/project-instructions.d.ts.map +1 -0
  27. package/dist/context/project-instructions.js +191 -0
  28. package/dist/context/project-instructions.js.map +1 -0
  29. package/dist/evals/baseline.d.ts +41 -0
  30. package/dist/evals/baseline.d.ts.map +1 -0
  31. package/dist/evals/baseline.js +121 -0
  32. package/dist/evals/baseline.js.map +1 -0
  33. package/dist/evals/cli.js +27 -1
  34. package/dist/evals/cli.js.map +1 -1
  35. package/dist/evals/parse-args.d.ts +2 -0
  36. package/dist/evals/parse-args.d.ts.map +1 -1
  37. package/dist/evals/parse-args.js +5 -0
  38. package/dist/evals/parse-args.js.map +1 -1
  39. package/dist/evals/report.d.ts +2 -0
  40. package/dist/evals/report.d.ts.map +1 -1
  41. package/dist/evals/report.js +12 -8
  42. package/dist/evals/report.js.map +1 -1
  43. package/dist/evals/runner.d.ts +3 -0
  44. package/dist/evals/runner.d.ts.map +1 -1
  45. package/dist/evals/runner.js +5 -1
  46. package/dist/evals/runner.js.map +1 -1
  47. package/dist/evals/task.d.ts.map +1 -1
  48. package/dist/evals/task.js +1 -0
  49. package/dist/evals/task.js.map +1 -1
  50. package/dist/evals/tools.d.ts +4 -1
  51. package/dist/evals/tools.d.ts.map +1 -1
  52. package/dist/evals/tools.js +11 -4
  53. package/dist/evals/tools.js.map +1 -1
  54. package/dist/events.d.ts +7 -1
  55. package/dist/events.d.ts.map +1 -1
  56. package/dist/events.js.map +1 -1
  57. package/dist/patch/parse.d.ts +26 -0
  58. package/dist/patch/parse.d.ts.map +1 -0
  59. package/dist/patch/parse.js +117 -0
  60. package/dist/patch/parse.js.map +1 -0
  61. package/dist/prompts.d.ts +2 -2
  62. package/dist/prompts.d.ts.map +1 -1
  63. package/dist/prompts.js +4 -3
  64. package/dist/prompts.js.map +1 -1
  65. package/dist/repl.d.ts +12 -1
  66. package/dist/repl.d.ts.map +1 -1
  67. package/dist/repl.js +305 -4
  68. package/dist/repl.js.map +1 -1
  69. package/dist/session-store.d.ts +6 -2
  70. package/dist/session-store.d.ts.map +1 -1
  71. package/dist/session-store.js +5 -1
  72. package/dist/session-store.js.map +1 -1
  73. package/dist/session.d.ts +1 -0
  74. package/dist/session.d.ts.map +1 -1
  75. package/dist/session.js +38 -12
  76. package/dist/session.js.map +1 -1
  77. package/dist/shell/approval.d.ts +15 -0
  78. package/dist/shell/approval.d.ts.map +1 -0
  79. package/dist/shell/approval.js +25 -0
  80. package/dist/shell/approval.js.map +1 -0
  81. package/dist/shell/docker.d.ts +18 -0
  82. package/dist/shell/docker.d.ts.map +1 -0
  83. package/dist/shell/docker.js +83 -0
  84. package/dist/shell/docker.js.map +1 -0
  85. package/dist/shell/local.d.ts +1 -1
  86. package/dist/shell/local.d.ts.map +1 -1
  87. package/dist/shell/local.js +1 -1
  88. package/dist/shell/local.js.map +1 -1
  89. package/dist/tools/apply-patch.d.ts +18 -0
  90. package/dist/tools/apply-patch.d.ts.map +1 -0
  91. package/dist/tools/apply-patch.js +53 -0
  92. package/dist/tools/apply-patch.js.map +1 -0
  93. package/dist/tools/bash.d.ts +4 -0
  94. package/dist/tools/bash.d.ts.map +1 -1
  95. package/dist/tools/bash.js +28 -3
  96. package/dist/tools/bash.js.map +1 -1
  97. package/dist/tools/parallel.d.ts +14 -0
  98. package/dist/tools/parallel.d.ts.map +1 -0
  99. package/dist/tools/parallel.js +20 -0
  100. package/dist/tools/parallel.js.map +1 -0
  101. package/dist/tools/tool.d.ts +7 -0
  102. package/dist/tools/tool.d.ts.map +1 -1
  103. package/dist/tools/tool.js +7 -1
  104. package/dist/tools/tool.js.map +1 -1
  105. package/dist/tools/update-plan.d.ts +57 -0
  106. package/dist/tools/update-plan.d.ts.map +1 -0
  107. package/dist/tools/update-plan.js +99 -0
  108. package/dist/tools/update-plan.js.map +1 -0
  109. package/dist/tui/activity.d.ts +17 -0
  110. package/dist/tui/activity.d.ts.map +1 -0
  111. package/dist/tui/activity.js +188 -0
  112. package/dist/tui/activity.js.map +1 -0
  113. package/dist/tui/diff.d.ts +14 -0
  114. package/dist/tui/diff.d.ts.map +1 -0
  115. package/dist/tui/diff.js +57 -0
  116. package/dist/tui/diff.js.map +1 -0
  117. package/dist/tui/editor-input.d.ts +53 -0
  118. package/dist/tui/editor-input.d.ts.map +1 -0
  119. package/dist/tui/editor-input.js +353 -0
  120. package/dist/tui/editor-input.js.map +1 -0
  121. package/dist/tui/editor.d.ts +72 -0
  122. package/dist/tui/editor.d.ts.map +1 -0
  123. package/dist/tui/editor.js +321 -0
  124. package/dist/tui/editor.js.map +1 -0
  125. package/dist/tui/git-diff.d.ts +7 -0
  126. package/dist/tui/git-diff.d.ts.map +1 -0
  127. package/dist/tui/git-diff.js +48 -0
  128. package/dist/tui/git-diff.js.map +1 -0
  129. package/dist/tui/input.d.ts +14 -0
  130. package/dist/tui/input.d.ts.map +1 -1
  131. package/dist/tui/input.js +77 -1
  132. package/dist/tui/input.js.map +1 -1
  133. package/dist/tui/live.d.ts +8 -0
  134. package/dist/tui/live.d.ts.map +1 -1
  135. package/dist/tui/live.js +37 -5
  136. package/dist/tui/live.js.map +1 -1
  137. package/dist/tui/markdown.d.ts +7 -0
  138. package/dist/tui/markdown.d.ts.map +1 -0
  139. package/dist/tui/markdown.js +55 -0
  140. package/dist/tui/markdown.js.map +1 -0
  141. package/dist/tui/menu.js +1 -1
  142. package/dist/tui/review.d.ts +27 -0
  143. package/dist/tui/review.d.ts.map +1 -0
  144. package/dist/tui/review.js +114 -0
  145. package/dist/tui/review.js.map +1 -0
  146. package/dist/tui/slash.d.ts +11 -0
  147. package/dist/tui/slash.d.ts.map +1 -0
  148. package/dist/tui/slash.js +13 -0
  149. package/dist/tui/slash.js.map +1 -0
  150. package/dist/tui/state.d.ts +5 -0
  151. package/dist/tui/state.d.ts.map +1 -1
  152. package/dist/tui/state.js +10 -1
  153. package/dist/tui/state.js.map +1 -1
  154. package/dist/tui/syntax.d.ts +6 -0
  155. package/dist/tui/syntax.d.ts.map +1 -0
  156. package/dist/tui/syntax.js +48 -0
  157. package/dist/tui/syntax.js.map +1 -0
  158. package/dist/tui/view.d.ts +1 -1
  159. package/dist/tui/view.d.ts.map +1 -1
  160. package/dist/tui/view.js +21 -9
  161. package/dist/tui/view.js.map +1 -1
  162. package/dist/tui/welcome.d.ts +5 -0
  163. package/dist/tui/welcome.d.ts.map +1 -1
  164. package/dist/tui/welcome.js +35 -20
  165. package/dist/tui/welcome.js.map +1 -1
  166. package/dist/workspace/patch.d.ts +11 -0
  167. package/dist/workspace/patch.d.ts.map +1 -0
  168. package/dist/workspace/patch.js +124 -0
  169. package/dist/workspace/patch.js.map +1 -0
  170. package/dist/workspace/types.d.ts +7 -0
  171. package/dist/workspace/types.d.ts.map +1 -1
  172. package/dist/workspace/types.js.map +1 -1
  173. package/dist/workspace.d.ts +2 -1
  174. package/dist/workspace.d.ts.map +1 -1
  175. package/dist/workspace.js +5 -0
  176. package/dist/workspace.js.map +1 -1
  177. package/package.json +1 -1
package/README.md CHANGED
@@ -1,960 +1,189 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/chivgent-banner-dark.svg">
4
+ <img src="docs/assets/chivgent-banner-light.svg" alt="chivgent: terminal coding agent. Read-only by default, Docker shell, OpenAI-compatible." width="100%">
5
+ </picture>
6
+ </p>
7
+
1
8
  # chivgent
2
9
 
3
10
  [English](README.md) | [简体中文](README.zh-CN.md)
4
11
 
5
- > A readable terminal coding agent with a streaming TUI and configurable AI providers.
6
-
7
- ![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22-339933?logo=nodedotjs&logoColor=white)
8
- ![TypeScript](https://img.shields.io/badge/TypeScript-ESM-3178C6?logo=typescript&logoColor=white)
12
+ [![npm](https://img.shields.io/npm/v/chivgent)](https://www.npmjs.com/package/chivgent)
9
13
  [![CI](https://github.com/chivopic/chivgent/actions/workflows/ci.yml/badge.svg)](https://github.com/chivopic/chivgent/actions/workflows/ci.yml)
10
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
- ![Version](https://img.shields.io/badge/version-1.0.0-blue)
12
-
13
- `chivgent` connects a Provider-independent agent loop to LLM APIs, tools, and a
14
- workspace boundary. It can discover files, search source text, read bounded
15
- file ranges, and optionally edit files or run commands. It works with OpenAI,
16
- DeepSeek, and compatible Chat Completions endpoints.
17
-
18
- The project is intentionally compact: it is designed to make the mechanics of
19
- tool calling, conversation state, Provider adapters, and loop termination easy
20
- to study before adding production-harness complexity.
21
-
22
- ## Features
23
-
24
- - A real multi-turn agent loop: model -> tool call -> tool result -> model.
25
- - Provider-independent runtime messages and tool contracts.
26
- - A Provider registry: adding a Provider is a declaration, not a CLI change.
27
- - API keys resolved from `--api-key`, the environment, then an optional file.
28
- - OpenAI support through the Responses API.
29
- - DeepSeek support through a reusable OpenAI-compatible Chat Completions client.
30
- - Custom OpenAI-compatible endpoints through the TUI, CLI options, or environment.
31
- - A rolling TUI with inline slash-command suggestions and a Provider picker.
32
- - An interactive session with slash commands, or a single-shot question.
33
- - Sessions that persist as JSON lines and can be resumed in a later process.
34
- - A context manager that summarises old turns to stay inside the window.
35
- - A `--json` event stream for scripting and other front ends.
36
- - Streamed answers rendered from a typed runtime event stream.
37
- - Interruptible runs: Ctrl+C stops the current answer; at an idle prompt it exits.
38
- - Per-attempt Provider timeouts and bounded exponential-backoff retries.
39
- - Deterministic project discovery through `list_files` and literal `search_text`.
40
- - Ranged `read_file` output with continuation hints and bounded tool results.
41
- - Read-only by default; `--allow-writes` adds `write_file` and `edit_file`.
42
- - An opt-in `bash` tool behind `--allow-shell`, with streamed output.
43
- - Token usage from the Provider, per turn, per run, per eval task.
44
- - An eval suite that measures whether the agent completes tasks, not just runs.
45
- - Remote sessions: one process holds a session, others attach over a local socket.
46
- - Several clients can watch one session; any of them can interrupt the run.
47
- - Extensions that add tools, slash commands, event handlers and prompt text.
48
- - Project extensions gated by a per-directory trust decision, taken before any
49
- module is imported and refused outright when there is no terminal to ask.
50
- - Commands run in their own process group, so cancelling kills the whole tree.
51
- - Command output is truncated from the tail; the full text goes to a temp file.
52
- - Exact-match `edit_file` that refuses missing or ambiguous edits.
53
- - Edits preserve the file's own byte order mark and CRLF line endings.
54
- - Atomic writes: a crash mid-write leaves the original file intact.
55
- - Safe workspace access with traversal and symlink-escape protection.
56
- - Root `.gitignore`, generated-directory, and sensitive-path filtering.
57
- - Tool argument validation, explicit tool errors, and a bounded turn limit.
58
- - A packaged Node.js CLI with no framework dependency.
59
- - Unit tests that do not spend API credits.
14
+ ![Node.js >=22](https://img.shields.io/badge/Node.js-%3E%3D22-339933?logo=nodedotjs&logoColor=white)
60
15
 
61
- ## Quick start
16
+ chivgent is a terminal coding agent you can read end to end. It asks an LLM
17
+ (OpenAI, DeepSeek, or any OpenAI-compatible endpoint) about the project in your
18
+ current directory, using read-only file tools by default. Writes and shell
19
+ commands are separate opt-in flags, and shell commands run in an offline Docker
20
+ container with per-command approval.
21
+
22
+ It is small on purpose: a TypeScript CLI with four runtime dependencies, where
23
+ the agent loop, Provider adapters, context compaction, and tool sandboxing are
24
+ each a few hundred lines you can study.
62
25
 
63
- ### Requirements
26
+ **Use it if** you want a coding agent whose every mechanism you can inspect and
27
+ change, or a reference implementation for building your own.
28
+ **Look elsewhere if** you want the most capable agent for daily work, a hosted
29
+ product, a fine-grained permission system, or native Windows shell support.
64
30
 
65
- - Node.js 22 or newer
66
- - npm
67
- - An API key for OpenAI, DeepSeek, or a compatible Provider
31
+ ## Quick start
68
32
 
69
- ### Install from npm
33
+ You need Node.js 22 or newer and an API key for one Provider.
70
34
 
71
35
  ```bash
72
36
  npm install -g chivgent
73
37
  chivgent --version
74
38
  ```
75
39
 
76
- Update an npm global installation with:
77
-
78
- ```bash
79
- chivgent update --check # Check without installing
80
- chivgent update # Install the newer version, then restart chivgent
81
- ```
82
-
83
- For older releases without this command, run `npm install -g chivgent@latest`
84
- once. The updater checks only when requested. Source checkouts, local dependencies
85
- and temporary `npx` runs receive instructions for their installation type.
86
- See [updating chivgent](docs/updating.md) for details and troubleshooting.
87
-
88
- To install the current source checkout instead:
89
-
90
- ```bash
91
- git clone https://github.com/chivopic/chivgent.git
92
- cd chivgent
93
- npm install
94
- npm run build
95
- npm install -g .
96
- ```
97
-
98
- ### Ask about a project
99
-
100
- Run `chivgent` from the project you want it to inspect.
101
-
102
- With OpenAI:
40
+ This prints the installed version, such as `1.0.0`. Then, from the project you want to ask
41
+ about:
103
42
 
104
43
  ```bash
105
44
  export OPENAI_API_KEY="your-api-key"
106
- chivgent "What does src/agent.ts do?"
45
+ chivgent "What does this project do, and where is its entry point?"
107
46
  ```
108
47
 
109
- With DeepSeek:
48
+ The answer streams to stdout. Tool activity (`list_files`, `search_text`,
49
+ `read_file`) is shown on stderr. Nothing in the project is changed.
50
+
51
+ Prefer DeepSeek or another Provider? Swap the key and add `--provider`:
110
52
 
111
53
  ```bash
112
54
  export DEEPSEEK_API_KEY="your-api-key"
113
55
  chivgent --provider deepseek "Explain the architecture in src/"
114
56
  ```
115
57
 
116
- With any OpenAI-compatible Chat Completions endpoint:
117
-
118
- ```bash
119
- export OPENAI_API_KEY="your-provider-api-key"
120
- export OPENAI_BASE_URL="https://api.vendor.example/v1"
121
- export OPENAI_MODEL="vendor-model"
122
-
123
- chivgent --provider openai-compatible "Explain the architecture in src/"
124
- ```
125
-
126
- Start an interactive session by running `chivgent` with no question:
127
-
128
- ```bash
129
- chivgent
130
- › What does src/agent.ts do?
131
- › Where is that loop tested?
132
- › /exit
133
- ```
134
-
135
- The conversation is kept across prompts, so follow-up questions do not repeat
136
- the earlier context. Resume it later with `chivgent --continue` (or
137
- `chivgent --resume <id>`; `chivgent --sessions` lists what is recorded).
138
-
139
- Answers stream to stdout as the model produces them, so stdout stays pipeable.
140
- Tool activity, retries, and run status go to stderr; Provider failures produce a
141
- non-zero exit code. Use `--no-stream` for one final write, and `--quiet` to hide
142
- tool activity.
143
-
144
- ## CLI reference
145
-
146
- ```text
147
- chivgent [options] "question" Answer one question and exit
148
- chivgent [options] Start an interactive session
149
-
150
- Options:
151
- --provider NAME openai, deepseek, openai-compatible, openrouter, groq, xai,
152
- moonshot (default: openai)
153
- --model MODEL Provider model override
154
- --max-turns N Tool-calling turn limit (default: 8, 16 with --allow-writes
155
- or --allow-shell)
156
- --no-stream Wait for the full answer instead of streaming tokens
157
- -q, --quiet Hide tool activity on stderr
158
- --json Write the run as JSON lines instead of rendered text
159
- -c, --continue Resume the most recent session for this workspace
160
- --resume ID Resume a specific session
161
- --api-key KEY API key for this run; prefer an environment variable
162
- --sessions List recorded sessions and exit
163
- --allow-writes Let the agent create and change files (default: read-only)
164
- --allow-shell Let the agent run shell commands. This implies write access:
165
- a shell is not bound by the workspace. Unix only.
166
- --serve Expose this session on a local socket and keep running
167
- --connect TARGET Attach to a served session, by id or socket path
168
- --servers List the servers still answering, then exit
169
- --no-extensions Do not load any extension, and do not ask about trust
170
- --extensions List loaded extensions and what they register, then exit
171
- --forget-trust Forget the trust decision covering this workspace, then exit
172
- --context-window N Token budget for the context (default: 128000)
173
- --no-compaction Send the whole transcript instead of summarising old turns
174
- --no-session Do not record this run
175
- -h, --help Show help
176
- -v, --version Show version
177
- ```
178
-
179
- In an interactive session, `/help` lists the slash commands, including
180
- `/provider`, `/model`, `/endpoint`, `/login`, `/session`, `/tools`, `/clear`, and `/exit`.
181
- In the TUI, typing `/` opens an inline command menu; `/provider` opens a Provider picker
182
- with Up/Down and Enter. Selecting a Provider without a saved key immediately asks for
183
- its API key. Use `/login` later to replace a key. Ctrl+C stops a running answer;
184
- at an idle prompt it exits with code 130. Ctrl+D also leaves the session.
185
-
186
- Exit codes: `0` answered, `1` configuration or Provider failure, `2` turn limit
187
- reached, `130` interrupted with Ctrl+C.
188
-
189
- ### Signing in
190
-
191
- The quickest way in is to start chivgent and let it ask:
192
-
193
- ```text
194
- $ chivgent --tui
195
- v1.0.0
196
- openai / gpt-5.6
197
- Start with /provider → choose a Provider
198
- › /provider deepseek
199
- Selected deepseek / deepseek-v4-flash. API key needed for deepseek.
200
- API key for deepseek (hidden, saved locally; Enter skips):
201
- Stored the key for deepseek. Ready for your next prompt.
202
- ›
203
- ```
204
-
205
- An interactive session starts even without a key. Choose a Provider in the TUI
206
- and enter its key when asked; the next prompt can use it immediately. An already
207
- configured Provider skips this step. `/login` remains available to replace a key.
208
-
209
- The key is not echoed as you type, and `auth.json` is written owner-only. It is
210
- not verified against the Provider when stored; the first prompt after it is what
211
- proves it works.
212
-
213
- A non-interactive run still fails fast when no key is configured: a pipeline has
214
- nobody to ask.
215
-
216
- You can switch Providers and models without restarting or losing history:
217
-
218
- ```text
219
- › /provider
220
- › /provider deepseek
221
- › /model deepseek-v4-pro
222
- › /provider openrouter
223
- › /model anthropic/claude-sonnet-4
224
- › /provider openai-compatible
225
- › /model vendor-model
226
- › /endpoint https://api.vendor.example/v1
227
- ```
228
-
229
- In the TUI, a newly selected Provider asks for its key if none is configured.
230
- For Providers that also need a model or endpoint, the key can be stored first.
231
-
232
- `--api-key` remains scoped to the Provider selected at startup; it is never
233
- reused for a different Provider.
234
- Switching from OpenAI to a custom compatible endpoint also does not reuse
235
- `OPENAI_API_KEY`; the TUI asks for that endpoint's own key.
236
-
237
- ### Provider configuration
238
-
239
- | Provider | API key | Model environment variable | Default model | API style |
240
- | --- | --- | --- | --- | --- |
241
- | OpenAI | `OPENAI_API_KEY` | `OPENAI_MODEL` | `gpt-5.6` | Responses API |
242
- | DeepSeek | `DEEPSEEK_API_KEY` | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | OpenAI-compatible Chat Completions |
243
- | Custom compatible | `OPENAI_API_KEY` | `OPENAI_MODEL` | Required | OpenAI-compatible Chat Completions |
244
- | OpenRouter | `OPENROUTER_API_KEY` | `OPENROUTER_MODEL` | Required | OpenAI-compatible Chat Completions |
245
- | Groq | `GROQ_API_KEY` | `GROQ_MODEL` | Required | OpenAI-compatible Chat Completions |
246
- | xAI | `XAI_API_KEY` | `XAI_MODEL` | Required | OpenAI-compatible Chat Completions |
247
- | Moonshot | `MOONSHOT_API_KEY` | `MOONSHOT_MODEL` | Required | OpenAI-compatible Chat Completions |
248
-
249
- An explicit `--model` value takes precedence over the Provider-specific model
250
- environment variable. Custom compatible Providers also require
251
- `OPENAI_BASE_URL`. Sessions are written under `CHIVGENT_HOME` (default
252
- `~/.chivgent`).
253
-
254
- Providers are declared in a registry rather than branched on in the CLI, so
255
- `--help` and `--provider` validation are generated from the same list that
256
- creates the client.
257
-
258
- #### API key resolution
259
-
260
- Keys are resolved in this order, first match wins:
261
-
262
- 1. `--api-key` for a single run
263
- 2. the Provider's environment variable
264
- 3. `<CHIVGENT_HOME>/auth.json`
58
+ Run `chivgent` with no question for an interactive session. If no key is set,
59
+ the session still starts: type `/provider`, pick one, and enter its key when
60
+ asked. Keys are saved owner-only in `~/.chivgent/auth.json`.
265
61
 
266
- An environment variable deliberately beats the stored file, matching the
267
- convention other CLIs use, so a stored key can be overridden for one run
268
- without editing anything.
62
+ **Try it without a key or network:** from a source checkout, `npm run demo:tui`
63
+ runs the TUI against a stub Provider and does not touch the workspace.
269
64
 
270
- `auth.json` is optional and holds literal keys only:
65
+ ## What it can do, and what it needs from you
271
66
 
272
- ```json
273
- {
274
- "openai": { "type": "api_key", "key": "sk-..." },
275
- "deepseek": "sk-..."
276
- }
277
- ```
278
-
279
- Neither `$VAR` expansion nor `!command` substitution is supported: letting a
280
- config file spawn a process is a large attack surface for a small convenience.
281
- chivgent warns when the file is readable by other users; keep it at `chmod 600`.
282
-
283
- ```bash
284
- chivgent --provider openai --model gpt-5.6 "Explain package.json"
285
- chivgent --provider deepseek --model deepseek-v4-pro "Explain package.json"
286
- chivgent --provider openai-compatible --model vendor-model "Explain package.json"
287
- ```
288
-
289
- ## Architecture
290
-
291
- ```text
292
- +-> OpenAI Responses API
293
- User -> CLI -> Agent -> LLMClient |
294
- | +-> OpenAI-compatible Chat -> DeepSeek / custom
295
- |
296
- +-> Tool Registry -> list_files / search_text / read_file -> Workspace
297
- | write_file / edit_file (--allow-writes)
298
- | bash (--allow-shell) -> ShellOperations
299
- ```
67
+ | Capability | Flag | What it can touch |
68
+ | --- | --- | --- |
69
+ | Discover, search, and read files | default | The current directory only. `.gitignore`d, generated, and credential paths (such as `.env`, `id_rsa`) are skipped. |
70
+ | Create and edit files (`write_file`, `edit_file`, `apply_patch`) | `--allow-writes` | Files inside the workspace. Edits are exact-match and atomic. |
71
+ | Run shell commands (`bash`) | `--allow-shell` | A fresh `node:22-alpine` Docker container with no network, the workspace mounted at `/workspace`, and no API keys. Each command needs a `y` to run. Docker is required; there is no fallback to the host. |
72
+ | Load project extensions | trust prompt on first run | Anything you can: extensions are in-process JavaScript. |
300
73
 
301
- The Agent runtime owns its own messages. Provider-specific schemas are converted
302
- only at the `LLMClient` boundary:
74
+ Costs: chivgent calls the Provider you choose with your key, so you pay that
75
+ Provider's normal token prices. It reports token counts per turn and per
76
+ session (`/session`) but never converts them to money.
303
77
 
304
- ```text
305
- Agent Message[] -> Provider adapter -> Provider request
306
- <- Provider response
307
- AssistantMessage <- normalized result
308
- ```
309
-
310
- This prevents the Agent, tools, and CLI from depending on one vendor's message
311
- format.
312
-
313
- ### Context management
314
-
315
- A session records everything that happened. The context manager decides what
316
- is worth sending for one request. Keeping those apart is what lets a long
317
- session stay inside a fixed context window.
318
-
319
- ```text
320
- Full transcript ──────────────→ Session store (what happened)
321
- │
322
- ↓
323
- ContextManager
324
- │ token estimate vs. contextWindow - reserveTokens
325
- ↓
326
- summary + recent messages ────→ Provider (what the model sees)
327
- ```
328
-
329
- When the estimate exceeds the budget, older messages are summarised into a
330
- single message and recent turns are kept verbatim. Three details matter:
331
-
332
- - **File lists are derived from tool calls, not from the summary.** A summary
333
- can forget or invent a path. For a coding agent, "which files did I read and
334
- change" is the part that most needs to survive intact, so it is collected
335
- from the `read_file`, `write_file`, and `edit_file` calls themselves.
336
- - **Tool calls are never separated from their results.** A split point that
337
- would orphan a tool result moves forward past the whole group.
338
- - **Compaction discards the Provider continuation.** A Provider that chains
339
- history server-side replays its own copy and ignores the messages sent with
340
- it, so keeping the continuation would send back the history just removed.
341
-
342
- Token counts are estimated from character length rather than with a real
343
- tokenizer, which would be model-specific and a large dependency for a number
344
- that only decides *when* to compact. The reserve budget absorbs the error.
345
-
346
- A single tool result larger than the whole budget cannot be compacted away;
347
- bound tool output instead. Compaction is disabled with `--no-compaction`.
348
-
349
- ### Runtime events
350
-
351
- The Agent Loop reports what it is doing through a typed event stream instead of
352
- printing anything itself. One run emits:
353
-
354
- ```text
355
- agent_start
356
- turn_start -> message_start -> message_update* -> message_end
357
- tool_execution_start -> tool_execution_end (once per tool call)
358
- turn_end
359
- ...
360
- agent_end (completed | max_turns | aborted | error)
361
- ```
78
+ ## Providers
362
79
 
363
- `message_update` carries deltas only, never a cumulative snapshot, so the stream
364
- stays linear in the length of the answer. Events are structured-cloneable and
365
- each listener receives a copy, so a renderer can never mutate the transcript.
366
- The CLI renderer in `src/render.ts` is one consumer; a log file, a JSON stream,
367
- or a TUI are others.
80
+ | Provider | `--provider` | API key variable | Default model |
81
+ | --- | --- | --- | --- |
82
+ | OpenAI (Responses API) | `openai` (default) | `OPENAI_API_KEY` | `gpt-5.6` |
83
+ | DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` | `deepseek-v4-flash` |
84
+ | Any OpenAI-compatible endpoint | `openai-compatible` | `OPENAI_API_KEY` + `OPENAI_BASE_URL` | set `OPENAI_MODEL` |
85
+ | OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | set `OPENROUTER_MODEL` |
86
+ | Groq | `groq` | `GROQ_API_KEY` | set `GROQ_MODEL` |
87
+ | xAI | `xai` | `XAI_API_KEY` | set `XAI_MODEL` |
88
+ | Moonshot | `moonshot` | `MOONSHOT_API_KEY` | set `MOONSHOT_MODEL` |
368
89
 
369
- `LLMClient.stream` is optional. When a Provider does not implement it, the Agent
370
- falls back to `complete` and the same events are emitted without deltas.
90
+ `--model` overrides the model variable. Keys resolve in this order: `--api-key`,
91
+ then the environment variable, then `~/.chivgent/auth.json` (literal keys only;
92
+ no `$VAR` expansion or command substitution). A custom `OPENAI_BASE_URL`
93
+ receives your key and prompts, so point it only at endpoints you trust.
371
94
 
372
- ### OpenAI-compatible Providers
95
+ Compatible endpoints must implement `POST /chat/completions` with function tool
96
+ calling. Changing only the base URL does not guarantee full compatibility; see
97
+ [architecture](docs/architecture.md#openai-compatible-providers).
373
98
 
374
- Compatible Providers reuse the official `openai` npm package by changing
375
- `baseURL`, credentials, and model. CLI users do not need to edit code:
99
+ ## Everyday use
376
100
 
377
101
  ```bash
378
- export OPENAI_API_KEY="your-provider-api-key"
379
- export OPENAI_BASE_URL="https://api.vendor.example/v1"
380
- export OPENAI_MODEL="vendor-model"
102
+ chivgent # interactive session
103
+ chivgent --continue # resume the latest session here
104
+ chivgent --allow-writes "Rename sayHi to greet everywhere"
105
+ chivgent --allow-shell "Run the tests and explain the failure"
106
+ chivgent --json "List the CLI flags" > run.jsonl # machine-readable events
107
+ ```
108
+
109
+ - **Interactive sessions** keep history across prompts and are saved as JSON
110
+ lines under `~/.chivgent/sessions` (`--no-session` to skip). `/help` lists
111
+ slash commands such as `/provider`, `/model`, `/login`, `/session`, `/diff`,
112
+ and `/exit`. Ctrl+C stops a running answer; at an idle prompt it exits.
113
+ - **`--tui`** adds a live status region and a multi-line editor with history,
114
+ an inline `/` command menu, and `/gitdiff` review. See the
115
+ [TUI guide](docs/tui-v5.md).
116
+ - **Long sessions** stay inside the context window: older turns are summarised,
117
+ while the list of files read and changed is kept from the tool calls
118
+ themselves. See [architecture](docs/architecture.md#context-management).
119
+ - **Exit codes:** `0` answered, `1` configuration or Provider failure, `2` turn
120
+ limit reached, `130` interrupted.
121
+ - **Updating:** `chivgent update --check`, then `chivgent update`. See
122
+ [updating](docs/updating.md).
123
+
124
+ `chivgent --help` prints every flag.
381
125
 
382
- chivgent --provider openai-compatible "What does src/agent.ts do?"
383
- ```
384
-
385
- `OPENAI_BASE_URL` must point to the Provider's OpenAI-compatible API root. The
386
- Provider must implement `POST /chat/completions` and function tool calling.
387
-
388
- When adding a named Provider in source code, use the same adapter:
389
-
390
- ```ts
391
- const client = new OpenAICompatibleChatClient({
392
- apiKey: process.env.VENDOR_API_KEY!,
393
- baseURL: "https://api.vendor.example/v1",
394
- model: "vendor-model",
395
- continuationTag: "vendor-chat",
396
- });
397
- ```
126
+ ## Security model
398
127
 
399
- `DeepSeekChatClient` is a small configuration wrapper around this shared client.
400
- The compatibility layer also preserves optional Provider-only fields such as
401
- DeepSeek's `reasoning_content` inside opaque continuation state.
402
-
403
- Changing only `baseURL` is not a promise of complete compatibility. Providers
404
- can differ in model names, authentication, tool-schema support, strict mode,
405
- reasoning fields, streaming events, and error behavior. Keep those differences
406
- inside thin Provider adapters rather than leaking them into the Agent loop.
407
-
408
- ## Project structure
409
-
410
- ```text
411
- src/
412
- cli.ts CLI entry point and process boundary
413
- cli-options.ts Argument and Provider configuration
414
- auth/
415
- credentials.ts Credential contract and resolution order
416
- runtime-credentials.ts --api-key override
417
- env-credentials.ts Environment variable lookup
418
- file-credentials.ts The auth.json store, read and write
419
- agent.ts Agent loop and run state
420
- events.ts Runtime event model
421
- render.ts Terminal renderer for runtime events
422
- llm.ts Provider-independent LLM contract
423
- retry.ts Provider timeout and retry decorator
424
- messages.ts Runtime message model
425
- session.ts Conversation state and event fan-out
426
- session-store.ts JSONL session log and resume support
427
- repl.ts Interactive prompt and slash commands
428
- context/
429
- context-manager.ts Builds the messages for one request
430
- compaction.ts Summarises old history and tracks files
431
- token-estimator.ts Character-based token approximation
432
- workspace.ts Workspace configuration and the read-only default
433
- workspace/
434
- types.ts Limits, errors, and the Workspace contract
435
- paths.ts Path normalisation and escape protection
436
- ignore.ts .gitignore and generated-directory filtering
437
- text.ts UTF-8 decoding, line splitting, previews
438
- read.ts Ranged reads
439
- list.ts Directory walking
440
- search.ts Literal text search
441
- write.ts Atomic whole-file writes and exact edits
442
- providers/
443
- registry.ts Provider registry
444
- usage.ts Reading and adding up Provider token counts
445
- deferred-client.ts A client whose Provider can arrive later
446
- definitions.ts Built-in Provider declarations
447
- client.ts Credential resolution into an LLM client
448
- openai.ts OpenAI Responses adapter
449
- openai-compatible-chat.ts Shared Chat Completions adapter
450
- deepseek.ts DeepSeek configuration wrapper
451
- tools/
452
- tool.ts Tool contract
453
- output.ts Shared 64 KiB tool-output boundary
454
- list-files.ts Deterministic project-tree discovery
455
- search-text.ts Bounded literal source search
456
- read-file.ts Ranged text-file reader
457
- write-file.ts Whole-file create and replace
458
- edit-file.ts Exact unique-match edit
459
- bash.ts Shell command execution
460
- prompts.ts The instructions the agent runs under
461
- evals/
462
- task.ts Task definitions and their validation
463
- fixture.ts A fresh workspace copy per attempt
464
- graders.ts Deterministic graders
465
- runner.ts Runs a task N times and aggregates
466
- report.ts Table and JSON output
467
- parse-args.ts Eval CLI arguments
468
- cli.ts npm run eval
469
- remote/
470
- protocol.ts Message shapes and version negotiation
471
- framing.ts JSON Lines framing with a bounded buffer
472
- server.ts Serves one session on a Unix socket
473
- client.ts Attaches to a served session
474
- repl.ts The interactive loop for an attached client
475
- socket-path.ts Socket paths, staleness, and discovery
476
- extensions/
477
- trust.ts Per-directory trust store
478
- decide-trust.ts The prompt, and the refusal when nobody can answer
479
- discover.ts Where extensions live and which files count
480
- api.ts What an extension may register
481
- registry.ts Collects registrations, rejects conflicts
482
- loader.ts Imports modules and contains their failures
483
- shell/
484
- types.ts Execution contract and shell error types
485
- config.ts Shell resolution and the Windows guard
486
- local.ts Local spawn backend
487
- process.ts Process-group kill and exit handling
488
- output.ts Bounded streaming output accumulator
489
- truncate.ts Tail truncation for command output
490
- sanitize.ts Control-character filtering
491
- tests/ Provider, loop, and workspace tests
492
- evals/ Eval tasks and their fixtures
493
- docs/ Architecture and learning notes
494
- ```
128
+ chivgent states its boundaries rather than pretending to enforce fine-grained
129
+ ones. Capabilities are coarse, session-level switches.
130
+
131
+ - File tools reject paths outside the workspace, including `..` traversal and
132
+ symlink escapes, and deny common credential and private-key paths. Tool output
133
+ is capped at 64 KiB.
134
+ - `--allow-shell` runs each command in a new offline container (dropped
135
+ capabilities, read-only root, non-root user, resource limits) and asks before
136
+ each one. Headless and served sessions deny commands unless you also pass
137
+ `--approve-all-shell`. Inside the container, commands can still change or
138
+ delete anything in the mounted project, and Docker is not proof against
139
+ kernel or VM escapes. Details: [shell sandbox](docs/shell-sandbox.md).
140
+ - Writes and trusted extensions are **not** inside the container. Trusting a
141
+ project's extensions means letting its authors run code as you, so chivgent
142
+ asks once per directory and never loads them when there is no terminal to ask.
143
+ - `auth.json`, session logs, and `trust.json` are written owner-only but in
144
+ plain text. Session logs contain prompts, answers, and file excerpts.
145
+ - `--serve` exposes a session on a Unix socket in an owner-only directory.
146
+ Anyone who can open the socket gets every capability the server started with.
147
+ There is no TCP listener; use SSH forwarding for another machine.
148
+
149
+ Work on committed code, and use a disposable copy for repositories you don't
150
+ trust.
151
+
152
+ ## Going deeper
153
+
154
+ - [Architecture](docs/architecture.md): agent loop, runtime events, context
155
+ compaction, Provider adapters, the TUI live region, and the source layout.
156
+ - [Evals and token usage](docs/evals.md): ten deterministic task suites that
157
+ measure pass rates over repeated attempts, separate from the free, offline
158
+ unit tests.
159
+ - [Extensions and remote sessions](docs/extensions-and-remote.md): adding
160
+ tools and slash commands, project trust, and attaching several terminals to
161
+ one session.
162
+ - [Patch format](docs/apply-patch.md), [scoped `AGENTS.md`](docs/scoped-agents-md.md),
163
+ [parallel read tools](docs/parallel-read-tools.md),
164
+ [plan tracking](docs/agent-plan-progress.md).
165
+ - Design notes for each build stage, from
166
+ [stage 1](docs/stage-1-minimal-agent.md) to [stage 14](docs/stage-14-tui.md),
167
+ and the [release process](docs/releasing.md).
495
168
 
496
169
  ## Development
497
170
 
498
171
  ```bash
172
+ git clone https://github.com/chivopic/chivgent.git
173
+ cd chivgent
499
174
  npm install
500
175
  npm run check
501
176
  npm test
502
177
  npm run build
503
178
  ```
504
179
 
505
- Run the complete release gate, including an npm tarball dry run:
506
-
507
- ```bash
508
- npm run release:check
509
- ```
510
-
511
- Build a locally installable tarball:
512
-
513
- ```bash
514
- npm pack
515
- npm install -g ./chivgent-0.6.0.tgz
516
- ```
517
-
518
- Tests use scripted or mocked LLM clients. A real API smoke test is deliberately
519
- manual so the default test suite never consumes credits.
520
-
521
- ## Evals
522
-
523
- The 300-odd unit tests check that the code runs the way it was written. They say
524
- nothing about whether this is a good agent. `npm run eval` answers that:
525
-
526
- ```bash
527
- npm run eval # every task in evals/
528
- npm run eval -- --task rename-symbol --attempts 10
529
- npm run eval -- --allow-writes --allow-shell --json report.json
530
- DEEPSEEK_API_KEY=sk-... npm run eval -- --provider deepseek
531
- ```
532
-
533
- `--api-key` works too, but prefer the environment variable here: npm echoes the
534
- fully expanded command before running it, so a key on the command line lands in
535
- the clear in any captured log.
536
-
537
- ```text
538
- task pass turns tokens tools p50
539
- find-auth-logic 4/5 2.0 3.4k list_files,search_text,... 6.1s
540
- rename-symbol 3/5 5.4 12.1k read_file,edit_file 9.8s
541
-
542
- overall 7/10 (70%) 58.3k tokens
543
-
544
- rename-symbol attempt 2,5: not-used-tool name="write_file" — called write_file 1 time(s)
545
- ```
546
-
547
- The `tokens` column is what the Provider reported, so a change can be judged on
548
- both axes at once: a prompt that lifts the pass rate from 70% to 75% while
549
- tripling the tokens is usually a bad trade, and without the second number that
550
- looks like a pure win.
551
-
552
- **An eval is not a test.** A model is nondeterministic, so one run of a task is a
553
- coin flip: passing and failing are properties of the model-and-harness
554
- combination, not of that run. The unit is therefore a pass rate over N attempts,
555
- and evals never gate CI — a merge gate that costs money and goes red six times in
556
- ten trains everyone to ignore CI. `npm test` stays free, offline and
557
- deterministic.
558
-
559
- A task is a directory under `evals/`: a `task.json` and a `fixture/` tree copied
560
- into a fresh temporary workspace for every attempt.
561
-
562
- ```jsonc
563
- {
564
- "prompt": "Rename the function sayHi to greet everywhere, including callers.",
565
- "capabilities": ["writes"],
566
- "attempts": 5,
567
- "graders": [
568
- { "type": "file-contains", "path": "src/greet.ts", "text": "export function greet(" },
569
- { "type": "file-excludes", "path": "src/greet.ts", "text": "sayHi" },
570
- { "type": "used-tool", "name": "edit_file" },
571
- { "type": "not-used-tool", "name": "write_file" }
572
- ]
573
- }
574
- ```
575
-
576
- Graders are deterministic — file contents, tool calls, answer patterns, turn
577
- counts. There is no LLM judge: a judge is itself a nondeterministic instrument,
578
- and measuring a nondeterministic subject with one compounds the error until "the
579
- agent got worse" cannot be told apart from "the judge felt different today".
580
-
581
- Those last two graders are the point. Rewriting the whole file also produces the
582
- right contents, and an eval that scores results alone would give it full marks.
583
- For a coding agent, tool misuse is the more common and the more interesting
584
- failure, so tasks assert on how the work was done.
585
-
586
- A grader naming a tool the task never grants is refused when the task loads.
587
- `not-used-tool: write_file` on a read-only task cannot fail — it asserts the
588
- model did not call something it was never given — and its mirror image cannot
589
- pass. Both are task-definition mistakes, and both are caught before the run
590
- spends anything. One such grader shipped for two releases before this check
591
- existed.
592
-
593
- Three graders exist because a set of tool names cannot answer the question they
594
- answer. `tool-never-failed` catches a guessed path, which shows up as a failed
595
- read that a deduplicated name list hides — it is sound only where a failed call
596
- means the model was wrong, so it fits `read_file` and not `bash`, which reports
597
- any non-zero exit as an error and is *supposed* to fail on a task that runs a
598
- failing test. `max-tool-calls` budgets calls rather than turns, because a model
599
- may issue any number of calls in one turn: "did it follow the imports or read
600
- the whole project" is a call count. `file-unchanged` compares against the
601
- fixture, so "don't touch what you weren't asked to touch" becomes a verdict
602
- rather than an intention — and it needs no second copy of the file in
603
- `task.json` to drift out of date. The JSON report carries `toolCalls`, every
604
- call in order with whether it succeeded, alongside the `toolsUsed` set the table
605
- prints.
606
-
607
- The nine tasks split into what the agent found and what it changed. Four exist
608
- because the first real baseline passed 29 of 30 attempts and therefore measured
609
- nothing: `needle-in-many-files` (40 files with uninformative names, so listing
610
- gives no signal), `trace-the-default` (a value overridden twice, where stopping
611
- at the first hop yields a plausible wrong number), `decoy-config` (two
612
- near-identical modules, one of them dead), and `wrong-test` (a failing test
613
- whose expectation contradicts the contract documented in the source — bending
614
- the source to fit it is the trap).
615
-
616
- A task that scores 0/N or N/N across different models is not measuring anything
617
- and should be made harder, fixed, or dropped. Passing is not evidence of quality
618
- when everything passes.
619
-
620
- ## Token usage
621
-
622
- Providers report what each call cost, and chivgent now keeps it: per turn on
623
- `message_end`, per run on `agent_end` and the run result, per attempt in the
624
- eval report, and cumulatively in `/session`.
625
-
626
- ```text
627
- › /session
628
- id: 2026-09-07T...
629
- workspace: /home/me/project
630
- prompts: 4
631
- messages: 18
632
- tokens: 38.2k total, 35.9k in, 2.3k out, 12.0k cached
633
- ```
634
-
635
- **Tokens only, never money.** Converting to a currency needs a model-to-price
636
- table, and that table goes stale silently when a Provider changes its rates —
637
- producing a figure that looks precise and is wrong, which is worse than no
638
- figure because nobody questions a number with a decimal point in it. Token
639
- counts are what the Provider itself reported and do not expire.
640
-
641
- A total is marked incomplete when some call reported nothing, rather than
642
- counting it as zero: a missing figure stays visibly missing instead of quietly
643
- understating the total. Compaction's own summarising call is included, since it
644
- trades a call now against smaller inputs later and that trade cannot be judged
645
- with half of it hidden.
646
-
647
- Streamed Chat Completions only report usage when asked, so chivgent sends
648
- `stream_options: { include_usage: true }`. A self-hosted OpenAI-compatible
649
- endpoint that predates that field may reject the request; `--no-stream` is the
650
- fallback.
651
-
652
- ## The live region
653
-
654
- `--tui` draws a status region above the prompt while a run is in progress: the
655
- turn, whether the model is thinking or running tools, which tools are running
656
- with what they are aimed at and their newest output, elapsed time, and the
657
- running token count. Long single-line replies keep their newest text visible;
658
- long paths retain the filename and tool progress. On narrow terminals, the
659
- status and stop shortcut take priority.
660
-
661
- ```text
662
- Let me look at how sessions are stored.
663
- read_file src/session.ts (2s)
664
- search_text (1s) src/store.ts:41: export class SessionStore
665
- running tools · turn 2/8 · 11s · 4.2k tokens · ctrl+c to stop
666
- ```
667
-
668
- It is the first consumer of the event stream that runs backwards. Every other
669
- one — streamed output, the session log, extensions, the remote client — is
670
- append-only: an event arrives, it is printed, it is forgotten. This one has to
671
- rebuild *what is happening now* from the same stream and keep it right as more
672
- arrives, which splits into three pieces:
673
-
674
- ```text
675
- reduce(state, event, now) -> ViewState pure
676
- view(state, {width, now}) -> string[] pure
677
- paint(previous, next) the only part that touches a terminal
678
- ```
679
-
680
- Both pure pieces are tested without a terminal, which is where the real
681
- guarantees live. One asymmetry in the event vocabulary is what makes the state
682
- model load-bearing: `message_update` carries a **delta**, `tool_execution_update`
683
- carries a truncated **snapshot**. Appending a snapshot stacks output that was
684
- never produced — the window has already had its front dropped — and replacing on
685
- a delta leaves the last token and loses the answer. An append-only renderer
686
- never has to know the difference, because it just prints whatever arrives. Two
687
- tests fail when either is written the wrong way round.
688
-
689
- It does not take the alternate screen buffer. That would cost the scrollback,
690
- and getting it back means reimplementing scrolling, search and selection —
691
- thousands of lines that teach terminal graphics rather than agent architecture.
692
- A finished turn is flushed to the terminal's own scrollback and dropped from
693
- the state, so a long session never grows it. Input stays with readline for the
694
- same reason: a line editor is not the interesting problem here.
695
-
696
- Painting rewrites only the lines whose content changed, so a ticking timer
697
- costs one line rather than a repaint. A resize invalidates that baseline — the
698
- terminal has reflowed what is on screen — and is the one moment a full repaint
699
- is correct rather than lazy.
700
-
701
- `--tui` needs a terminal on stdin and stderr and an interactive session; it
702
- refuses rather than falling back silently, since a switch that quietly does
703
- nothing makes a mistyped command look like it worked. It is not the default
704
- yet. The startup wordmark shows the Provider, model, workspace and next action;
705
- use `/session` for the session id. Type `/` to open the inline command menu
706
- (including extension commands); Up/Down selects, Tab completes, and Enter runs.
707
- Outside a menu, Up/Down browses input history. During a run ordinary typing is ignored; Ctrl+C cancels immediately.
708
- At an idle prompt Ctrl+C exits, clearing the draft and returning the terminal
709
- to the shell. Ctrl+D on an empty line also exits.
710
- Input is single-line: only the first submitted line in an input batch is accepted.
711
- Wait for the prompt before sending another question.
712
- After cancellation the prompt is ready again and partial text stays in scrollback.
713
- A completion line records the outcome, elapsed time and reported token usage.
714
-
715
- Chinese, combining characters and joined emoji are clipped by terminal columns;
716
- short terminals keep the status visible. Resizing repaints the existing region.
717
- All prompts and UI output go to stderr; stdout receives final answers only, so
718
- `chivgent --tui > answers.txt` keeps the terminal interactive while saving answers.
719
-
720
- ```sh
721
- npm run build
722
- node dist/cli.js --tui
723
- npm run demo:tui # Offline interactive demo; no API key or network required
724
- ```
725
-
726
- The demo uses a stub Provider and simulated tool progress; it does not modify the
727
- workspace. See [the TUI guide](docs/tui.md) for controls and verification.
728
-
729
- ## Remote sessions
730
-
731
- A session normally lives and dies with the terminal that started it. `--serve`
732
- keeps it in one process and lets others attach:
733
-
734
- ```bash
735
- # One terminal
736
- chivgent --serve --allow-writes
737
- # chivgent 0.12.0 serving session 2026-09-06T...
738
- # socket: ~/.chivgent/sockets/2026-09-06T....sock
739
- # capabilities: --allow-writes
740
-
741
- # Another terminal
742
- chivgent --servers # list what is running
743
- chivgent --connect <id> # attach interactively
744
- chivgent --connect <id> "question" # ask once and leave
745
- ```
746
-
747
- A client holds no state: it sends prompts and renders the event stream the
748
- server broadcasts. Several clients can attach at once — the extra ones watch the
749
- same run as it happens, and any of them can interrupt it, because the run
750
- belongs to the session rather than to whoever started it. A prompt sent while
751
- another is running is refused rather than queued, and a client that disconnects
752
- mid-run does not cancel it.
753
-
754
- Attaching does not replay history; the session log already holds that. The
755
- protocol is JSON Lines over a Unix socket, one object per line, the same shape
756
- `--json` writes.
757
-
758
- **Whoever can reach the socket has everything the server was started with.** If
759
- it was started with `--allow-shell`, anyone who can connect can make the model
760
- run commands, which is why the server prints its capabilities on startup: the
761
- person attaching cannot see the flags you typed. The boundary is filesystem
762
- permissions — sockets live in an owner-only directory under `CHIVGENT_HOME` —
763
- and there is no TCP listener. For another machine, forward the socket over SSH
764
- so that authentication is SSH's job.
765
-
766
- ## Extensions
767
-
768
- An extension is an ES module that default-exports a function. It is called once
769
- at startup with an API that can add a tool, add a slash command, subscribe to
770
- runtime events, and append to the system prompt.
771
-
772
- ```js
773
- // .chivgent/extensions/word-count.js
774
- export default function (api) {
775
- api.registerTool({
776
- name: "word_count",
777
- description: "Count the words in a workspace file.",
778
- inputSchema: {
779
- type: "object",
780
- properties: { path: { type: "string" } },
781
- required: ["path"],
782
- additionalProperties: false,
783
- },
784
- async execute(args, context) {
785
- const file = await context.workspace.readTextFile(args.path);
786
- return {
787
- content: `${file.content.split(/\s+/).filter(Boolean).length} words`,
788
- isError: false,
789
- };
790
- },
791
- });
792
-
793
- api.contributeSystemPrompt("Use word_count instead of reading a file to count words.");
794
- }
795
- ```
796
-
797
- Extensions are loaded from two places:
798
-
799
- | Location | Loaded |
800
- | --- | --- |
801
- | `<CHIVGENT_HOME>/extensions/` | Always: it is your own machine's configuration |
802
- | `<workspace>/.chivgent/extensions/` | Only after you trust the project |
803
-
804
- Both accept `name.js` and `name/index.js`; nesting stops there. Plain JavaScript
805
- only — compiling TypeScript yourself is cheaper than making chivgent carry a
806
- TypeScript loader. A name already taken by a built-in tool or command is refused,
807
- so an extension cannot shadow `read_file` or `/clear`, and a broken extension is
808
- reported and skipped rather than taking chivgent down with it.
809
-
810
- `chivgent --extensions` lists what loaded and what each one registered.
811
-
812
- ### Project trust
813
-
814
- The first time you run chivgent in a project that ships extensions, it describes
815
- what they are and asks once. The answer is stored in `<CHIVGENT_HOME>/trust.json`
816
- against the resolved directory path, and matched by nearest ancestor, so trusting
817
- `~/work` covers every repository under it. `--forget-trust` removes the decision
818
- covering the current workspace.
819
-
820
- **Trusting a project means letting its authors run code as you.** An extension
821
- executes inside the chivgent process: it is not bound by the workspace, it does
822
- not need `--allow-shell` to run a command, and it can read your environment
823
- including the API key. That is why the question is asked before anything is
824
- imported, and why the answer is no whenever there is nobody to ask — a CI job or
825
- a piped run never loads a checkout's extensions on its own.
826
-
827
- ## Security model
828
-
829
- - API keys come from `--api-key`, an environment variable, or an optional
830
- `auth.json`, in that order, and must never be committed.
831
- - `auth.json` stores keys in plain text. It is opt-in for that reason, and
832
- chivgent warns when its permissions let other users read it.
833
- - `/login` writes it owner-only and does not echo what you type, but the key is
834
- still stored in plain text; it is a convenience, not a secret manager.
835
- - The auth file accepts literal keys only; it cannot expand environment
836
- variables or run shell commands.
837
- - A custom `OPENAI_BASE_URL` receives the configured API key and prompts; use
838
- only endpoints you trust.
839
- - Workspace tools are read-only unless `--allow-writes` is passed; `write_file`
840
- and `edit_file` are not registered at all without it.
841
- - The `bash` tool needs `--allow-shell`, which is separate from `--allow-writes`
842
- and never implied by it. Granting it grants far more: a shell can change or
843
- delete anything the user running chivgent can, inside the workspace or not,
844
- and none of the workspace limits below apply to it.
845
- - Commands are spawned in their own process group and killed as a group, so
846
- cancelling a run does not leave descendants behind.
847
- - Commands inherit chivgent's environment, which includes the API key it is
848
- using. A model with `--allow-shell` can read that key and anything else the
849
- environment holds. Run it with an environment that carries only what the task
850
- needs.
851
- - Truncated command output is written to a temp file so the model can read the
852
- rest. The file is owner-only, but it is not deleted when the run ends: it can
853
- hold anything the command printed. Clear the temp directory after sensitive
854
- work.
855
- - Session logs record command output as well as file excerpts once
856
- `--allow-shell` is on.
857
- - Project extensions are loaded only after an explicit, recorded trust decision,
858
- and never without a terminal to ask at. An extension runs in-process with your
859
- permissions and is bound by none of the workspace limits above, so trusting a
860
- project is the same order of authority as `--allow-shell`, reached by cloning
861
- a repository rather than by typing a flag.
862
- - User extensions under `<CHIVGENT_HOME>/extensions/` are always loaded; that
863
- directory is your own configuration.
864
- - Extensions cannot take the name of a built-in tool or command.
865
- - `trust.json`, like the session log and the auth file, is written owner-only.
866
- - A served session is reachable by anyone who can open its socket, and they get
867
- every capability the server was started with. Sockets live in an owner-only
868
- directory; the directory is the real gate, because a socket file exists
869
- briefly with default permissions before it can be restricted.
870
- - chivgent never listens on TCP. Use SSH forwarding to reach another machine.
871
- - Writes resolve the deepest existing ancestor and reject a symlink at any
872
- segment, so a planted link cannot redirect a write out of the workspace.
873
- - Writes are staged in a sibling temp file and renamed into place, so an
874
- interrupted write cannot truncate an existing file.
875
- - `edit_file` refuses an edit whose `old_text` is missing or matches more than
876
- once, so an imprecise edit fails instead of changing the wrong line.
877
- - Paths must remain inside the current workspace.
878
- - Real-path checks block `..` traversal and symlink escapes.
879
- - File size and binary-content checks limit unsafe reads.
880
- - Discovery respects the root `.gitignore` and fixed generated-directory ignores.
881
- - Common credential and private-key paths are denied across all workspace tools.
882
- - Tool results are limited to 64 KiB; reads, scans, depth, and result counts are bounded.
883
- - Tool inputs are untrusted and validated before execution.
884
- - The agent stops after a bounded number of model turns.
885
- - Session logs under `~/.chivgent/sessions` contain prompts, answers, and tool
886
- results, including file excerpts. Use `--no-session` in sensitive workspaces,
887
- and treat the log directory like the project it describes.
888
- - Session ids are validated before they become file paths.
889
-
890
- This is an educational MVP, not a hardened sandbox. There is no permission
891
- system: capabilities are coarse, session-level switches, and neither
892
- `--allow-writes` nor `--allow-shell` asks for per-action confirmation. That is
893
- deliberate. Once a shell exists, a command allowlist is bypassed by a single
894
- `sh -c`, and a prompt on every action only trains people to approve without
895
- reading, so chivgent states the boundary instead of pretending to enforce one.
896
-
897
- Use these switches on work you have committed. If you need a real boundary,
898
- put the whole process in a container and give that container only what the task
899
- needs. Review the code and threat model before pointing chivgent at a sensitive
900
- project.
901
-
902
- ## Roadmap
903
-
904
- - [x] Minimal tool-calling agent loop
905
- - [x] Safe `read_file` tool
906
- - [x] OpenAI and DeepSeek Providers
907
- - [x] Reusable OpenAI-compatible Chat Completions adapter
908
- - [x] Custom OpenAI-compatible CLI Provider
909
- - [x] Project discovery tools: `list_files`, `search_text`, and ranged `read_file`
910
- - [x] Streaming output and runtime events
911
- - [x] Persistent multi-turn sessions
912
- - [x] Context-window management and compaction
913
- - [x] Opt-in `write_file` and `edit_file` behind `--allow-writes`
914
- - [x] Provider registry and credential resolution chain
915
- - [x] `bash` tool with streaming output, behind `--allow-shell`
916
- - [x] Extensions and project trust
917
- - [x] Remote sessions over a local socket
918
- - [x] An eval suite with deterministic graders and pass rates
919
- - [x] Token usage from the Provider, aggregated per run and per eval
920
- - [x] A live status region driven by the event stream (`--tui`)
921
-
922
- Per-command confirmation prompts and command allowlists are deliberately not
923
- planned. Once a shell tool exists, `bash` can do anything `write_file` can and
924
- more, so an allowlist is bypassed by a single `sh -c` and a per-command prompt
925
- only trains people to approve without reading. Capabilities are coarse,
926
- session-level switches; the real boundary is a container.
927
-
928
- ## Documentation
929
-
930
- - [Stage 1: Minimal Agent design](docs/stage-1-minimal-agent.md)
931
- - [DeepSeek Provider design](docs/deepseek-provider.md)
932
- - [Stage 2: Project Discovery implementation design](docs/stage-2-project-discovery.md)
933
- - [Stage 3: Runtime Events and Streaming design](docs/stage-3-runtime-events.md)
934
- - [Stage 4: Sessions and interactive mode design](docs/stage-4-sessions.md)
935
- - [Stage 5: Write tools and the workspace split](docs/stage-5-write-tools.md)
936
- - [Stage 6: Provider registry and credential chain](docs/stage-6-provider-registry.md)
937
- - [Stage 7: Context budget and compaction](docs/stage-7-context-management.md)
938
- - [Stage 8: Shell tool and streaming subprocesses](docs/stage-8-shell-tool.md)
939
- - [Stage 9: Extensions and project trust](docs/stage-9-extensions.md)
940
- - [Stage 10: Remote sessions](docs/stage-10-remote-sessions.md)
941
- - [Stage 11: Evals](docs/stage-11-evals.md)
942
- - [Stage 12: Token usage](docs/stage-12-usage.md)
943
- - [Release process](docs/releasing.md)
944
-
945
- ## Contributing
946
-
947
- Issues and focused pull requests are welcome. Before submitting a change, run:
948
-
949
- ```bash
950
- npm run check
951
- npm test
952
- npm run build
953
- ```
180
+ Tests use scripted LLM clients and never spend API credits.
181
+ `npm run release:check` runs the full gate, including an `npm pack` dry run.
182
+ To try the checkout as a global command: `npm install -g .`
954
183
 
955
- Please keep Provider-specific types inside `src/providers/` and keep the core
956
- Agent runtime independent from vendor SDK schemas.
184
+ Issues and focused pull requests are welcome. Keep Provider-specific types in
185
+ `src/providers/` so the agent runtime stays independent of vendor SDKs.
957
186
 
958
187
  ## License
959
188
 
960
- Licensed under the [MIT License](LICENSE).
189
+ [MIT](LICENSE)