@giovannijecha/jecode 0.7.0 → 0.7.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.
package/README.md CHANGED
@@ -9,8 +9,8 @@
9
9
  <p align="center"><strong>Your code. Your loop.</strong></p>
10
10
 
11
11
  <p align="center">
12
- A focused coding agent that lives in your terminal, keeps tool use visible,
13
- and stays under your control.
12
+ A focused terminal coding agent with visible tool use, durable sessions, and
13
+ one controller that stays under your control.
14
14
  </p>
15
15
 
16
16
  <p align="center">
@@ -21,367 +21,342 @@
21
21
  </p>
22
22
 
23
23
  <p align="center">
24
- <a href="https://github.com/giovannijecha/jecode/blob/main/CHANGELOG.md">Changelog</a> ·
24
+ <a href="https://github.com/giovannijecha/jecode/blob/main/CHANGELOG.md">Changelog</a> &middot;
25
25
  <a href="https://github.com/giovannijecha/jecode/releases">Releases</a>
26
26
  </p>
27
27
 
28
- > Jecode is an early 0.7.x release. The core loop is usable today; commands and
28
+ > Jecode is currently pre-1.0. The core loop is usable today, but commands and
29
29
  > terminal interactions may still evolve before 1.0.
30
30
 
31
31
  ## Why Jecode
32
32
 
33
- - **One controller.** One visible loop talks to the model, runs tools, and returns
34
- control to you. Independent reads can overlap inside one step; writes and
35
- commands remain ordered. There are no hidden workers or delegated agents.
36
- - **Terminal-native.** The transcript, composer, searchable menus, tool output,
37
- diffs, approvals, reasoning, and status all share one full-screen TUI.
38
- - **Permission-aware.** Reads stay transparent; dangerous actions ask first.
39
- Session approvals can be reviewed and revoked.
40
- - **Durable by default.** Interactive conversations survive terminal exits and
41
- can be resumed without replaying tools. Batch runs remain stateless.
42
- - **Context-bounded.** Older model context is summarized automatically while
43
- the complete conversation and transcript remain available in the session.
44
- - **Provider-neutral.** Use Anthropic or OpenAI API keys, a ChatGPT account, or
45
- a local/remote Ollama server without changing the workflow.
46
- - **Lean by construction.** Jecode installs as plain JavaScript, runs on
47
- Node.js 22.18+ (22.x) or Node.js 24+, executes no installation scripts, and
48
- has zero third-party runtime dependencies.
49
-
50
- ## Install
51
-
52
- Jecode requires **Node.js 22.18+ on the 22.x line, or Node.js 24+**, and npm:
53
-
54
- ~~~console
33
+ - **One visible loop.** One controller talks to the model, runs tools, and
34
+ returns control to you. Independent reads may overlap inside a step; writes
35
+ and commands stay ordered. There are no delegated agents or hidden model
36
+ workers.
37
+ - **Terminal-native today.** The transcript, composer, searchable menus, tool
38
+ output, diffs, approvals, reasoning, and status share one full-screen TUI.
39
+ - **Permission-aware.** Tool use is visible. Dangerous actions ask first, and
40
+ remembered session approvals can be reviewed or revoked.
41
+ - **Durable and context-bounded.** Interactive conversations survive terminal
42
+ exits. Older model context can be compacted while the complete conversation
43
+ and transcript remain available in the saved session.
44
+ - **Multi-provider.** Use Anthropic or OpenAI API keys, an eligible ChatGPT
45
+ account, or a local, cloud, or remote Ollama server without changing the
46
+ workflow.
47
+ - **Lean by construction.** Jecode ships as owned JavaScript built on Node.js
48
+ primitives, executes no installation scripts, and has zero third-party
49
+ runtime dependencies. This is a permanent product constraint, not a temporary
50
+ optimization.
51
+
52
+ ## Quick start
53
+
54
+ Jecode requires **Node.js 22.18+ on the 22.x line, or Node.js 24+**, and npm.
55
+
56
+ ```console
55
57
  npm install --global @giovannijecha/jecode
56
58
  jecode --version
57
- ~~~
59
+ cd path/to/your/project
60
+ jecode
61
+ ```
58
62
 
59
- To try prereleases instead, install the opt-in **next** channel with
60
- `npm install --global @giovannijecha/jecode@next`.
63
+ Jecode opens directly on an empty composer. Use `/providers` to connect a
64
+ service, `/models` to choose a model, then describe the work you want done.
61
65
 
62
- Published npm packages are the supported installation artifacts. Git URL
63
- installs are intentionally unsupported: the source tree contains no generated
64
- runtime and defines no install-time build hook.
66
+ ```text
67
+ Review this project, explain its architecture, and propose the smallest safe
68
+ change to improve startup performance.
69
+ ```
65
70
 
66
- Then open the project you want to work on and run Jecode:
71
+ Use `jecode --root path/to/project` to select another workspace, or
72
+ `jecode --ephemeral` when the conversation must stay memory-only.
67
73
 
68
- ~~~console
69
- cd path/to/your/project
70
- jecode
71
- ~~~
74
+ Resume a saved conversation for the current workspace with a searchable picker:
72
75
 
73
- Resume a saved conversation for the current project with a searchable picker,
74
- or open the most recent one directly:
75
-
76
- ~~~console
76
+ ```console
77
77
  jecode resume
78
78
  jecode resume --latest
79
- ~~~
79
+ ```
80
80
 
81
- Use `jecode --ephemeral` when a conversation must stay memory-only.
81
+ Run `jecode --help` for every startup option. Windows, Ubuntu, and macOS are
82
+ covered by the project test matrix.
82
83
 
83
- You can point at another workspace explicitly:
84
+ ### Prereleases
84
85
 
85
- ~~~console
86
- jecode --root path/to/your/project
87
- ~~~
86
+ The stable npm package is the supported installation artifact. To try an
87
+ opt-in prerelease:
88
88
 
89
- Run **jecode --help** for all startup options. Tested platforms are Windows,
90
- Ubuntu, and macOS.
89
+ ```console
90
+ npm install --global @giovannijecha/jecode@next
91
+ ```
91
92
 
