@giovannijecha/jecode 0.8.1 → 0.8.3
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 +19 -278
- package/dist/accounts.js +17 -13
- package/dist/batch.js +54 -6
- package/dist/context/budget.js +28 -4
- package/dist/context/compactor.js +5 -4
- package/dist/context/estimate.js +43 -1
- package/dist/context/manual.js +8 -4
- package/dist/context/policy.js +68 -18
- package/dist/controller-request.js +23 -15
- package/dist/controller.js +26 -5
- package/dist/conversation.js +94 -33
- package/dist/credential-safety.js +56 -9
- package/dist/credentials.js +32 -4
- package/dist/input-boundary.js +80 -0
- package/dist/main.js +4 -1
- package/dist/openai-oauth-callback.js +1 -1
- package/dist/openai-oauth.js +59 -15
- package/dist/process-shutdown.js +52 -0
- package/dist/providers/anthropic-stream.js +24 -20
- package/dist/providers/anthropic-wire.js +7 -2
- package/dist/providers/ollama-wire.js +7 -15
- package/dist/providers/ollama.js +27 -10
- package/dist/providers/openai-wire.js +2 -16
- package/dist/providers/tool-input.js +17 -0
- package/dist/sessions/codec.js +2 -1
- package/dist/sessions/lease.js +7 -0
- package/dist/sessions/runtime.js +11 -3
- package/dist/sessions/store.js +90 -31
- package/dist/settings.js +10 -5
- package/dist/start.js +12 -2
- package/dist/text-boundary.js +2 -0
- package/dist/tui/app-input.js +40 -5
- package/dist/tui/app-state.js +1 -0
- package/dist/tui/app-workflows.js +1 -0
- package/dist/tui/app.js +11 -3
- package/dist/tui/blocks.js +1 -3
- package/dist/tui/components/messages.js +9 -13
- package/dist/tui/components/tool.js +2 -4
- package/dist/tui/editor.js +2 -0
- package/dist/tui/keys.js +64 -5
- package/dist/tui/overlay.js +12 -4
- package/dist/tui/picker.js +2 -0
- package/dist/tui/screen.js +5 -17
- package/dist/tui/transcript-grammar.js +1 -1
- package/dist/ui/theme.js +18 -18
- package/dist/user-store.js +54 -0
- package/package.json +6 -3
- /package/{docs/assets/brand → assets}/jeco-256.png +0 -0
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="
|
|
2
|
+
<img src="assets/jeco-256.png" width="128" alt="Jeco, the steel-blue Jecode gecko">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<img src="
|
|
6
|
+
<img src="assets/wordmark-steel.svg" width="280" alt="Jecode">
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center"><strong>Your code. Your loop.</strong></p>
|
|
@@ -22,35 +22,26 @@
|
|
|
22
22
|
|
|
23
23
|
<p align="center">
|
|
24
24
|
<a href="https://github.com/giovannijecha/jecode/blob/main/CHANGELOG.md">Changelog</a> ·
|
|
25
|
-
<a href="https://github.com/giovannijecha/jecode/blob/main/docs/
|
|
25
|
+
<a href="https://github.com/giovannijecha/jecode/blob/main/docs/COMPATIBILITY.md">Compatibility</a> ·
|
|
26
26
|
<a href="https://github.com/giovannijecha/jecode/releases">Releases</a>
|
|
27
27
|
</p>
|
|
28
28
|
|
|
29
29
|
> Jecode is currently pre-1.0. The core loop is usable today, but commands and
|
|
30
30
|
> terminal interactions may still evolve before 1.0. The release-candidate
|
|
31
|
-
> surface is now frozen in the [compatibility contract](https://github.com/giovannijecha/jecode/blob/main/docs/
|
|
31
|
+
> surface is now frozen in the [compatibility contract](https://github.com/giovannijecha/jecode/blob/main/docs/COMPATIBILITY.md).
|
|
32
32
|
|
|
33
33
|
## Why Jecode
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
exits. Older model context can be compacted while the complete conversation
|
|
46
|
-
and transcript remain available in the saved session.
|
|
47
|
-
- **Multi-provider.** Use Anthropic or OpenAI API keys, an eligible ChatGPT
|
|
48
|
-
account, or a local, cloud, or remote Ollama server without changing the
|
|
49
|
-
workflow.
|
|
50
|
-
- **Lean by construction.** Jecode ships as owned JavaScript built on Node.js
|
|
51
|
-
primitives, executes no installation scripts, and has zero third-party
|
|
52
|
-
runtime dependencies. This is a permanent product constraint, not a temporary
|
|
53
|
-
optimization.
|
|
35
|
+
Jecode keeps the coding loop yours. Work stays in one terminal where you can
|
|
36
|
+
follow tool calls and diffs, steer the model while it runs, and resume the same
|
|
37
|
+
conversation later. One controller carries each turn from prompt to result—no
|
|
38
|
+
delegated agents or hidden model workers.
|
|
39
|
+
|
|
40
|
+
Choose Anthropic, OpenAI, an eligible ChatGPT account, or Ollama without
|
|
41
|
+
changing the workflow. Jecode is written in TypeScript and released as plain
|
|
42
|
+
JavaScript for Node.js, with no installation scripts and zero third-party
|
|
43
|
+
runtime dependencies. The runtime stays small enough to inspect, understand,
|
|
44
|
+
and change.
|
|
54
45
|
|
|
55
46
|
## Quick start
|
|
56
47
|
|
|
@@ -73,13 +64,7 @@ jecode --version
|
|
|
73
64
|
|
|
74
65
|
### Start Jecode
|
|
75
66
|
|
|
76
|
-
Open the project you want Jecode to work on:
|
|
77
|
-
|
|
78
|
-
```console
|
|
79
|
-
cd path/to/your/project
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Then start Jecode:
|
|
67
|
+
Open the project directory you want Jecode to work on, then start Jecode:
|
|
83
68
|
|
|
84
69
|
```console
|
|
85
70
|
jecode
|
|
@@ -118,16 +103,6 @@ jecode --help
|
|
|
118
103
|
|
|
119
104
|
Windows, Ubuntu, and macOS are covered by the project test matrix.
|
|
120
105
|
|
|
121
|
-
### Prereleases
|
|
122
|
-
|
|
123
|
-
Prereleases exist only during an announced release-candidate cycle. When one is
|
|
124
|
-
active, its GitHub release provides the exact installation command. Outside an
|
|
125
|
-
active cycle, the stable npm package is the only supported installation
|
|
126
|
-
artifact.
|
|
127
|
-
|
|
128
|
-
Git URL installs are intentionally unsupported: the source tree contains no
|
|
129
|
-
generated runtime and defines no install-time build hook.
|
|
130
|
-
|
|
131
106
|
### Uninstall
|
|
132
107
|
|
|
133
108
|
```console
|
|
@@ -138,23 +113,6 @@ Uninstalling the command preserves `~/.jecode`. Remove that directory only when
|
|
|
138
113
|
you intentionally want to erase saved settings, credentials, accounts, and
|
|
139
114
|
sessions.
|
|
140
115
|
|
|
141
|
-
### Replace a legacy installation
|
|
142
|
-
|
|
143
|
-
If an older GitHub installation still owns the `jecode` executable, first
|
|
144
|
-
remove the legacy unscoped package:
|
|
145
|
-
|
|
146
|
-
```console
|
|
147
|
-
npm uninstall --global jecode
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Then install the current scoped package:
|
|
151
|
-
|
|
152
|
-
```console
|
|
153
|
-
npm install --global @giovannijecha/jecode@latest
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Do not work around the resulting `EEXIST` error with `--force`.
|
|
157
|
-
|
|
158
116
|
### Linux and WSL
|
|
159
117
|
|
|
160
118
|
WSL uses its own Node.js installation and `PATH`; the Node.js version installed
|
|
@@ -200,228 +158,11 @@ WSL, `command -v jecode` should resolve below `/home/...`, not through an
|
|
|
200
158
|
inherited Windows path below `/mnt/c`. Do not ignore `EBADENGINE`: `node
|
|
201
159
|
--version` must report 22.18+ on the 22.x line, or 24+.
|
|
202
160
|
|
|
203
|
-
##
|
|
204
|
-
|
|
205
|
-
| Provider ID | Authentication | Notes |
|
|
206
|
-
| --- | --- | --- |
|
|
207
|
-
| `anthropic` | `ANTHROPIC_API_KEY` | Anthropic API |
|
|
208
|
-
| `openai` | `OPENAI_API_KEY` | OpenAI API |
|
|
209
|
-
| `openai-codex` | ChatGPT OAuth | Experimental; uses eligible ChatGPT Codex access |
|
|
210
|
-
| `ollama` | `OLLAMA_API_KEY` for cloud or remote use | Cloud with a key, local without one |
|
|
211
|
-
|
|
212
|
-
Choose **ChatGPT** in `/providers` to sign in on OpenAI's website without
|
|
213
|
-
pasting a key. Jecode supports a local browser callback and a device-code flow;
|
|
214
|
-
WSL and remote terminals default to the device code. Availability and usage
|
|
215
|
-
limits depend on the ChatGPT account and plan, not on OpenAI API credits. This
|
|
216
|
-
integration is experimental and is not an endorsement of Jecode by OpenAI.
|
|
217
|
-
|
|
218
|
-
Anthropic remains API-key only. Jecode does not reuse a Claude consumer
|
|
219
|
-
subscription or copy credentials from another client.
|
|
220
|
-
|
|
221
|
-
For Ollama, `/providers` can select cloud, local, or a custom endpoint. With an
|
|
222
|
-
Ollama API key Jecode defaults to `https://ollama.com`; without one it defaults
|
|
223
|
-
to `http://127.0.0.1:11434`. Remote custom endpoints must use HTTPS.
|
|
224
|
-
|
|
225
|
-
## Use the TUI
|
|
226
|
-
|
|
227
|
-
Type `/` to open searchable command completion inside the composer.
|
|
228
|
-
|
|
229
|
-
| Command | What it does |
|
|
230
|
-
| --- | --- |
|
|
231
|
-
| `/settings` | Manage the selected model and saved non-secret defaults |
|
|
232
|
-
| `/effort` | Change and save reasoning effort directly |
|
|
233
|
-
| `/providers` | Manage provider connections, API keys, ChatGPT sign-in, and Ollama endpoints |
|
|
234
|
-
| `/models` | Search all currently usable provider catalogues and select a model |
|
|
235
|
-
| `/permissions` | Change session tool access and review remembered approvals |
|
|
236
|
-
| `/timeline` | Browse resumable turns and select where the next branch should begin |
|
|
237
|
-
| `/compact` | Compact the current branch context without deleting saved conversation history |
|
|
238
|
-
| `/new` | Start a new conversation and reset session tool permissions |
|
|
239
|
-
| `/export` | Save a timestamped Markdown transcript in the launch directory |
|
|
240
|
-
| `/help` | Open a temporary keyboard reference in the composer dock |
|
|
241
|
-
| `/exit` | Restore the terminal and exit |
|
|
242
|
-
|
|
243
|
-
Useful controls:
|
|
244
|
-
|
|
245
|
-
- **Up/Down** moves through suggestions, menus, and input history.
|
|
246
|
-
- **Left/Right** moves the cursor or changes an inline value;
|
|
247
|
-
**Ctrl+Left/Right** moves by word.
|
|
248
|
-
- **Backspace/Delete** removes one character;
|
|
249
|
-
**Ctrl+Backspace/Delete** removes one word.
|
|
250
|
-
- **Home/End** moves to the beginning or end of the composer.
|
|
251
|
-
- **Tab** completes a slash command without running it; **Enter** sends. During
|
|
252
|
-
an active model turn, **Enter** queues guidance for its next safe boundary.
|
|
253
|
-
- **Alt+Enter** inserts a newline.
|
|
254
|
-
- **Esc** closes the active menu or interrupts foreground work.
|
|
255
|
-
- **Ctrl+C** interrupts, or exits while idle. **Ctrl+D** requests a clean exit.
|
|
256
|
-
- **PageUp/PageDown** and the mouse wheel scroll the transcript.
|
|
257
|
-
- **Ctrl+O** expands or compacts the latest reasoning or tool-detail block.
|
|
258
|
-
|
|
259
|
-
The footer keeps the active model, effort, and workspace visible. During work,
|
|
260
|
-
it adds the current state, elapsed time, steering availability or queue count,
|
|
261
|
-
and interrupt hint. Queued guidance joins the same conversation turn after the
|
|
262
|
-
provider response or complete tool batch already in progress; `Esc` remains an
|
|
263
|
-
immediate interruption. Operational feedback uses the same replaceable status
|
|
264
|
-
area instead of adding noise to the conversation or its Markdown export.
|
|
265
|
-
|
|
266
|
-
## Sessions and context
|
|
267
|
-
|
|
268
|
-
Interactive conversations are stored under `~/.jecode/sessions` and scoped to
|
|
269
|
-
the canonical workspace path. A fresh or `/new` conversation is not added to
|
|
270
|
-
the resume picker until it has a settled turn. Resuming and continuing a
|
|
271
|
-
conversation keeps its durable session identity and updates one picker entry
|
|
272
|
-
instead of creating duplicates.
|
|
273
|
-
|
|
274
|
-
`/timeline` shows completed, failed, and interrupted turns in the conversation
|
|
275
|
-
tree. Selecting an older turn changes only the in-memory path: it creates and
|
|
276
|
-
saves a branch only after the next real user message. Cancelling the picker or
|
|
277
|
-
exiting before that message leaves the durable head unchanged. A failed turn
|
|
278
|
-
keeps the same partial evidence and outcome in the live transcript, export, and
|
|
279
|
-
resume, while the next model receives a neutral failure boundary instead of
|
|
280
|
-
incomplete streamed text. Historical tools are displayed but never executed.
|
|
281
|
-
If a process stops abruptly inside a tool loop, Jecode resumes from the latest
|
|
282
|
-
safe ancestor and lets the next user turn create a branch. `/export` writes
|
|
283
|
-
only the currently selected path.
|
|
284
|
-
|
|
285
|
-
When model-facing context approaches the selected model's usable capacity,
|
|
286
|
-
Jecode asks the provider for a bounded summary of the older prefix and keeps
|
|
287
|
-
recent turns exact. The default trigger is 85% and can be changed from 50% to
|
|
288
|
-
95%. Live provider metadata or Ollama's allocated context determines the budget
|
|
289
|
-
when available; provider safety limits always win.
|
|
290
|
-
|
|
291
|
-
Compaction changes only the projection sent to the model. Complete messages,
|
|
292
|
-
tool evidence, transcript, export, and conversation tree remain intact. The
|
|
293
|
-
branch-local summary anchor is saved with the session so resume does not repeat
|
|
294
|
-
the same compaction. `/compact` requests this process immediately, even below
|
|
295
|
-
the automatic threshold; it leaves very small contexts unchanged. After
|
|
296
|
-
selecting a historical turn, send the first new message before compacting so
|
|
297
|
-
shared history is never rewritten.
|
|
298
|
-
|
|
299
|
-
## Batch mode
|
|
300
|
-
|
|
301
|
-
When stdin or stdout is not a terminal, Jecode switches to a plain
|
|
302
|
-
line-oriented mode:
|
|
303
|
-
|
|
304
|
-
```console
|
|
305
|
-
printf "explain this project\n" | jecode --root .
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
Batch conversations are stateless. Dangerous tools remain denied unless
|
|
309
|
-
`--auto-approve` is supplied explicitly. Terminal failures are written to
|
|
310
|
-
stderr and exit non-zero so scripts and CI pipelines can stop reliably.
|
|
311
|
-
|
|
312
|
-
## Configuration
|
|
313
|
-
|
|
314
|
-
Startup precedence is: command-line flags, environment variables, saved
|
|
315
|
-
settings, then built-in defaults.
|
|
316
|
-
|
|
317
|
-
| Flag | Environment | Default |
|
|
318
|
-
| --- | --- | --- |
|
|
319
|
-
| `--provider` | `JECODE_PROVIDER` | `anthropic` |
|
|
320
|
-
| `--model` | `JECODE_MODEL` | Provider default or interactive selection |
|
|
321
|
-
| `--ollama-host` | `OLLAMA_HOST` | Cloud with an Ollama key, local without one |
|
|
322
|
-
| `--root` | - | Current directory |
|
|
323
|
-
| `--effort` | `JECODE_EFFORT` | `high` |
|
|
324
|
-
| `--max-tokens` | `JECODE_MAX_TOKENS` | `64000` ceiling, clamped to the usable request budget; not sent by `openai-codex` |
|
|
325
|
-
| `--max-steps` | `JECODE_MAX_STEPS` | No limit |
|
|
326
|
-
| `--compaction-percent` | `JECODE_COMPACTION_PERCENT` | `85`; accepts `50` through `95` |
|
|
327
|
-
| `--reduced-motion` | `JECODE_REDUCED_MOTION=1` | Off |
|
|
328
|
-
| `--auto-approve` | `JECODE_AUTO_APPROVE=1` | Off |
|
|
329
|
-
| `--ephemeral` | `JECODE_EPHEMERAL=1` | Off |
|
|
330
|
-
|
|
331
|
-
`--max-steps` is an opt-in, process-only budget for deterministic automation
|
|
332
|
-
and diagnostics. Interactive work has no arbitrary model-loop ceiling, and the
|
|
333
|
-
budget is neither shown nor saved by `/settings`.
|
|
334
|
-
|
|
335
|
-
Non-secret preferences live in `~/.jecode/settings.json`. Explicitly saved API
|
|
336
|
-
keys live in `~/.jecode/credentials.json`, while ChatGPT OAuth accounts live in
|
|
337
|
-
`~/.jecode/accounts.json`. Environment credentials always take precedence.
|
|
338
|
-
Secret stores use owner-only permissions where the operating system supports
|
|
339
|
-
them.
|
|
340
|
-
|
|
341
|
-
Jecode has one current interface theme, Slate. `NO_COLOR` is supported for
|
|
342
|
-
terminals and pipelines that disable colour.
|
|
343
|
-
|
|
344
|
-
## Safety model
|
|
345
|
-
|
|
346
|
-
Jecode treats model output, workspace content, tool output, and terminal text as
|
|
347
|
-
untrusted data.
|
|
348
|
-
|
|
349
|
-
- Current filesystem tools are confined to the selected workspace. Writes
|
|
350
|
-
reject symlink and junction components, revalidate boundaries and the
|
|
351
|
-
approved file state immediately before atomic replacement.
|
|
352
|
-
- Dangerous tools ask by default unless explicitly allowed for the session or
|
|
353
|
-
the process starts with `--auto-approve`.
|
|
354
|
-
- Credential fields are masked and excluded from transcripts. Recognized
|
|
355
|
-
credential values are redacted before output reaches the model, screen,
|
|
356
|
-
history, or export.
|
|
357
|
-
- Approved shell commands receive no secret-bearing environment variables.
|
|
358
|
-
`SSH_AUTH_SOCK` is preserved so Git and SSH can use the user's agent, which
|
|
359
|
-
means an approved command may ask that agent to authenticate.
|
|
360
|
-
- ChatGPT OAuth uses PKCE and an exact loopback callback or the OpenAI device
|
|
361
|
-
flow. Refresh-token rotation is serialized across Jecode processes.
|
|
362
|
-
- Terminal control characters are neutralized before rendering.
|
|
363
|
-
- Remote Ollama endpoints require HTTPS, and provider redirects are rejected.
|
|
364
|
-
- Provider handshakes and idle response bodies have finite deadlines. Only
|
|
365
|
-
idempotent catalogue reads retry; generation requests are never replayed.
|
|
366
|
-
- Model, terminal, and filesystem input are bounded before use.
|
|
367
|
-
- Session files are versioned, symmetrically size-bounded before write and
|
|
368
|
-
after read, atomically checkpointed, and treated as untrusted when loaded. A
|
|
369
|
-
live lease prevents concurrent resume.
|
|
370
|
-
|
|
371
|
-
`run_command` is not an operating-system sandbox. An approved command can still
|
|
372
|
-
access files and account resources available to the current user. Review
|
|
373
|
-
commands carefully and reserve `--auto-approve` for controlled environments.
|
|
374
|
-
|
|
375
|
-
Read [SECURITY.md](SECURITY.md) before reporting a vulnerability.
|
|
376
|
-
|
|
377
|
-
## Project direction
|
|
378
|
-
|
|
379
|
-
The terminal is Jecode's current primary interface, not the limit of the
|
|
380
|
-
product. Future interfaces may reuse the same controller, session model, and
|
|
381
|
-
visible control system. Expansion must remain deliberate: no hidden model
|
|
382
|
-
hierarchy, no delegated authority, and no weakening of the zero-dependency
|
|
383
|
-
runtime.
|
|
384
|
-
|
|
385
|
-
The single-controller rule limits delegation, not duration or scope. Jecode may
|
|
386
|
-
eventually supervise visible, interruptible processes such as development
|
|
387
|
-
servers, file watchers, and test runners. Those processes remain tools owned by
|
|
388
|
-
the controller; they do not receive independent goals, model loops, or tool
|
|
389
|
-
authority.
|
|
390
|
-
|
|
391
|
-
The selected workspace is the current default filesystem boundary. Any future
|
|
392
|
-
access to additional directories or resources must use explicit, reviewable,
|
|
393
|
-
revocable grants rather than silently widening that boundary.
|
|
394
|
-
|
|
395
|
-
## Build from source
|
|
396
|
-
|
|
397
|
-
```console
|
|
398
|
-
git clone https://github.com/giovannijecha/jecode.git
|
|
399
|
-
cd jecode
|
|
400
|
-
npm ci --ignore-scripts
|
|
401
|
-
npm run build:release
|
|
402
|
-
npm link
|
|
403
|
-
jecode --version
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
Development runs TypeScript directly with `npm run start`. `dist/` is an
|
|
407
|
-
ignored generated tree used only by linked commands and release tarballs.
|
|
408
|
-
`npm run pack:release` rebuilds it from a clean target. Installing the published
|
|
409
|
-
package runs no compilation or installation scripts.
|
|
410
|
-
|
|
411
|
-
Run the complete project checks with:
|
|
412
|
-
|
|
413
|
-
```console
|
|
414
|
-
npm run check
|
|
415
|
-
```
|
|
161
|
+
## Documentation
|
|
416
162
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
security boundaries are documented in
|
|
421
|
-
[docs/architecture.md](docs/architecture.md); brand assets and usage rules live
|
|
422
|
-
in [docs/brand.md](docs/brand.md). The public 1.0 contract lives in
|
|
423
|
-
[docs/compatibility.md](docs/compatibility.md), and the maintainer release
|
|
424
|
-
procedure lives in [docs/releasing.md](docs/releasing.md).
|
|
163
|
+
The [user guide](https://github.com/giovannijecha/jecode/blob/main/docs/USAGE.md)
|
|
164
|
+
covers provider access, TUI controls, sessions and compaction, batch mode,
|
|
165
|
+
configuration, and safety boundaries.
|
|
425
166
|
|
|
426
167
|
## Community
|
|
427
168
|
|
package/dist/accounts.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// OAuth accounts persisted under ~/.jecode, apart from API keys.
|
|
2
2
|
import { chmod, mkdir } from "node:fs/promises";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
3
|
import * as path from "node:path";
|
|
5
4
|
import { atomicWrite } from "./atomic.js";
|
|
6
5
|
import { withStoreLock } from "./store-lock.js";
|
|
7
6
|
import { userDataLabel, userDataPath } from "./user-data.js";
|
|
7
|
+
import { assertStoreText, readBoundedJsonSync, USER_STORE_LIMITS } from "./user-store.js";
|
|
8
8
|
let cached;
|
|
9
9
|
export function openAICodexAccount() {
|
|
10
10
|
const account = store().accounts["openai-codex"];
|
|
@@ -39,15 +39,20 @@ export async function updateOpenAICodexAccount(change, signal) {
|
|
|
39
39
|
return withStoreLock(file, async () => {
|
|
40
40
|
const current = readStore(file);
|
|
41
41
|
const next = await change(current.accounts["openai-codex"]);
|
|
42
|
+
const normalized = next === undefined ? undefined : normalizeOpenAI(next);
|
|
43
|
+
if (next !== undefined && normalized === undefined)
|
|
44
|
+
throw new Error("invalid OpenAI account");
|
|
42
45
|
const accounts = { ...current.accounts };
|
|
43
46
|
if (next === undefined)
|
|
44
47
|
delete accounts["openai-codex"];
|
|
45
48
|
else
|
|
46
|
-
accounts["openai-codex"] =
|
|
49
|
+
accounts["openai-codex"] = normalized;
|
|
47
50
|
const updated = { version: 1, accounts };
|
|
48
|
-
|
|
51
|
+
const text = `${JSON.stringify(updated, null, 2)}\n`;
|
|
52
|
+
assertStoreText(text, USER_STORE_LIMITS.accountsBytes);
|
|
53
|
+
await atomicWrite(file, text, { mode: 0o600 });
|
|
49
54
|
cached = updated;
|
|
50
|
-
return
|
|
55
|
+
return normalized === undefined ? undefined : { ...normalized };
|
|
51
56
|
}, signal);
|
|
52
57
|
}
|
|
53
58
|
export function reloadAccounts() {
|
|
@@ -60,8 +65,7 @@ function store() {
|
|
|
60
65
|
}
|
|
61
66
|
function readStore(file) {
|
|
62
67
|
try {
|
|
63
|
-
|
|
64
|
-
return normalize(parsed);
|
|
68
|
+
return normalize(readBoundedJsonSync(file, USER_STORE_LIMITS.accountsBytes));
|
|
65
69
|
}
|
|
66
70
|
catch {
|
|
67
71
|
return { version: 1, accounts: {} };
|
|
@@ -80,9 +84,9 @@ function normalize(value) {
|
|
|
80
84
|
function normalizeOpenAI(value) {
|
|
81
85
|
if (!record(value))
|
|
82
86
|
return undefined;
|
|
83
|
-
const accessToken = nonempty(value["accessToken"]);
|
|
84
|
-
const refreshToken = nonempty(value["refreshToken"]);
|
|
85
|
-
const accountId = nonempty(value["accountId"]);
|
|
87
|
+
const accessToken = nonempty(value["accessToken"], USER_STORE_LIMITS.accountToken);
|
|
88
|
+
const refreshToken = nonempty(value["refreshToken"], USER_STORE_LIMITS.accountToken);
|
|
89
|
+
const accountId = nonempty(value["accountId"], USER_STORE_LIMITS.accountLabel);
|
|
86
90
|
const expiresAt = value["expiresAt"];
|
|
87
91
|
if (accessToken === undefined ||
|
|
88
92
|
refreshToken === undefined ||
|
|
@@ -91,8 +95,8 @@ function normalizeOpenAI(value) {
|
|
|
91
95
|
!Number.isSafeInteger(expiresAt) ||
|
|
92
96
|
expiresAt <= 0)
|
|
93
97
|
return undefined;
|
|
94
|
-
const email = nonempty(value["email"]);
|
|
95
|
-
const plan = nonempty(value["plan"]);
|
|
98
|
+
const email = nonempty(value["email"], USER_STORE_LIMITS.accountLabel);
|
|
99
|
+
const plan = nonempty(value["plan"], USER_STORE_LIMITS.accountLabel);
|
|
96
100
|
return {
|
|
97
101
|
accessToken,
|
|
98
102
|
refreshToken,
|
|
@@ -102,8 +106,8 @@ function normalizeOpenAI(value) {
|
|
|
102
106
|
...(plan === undefined ? {} : { plan }),
|
|
103
107
|
};
|
|
104
108
|
}
|
|
105
|
-
function nonempty(value) {
|
|
106
|
-
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
109
|
+
function nonempty(value, max) {
|
|
110
|
+
return typeof value === "string" && value.length <= max && value.trim() !== "" ? value : undefined;
|
|
107
111
|
}
|
|
108
112
|
function record(value) {
|
|
109
113
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
package/dist/batch.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
//
|
|
3
3
|
// It exists so the agent can be scripted and tested. It shares the controller,
|
|
4
4
|
// the tools and the block renderer with the TUI — only the surface differs.
|
|
5
|
-
import * as readline from "node:readline/promises";
|
|
6
5
|
import { stdin, stdout } from "node:process";
|
|
7
6
|
import { runTurn } from "./controller.js";
|
|
8
7
|
import { resolveContextPolicy } from "./context/capacity.js";
|
|
@@ -14,24 +13,30 @@ import { renderBatch } from "./batch-view.js";
|
|
|
14
13
|
import { columns } from "./ui/render.js";
|
|
15
14
|
import { terminalText } from "./ui/terminal-text.js";
|
|
16
15
|
import { recordAuxiliaryUsage, recordRequestInput, recordUsage } from "./usage.js";
|
|
16
|
+
import { assertPromptLength, boundedInputLines } from "./input-boundary.js";
|
|
17
17
|
export async function runBatch(session, environment = {}) {
|
|
18
|
-
const rl = environment.lines === undefined ? readline.createInterface({ input: stdin }) : undefined;
|
|
19
|
-
const lines = environment.lines ?? rl;
|
|
20
18
|
const write = environment.write ?? ((text) => stdout.write(text));
|
|
21
19
|
const width = environment.width ?? columns();
|
|
20
|
+
const signal = environment.signal;
|
|
21
|
+
const source = environment.lines ?? boundedInputLines(stdin);
|
|
22
|
+
const lines = abortableLines(source, signal);
|
|
22
23
|
const emit = (block) => {
|
|
23
24
|
for (const line of renderBatch(block, width, session.palette))
|
|
24
25
|
write(`${line}\n`);
|
|
25
26
|
};
|
|
26
27
|
try {
|
|
28
|
+
throwIfAborted(signal);
|
|
27
29
|
for await (const raw of lines) {
|
|
30
|
+
throwIfAborted(signal);
|
|
31
|
+
assertPromptLength(raw.length);
|
|
28
32
|
const line = raw.trim();
|
|
29
33
|
if (line === "")
|
|
30
34
|
continue;
|
|
31
35
|
if (line.startsWith("/")) {
|
|
32
36
|
if ((await handleCommand(line, session, {
|
|
33
37
|
emit,
|
|
34
|
-
|
|
38
|
+
signal,
|
|
39
|
+
compact: () => compactSession(session, { signal }),
|
|
35
40
|
})) === "exit")
|
|
36
41
|
break;
|
|
37
42
|
continue;
|
|
@@ -87,6 +92,7 @@ export async function runBatch(session, environment = {}) {
|
|
|
87
92
|
nodeId: nodeId ?? prospectiveNodeId,
|
|
88
93
|
coveredMessages: context?.messageCount ?? 0,
|
|
89
94
|
lastInputTokens: Math.max(session.usage.lastInputTokens, request.inputTokens),
|
|
95
|
+
estimatedInputTokens: request.inputTokens,
|
|
90
96
|
force,
|
|
91
97
|
policy: request.policy,
|
|
92
98
|
});
|
|
@@ -109,12 +115,54 @@ export async function runBatch(session, environment = {}) {
|
|
|
109
115
|
commit(checkpoint, settlement);
|
|
110
116
|
return compacted;
|
|
111
117
|
};
|
|
112
|
-
await runTurn(history, options(session, policy), turn,
|
|
118
|
+
await runTurn(history, options(session, policy), turn, signal, modelHistory);
|
|
113
119
|
turn.flush();
|
|
114
120
|
}
|
|
121
|
+
throwIfAborted(signal);
|
|
115
122
|
}
|
|
116
123
|
finally {
|
|
117
|
-
|
|
124
|
+
if (environment.lines === undefined)
|
|
125
|
+
stdin.pause();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function throwIfAborted(signal) {
|
|
129
|
+
if (signal?.aborted === true)
|
|
130
|
+
throw signal.reason;
|
|
131
|
+
}
|
|
132
|
+
async function* abortableLines(source, signal) {
|
|
133
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
134
|
+
let exhausted = false;
|
|
135
|
+
try {
|
|
136
|
+
while (true) {
|
|
137
|
+
throwIfAborted(signal);
|
|
138
|
+
const next = signal === undefined
|
|
139
|
+
? await iterator.next()
|
|
140
|
+
: await new Promise((resolve, reject) => {
|
|
141
|
+
const abort = () => reject(signal.reason);
|
|
142
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
143
|
+
iterator.next().then((result) => {
|
|
144
|
+
signal.removeEventListener("abort", abort);
|
|
145
|
+
resolve(result);
|
|
146
|
+
}, (error) => {
|
|
147
|
+
signal.removeEventListener("abort", abort);
|
|
148
|
+
reject(error);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
if (next.done === true) {
|
|
152
|
+
exhausted = true;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
yield next.value;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
if (!exhausted && iterator.return !== undefined) {
|
|
160
|
+
const closing = iterator.return();
|
|
161
|
+
if (signal?.aborted === true)
|
|
162
|
+
void closing.catch(() => { });
|
|
163
|
+
else
|
|
164
|
+
await closing;
|
|
165
|
+
}
|
|
118
166
|
}
|
|
119
167
|
}
|
|
120
168
|
function options(session, contextPolicy) {
|
package/dist/context/budget.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Conservative provider-neutral budgeting for one complete model request.
|
|
2
|
-
import { estimateSerializedTokens } from "./estimate.js";
|
|
2
|
+
import { estimateSerializedTokens, estimateSerializedTokensResponsive, } from "./estimate.js";
|
|
3
3
|
import { MIN_REQUEST_OUTPUT_TOKENS } from "./policy.js";
|
|
4
4
|
const ENVELOPE_OVERHEAD_TOKENS = 64;
|
|
5
5
|
const MESSAGE_OVERHEAD_TOKENS = 8;
|
|
@@ -16,12 +16,31 @@ export function estimateRequestInputTokens(envelope) {
|
|
|
16
16
|
envelope.messages.length * MESSAGE_OVERHEAD_TOKENS +
|
|
17
17
|
envelope.tools.length * TOOL_OVERHEAD_TOKENS;
|
|
18
18
|
}
|
|
19
|
+
/** Preserve the request estimate while yielding during large local inputs. */
|
|
20
|
+
export async function estimateRequestInputTokensResponsive(envelope, signal) {
|
|
21
|
+
const contentTokens = await estimateSerializedTokensResponsive({
|
|
22
|
+
system: envelope.system,
|
|
23
|
+
messages: envelope.messages,
|
|
24
|
+
tools: envelope.tools,
|
|
25
|
+
}, signal);
|
|
26
|
+
return contentTokens +
|
|
27
|
+
ENVELOPE_OVERHEAD_TOKENS +
|
|
28
|
+
envelope.messages.length * MESSAGE_OVERHEAD_TOKENS +
|
|
29
|
+
envelope.tools.length * TOOL_OVERHEAD_TOKENS;
|
|
30
|
+
}
|
|
19
31
|
/** Clamp the configured output ceiling so the complete request remains usable. */
|
|
20
32
|
export function budgetRequest(envelope, configuredMaxOutputTokens, policy) {
|
|
21
|
-
|
|
22
|
-
throw new Error("max output tokens must be a positive safe integer");
|
|
23
|
-
}
|
|
33
|
+
requirePositiveInteger(configuredMaxOutputTokens, "max output tokens");
|
|
24
34
|
const inputTokens = estimateRequestInputTokens(envelope);
|
|
35
|
+
return finishBudget(inputTokens, configuredMaxOutputTokens, policy);
|
|
36
|
+
}
|
|
37
|
+
/** Reuse an exact estimate already computed for the same request envelope. */
|
|
38
|
+
export function budgetRequestFromInputTokens(inputTokens, configuredMaxOutputTokens, policy) {
|
|
39
|
+
requirePositiveInteger(inputTokens, "request input tokens");
|
|
40
|
+
requirePositiveInteger(configuredMaxOutputTokens, "max output tokens");
|
|
41
|
+
return finishBudget(inputTokens, configuredMaxOutputTokens, policy);
|
|
42
|
+
}
|
|
43
|
+
function finishBudget(inputTokens, configuredMaxOutputTokens, policy) {
|
|
25
44
|
const available = policy.requestLimitTokens - inputTokens;
|
|
26
45
|
const minimum = Math.min(configuredMaxOutputTokens, MIN_REQUEST_OUTPUT_TOKENS);
|
|
27
46
|
if (available < minimum) {
|
|
@@ -35,3 +54,8 @@ export function budgetRequest(envelope, configuredMaxOutputTokens, policy) {
|
|
|
35
54
|
limitTokens: policy.requestLimitTokens,
|
|
36
55
|
});
|
|
37
56
|
}
|
|
57
|
+
function requirePositiveInteger(value, label) {
|
|
58
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
59
|
+
throw new Error(`${label} must be a positive safe integer`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// One bounded provider request that condenses an older context prefix.
|
|
2
|
-
import {
|
|
2
|
+
import { budgetRequestFromInputTokens, estimateRequestInputTokensResponsive, } from "./budget.js";
|
|
3
3
|
import { CONTEXT_LIMITS, summaryMessage } from "./projection.js";
|
|
4
4
|
import { planCompaction } from "./policy.js";
|
|
5
5
|
const SUMMARY_SYSTEM = [
|
|
@@ -13,17 +13,18 @@ const SUMMARY_SYSTEM = [
|
|
|
13
13
|
].join("\n");
|
|
14
14
|
export async function compactContext(options) {
|
|
15
15
|
const policy = options.policy;
|
|
16
|
-
const plan = planCompaction(options.context, options.turn, options.coveredMessages, options.lastInputTokens, options.force ?? false, policy);
|
|
16
|
+
const plan = options.precomputedPlan ?? await planCompaction(options.context, options.turn, options.coveredMessages, options.lastInputTokens, options.force ?? false, policy, options.estimatedInputTokens, options.signal);
|
|
17
17
|
if (plan === undefined)
|
|
18
18
|
return undefined;
|
|
19
19
|
options.onBegin?.();
|
|
20
20
|
try {
|
|
21
21
|
const messages = normalized(plan.prefix);
|
|
22
|
-
const
|
|
22
|
+
const inputTokens = await estimateRequestInputTokensResponsive({
|
|
23
23
|
system: SUMMARY_SYSTEM,
|
|
24
24
|
messages,
|
|
25
25
|
tools: [],
|
|
26
|
-
},
|
|
26
|
+
}, options.signal);
|
|
27
|
+
const budget = budgetRequestFromInputTokens(inputTokens, policy.summaryMaxTokens, policy);
|
|
27
28
|
const response = await options.provider.send({
|
|
28
29
|
model: options.model,
|
|
29
30
|
system: SUMMARY_SYSTEM,
|