@giovannijecha/jecode 0.1.5-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/bin/jecode.js +11 -0
- package/dist/atomic.js +78 -0
- package/dist/batch-view.js +17 -0
- package/dist/batch.js +100 -0
- package/dist/cli-info.js +40 -0
- package/dist/commands.js +171 -0
- package/dist/config.js +97 -0
- package/dist/controller.js +90 -0
- package/dist/credential-commands.js +134 -0
- package/dist/credential-safety.js +86 -0
- package/dist/credentials.js +124 -0
- package/dist/main.js +7 -0
- package/dist/ollama-settings-command.js +75 -0
- package/dist/prompt.js +29 -0
- package/dist/provider-commands.js +236 -0
- package/dist/provider-errors.js +10 -0
- package/dist/providers/anthropic-stream.js +111 -0
- package/dist/providers/anthropic-wire.js +79 -0
- package/dist/providers/anthropic.js +77 -0
- package/dist/providers/catalog.js +37 -0
- package/dist/providers/http.js +219 -0
- package/dist/providers/index.js +18 -0
- package/dist/providers/ollama-endpoint.js +40 -0
- package/dist/providers/ollama-stream.js +60 -0
- package/dist/providers/ollama-wire.js +95 -0
- package/dist/providers/ollama.js +87 -0
- package/dist/providers/openai-stream.js +48 -0
- package/dist/providers/openai-wire.js +112 -0
- package/dist/providers/openai.js +70 -0
- package/dist/providers/sse.js +81 -0
- package/dist/providers/stream-limits.js +11 -0
- package/dist/session.js +3 -0
- package/dist/settings-command.js +202 -0
- package/dist/settings.js +98 -0
- package/dist/start.js +47 -0
- package/dist/tools/args.js +38 -0
- package/dist/tools/fs.js +277 -0
- package/dist/tools/index.js +39 -0
- package/dist/tools/paths.js +122 -0
- package/dist/tools/search.js +213 -0
- package/dist/tools/shell.js +139 -0
- package/dist/tools/text-boundary.js +102 -0
- package/dist/tools/types.js +1 -0
- package/dist/transcript-export.js +11 -0
- package/dist/transcript.js +49 -0
- package/dist/tui/activity.js +11 -0
- package/dist/tui/app-input.js +155 -0
- package/dist/tui/app-state.js +17 -0
- package/dist/tui/app-workflows.js +105 -0
- package/dist/tui/app.js +215 -0
- package/dist/tui/approve.js +67 -0
- package/dist/tui/blocks.js +23 -0
- package/dist/tui/complete.js +54 -0
- package/dist/tui/components/command-menu.js +17 -0
- package/dist/tui/components/composer.js +47 -0
- package/dist/tui/components/dock.js +10 -0
- package/dist/tui/components/footer.js +21 -0
- package/dist/tui/components/menu.js +38 -0
- package/dist/tui/components/messages.js +43 -0
- package/dist/tui/components/misc.js +22 -0
- package/dist/tui/components/status.js +45 -0
- package/dist/tui/components/tool.js +85 -0
- package/dist/tui/components/types.js +1 -0
- package/dist/tui/editor.js +105 -0
- package/dist/tui/feedback.js +74 -0
- package/dist/tui/field.js +60 -0
- package/dist/tui/frame.js +33 -0
- package/dist/tui/input.js +52 -0
- package/dist/tui/keys.js +177 -0
- package/dist/tui/modal.js +24 -0
- package/dist/tui/overlay.js +93 -0
- package/dist/tui/picker.js +99 -0
- package/dist/tui/screen.js +94 -0
- package/dist/tui/scroll.js +7 -0
- package/dist/tui/session-view.js +49 -0
- package/dist/tui/transcript-view.js +134 -0
- package/dist/tui/turn.js +255 -0
- package/dist/tui/view.js +88 -0
- package/dist/tui/workspace.js +59 -0
- package/dist/types.js +9 -0
- package/dist/ui/diff.js +109 -0
- package/dist/ui/highlight.js +158 -0
- package/dist/ui/inline.js +39 -0
- package/dist/ui/markdown.js +147 -0
- package/dist/ui/render.js +232 -0
- package/dist/ui/table.js +127 -0
- package/dist/ui/terminal-text.js +42 -0
- package/dist/ui/theme.js +25 -0
- package/dist/ui/width.js +196 -0
- package/dist/usage.js +30 -0
- package/dist/user-data.js +29 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Giovanni Jecha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/brand/jeco-256.png" width="128" alt="Jeco, the steel-blue Jecode gecko">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<picture>
|
|
7
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/brand/wordmark-light.svg">
|
|
8
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/assets/brand/wordmark-dark.svg">
|
|
9
|
+
<img src="docs/assets/brand/wordmark-dark.svg" width="280" alt="Jecode">
|
|
10
|
+
</picture>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center"><strong>Your code. Your loop.</strong></p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
A focused coding agent that lives in your terminal, keeps tool use visible,
|
|
17
|
+
and stays under your control.
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="https://github.com/giovannijecha/jecode/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/giovannijecha/jecode/actions/workflows/ci.yml/badge.svg"></a>
|
|
22
|
+
<a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-669BD2"></a>
|
|
23
|
+
<img alt="Node.js 24+" src="https://img.shields.io/badge/node-%3E%3D24-86CB92">
|
|
24
|
+
<img alt="Runtime dependencies: zero" src="https://img.shields.io/badge/runtime_dependencies-0-8DB4DD">
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
> Jecode is an early 0.1.x release. The core loop is usable today; commands and
|
|
28
|
+
> terminal interactions may still evolve before 1.0.
|
|
29
|
+
|
|
30
|
+
## Why Jecode
|
|
31
|
+
|
|
32
|
+
- **One controller.** One visible loop talks to the model, runs tools, and returns
|
|
33
|
+
control to you. There are no hidden workers or delegated agents.
|
|
34
|
+
- **Terminal-native.** The transcript, composer, searchable menus, tool output,
|
|
35
|
+
diffs, approvals, reasoning, and status all share one full-screen TUI.
|
|
36
|
+
- **Permission-aware.** Reads stay transparent; dangerous actions ask first.
|
|
37
|
+
Session approvals can be reviewed and revoked.
|
|
38
|
+
- **Provider-neutral.** Use Anthropic, OpenAI, or a local/remote Ollama server
|
|
39
|
+
without changing the workflow.
|
|
40
|
+
- **Lean by construction.** Jecode installs as plain JavaScript, runs on
|
|
41
|
+
Node.js 24, executes no installation scripts, and has zero third-party
|
|
42
|
+
runtime dependencies.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
Jecode requires **Node.js 24 or newer** and npm. Release candidates are
|
|
47
|
+
published on the **next** channel:
|
|
48
|
+
|
|
49
|
+
~~~console
|
|
50
|
+
npm install --global @giovannijecha/jecode@next
|
|
51
|
+
jecode --version
|
|
52
|
+
~~~
|
|
53
|
+
|
|
54
|
+
Then open the project you want to work on and run Jecode:
|
|
55
|
+
|
|
56
|
+
~~~console
|
|
57
|
+
cd path/to/your/project
|
|
58
|
+
jecode
|
|
59
|
+
~~~
|
|
60
|
+
|
|
61
|
+
You can point at another workspace explicitly:
|
|
62
|
+
|
|
63
|
+
~~~console
|
|
64
|
+
jecode --root path/to/your/project
|
|
65
|
+
~~~
|
|
66
|
+
|
|
67
|
+
Run **jecode --help** for all startup options. Tested platforms are Windows,
|
|
68
|
+
Ubuntu, and macOS.
|
|
69
|
+
|
|
70
|
+
### Update
|
|
71
|
+
|
|
72
|
+
Install the current release candidate over the existing global command:
|
|
73
|
+
|
|
74
|
+
~~~console
|
|
75
|
+
npm install --global @giovannijecha/jecode@next
|
|
76
|
+
jecode --version
|
|
77
|
+
~~~
|
|
78
|
+
|
|
79
|
+
### Uninstall
|
|
80
|
+
|
|
81
|
+
~~~console
|
|
82
|
+
npm uninstall --global @giovannijecha/jecode
|
|
83
|
+
~~~
|
|
84
|
+
|
|
85
|
+
Uninstalling the command preserves **~/.jecode** so saved settings and
|
|
86
|
+
credentials remain available after a reinstall. Remove that directory only
|
|
87
|
+
when you intentionally want to erase Jecode's local data.
|
|
88
|
+
|
|
89
|
+
### Linux and WSL
|
|
90
|
+
|
|
91
|
+
WSL has its own Node.js installation and `PATH`; the Node.js version installed
|
|
92
|
+
on Windows does not apply inside it. If installation succeeds but `jecode` is
|
|
93
|
+
not found, run **npm config get prefix** and ensure its **bin/** directory is on
|
|
94
|
+
your Linux `PATH`. Do not ignore an `EBADENGINE` warning: **node --version** must
|
|
95
|
+
report 24 or newer.
|
|
96
|
+
|
|
97
|
+
### Build from source
|
|
98
|
+
|
|
99
|
+
~~~console
|
|
100
|
+
git clone https://github.com/giovannijecha/jecode.git
|
|
101
|
+
cd jecode
|
|
102
|
+
npm ci
|
|
103
|
+
npm run build:release
|
|
104
|
+
npm link
|
|
105
|
+
jecode
|
|
106
|
+
~~~
|
|
107
|
+
|
|
108
|
+
Development runs TypeScript directly with **npm run start**. **dist/** is an
|
|
109
|
+
ignored, generated tree used only by linked commands and release tarballs.
|
|
110
|
+
**npm pack** and **npm publish** rebuild it from a clean target; installing the
|
|
111
|
+
published package runs no compilation or installation scripts.
|
|
112
|
+
|
|
113
|
+
## First session
|
|
114
|
+
|
|
115
|
+
Jecode opens on an empty composer instead of forcing a setup wizard. Type
|
|
116
|
+
**/settings** when you are ready to choose a provider, select a model, and add a
|
|
117
|
+
credential. A credential can remain in memory for the current session or be
|
|
118
|
+
saved explicitly under **~/.jecode**; it is never stored in the workspace.
|
|
119
|
+
|
|
120
|
+
| Provider | Credential | Notes |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| Anthropic | ANTHROPIC_API_KEY | Cloud |
|
|
123
|
+
| OpenAI | OPENAI_API_KEY | Cloud |
|
|
124
|
+
| Ollama | OLLAMA_API_KEY for Cloud/remote | Cloud with a key, local without one |
|
|
125
|
+
|
|
126
|
+
Choose **cloud**, **local**, or a custom endpoint from the Ollama connection row
|
|
127
|
+
in **/settings**. Existing users with an Ollama API key automatically use
|
|
128
|
+
**https://ollama.com**; without a key, Jecode uses the local daemon at
|
|
129
|
+
**http://127.0.0.1:11434**. Remote custom endpoints must use HTTPS.
|
|
130
|
+
|
|
131
|
+
## Use the TUI
|
|
132
|
+
|
|
133
|
+
Type **/** to open searchable command completion inside the composer.
|
|
134
|
+
|
|
135
|
+
| Command | What it does |
|
|
136
|
+
|---|---|
|
|
137
|
+
| /settings | Manage provider, connection, model, limits, motion, and credentials |
|
|
138
|
+
| /effort | Change and save reasoning effort directly |
|
|
139
|
+
| /providers | Switch the provider for the next turn |
|
|
140
|
+
| /models | Search the live model catalogue |
|
|
141
|
+
| /credentials | Add, replace, inspect, or forget saved credentials |
|
|
142
|
+
| /permissions | Review or revoke remembered session approvals |
|
|
143
|
+
| /usage | Show normalized token usage |
|
|
144
|
+
| /new | Start a clean in-memory conversation |
|
|
145
|
+
| /export | Save a timestamped Markdown transcript in the launch directory |
|
|
146
|
+
| /help | Show commands and controls |
|
|
147
|
+
| /exit | Restore the terminal and exit |
|
|
148
|
+
|
|
149
|
+
Useful controls:
|
|
150
|
+
|
|
151
|
+
- **Up/Down** moves through command suggestions, menus, and input history.
|
|
152
|
+
- **Tab** completes a slash command without running it; **Enter** sends.
|
|
153
|
+
- **Alt+Enter** inserts a newline.
|
|
154
|
+
- **Esc** closes a menu or interrupts the foreground operation.
|
|
155
|
+
- **Ctrl+C** interrupts, or exits while idle. **Ctrl+D** requests a clean exit.
|
|
156
|
+
- **PageUp/PageDown** and the mouse wheel scroll the transcript without losing
|
|
157
|
+
the place you are reading.
|
|
158
|
+
- **Ctrl+O** expands or compacts the latest reasoning or tool-detail block.
|
|
159
|
+
|
|
160
|
+
The one-line footer keeps model, effort, and workspace on the left. Live work,
|
|
161
|
+
readiness guidance, and temporary feedback use the right edge without polluting
|
|
162
|
+
the transcript.
|
|
163
|
+
|
|
164
|
+
## Configuration
|
|
165
|
+
|
|
166
|
+
Startup precedence is: command-line flags, environment variables, saved
|
|
167
|
+
settings, built-in defaults.
|
|
168
|
+
|
|
169
|
+
| Flag | Environment | Default |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| --provider | JECODE_PROVIDER | anthropic |
|
|
172
|
+
| --model | JECODE_MODEL | Provider default or interactive selection |
|
|
173
|
+
| --ollama-host | OLLAMA_HOST | Cloud with an Ollama key, local without one |
|
|
174
|
+
| --root | — | Current directory |
|
|
175
|
+
| --effort | JECODE_EFFORT | high |
|
|
176
|
+
| --max-tokens | JECODE_MAX_TOKENS | 64000 |
|
|
177
|
+
| --max-steps | JECODE_MAX_STEPS | 40 |
|
|
178
|
+
| --reduced-motion | JECODE_REDUCED_MOTION=1 | Off |
|
|
179
|
+
| --auto-approve | JECODE_AUTO_APPROVE=1 | Off |
|
|
180
|
+
|
|
181
|
+
Persistent preferences live in **~/.jecode/settings.json**. Explicitly saved
|
|
182
|
+
credentials live in **~/.jecode/credentials.json** with owner-only permissions
|
|
183
|
+
where the operating system supports them. Environment credentials always win.
|
|
184
|
+
|
|
185
|
+
Jecode has one interface theme: dark Steel. **NO_COLOR** is supported for
|
|
186
|
+
terminals and pipelines that disable colour.
|
|
187
|
+
|
|
188
|
+
## Automation
|
|
189
|
+
|
|
190
|
+
When stdin or stdout is piped, Jecode switches to a plain line-oriented mode:
|
|
191
|
+
|
|
192
|
+
~~~console
|
|
193
|
+
printf "explain this project\n" | jecode --root .
|
|
194
|
+
~~~
|
|
195
|
+
|
|
196
|
+
Dangerous tools stay denied in batch mode unless **--auto-approve** is supplied
|
|
197
|
+
explicitly.
|
|
198
|
+
|
|
199
|
+
## Safety model
|
|
200
|
+
|
|
201
|
+
Jecode treats model output, workspace content, tool output, and terminal text as
|
|
202
|
+
untrusted data.
|
|
203
|
+
|
|
204
|
+
- Tool paths are confined to the selected workspace. Writes reject symlink and
|
|
205
|
+
junction components, then revalidate the boundary during atomic replacement.
|
|
206
|
+
- Dangerous tools require approval unless the process was started with
|
|
207
|
+
**--auto-approve**.
|
|
208
|
+
- Credential fields are masked and excluded from transcripts. Approved shell
|
|
209
|
+
commands receive no credential-like environment variables, and recognized
|
|
210
|
+
credential values are redacted before tool output reaches the model, screen,
|
|
211
|
+
history, or export.
|
|
212
|
+
- Terminal control characters are neutralized before rendering.
|
|
213
|
+
- Remote Ollama endpoints require HTTPS. Provider HTTP redirects are rejected
|
|
214
|
+
rather than followed across an implicit trust boundary.
|
|
215
|
+
- Provider handshakes and idle response bodies have finite deadlines. Only
|
|
216
|
+
idempotent catalogue reads retry; generation requests are never replayed.
|
|
217
|
+
- Model and filesystem input are bounded before they reach the screen or
|
|
218
|
+
provider.
|
|
219
|
+
|
|
220
|
+
`run_command` is not an operating-system sandbox: an approved shell command can
|
|
221
|
+
still access files and account resources available to the current user. Review
|
|
222
|
+
commands carefully and reserve **--auto-approve** for controlled environments.
|
|
223
|
+
|
|
224
|
+
Please read [SECURITY.md](SECURITY.md) before reporting a vulnerability.
|
|
225
|
+
|
|
226
|
+
## Community
|
|
227
|
+
|
|
228
|
+
Jecode is built around people using the product and telling us where the loop
|
|
229
|
+
can improve.
|
|
230
|
+
|
|
231
|
+
- Ask questions, share workflows, and explore ideas in
|
|
232
|
+
[GitHub Discussions](https://github.com/giovannijecha/jecode/discussions).
|
|
233
|
+
- Report reproducible bugs and focused feature requests through
|
|
234
|
+
[GitHub Issues](https://github.com/giovannijecha/jecode/issues).
|
|
235
|
+
- Report security concerns privately through the repository Security tab.
|
|
236
|
+
|
|
237
|
+
Public pull requests are not accepted at this stage; code changes remain a
|
|
238
|
+
maintainer/collaborator workflow. See [CONTRIBUTING.md](CONTRIBUTING.md) for the
|
|
239
|
+
short routing guide and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for community
|
|
240
|
+
expectations.
|
|
241
|
+
|
|
242
|
+
## Development
|
|
243
|
+
|
|
244
|
+
~~~console
|
|
245
|
+
npm ci
|
|
246
|
+
npm run check
|
|
247
|
+
~~~
|
|
248
|
+
|
|
249
|
+
The visual lab exercises the TUI without a provider, network access, tool
|
|
250
|
+
execution, or workspace writes:
|
|
251
|
+
|
|
252
|
+
~~~console
|
|
253
|
+
npm run tui:lab
|
|
254
|
+
~~~
|
|
255
|
+
|
|
256
|
+
For a manual long-session rendering probe, run **npm run bench:transcript**.
|
|
257
|
+
|
|
258
|
+
Architecture and security boundaries are documented in
|
|
259
|
+
[docs/architecture.md](docs/architecture.md). Brand assets and usage rules live
|
|
260
|
+
in [docs/brand.md](docs/brand.md).
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
Jecode is available under the [MIT License](LICENSE).
|
package/bin/jecode.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const major = Number.parseInt(process.versions.node, 10);
|
|
4
|
+
if (major < 24) {
|
|
5
|
+
process.stderr.write(
|
|
6
|
+
`jecode requires Node.js 24 or newer (current: ${process.versions.node})\n`,
|
|
7
|
+
);
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
} else {
|
|
10
|
+
await import("../dist/main.js");
|
|
11
|
+
}
|
package/dist/atomic.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Crash-safe replacement of one file, using a verified temporary sibling and
|
|
2
|
+
// rename.
|
|
3
|
+
import { lstat, open, rename, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
export async function atomicWrite(file, content, options = {}) {
|
|
7
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
|
|
8
|
+
let handle;
|
|
9
|
+
let identity;
|
|
10
|
+
try {
|
|
11
|
+
await options.validate?.("before-open");
|
|
12
|
+
const permissions = options.mode ?? (await existingMode(file));
|
|
13
|
+
handle = await open(temporary, "wx", permissions);
|
|
14
|
+
identity = fileIdentity(await handle.stat());
|
|
15
|
+
await options.validate?.("before-write");
|
|
16
|
+
await assertNamedFile(temporary, identity);
|
|
17
|
+
await handle.writeFile(content, "utf8");
|
|
18
|
+
if (permissions !== undefined && process.platform !== "win32") {
|
|
19
|
+
await handle.chmod(permissions);
|
|
20
|
+
}
|
|
21
|
+
await handle.sync();
|
|
22
|
+
identity = fileIdentity(await handle.stat());
|
|
23
|
+
await options.validate?.("before-rename");
|
|
24
|
+
await assertNamedFile(temporary, identity);
|
|
25
|
+
await rename(temporary, file);
|
|
26
|
+
const completed = handle;
|
|
27
|
+
handle = undefined;
|
|
28
|
+
await completed.close().catch(() => undefined);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (handle !== undefined) {
|
|
32
|
+
await handle.truncate(0).catch(() => undefined);
|
|
33
|
+
await handle.sync().catch(() => undefined);
|
|
34
|
+
await handle.close().catch(() => undefined);
|
|
35
|
+
handle = undefined;
|
|
36
|
+
}
|
|
37
|
+
await removeTemporary(temporary, identity, options.validate);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function assertNamedFile(file, expected) {
|
|
42
|
+
const details = await lstat(file);
|
|
43
|
+
if (details.isSymbolicLink() || !sameFile(expected, fileIdentity(details))) {
|
|
44
|
+
throw new Error("atomic write target changed during replacement");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function removeTemporary(file, expected, validate) {
|
|
48
|
+
if (expected === undefined)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
await validate?.("before-cleanup");
|
|
52
|
+
await assertNamedFile(file, expected);
|
|
53
|
+
await rm(file);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// A changed path is no longer ours to remove.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function fileIdentity(details) {
|
|
60
|
+
return { dev: details.dev, ino: details.ino, birthtimeMs: details.birthtimeMs };
|
|
61
|
+
}
|
|
62
|
+
function sameFile(left, right) {
|
|
63
|
+
if (left.dev !== right.dev || left.ino !== right.ino)
|
|
64
|
+
return false;
|
|
65
|
+
return left.ino !== 0 || left.birthtimeMs === right.birthtimeMs;
|
|
66
|
+
}
|
|
67
|
+
async function existingMode(file) {
|
|
68
|
+
if (process.platform === "win32")
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
return (await stat(file)).mode & 0o777;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error.code === "ENOENT")
|
|
75
|
+
return undefined;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Project semantic blocks into a compact transcript for pipes.
|
|
2
|
+
import { render } from "./tui/blocks.js";
|
|
3
|
+
export function renderBatch(block, width, pal) {
|
|
4
|
+
const rows = [];
|
|
5
|
+
let lastWasBlank = false;
|
|
6
|
+
for (const rendered of render(block, width, pal)) {
|
|
7
|
+
const line = rendered.trimEnd();
|
|
8
|
+
const blank = line === "";
|
|
9
|
+
if (blank && lastWasBlank)
|
|
10
|
+
continue;
|
|
11
|
+
rows.push(line);
|
|
12
|
+
lastWasBlank = blank;
|
|
13
|
+
}
|
|
14
|
+
while (rows.at(-1) === "")
|
|
15
|
+
rows.pop();
|
|
16
|
+
return rows;
|
|
17
|
+
}
|
package/dist/batch.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// The non-interactive path: stdin is a pipe, so there is no screen to own.
|
|
2
|
+
//
|
|
3
|
+
// It exists so the agent can be scripted and tested. It shares the controller,
|
|
4
|
+
// the tools and the block renderer with the TUI — only the surface differs.
|
|
5
|
+
import * as readline from "node:readline/promises";
|
|
6
|
+
import { stdin, stdout } from "node:process";
|
|
7
|
+
import { runTurn } from "./controller.js";
|
|
8
|
+
import { handleCommand } from "./commands.js";
|
|
9
|
+
import { renderBatch } from "./batch-view.js";
|
|
10
|
+
import { columns } from "./ui/render.js";
|
|
11
|
+
import { terminalText } from "./ui/terminal-text.js";
|
|
12
|
+
import { recordUsage } from "./usage.js";
|
|
13
|
+
export async function runBatch(session, environment = {}) {
|
|
14
|
+
const rl = environment.lines === undefined ? readline.createInterface({ input: stdin }) : undefined;
|
|
15
|
+
const lines = environment.lines ?? rl;
|
|
16
|
+
const write = environment.write ?? ((text) => stdout.write(text));
|
|
17
|
+
const width = environment.width ?? columns();
|
|
18
|
+
const emit = (block) => {
|
|
19
|
+
for (const line of renderBatch(block, width, session.palette))
|
|
20
|
+
write(`${line}\n`);
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
for await (const raw of lines) {
|
|
24
|
+
const line = raw.trim();
|
|
25
|
+
if (line === "")
|
|
26
|
+
continue;
|
|
27
|
+
if (line.startsWith("/")) {
|
|
28
|
+
if ((await handleCommand(line, session, { emit })) === "exit")
|
|
29
|
+
break;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
write(`> ${terminalText(line)}\n`);
|
|
33
|
+
session.history.push({ role: "user", content: [{ kind: "text", text: line }] });
|
|
34
|
+
const turn = events(emit, session);
|
|
35
|
+
try {
|
|
36
|
+
await runTurn(session.history, options(session), turn);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
emit({ kind: "notice", text: error.message, tone: "error" });
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
turn.flush();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
rl?.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function options(session) {
|
|
51
|
+
return {
|
|
52
|
+
provider: session.provider,
|
|
53
|
+
tools: session.tools,
|
|
54
|
+
model: session.model,
|
|
55
|
+
system: session.system,
|
|
56
|
+
maxTokens: session.config.maxTokens,
|
|
57
|
+
effort: session.config.effort,
|
|
58
|
+
maxSteps: session.config.maxSteps,
|
|
59
|
+
toolContext: { root: session.config.root },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Prose is buffered rather than streamed: without a screen to repaint there is
|
|
63
|
+
// nothing to gain from partial lines, and plenty to lose in readability.
|
|
64
|
+
function events(emit, session) {
|
|
65
|
+
let answer = "";
|
|
66
|
+
const flush = () => {
|
|
67
|
+
if (answer === "")
|
|
68
|
+
return;
|
|
69
|
+
emit({ kind: "answer", text: answer });
|
|
70
|
+
answer = "";
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
flush,
|
|
74
|
+
onStream(event) {
|
|
75
|
+
if (event.kind === "text")
|
|
76
|
+
answer += event.text;
|
|
77
|
+
},
|
|
78
|
+
onToolCall() {
|
|
79
|
+
flush();
|
|
80
|
+
},
|
|
81
|
+
onToolResult(call, result, summary) {
|
|
82
|
+
emit({
|
|
83
|
+
kind: "tool",
|
|
84
|
+
name: call.name,
|
|
85
|
+
target: "",
|
|
86
|
+
right: summary ?? "",
|
|
87
|
+
tone: result.isError ? "fail" : "ok",
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
approve() {
|
|
91
|
+
flush();
|
|
92
|
+
// Nobody is watching a pipe. Approval has to be granted up front with
|
|
93
|
+
// --auto-approve, never inferred from silence.
|
|
94
|
+
return Promise.resolve(session.config.autoApprove);
|
|
95
|
+
},
|
|
96
|
+
onUsage(usage) {
|
|
97
|
+
recordUsage(session.usage, usage);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
package/dist/cli-info.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Information requests that finish before configuration or terminal takeover.
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
const HELP = `jecode — an owned coding agent for the terminal
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
jecode [options]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--root <path> workspace root (default: current directory)
|
|
11
|
+
--provider <id> anthropic, openai, or ollama
|
|
12
|
+
--model <id> model for the selected provider
|
|
13
|
+
--ollama-host <url> Ollama Cloud, local, or custom endpoint
|
|
14
|
+
--effort <level> low, medium, high, or max
|
|
15
|
+
--max-tokens <number> output-token ceiling
|
|
16
|
+
--max-steps <number> tool-loop ceiling
|
|
17
|
+
--reduced-motion disable animated terminal states
|
|
18
|
+
--auto-approve allow dangerous tools for this process
|
|
19
|
+
-h, --help show this help
|
|
20
|
+
-v, --version show the installed version
|
|
21
|
+
|
|
22
|
+
Inside Jecode, type / to discover interactive commands.`;
|
|
23
|
+
export async function showCliInfo(args, applicationRoot, write) {
|
|
24
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
25
|
+
write(`${HELP}\n`);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
29
|
+
write(`${await packageVersion(applicationRoot)}\n`);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
async function packageVersion(applicationRoot) {
|
|
35
|
+
const manifest = JSON.parse(await readFile(path.join(applicationRoot, "package.json"), "utf8"));
|
|
36
|
+
if (typeof manifest.version !== "string" || manifest.version === "") {
|
|
37
|
+
throw new Error("package version is missing");
|
|
38
|
+
}
|
|
39
|
+
return manifest.version;
|
|
40
|
+
}
|