92
- ### Update
93
+ Git URL installs are intentionally unsupported: the source tree contains no
94
+ generated runtime and defines no install-time build hook.
93
95
 
94
- Install the current stable release over the existing global command:
96
+ ### Update and uninstall
95
97
 
96
- ~~~console
98
+ ```console
97
99
  npm install --global @giovannijecha/jecode
98
100
  jecode --version
99
- ~~~
101
+ npm uninstall --global @giovannijecha/jecode
102
+ ```
103
+
104
+ Uninstalling the command preserves `~/.jecode`. Remove that directory only when
105
+ you intentionally want to erase saved settings, credentials, accounts, and
106
+ sessions.
100
107
 
101
- If you installed Jecode from GitHub before the scoped npm package existed,
102
- remove the old unscoped package once before updating:
108
+ If an older GitHub installation still owns the `jecode` executable, remove the
109
+ legacy unscoped package before installing the scoped package:
103
110
 
104
- ~~~console
111
+ ```console
105
112
  npm uninstall --global jecode
106
113
  npm install --global @giovannijecha/jecode
107
- ~~~
114
+ ```
108
115
 
109
- An `EEXIST` error for a `bin/jecode` path usually means that this legacy
110
- executable still owns the command. Remove it instead of installing with
111
- `--force`.
112
-
113
- ### Uninstall
114
-
115
- ~~~console
116
- npm uninstall --global @giovannijecha/jecode
117
- ~~~
118
-
119
- Uninstalling the command preserves **~/.jecode** so saved settings and
120
- credentials remain available after a reinstall. Remove that directory only
121
- when you intentionally want to erase Jecode's local data.
116
+ Do not work around the resulting `EEXIST` error with `--force`.
122
117
 
123
118
  ### Linux and WSL
124
119
 
125
- WSL has its own Node.js installation and `PATH`; the Node.js version installed
120
+ WSL uses its own Node.js installation and `PATH`; the Node.js version installed
126
121
  on Windows does not apply inside it. Keep user-installed npm commands in the
127
- Linux user path and put that path before inherited Windows entries:
122
+ Linux user path:
128
123
 
129
- ~~~console
124
+ ```console
130
125
  npm config set prefix "$HOME/.local"
131
126
  export PATH="$HOME/.local/bin:$PATH"
132
127
  npm install --global @giovannijecha/jecode
133
128
  hash -r
134
129
  command -v jecode
135
130
  jecode --version
136
- ~~~
131
+ ```
137
132
 
138
133
  Persist the `PATH` export in `~/.profile` or your shell's startup file. Inside
139
- WSL, **command -v jecode** should resolve below `/home/...`, not below
140
- `/mnt/c/.../Volta`. Do not ignore an `EBADENGINE` warning: **node --version**
141
- must report 22.18+ on the 22.x line, or 24+.
134
+ WSL, `command -v jecode` should resolve below `/home/...`, not through an
135
+ inherited Windows path below `/mnt/c`. Do not ignore `EBADENGINE`: `node
136
+ --version` must report 22.18+ on the 22.x line, or 24+.
142
137
 
143
- ### Build from source
144
-
145
- ~~~console
146
- git clone https://github.com/giovannijecha/jecode.git
147
- cd jecode
148
- npm ci --ignore-scripts
149
- npm run build:release
150
- npm link
151
- jecode
152
- ~~~
153
-
154
- Development runs TypeScript directly with **npm run start**. **dist/** is an
155
- ignored, generated tree used only by linked commands and release tarballs.
156
- **npm run pack:release** rebuilds it from a clean target before packing; the
157
- trusted publish workflow performs the same explicit build. Installing the
158
- published package runs no compilation or installation scripts.
159
-
160
- ## First session
161
-
162
- Jecode opens on an empty composer instead of forcing a setup wizard. Type
163
- **/providers** to connect the services you use, then **/models** to choose from
164
- their combined live catalogues. **/settings** keeps the selected model and the
165
- remaining non-secret defaults together. An API key can remain in memory for
166
- the current session or be saved explicitly under **~/.jecode**; it is never
167
- stored in the workspace.
138
+ ## Providers
168
139
 
169
140
  | Provider ID | Authentication | Notes |
170
- |---|---|---|
171
- | anthropic | ANTHROPIC_API_KEY | Anthropic API |
172
- | openai | OPENAI_API_KEY | OpenAI API |
173
- | openai-codex | ChatGPT OAuth | Experimental; uses eligible ChatGPT Codex access |
174
- | ollama | OLLAMA_API_KEY for Cloud/remote | Cloud with a key, local without one |
175
-
176
- Choose **ChatGPT** in **/providers** to sign in on OpenAI's website without
177
- pasting a key.
178
- Jecode offers a local browser callback and a device-code flow; WSL and remote
179
- terminals default to the device code. The connection is saved only after the
180
- flow completes. Availability and usage limits are determined by the ChatGPT
181
- account and plan, not by OpenAI API credits. This integration is experimental
182
- and is not an endorsement of Jecode by OpenAI.
141
+ | --- | --- | --- |
142
+ | `anthropic` | `ANTHROPIC_API_KEY` | Anthropic API |
143
+ | `openai` | `OPENAI_API_KEY` | OpenAI API |
144
+ | `openai-codex` | ChatGPT OAuth | Experimental; uses eligible ChatGPT Codex access |
145
+ | `ollama` | `OLLAMA_API_KEY` for cloud or remote use | Cloud with a key, local without one |
146
+
147
+ Choose **ChatGPT** in `/providers` to sign in on OpenAI's website without
148
+ pasting a key. Jecode supports a local browser callback and a device-code flow;
149
+ WSL and remote terminals default to the device code. Availability and usage
150
+ limits depend on the ChatGPT account and plan, not on OpenAI API credits. This
151
+ integration is experimental and is not an endorsement of Jecode by OpenAI.
183
152
 
184
153
  Anthropic remains API-key only. Jecode does not reuse a Claude consumer
185
154
  subscription or copy credentials from another client.
186
155
 
187
- Choose **Ollama** in **/providers** to manage its API key and select **cloud**,
188
- **local**, or a custom endpoint. Existing users with an Ollama API key
189
- automatically use **https://ollama.com**; without a key, Jecode uses the local
190
- daemon at **http://127.0.0.1:11434**. Remote custom endpoints must use HTTPS.
156
+ For Ollama, `/providers` can select cloud, local, or a custom endpoint. With an
157
+ Ollama API key Jecode defaults to `https://ollama.com`; without one it defaults
158
+ to `http://127.0.0.1:11434`. Remote custom endpoints must use HTTPS.
191
159
 
192
160
  ## Use the TUI
193
161
 
194
- Type **/** to open searchable command completion inside the composer.
162
+ Type `/` to open searchable command completion inside the composer.
195
163
 
196
164
  | Command | What it does |
197
- |---|---|
198
- | /settings | Manage the selected model, limits, context compaction, effort, motion, and provider access |
199
- | /effort | Change and save reasoning effort directly |
200
- | /providers | Manage API keys, ChatGPT sign-in, and Ollama connections |
201
- | /models | Search models across every available provider and select one |
202
- | /permissions | Change session tool access inline and review remembered approvals |
203
- | /new | Close the current conversation, start clean, and reset tool permissions |
204
- | /timeline | Navigate completed turns and select where the next branch starts |
205
- | /compact | Compact the active model context immediately |
206
- | /export | Save a timestamped Markdown transcript in the launch directory |
207
- | /help | Open a temporary keyboard reference in the composer dock |
208
- | /exit | Restore the terminal and exit |
165
+ | --- | --- |
166
+ | `/settings` | Manage the selected model and saved non-secret defaults |
167
+ | `/effort` | Change and save reasoning effort directly |
168
+ | `/providers` | Manage provider connections, API keys, ChatGPT sign-in, and Ollama endpoints |
169
+ | `/models` | Search all currently usable provider catalogues and select a model |
170
+ | `/permissions` | Change session tool access and review remembered approvals |
171
+ | `/timeline` | Browse completed turns and select where the next branch should begin |
172
+ | `/compact` | Compact the current branch context without deleting saved conversation history |
173
+ | `/new` | Start a new conversation and reset session tool permissions |
174
+ | `/export` | Save a timestamped Markdown transcript in the launch directory |
175
+ | `/help` | Open a temporary keyboard reference in the composer dock |
176
+ | `/exit` | Restore the terminal and exit |
209
177
 
210
178
  Useful controls:
211
179
 
212
- - **Up/Down** moves through command suggestions, menus, and input history.
213
- - **Left/Right** moves the composer cursor or changes an inline menu value;
180
+ - **Up/Down** moves through suggestions, menus, and input history.
181
+ - **Left/Right** moves the cursor or changes an inline value;
214
182
  **Ctrl+Left/Right** moves by word.
215
- - **Backspace/Delete** removes one character; **Ctrl+Backspace/Delete** removes
216
- one word. **Home/End** moves to the start or end of the composer.
183
+ - **Backspace/Delete** removes one character;
184
+ **Ctrl+Backspace/Delete** removes one word.
185
+ - **Home/End** moves to the beginning or end of the composer.
217
186
  - **Tab** completes a slash command without running it; **Enter** sends.
218
187
  - **Alt+Enter** inserts a newline.
219
- - **Esc** closes a menu or interrupts the foreground operation.
188
+ - **Esc** closes the active menu or interrupts foreground work.
220
189
  - **Ctrl+C** interrupts, or exits while idle. **Ctrl+D** requests a clean exit.
221
- - **PageUp/PageDown** and the mouse wheel scroll the transcript without losing
222
- the place you are reading.
190
+ - **PageUp/PageDown** and the mouse wheel scroll the transcript.
223
191
  - **Ctrl+O** expands or compacts the latest reasoning or tool-detail block.
224
192
 
225
- The one-line footer keeps model, effort, and workspace on the left. While work
226
- is active, the right edge shows its current state, elapsed time, and interrupt
227
- hint; readiness guidance and temporary feedback use the same replaceable space
228
- without polluting the transcript. Slash commands never append content to the
229
- conversation or its Markdown export; **/help** closes with **Esc**, and token
230
- accounting remains internal to the active session.
193
+ The footer keeps the active model, effort, and workspace visible. During work,
194
+ it adds the current state, elapsed time, and interrupt hint. Operational
195
+ feedback uses the same replaceable status area instead of adding noise to the
196
+ conversation or its Markdown export.
197
+
198
+ ## Sessions and context
199
+
200
+ Interactive conversations are stored under `~/.jecode/sessions` and scoped to
201
+ the canonical workspace path. A fresh or `/new` conversation is not added to
202
+ the resume picker until it has a settled turn. Resuming and continuing a
203
+ conversation keeps its durable session identity and updates one picker entry
204
+ instead of creating duplicates.
205
+
206
+ `/timeline` shows completed turns in the conversation tree. Selecting an older
207
+ turn changes only the in-memory path: it creates and saves a branch only after
208
+ the next real user message. Cancelling the picker or exiting before that message
209
+ leaves the durable head unchanged. Historical tools are displayed but never
210
+ executed. If a crash interrupted a tool loop, Jecode resumes from the latest
211
+ completed ancestor and lets the next user turn create a branch. `/export`
212
+ writes only the currently selected path.
213
+
214
+ When model-facing context approaches the selected model's usable capacity,
215
+ Jecode asks the provider for a bounded summary of the older prefix and keeps
216
+ recent turns exact. The default trigger is 85% and can be changed from 50% to
217
+ 95%. Live provider metadata or Ollama's allocated context determines the budget
218
+ when available; provider safety limits always win.
219
+
220
+ Compaction changes only the projection sent to the model. Complete messages,
221
+ tool evidence, transcript, export, and conversation tree remain intact. The
222
+ branch-local summary anchor is saved with the session so resume does not repeat
223
+ the same compaction. `/compact` requests this process immediately, even below
224
+ the automatic threshold; it leaves very small contexts unchanged. After
225
+ selecting a historical turn, send the first new message before compacting so
226
+ shared history is never rewritten.
227
+
228
+ ## Batch mode
229
+
230
+ When stdin or stdout is not a terminal, Jecode switches to a plain
231
+ line-oriented mode:
232
+
233
+ ```console
234
+ printf "explain this project\n" | jecode --root .
235
+ ```
236
+
237
+ Batch conversations are stateless. Dangerous tools remain denied unless
238
+ `--auto-approve` is supplied explicitly. Terminal failures are written to
239
+ stderr and exit non-zero so scripts and CI pipelines can stop reliably.
231
240
 
232
241
  ## Configuration
233
242
 
234
243
  Startup precedence is: command-line flags, environment variables, saved
235
- settings, built-in defaults.
244
+ settings, then built-in defaults.
236
245
 
237
246
  | Flag | Environment | Default |
238
- |---|---|---|
239
- | --provider | JECODE_PROVIDER | anthropic |
240
- | --model | JECODE_MODEL | Provider default or interactive selection |
241
- | --ollama-host | OLLAMA_HOST | Cloud with an Ollama key, local without one |
242
- | --root | | Current directory |
243
- | --effort | JECODE_EFFORT | high |
244
- | --max-tokens | JECODE_MAX_TOKENS | 64000; not sent by openai-codex |
245
- | --max-steps | JECODE_MAX_STEPS | 40 |
246
- | --compaction-percent | JECODE_COMPACTION_PERCENT | 85; accepts 50 through 95 |
247
- | --reduced-motion | JECODE_REDUCED_MOTION=1 | Off |
248
- | --auto-approve | JECODE_AUTO_APPROVE=1 | Off |
249
- | --ephemeral | JECODE_EPHEMERAL=1 | Off |
250
-
251
- Persistent preferences live in **~/.jecode/settings.json**. Explicitly saved
252
- API keys live in **~/.jecode/credentials.json**; the ChatGPT OAuth account lives
253
- separately in **~/.jecode/accounts.json**. Both secret stores use owner-only
254
- permissions where the operating system supports them. Environment API keys
255
- always win. Model selection saves the provider and model as one change; the
256
- separate startup flags remain available for automation and override that saved
257
- choice.
258
-
259
- Interactive conversations are stored under **~/.jecode/sessions**, scoped to
260
- the canonical workspace path. A checkpoint contains normalized messages and
261
- the settled transcript needed to redraw the conversation. It excludes stored
262
- provider credentials, OAuth tokens, provider-only opaque response data,
263
- permission choices, draft composer text, transient footer notices, and pending
264
- tool state. Session
265
- files use owner-only modes on POSIX; Windows relies on the user-profile ACL.
266
- `jecode resume` keeps the same durable session identity and advances that
267
- session's conversation tree, so reopening and continuing a conversation does
268
- not create duplicate picker entries. `/new` or a fresh launch starts another
269
- logical session. **/timeline** shows the completed turns in that tree. Selecting
270
- an earlier turn changes only the visible path; it creates and persists a branch
271
- only when the next real message is sent. Cancelling the picker or exiting first
272
- leaves the durable head untouched, and resume returns to the last branch with a
273
- persisted turn. Historical tools are displayed but never executed. If a crash
274
- left the newest turn inside a tool loop, the same session resumes from its
275
- latest completed ancestor and the next turn becomes a branch because
276
- provider-only continuation data is intentionally not stored. **/export** writes
277
- only the currently selected path.
278
-
279
- When the model-facing context approaches the selected model's usable capacity,
280
- Jecode asks the provider for one bounded summary of its older prefix and keeps
281
- the recent turn exact. The trigger defaults to 85% and can be changed from 50%
282
- through 95% in **/settings**. Live provider metadata or Ollama's allocated
283
- runtime context determines the budget when available; a metadata failure falls
284
- back safely without blocking the turn. Only the provider projection is
285
- replaced: complete messages, tool evidence, transcript, and conversation tree
286
- remain unchanged. The branch-local summary anchor is checkpointed with the
287
- session, so resume reuses it instead of summarizing the same prefix again. A
288
- failed or cancelled optional summary leaves the original context intact; a
289
- definite provider context-limit rejection may trigger one compacted retry.
290
- Internal summary requests count toward provider usage but never appear in the
291
- transcript or Markdown export. **/compact** requests the same model-aware,
292
- branch-local compaction immediately, even below the automatic trigger. Very
293
- small contexts are left unchanged. After selecting a historical branch point,
294
- send its first new message before compacting so shared history is never
295
- rewritten.
296
-
297
- Jecode has one interface theme: dark Steel. **NO_COLOR** is supported for
247
+ | --- | --- | --- |
248
+ | `--provider` | `JECODE_PROVIDER` | `anthropic` |
249
+ | `--model` | `JECODE_MODEL` | Provider default or interactive selection |
250
+ | `--ollama-host` | `OLLAMA_HOST` | Cloud with an Ollama key, local without one |
251
+ | `--root` | - | Current directory |
252
+ | `--effort` | `JECODE_EFFORT` | `high` |
253
+ | `--max-tokens` | `JECODE_MAX_TOKENS` | `64000`; not sent by `openai-codex` |
254
+ | `--max-steps` | `JECODE_MAX_STEPS` | `40` |
255
+ | `--compaction-percent` | `JECODE_COMPACTION_PERCENT` | `85`; accepts `50` through `95` |
256
+ | `--reduced-motion` | `JECODE_REDUCED_MOTION=1` | Off |
257
+ | `--auto-approve` | `JECODE_AUTO_APPROVE=1` | Off |
258
+ | `--ephemeral` | `JECODE_EPHEMERAL=1` | Off |
259
+
260
+ Non-secret preferences live in `~/.jecode/settings.json`. Explicitly saved API
261
+ keys live in `~/.jecode/credentials.json`, while ChatGPT OAuth accounts live in
262
+ `~/.jecode/accounts.json`. Environment credentials always take precedence.
263
+ Secret stores use owner-only permissions where the operating system supports
264
+ them.
265
+
266
+ Jecode has one current interface theme, Dark Steel. `NO_COLOR` is supported for
298
267
  terminals and pipelines that disable colour.
299
268
 
300
- ## Automation
301
-
302
- When stdin or stdout is piped, Jecode switches to a plain line-oriented mode:
303
-
304
- ~~~console
305
- printf "explain this project\n" | jecode --root .
306
- ~~~
307
-
308
- Batch conversations are never written to the session store.
309
-
310
- Dangerous tools stay denied in batch mode unless **--auto-approve** is supplied
311
- explicitly. A terminal batch failure is written to stderr and exits non-zero,
312
- so shell pipelines can stop reliably.
313
-
314
269
  ## Safety model
315
270
 
316
271
  Jecode treats model output, workspace content, tool output, and terminal text as
317
272
  untrusted data.
318
273
 
319
- - Tool paths are confined to the selected workspace. Writes reject symlink and
320
- junction components, then revalidate the boundary during atomic replacement.
321
- - Dangerous tools ask by default unless explicitly allowed for the session in
322
- **/permissions** or the process started with **--auto-approve**.
323
- - Credential fields are masked and excluded from transcripts. Approved shell
324
- commands receive no secret-bearing environment variables; `SSH_AUTH_SOCK`
325
- is preserved so Git and SSH can use the user's agent, which means an approved
326
- command can request that agent to authenticate. Recognized credential values
327
- are redacted before tool output reaches the model, screen, history, or export.
274
+ - Current filesystem tools are confined to the selected workspace. Writes
275
+ reject symlink and junction components, revalidate boundaries, and use atomic
276
+ replacement.
277
+ - Dangerous tools ask by default unless explicitly allowed for the session or
278
+ the process starts with `--auto-approve`.
279
+ - Credential fields are masked and excluded from transcripts. Recognized
280
+ credential values are redacted before output reaches the model, screen,
281
+ history, or export.
282
+ - Approved shell commands receive no secret-bearing environment variables.
283
+ `SSH_AUTH_SOCK` is preserved so Git and SSH can use the user's agent, which
284
+ means an approved command may ask that agent to authenticate.
328
285
  - ChatGPT OAuth uses PKCE and an exact loopback callback or the OpenAI device
329
- flow. Refresh-token rotation is serialized across Jecode processes; OAuth
330
- tokens are withheld and redacted like API keys.
286
+ flow. Refresh-token rotation is serialized across Jecode processes.
331
287
  - Terminal control characters are neutralized before rendering.
332
- - Remote Ollama endpoints require HTTPS. Provider HTTP redirects are rejected
333
- rather than followed across an implicit trust boundary.
288
+ - Remote Ollama endpoints require HTTPS, and provider redirects are rejected.
334
289
  - Provider handshakes and idle response bodies have finite deadlines. Only
335
290
  idempotent catalogue reads retry; generation requests are never replayed.
336
- - Model and filesystem input are bounded before they reach the screen or
337
- provider.
338
- - Durable session files are versioned, size-bounded, atomically checkpointed,
339
- and treated as untrusted when loaded. A live lease prevents the same saved
340
- session from being resumed by two Jecode processes at once.
291
+ - Model, terminal, and filesystem input are bounded before use.
292
+ - Session files are versioned, size-bounded, atomically checkpointed, and
293
+ treated as untrusted when loaded. A live lease prevents concurrent resume.
341
294
 
342
- `run_command` is not an operating-system sandbox: an approved shell command can
343
- still access files and account resources available to the current user. Review
344
- commands carefully and reserve **--auto-approve** for controlled environments.
295
+ `run_command` is not an operating-system sandbox. An approved command can still
296
+ access files and account resources available to the current user. Review
297
+ commands carefully and reserve `--auto-approve` for controlled environments.
345
298
 
346
- Please read [SECURITY.md](SECURITY.md) before reporting a vulnerability.
299
+ Read [SECURITY.md](SECURITY.md) before reporting a vulnerability.
347
300
 
348
- ## Community
301
+ ## Project direction
349
302
 
350
- Jecode is built around people using the product and telling us where the loop
351
- can improve.
303
+ The terminal is Jecode's current primary interface, not the limit of the
304
+ product. Future interfaces may reuse the same controller, session model, and
305
+ visible control system. Expansion must remain deliberate: no hidden model
306
+ hierarchy, no delegated authority, and no weakening of the zero-dependency
307
+ runtime.
352
308
 
353
- - Ask questions, share workflows, and explore ideas in
354
- [GitHub Discussions](https://github.com/giovannijecha/jecode/discussions).
355
- - Report reproducible bugs and focused feature requests through
356
- [GitHub Issues](https://github.com/giovannijecha/jecode/issues).
357
- - Report security concerns privately through the repository Security tab.
309
+ The single-controller rule limits delegation, not duration or scope. Jecode may
310
+ eventually supervise visible, interruptible processes such as development
311
+ servers, file watchers, and test runners. Those processes remain tools owned by
312
+ the controller; they do not receive independent goals, model loops, or tool
313
+ authority.
358
314
 
359
- Public pull requests are not accepted at this stage; code changes remain a
360
- maintainer/collaborator workflow. See [CONTRIBUTING.md](CONTRIBUTING.md) for the
361
- short routing guide and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for community
362
- expectations.
315
+ The selected workspace is the current default filesystem boundary. Any future
316
+ access to additional directories or resources must use explicit, reviewable,
317
+ revocable grants rather than silently widening that boundary.
363
318
 
364
- ## Development
319
+ ## Build from source
365
320
 
366
- ~~~console
321
+ ```console
322
+ git clone https://github.com/giovannijecha/jecode.git
323
+ cd jecode
367
324
  npm ci --ignore-scripts
368
- npm run check
369
- ~~~
325
+ npm run build:release
326
+ npm link
327
+ jecode --version
328
+ ```
370
329
 
371
- The visual lab exercises the complete production TUI—golden conversation,
372
- live tool trace, output tails, change-centric diffs, approvals, menus, and
373
- fields—without a provider, network access, tool execution, or workspace writes:
330
+ Development runs TypeScript directly with `npm run start`. `dist/` is an
331
+ ignored generated tree used only by linked commands and release tarballs.
332
+ `npm run pack:release` rebuilds it from a clean target. Installing the published
333
+ package runs no compilation or installation scripts.
374
334
 
375
- ~~~console
376
- npm run tui:lab
377
- ~~~
335
+ Run the complete project checks with:
378
336
 
379
- For a manual long-session rendering probe, run **npm run bench:transcript**.
337
+ ```console
338
+ npm run check
339
+ ```
380
340
 
381
- Architecture and security boundaries are documented in
382
- [docs/architecture.md](docs/architecture.md). Brand assets and usage rules live
341
+ Use `npm run tui:lab` to inspect production TUI components with inert local
342
+ fixtures, and `npm run bench:transcript` for a manual long-session rendering
343
+ probe. Architecture and security boundaries are documented in
344
+ [docs/architecture.md](docs/architecture.md); brand assets and usage rules live
383
345
  in [docs/brand.md](docs/brand.md).
384
346
 
347
+ ## Community
348
+
349
+ - Ask questions, share workflows, and explore early ideas in
350
+ [GitHub Discussions](https://github.com/giovannijecha/jecode/discussions).
351
+ - Report reproducible bugs and focused feature requests through
352
+ [GitHub Issues](https://github.com/giovannijecha/jecode/issues).
353
+ - Report security concerns privately through the repository Security tab.
354
+
355
+ Public pull requests are not accepted at this stage. Code changes remain a
356
+ maintainer and invited-collaborator workflow. See
357
+ [CONTRIBUTING.md](CONTRIBUTING.md) and
358
+ [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
359
+
385
360
  ## License
386
361
 
387
362
  Jecode is available under the [MIT License](LICENSE).
package/dist/config.js CHANGED
@@ -3,29 +3,24 @@ import * as path from "node:path";
3
3
  import { DEFAULT_COMPACTION_PERCENT, MAX_COMPACTION_PERCENT, MIN_COMPACTION_PERCENT, } from "./context/policy.js";
4
4
  import { EFFORTS, readSettings } from "./settings.js";
5
5
  import { parseOllamaEndpoint } from "./providers/ollama-endpoint.js";
6
- const FLAGS = [
6
+ const VALUE_FLAGS = [
7
7
  "provider",
8
8
  "model",
9
9
  "ollama-host",
10
- "reduced-motion",
11
10
  "effort",
12
11
  "max-tokens",
13
12
  "max-steps",
14
13
  "compaction-percent",
15
14
  "root",
15
+ ];
16
+ const BOOLEAN_FLAGS = [
17
+ "reduced-motion",
16
18
  "auto-approve",
17
19
  "ephemeral",
18
20
  ];
21
+ const FLAGS = [...VALUE_FLAGS, ...BOOLEAN_FLAGS];
19
22
  export function loadConfig(argv, saved = readSettings()) {
20
23
  const flags = parseFlags(argv);
21
- // A flag nobody declared is a typo. Swallowed in silence it becomes a
22
- // setting the user believes is on, and the run that ignores it looks like
23
- // the feature is broken rather than misspelled.
24
- for (const name of Object.keys(flags)) {
25
- if (!FLAGS.includes(name)) {
26
- throw new Error(`unknown flag --${name} (known: ${FLAGS.map((f) => `--${f}`).join(", ")})`);
27
- }
28
- }
29
24
  const providerId = pick(flags.provider, process.env.JECODE_PROVIDER, saved.provider ?? "anthropic");
30
25
  const ollamaHost = optional(flags["ollama-host"], process.env.OLLAMA_HOST, saved.ollamaHost);
31
26
  const effort = pick(flags.effort, process.env.JECODE_EFFORT, saved.effort ?? "high");
@@ -45,7 +40,9 @@ export function loadConfig(argv, saved = readSettings()) {
45
40
  maxSteps: toInt(pick(flags["max-steps"], process.env.JECODE_MAX_STEPS, String(saved.maxSteps ?? 40)), "max-steps"),
46
41
  compactionPercent: toPercent(pick(flags["compaction-percent"], process.env.JECODE_COMPACTION_PERCENT, String(saved.compactionPercent ?? DEFAULT_COMPACTION_PERCENT))),
47
42
  root: path.resolve(pick(flags.root, undefined, process.cwd())),
48
- autoApprove: flags["auto-approve"] === "true" || process.env.JECODE_AUTO_APPROVE === "1",
43
+ autoApprove: flags["auto-approve"] === "true" ||
44
+ flags["auto-approve"] === "1" ||
45
+ process.env.JECODE_AUTO_APPROVE === "1",
49
46
  ephemeral: bool(flags.ephemeral, process.env.JECODE_EPHEMERAL, false),
50
47
  };
51
48
  }
@@ -85,27 +82,55 @@ function toPercent(value) {
85
82
  }
86
83
  return percent;
87
84
  }
88
- // Accepts --key value, --key=value, and bare --flag (which reads as "true").
85
+ // Value flags accept --key value and --key=value. Boolean flags are bare or
86
+ // take a real boolean value, so an accidental positional argument is never
87
+ // swallowed as configuration.
89
88
  function parseFlags(argv) {
90
89
  const flags = {};
91
90
  for (let i = 0; i < argv.length; i++) {
92
91
  const arg = argv[i];
93
- if (arg === undefined || !arg.startsWith("--"))
92
+ if (arg === undefined)
94
93
  continue;
94
+ if (!arg.startsWith("--"))
95
+ throw new Error(`unexpected argument "${arg}"`);
95
96
  const body = arg.slice(2);
96
97
  const eq = body.indexOf("=");
97
- if (eq !== -1) {
98
- flags[body.slice(0, eq)] = body.slice(eq + 1);
98
+ const name = eq === -1 ? body : body.slice(0, eq);
99
+ const inline = eq === -1 ? undefined : body.slice(eq + 1);
100
+ if (!FLAGS.includes(name)) {
101
+ throw new Error(`unknown flag --${name} (known: ${FLAGS.map((flag) => `--${flag}`).join(", ")})`);
102
+ }
103
+ if (BOOLEAN_FLAGS.includes(name)) {
104
+ if (inline === undefined) {
105
+ const next = argv[i + 1];
106
+ if (next !== undefined && ["true", "false", "1", "0"].includes(next)) {
107
+ flags[name] = next;
108
+ i++;
109
+ }
110
+ else {
111
+ flags[name] = "true";
112
+ }
113
+ }
114
+ else if (["true", "false", "1", "0"].includes(inline)) {
115
+ flags[name] = inline;
116
+ }
117
+ else {
118
+ throw new Error(`--${name} must be true or false`);
119
+ }
99
120
  continue;
100
121
  }
101
- const next = argv[i + 1];
102
- if (next !== undefined && !next.startsWith("--")) {
103
- flags[body] = next;
104
- i++;
122
+ if (inline !== undefined) {
123
+ if (inline === "")
124
+ throw new Error(`--${name} requires a value`);
125
+ flags[name] = inline;
126
+ continue;
105
127
  }
106
- else {
107
- flags[body] = "true";
128
+ const next = argv[i + 1];
129
+ if (next === undefined || next.startsWith("--")) {
130
+ throw new Error(`--${name} requires a value`);
108
131
  }
132
+ flags[name] = next;
133
+ i++;
109
134
  }
110
135
  return flags;
111
136
  }
@@ -40,17 +40,21 @@ export function credentialRedactor(source = process.env) {
40
40
  const ready = [];
41
41
  let at = 0;
42
42
  while (at < combined.length) {
43
+ const rest = combined.slice(at);
44
+ // A complete shorter credential can also be the prefix of a longer
45
+ // one. Hold that ambiguous suffix until the next chunk proves which
46
+ // value arrived, otherwise the longer credential leaks its tail.
47
+ if (rest.length < longest &&
48
+ values.some((value) => value.length > rest.length && value.startsWith(rest))) {
49
+ pending = rest;
50
+ return ready.join("");
51
+ }
43
52
  const complete = values.find((value) => combined.startsWith(value, at));
44
53
  if (complete !== undefined) {
45
54
  ready.push(REDACTED);
46
55
  at += complete.length;
47
56
  continue;
48
57
  }
49
- const rest = combined.slice(at);
50
- if (rest.length < longest && values.some((value) => value.startsWith(rest))) {
51
- pending = rest;
52
- return ready.join("");
53
- }
54
58
  ready.push(combined[at]);
55
59
  at++;
56
60
  }
@@ -1,5 +1,6 @@
1
1
  // Translation between the normalized vocabulary and the Anthropic wire shape.
2
2
  // Pure functions, no I/O — which is what makes them testable without a key.
3
+ import { wireTokenCount } from "./wire-usage.js";
3
4
  export function toWireTool(tool) {
4
5
  return { name: tool.name, description: tool.description, input_schema: tool.input };
5
6
  }
@@ -70,10 +71,10 @@ function normalizeUsage(data) {
70
71
  if (usage === undefined)
71
72
  return undefined;
72
73
  return {
73
- inputTokens: usage.input_tokens ?? 0,
74
- outputTokens: usage.output_tokens ?? 0,
75
- cachedInputTokens: usage.cache_read_input_tokens ?? 0,
76
- cacheWriteInputTokens: usage.cache_creation_input_tokens ?? 0,
74
+ inputTokens: wireTokenCount(usage.input_tokens),
75
+ outputTokens: wireTokenCount(usage.output_tokens),
76
+ cachedInputTokens: wireTokenCount(usage.cache_read_input_tokens),
77
+ cacheWriteInputTokens: wireTokenCount(usage.cache_creation_input_tokens),
77
78
  reasoningTokens: 0,
78
79
  };
79
80
  }
@@ -6,6 +6,7 @@
6
6
  // Chat Completions differs on two points that matter here — a tool result is a
7
7
  // message of its own with role "tool", not a block inside a user turn, and tool
8
8
  // arguments travel as a JSON string rather than an object.
9
+ import { wireTokenCount } from "./wire-usage.js";
9
10
  export function toWireTool(tool) {
10
11
  return {
11
12
  type: "function",
@@ -83,8 +84,8 @@ function normalizeUsage(reply) {
83
84
  if (reply.usage === undefined)
84
85
  return undefined;
85
86
  return {
86
- inputTokens: reply.usage.prompt_tokens ?? 0,
87
- outputTokens: reply.usage.completion_tokens ?? 0,
87
+ inputTokens: wireTokenCount(reply.usage.prompt_tokens),
88
+ outputTokens: wireTokenCount(reply.usage.completion_tokens),
88
89
  cachedInputTokens: 0,
89
90
  cacheWriteInputTokens: 0,
90
91
  reasoningTokens: 0,
@@ -1,6 +1,7 @@
1
1
  // Translation between the normalized vocabulary and the OpenAI Responses wire
2
2
  // shape: a flat `input` list where tool calls and their outputs are top-level
3
3
  // items keyed by `call_id`, rather than blocks nested inside a message.
4
+ import { wireTokenCount } from "./wire-usage.js";
4
5
  export function toWireTool(tool) {
5
6
  return {
6
7
  type: "function",
@@ -83,11 +84,11 @@ function normalizeUsage(data) {
83
84
  if (usage === undefined || usage === null)
84
85
  return undefined;
85
86
  return {
86
- inputTokens: usage.input_tokens ?? 0,
87
- outputTokens: usage.output_tokens ?? 0,
88
- cachedInputTokens: usage.input_tokens_details?.cached_tokens ?? 0,
89
- cacheWriteInputTokens: usage.input_tokens_details?.cache_write_tokens ?? 0,
90
- reasoningTokens: usage.output_tokens_details?.reasoning_tokens ?? 0,
87
+ inputTokens: wireTokenCount(usage.input_tokens),
88
+ outputTokens: wireTokenCount(usage.output_tokens),
89
+ cachedInputTokens: wireTokenCount(usage.input_tokens_details?.cached_tokens),
90
+ cacheWriteInputTokens: wireTokenCount(usage.input_tokens_details?.cache_write_tokens),
91
+ reasoningTokens: wireTokenCount(usage.output_tokens_details?.reasoning_tokens),
91
92
  };
92
93
  }
93
94
  // Arguments arrive as a JSON string and models vary in how they escape it, so
@@ -0,0 +1,6 @@
1
+ // Usage comes from remote JSON and eventually reaches the strict session
2
+ // codec. Normalize it at the wire boundary so one malformed counter cannot
3
+ // make an otherwise valid saved session unreadable.
4
+ export function wireTokenCount(value) {
5
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
6
+ }
package/dist/tui/app.js CHANGED
@@ -40,12 +40,21 @@ export async function runApp(session, transcriptRoot, environment = {}) {
40
40
  let escapeTimer;
41
41
  let stopResize = () => { };
42
42
  let stopInput = () => { };
43
+ let failure;
43
44
  // Timers outlive the teardown they were scheduled before. Painting after the
44
45
  // terminal has been handed back would write escapes into the user's shell.
45
46
  let live = true;
46
47
  const done = new Promise((resolve) => {
47
48
  closed = resolve;
48
49
  });
50
+ const guard = (action) => {
51
+ try {
52
+ action();
53
+ }
54
+ catch (error) {
55
+ fail(error);
56
+ }
57
+ };
49
58
  const view = () => {
50
59
  const now = Date.now();
51
60
  return {
@@ -94,8 +103,9 @@ export async function runApp(session, transcriptRoot, environment = {}) {
94
103
  const render = (block) => {
95
104
  if (block !== undefined)
96
105
  transcript.invalidate(block);
97
- if (live && frameTimer === undefined)
98
- frameTimer = setTimeout(draw, FRAME_MS);
106
+ if (live && frameTimer === undefined) {
107
+ frameTimer = setTimeout(() => guard(draw), FRAME_MS);
108
+ }
99
109
  };
100
110
  const feedback = feedbackController((next) => {
101
111
  state.feedback = next;
@@ -143,6 +153,10 @@ export async function runApp(session, transcriptRoot, environment = {}) {
143
153
  terminal.leave();
144
154
  closed?.();
145
155
  }
156
+ function fail(error) {
157
+ failure ??= { error };
158
+ quit();
159
+ }
146
160
  function requestQuit() {
147
161
  const activity = state.activity;
148
162
  if (activity === undefined) {
@@ -159,7 +173,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
159
173
  const activity = begin(kind, label);
160
174
  state.activity = activity;
161
175
  state.status = label;
162
- spinTimer = setInterval(() => {
176
+ spinTimer = setInterval(() => guard(() => {
163
177
  if (!session.config.reducedMotion)
164
178
  state.spin++;
165
179
  let activeTool;
@@ -171,7 +185,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
171
185
  break;
172
186
  }
173
187
  render(activeTool);
174
- }, session.config.reducedMotion ? 1_000 : SPIN_MS);
188
+ }), session.config.reducedMotion ? 1_000 : SPIN_MS);
175
189
  render();
176
190
  return activity;
177
191
  }
@@ -253,39 +267,47 @@ export async function runApp(session, transcriptRoot, environment = {}) {
253
267
  }
254
268
  }
255
269
  }
256
- terminal.enter(session.config.reducedMotion);
257
- stopResize = terminal.onResize(() => {
258
- paint.invalidate();
259
- draw();
260
- });
261
- stopInput = terminal.onInput((chunk) => {
262
- if (escapeTimer !== undefined)
263
- clearTimeout(escapeTimer);
264
- for (const key of keys.push(chunk)) {
265
- if (!live)
266
- break;
267
- input.handle(key);
268
- }
269
- if (!live)
270
- return;
271
- escapeTimer = setTimeout(() => {
272
- escapeTimer = undefined;
270
+ try {
271
+ terminal.enter(session.config.reducedMotion);
272
+ stopResize = terminal.onResize(() => guard(() => {
273
+ paint.invalidate();
274
+ draw();
275
+ }));
276
+ stopInput = terminal.onInput((chunk) => guard(() => {
277
+ if (escapeTimer !== undefined)
278
+ clearTimeout(escapeTimer);
279
+ for (const key of keys.push(chunk)) {
280
+ if (!live)
281
+ break;
282
+ input.handle(key);
283
+ }
273
284
  if (!live)
274
285
  return;
275
- for (const key of keys.flush())
276
- input.handle(key);
277
- if (live)
278
- render();
279
- }, ESCAPE_MS);
280
- render();
281
- });
282
- draw();
283
- try {
284
- await resumeAtLaunch;
286
+ escapeTimer = setTimeout(() => guard(() => {
287
+ escapeTimer = undefined;
288
+ if (!live)
289
+ return;
290
+ for (const key of keys.flush())
291
+ input.handle(key);
292
+ if (live)
293
+ render();
294
+ }), ESCAPE_MS);
295
+ render();
296
+ }));
297
+ draw();
298
+ if (resumeAtLaunch !== undefined)
299
+ await Promise.race([resumeAtLaunch, done]);
285
300
  if (live)
286
301
  await done;
302
+ if (failure !== undefined)
303
+ throw failure.error;
287
304
  }
288
305
  finally {
289
- await session.persistence?.close();
306
+ try {
307
+ quit();
308
+ }
309
+ finally {
310
+ await session.persistence?.close();
311
+ }
290
312
  }
291
313
  }
@@ -1,6 +1,6 @@
1
1
  // One interaction model for every terminal selector.
2
2
  import { row } from "../ui/render.js";
3
- import { elide } from "../ui/width.js";
3
+ import { elide, graphemes } from "../ui/width.js";
4
4
  import { menuWindow, renderMenuRows } from "./components/menu.js";
5
5
  import { promptCursor, promptLine } from "./components/prompt.js";
6
6
  const WINDOW = 6;
@@ -33,7 +33,7 @@ export function type(picker, text) {
33
33
  export function backspace(picker) {
34
34
  if (picker.searchable !== true || (picker.query ?? "") === "")
35
35
  return picker;
36
- return withQuery(picker, Array.from(picker.query ?? "").slice(0, -1).join(""));
36
+ return withQuery(picker, graphemes(picker.query ?? "").slice(0, -1).join(""));
37
37
  }
38
38
  export function clear(picker) {
39
39
  return withQuery(picker, "");
@@ -38,5 +38,8 @@ export function terminalText(text, options = {}) {
38
38
  return safe;
39
39
  }
40
40
  function isBidiControl(code) {
41
- return (code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069);
41
+ return code === 0x061c ||
42
+ (code >= 0x200e && code <= 0x200f) ||
43
+ (code >= 0x202a && code <= 0x202e) ||
44
+ (code >= 0x2066 && code <= 0x2069);
42
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {