wrosecode 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +692 -2
- package/npm/check-publish.js +164 -0
- package/npm/install.js +142 -0
- package/npm/wrosecode.js +69 -0
- package/package.json +39 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WROSECODE contributors
|
|
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
CHANGED
|
@@ -1,3 +1,693 @@
|
|
|
1
|
-
#
|
|
1
|
+
# WROSECODE
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A terminal coding agent written in Rust. Build with `cargo install --path .` or `cargo build --release`. Run `wrosecode` from a project directory, or `wrosecode 'your task'` for one turn. Set `ANTHROPIC_API_KEY` for the Anthropic provider. For an OpenAI-compatible server, set `WROSECODE_OPENAI_URL` and run `wrosecode --provider openai-compat --model MODEL`.
|
|
4
|
+
|
|
5
|
+
## Install and run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cargo build --release
|
|
9
|
+
install -Dm755 target/release/wrosecode "$HOME/.local/bin/wrosecode"
|
|
10
|
+
wrosecode --permission yolo
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For a fully static Linux binary with no glibc dependency, run
|
|
14
|
+
`./scripts/build-musl.sh` (it needs `rustup target add
|
|
15
|
+
x86_64-unknown-linux-musl` and `musl-tools` for `musl-gcc`) and install
|
|
16
|
+
`target/x86_64-unknown-linux-musl/release/wrosecode` instead.
|
|
17
|
+
|
|
18
|
+
## Install from npm
|
|
19
|
+
|
|
20
|
+
Install globally:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g wrosecode
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Run the CLI:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
wrosecode
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Run without a permanent installation:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx wrosecode
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Update:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g wrosecode@latest
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Uninstall:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm uninstall -g wrosecode
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The npm package is a small launcher; it downloads the matching native binary
|
|
51
|
+
from the [GitHub Releases](https://github.com/whiterose717/WROSECODE-CLI/releases)
|
|
52
|
+
for this package version and verifies the binary against its SHA-256 sidecar
|
|
53
|
+
before installation. Supported targets are Linux x86_64/aarch64, macOS
|
|
54
|
+
x86_64/aarch64, and Windows x86_64/aarch64. Node.js 18 or newer is required
|
|
55
|
+
for the npm installer; Rust and Cargo are not required for npm users.
|
|
56
|
+
|
|
57
|
+
Source: <https://github.com/whiterose717/WROSECODE-CLI>
|
|
58
|
+
|
|
59
|
+
### Maintainer release process
|
|
60
|
+
|
|
61
|
+
Push a version-matched tag such as `v1.0.0`. The GitHub Actions release
|
|
62
|
+
workflow builds all six supported native targets, creates a SHA-256 sidecar
|
|
63
|
+
for each binary, validates the full set, then creates or updates the GitHub
|
|
64
|
+
Release. Publish to npm only after that workflow succeeds and all twelve
|
|
65
|
+
assets are visible on the release page.
|
|
66
|
+
|
|
67
|
+
Rust was chosen for low input latency, predictable memory use, a single native binary, and direct terminal control. The architecture combines a differential TUI, streamed provider adapters, durable sessions, MCP tools, repository maps, skills, permission tiers, and parallel tool execution.
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
export ANTHROPIC_API_KEY=sk-ant-... # or a local server: WROSECODE_OPENAI_URL + --provider openai-compat
|
|
73
|
+
wrosecode # interactive session in the current directory
|
|
74
|
+
wrosecode 'inventory the attack surface' # one turn, then exit
|
|
75
|
+
wrosecode --permission yolo # skip approval prompts (see Permissions)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Worth touching in the first session:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
/ctf point the agent at a challenge file, URL, or CTFd task
|
|
82
|
+
/providers add, key, and health-check models without editing files
|
|
83
|
+
Ctrl+T cycle thinking levels (off → max) Ctrl+D eight-panel dashboard
|
|
84
|
+
/help every command, every keybinding
|
|
85
|
+
/resume pick a past session (the start screen lists the three most recent)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Sessions persist under `~/.wrosecode/sessions`; model profiles and keys live
|
|
89
|
+
in `~/.wrosecode/providers.toml` (keys display as `••••last4` and are typed
|
|
90
|
+
through a masked prompt).
|
|
91
|
+
|
|
92
|
+
## CTF dashboard
|
|
93
|
+
|
|
94
|
+
The first view is an opencode-style start screen: a large `WROSECODE` wordmark
|
|
95
|
+
with a smooth colour gradient (truecolor where the terminal advertises it, 256
|
|
96
|
+
or 16 colours otherwise, plain text under `NO_COLOR`/`--no-color`), carrying the
|
|
97
|
+
live spinner, the session ID, the challenge category, and the speed tier the
|
|
98
|
+
current thinking level maps to. Under it an info panel shows the provider and
|
|
99
|
+
model, thinking level and resource budget, sandbox and approval modes, working
|
|
100
|
+
directory, mode, harness, git branch, and MCP and skill counts — followed by
|
|
101
|
+
provider health with a `/providers` hint, a rotating tip (`/` commands, `@`
|
|
102
|
+
files, `ctrl+t` thinking, `ctrl+d` dashboard, `ctrl+o` output), the three most
|
|
103
|
+
recent sessions with one-line `resume` suggestions, and a `/ctf` hint. It only
|
|
104
|
+
draws while the transcript is still empty — as soon as you type or a turn
|
|
105
|
+
produces output it folds back into a slim `WROSECODE v…` header so the
|
|
106
|
+
conversation gets the screen. Below 80 columns the block art collapses to a
|
|
107
|
+
single `WROSECODE v…` line.
|
|
108
|
+
|
|
109
|
+
Example terminal view at 110×36 (session ID and project path anonymized):
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
╦ ╦╔╗ ╭─╮╔═╗╔═╗╭─╮╭─╮╔═╗╔═╗ ⠇ session ses_<session-id> · [misc] · paint 13ms
|
|
113
|
+
║ ║╠╩╗│ │╠═╝╠═╗│ │ │║ ║╠═╗
|
|
114
|
+
╚═╝╚═╝╰─╯╚═╝╚═╝╰─╯╰─╯╚═╝╚═╝ WROSECODE v1.0.0 · FAST
|
|
115
|
+
anthropic / claude-sonnet-5 · think medium · up to 3 agents · sandbox none · ask · /path/to/project
|
|
116
|
+
BUILD · claude · [misc] · git master* · 0 mcp · 636 skills
|
|
117
|
+
2 connected · 10 need setup · /providers
|
|
118
|
+
tip · @ files
|
|
119
|
+
ctf wrosecode ctf <file|dir|url> · or /ctf here
|
|
120
|
+
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
TRANSCRIPT 1 entry
|
|
122
|
+
INFO WROSECODE v1.0.0 · session ses_<session-id>
|
|
123
|
+
Type a request · /help commands · Ctrl+P palette
|
|
124
|
+
Tab build/plan · [ ] thinking · PgUp/PgDn scroll · /clear
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
Ready · session ses_<session-id> · claude-sonnet-5 · think:medium · tok 0/0 · cache 0% · 4s · step 0
|
|
146
|
+
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
│ >
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The rows above the rule are the start screen — wordmark, info panel, provider
|
|
151
|
+
health, rotating tip, recent sessions, and the `/ctf` hint — and they are what
|
|
152
|
+
the spinner animates in. The wordmark carries `TURBO` / `FAST` / `SMART` /
|
|
153
|
+
`DEEP` plus the worker count that tier actually allows (1 / 3 / 5 / 10).
|
|
154
|
+
|
|
155
|
+
The transcript owns the full terminal width: there is no always-on token or
|
|
156
|
+
timeline panel. Token, latency, and cost counters are still tracked
|
|
157
|
+
internally and surface in the status line, in `/stats` on demand, in
|
|
158
|
+
`~/.wrosecode/live.json`, and in the closing `── RESULT ─` receipt. The
|
|
159
|
+
bottom pane shows the subagent tree — only in-flight work, newest last, with
|
|
160
|
+
`+N more running` when it overflows — and finished subagents collapse to one
|
|
161
|
+
`✓ N subagents done` line. The bottom status
|
|
162
|
+
line leads with the run state, session ID, model, and `think:` label (the level name, or
|
|
163
|
+
`auto→medium` while the auto controller drives it), then tokens, cache
|
|
164
|
+
hit rate, elapsed time, tool step, sandbox and approval, speed tier and
|
|
165
|
+
workers, cost, and tool counts — for example
|
|
166
|
+
` Ready · session ses_1791153855-1574184-0 · claude-sonnet-5 · think:medium · tok 0/0 · cache 0% · 4s · step 0 ·
|
|
167
|
+
none/ask · FAST · 3w · cost n/a · tools 0:0`. During a turn it swaps in an
|
|
168
|
+
animated spinner, the queue note, and the live timeout and tool progress. The
|
|
169
|
+
input box below it is an open-bottomed `┌──` / `│ >` frame that grows with
|
|
170
|
+
the draft.
|
|
171
|
+
|
|
172
|
+
File magic and extensions categorize common web, pwn, crypto, reverse
|
|
173
|
+
engineering, and forensics tasks.
|
|
174
|
+
|
|
175
|
+
A found flag raises a `🚩 FLAG ALERT` banner under the header and survives until
|
|
176
|
+
the next turn or `/clear`. It also emits a terminal bell (`\x07`, disabled with
|
|
177
|
+
`alert_bell = false`) and a desktop notification when `notify-send` is available,
|
|
178
|
+
so a flag found in a background pane is not missed.
|
|
179
|
+
|
|
180
|
+
Every model response and tool result is scanned for `flag{...}`, `CTF{...}`, `picoCTF{...}`, and `HTB{...}`. The patterns live in `config.toml` under `[ctf]`; `auto_copy` controls the clipboard copy. New flags are highlighted and appended to `.ctf/flags.log`. Optional CTFd submission is enabled with `auto_submit = true` in `config.toml`, or from the environment:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
export WROSECODE_CTFD_AUTO_SUBMIT=1 # overrides config.toml
|
|
184
|
+
export CTFD_URL=https://ctf.example
|
|
185
|
+
export CTFD_TOKEN=your-token
|
|
186
|
+
export CTFD_CHALLENGE_ID=42
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
CTF skills cover recon, web, pwn, crypto, reverse engineering, forensics, stego, OSINT, and network analysis. Independent model tool calls run concurrently; `delegate_task` supports provider and model routing. Identical read-only calls use the in-memory result cache, while file edits invalidate cached results.
|
|
190
|
+
|
|
191
|
+
### Keyboard controls
|
|
192
|
+
|
|
193
|
+
| Key | Action |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `↑` / `↓` | Scroll the transcript whenever the input box is empty |
|
|
196
|
+
| `PageUp` / `PageDown` | Move the transcript a full viewport at a time (at the real page size, not a fixed five lines) |
|
|
197
|
+
| `Home` / `End` | Jump to the top or bottom of the transcript when the input is empty; otherwise move the caret |
|
|
198
|
+
| `Ctrl+Home` / `Ctrl+End` | Always jump to the top or bottom of the transcript |
|
|
199
|
+
| `Esc`, then `j` / `k` | Enter navigation mode and scroll with the single keys; `Space`/`b` page down/up |
|
|
200
|
+
| `Shift+Enter` | Insert a newline |
|
|
201
|
+
| `Enter` | Submit the prompt or selected command; during a turn it queues the message for after the reply |
|
|
202
|
+
| `Ctrl+K` | Clear input |
|
|
203
|
+
| `Ctrl+U` | Clear input when there is text, otherwise scroll half a page up |
|
|
204
|
+
| `Ctrl+D` | Delete the character under the caret when there is text; with an empty prompt, open the live eight-panel dashboard (`Esc` or `Ctrl+D` closes it) |
|
|
205
|
+
| `Ctrl+W` | Delete the word before the caret |
|
|
206
|
+
| `Ctrl+A` / `Ctrl+E` | Jump to the start / end of the line |
|
|
207
|
+
| `Alt+←` / `Alt+→` (also `Alt+B` / `Alt+F`) | Jump a word left / right |
|
|
208
|
+
| `Ctrl+L` | Clear the visible transcript and redraw (history and conversation context are kept; `/clear --context` drops the context too) |
|
|
209
|
+
| `Ctrl+T` | Cycle the thinking level (`off` → `low` → `medium` → `high` → `max` → `auto`) |
|
|
210
|
+
| `Ctrl+Shift+C` | Copy latest output |
|
|
211
|
+
| `[` / `]` | Step the thinking level down or up (same cycle as `Ctrl+T`) |
|
|
212
|
+
| `@` at the start of a prompt | Open the file-mention picker (`Mention files`); `Esc` closes it |
|
|
213
|
+
| Bracketed paste | A paste of 4+ lines collapses into `[Pasted N lines]`: `Enter` inserts it, `Esc` discards it |
|
|
214
|
+
| `Tab` | Complete the current word (paths, slash commands, tool names) when the input is not empty; otherwise switch build and plan modes |
|
|
215
|
+
| `Ctrl+C` | With text typed: clear the input. At an empty prompt: arm a ~2s exit window (`press Ctrl+C again to exit`); a second press inside it quits. During a turn it cancels the turn instead of closing the shell (which also arms the exit window, so Ctrl+C, Ctrl+C stops a run and exits) |
|
|
216
|
+
| Mouse wheel | Scroll the transcript (smooth scrolling follows `smooth_scroll_lines`) |
|
|
217
|
+
|
|
218
|
+
While scrolled up, the transcript header shows `· ↓ N new lines (End to jump)`; each new line arriving is counted there until `End` returns to the bottom. Multi-line drafts grow the input box to four rows and then scroll with the caret. Prompt history (last 500 entries, consecutive duplicates collapsed) persists to `~/.wrosecode/history` and is recalled with `↑`/`↓`.
|
|
219
|
+
|
|
220
|
+
Shell calls have a 30 second timeout and are terminated if stuck. Panics and turn errors are written to `.ctf/errors.log`; the TUI keeps provider errors visible with a recovery suggestion. One-shot runs print a computed usage report, and `--summary json` emits a machine-readable `TaskComplete` object.
|
|
221
|
+
|
|
222
|
+
### Thinking levels
|
|
223
|
+
|
|
224
|
+
Six levels decide how much reasoning each provider call spends: `off`, `low`, `medium`, `high`, `max`, and `auto`. `Ctrl+T` cycles them, `[` / `]` step down and up, `/think <level>` sets one directly (bare `/think` prints the current level and the list), and `--think <level>` fixes the startup default. The mode resolves `--think` → the provider profile's `think` key in `~/.wrosecode/providers.toml` (so a model can carry its own default) → `[agent] think` in `config.toml` → the legacy numeric `[agent] thinking_level`. Each concrete level maps to the provider's native control — Anthropic extended-thinking `budget_tokens` (1024 / 2048 / 4096 / 6144 for low / medium / high / max, clamped to 8191) or an OpenAI-style `reasoning_effort` (`low` / `medium` / `high`, with `max` reported as `high`) — and a per-model `think_map` on the profile overrides that with either a string or a token count per level. A provider that rejects the parameter retries once without it, records `think control ignored` in the transcript, and marks the status bar `think:… (ignored)`.
|
|
225
|
+
|
|
226
|
+
`auto` is a controller rather than a strength: it starts at `medium`, escalates one concrete level after three consecutive failing steps (the transcript shows `think: medium → high (no progress ×3)`), drops back one level once progress returns, and settles a trivial read/answer task at `low`. The status bar shows it as `auto→high` — mode plus the live level. The dashboard's Thinking panel reports the previous turn's model time and reasoning tokens (`last turn 4.2s · reasoning 1280 tok`).
|
|
227
|
+
|
|
228
|
+
## Advanced dashboard and persistence
|
|
229
|
+
|
|
230
|
+
The terminal shows two live panes: the full-width transcript on top and the subagent tree below it (in-flight work only, capped, with finished work collapsed to one line). Clicking a transcript row expands or collapses its tool cell. `Ctrl+T` now cycles the thinking level; for colours `/theme` opens a picker over the palettes — dark, light, solarized, Dracula, Nord, and Mono — and `/theme NAME` applies one directly (`/theme` on an unknown name lists what exists). Your own palettes drop into `~/.wrosecode/themes/*.toml` as five hex colours (`text`, `muted`, `accent`, `status`, `background`, with an optional `name`); a file that fails to parse, has bad hex, or reuses a built-in name is skipped. `NO_COLOR` (any non-empty value) or `--no-color` turns colour off everywhere: the gradient wordmark becomes plain text and every frame renders without colour codes. The shell paints its first frame — gradient wordmark, info panel with provider, thinking, sandbox, approval, git, MCP and skill counts, provider health, a rotating tip, recent sessions, and the `/ctf` hint — before provider setup, skill discovery, MCP reconnects, and session restore, so startup never waits on the network: the measured time is stamped next to the session line (`· paint 9ms`), reported by `/debug` as `First paint`, and must stay inside a 50 ms budget. `Ctrl+P` opens fuzzy command search; `Ctrl+F` and `Ctrl+R` open the flag and prompt pickers with the same filter-as-you-type behaviour. The renderer targets 60 frames per second while writing only changed terminal rows.
|
|
231
|
+
|
|
232
|
+
Sessions checkpoint to SQLite WAL every ten seconds at `~/.wrosecode/state.db`, while portable JSON session files remain available. Every session gets a stable `ses_…` ID, shown in the status line and in `/sessions` with a one-line summary and age; resuming restores conversation history, provider, and model. Resume or fork work with:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
wrosecode --session SESSION_ID
|
|
236
|
+
wrosecode -s SESSION_ID
|
|
237
|
+
wrosecode --session SESSION_ID --fork
|
|
238
|
+
wrosecode --session SESSION_ID --export-session session.json
|
|
239
|
+
wrosecode --import-session session.json
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Token counts, model latency percentiles, cache traffic, and costs come from provider usage. Configure prices in `pricing.toml` or `~/.wrosecode/pricing.toml`. Press `Ctrl+E` or run `/export` to write `metrics.json`, `metrics.csv`, and `latency.csv` (a latency histogram with one row per bucket: `bucket_upper_ms,count`). Set `OTEL_EXPORTER_OTLP_ENDPOINT` to export the same counters to an OTLP HTTP collector. Pass `--trace` to log per-step timings (API latency, tool time, slow renders) as NDJSON to `~/.wrosecode/trace.log`.
|
|
243
|
+
|
|
244
|
+
Rate-limited provider replies (HTTP 429 / quota errors) are classified separately
|
|
245
|
+
from other failures, counted in the dashboard (`WARN 429s n`), and exported with
|
|
246
|
+
the rest of the metrics.
|
|
247
|
+
|
|
248
|
+
## Live dashboard and transcript
|
|
249
|
+
|
|
250
|
+
With an empty prompt, `Ctrl+D` (or `/dashboard`) replaces the split view with
|
|
251
|
+
the live eight-panel dashboard: Processes, Thinking, Timeline, Plan, Tokens &
|
|
252
|
+
Cost, Files, CTF, and Budget. `←`/`→` move between panels, `↑`/`↓` (or
|
|
253
|
+
`j`/`k`) move the selection inside Processes and Files, `Enter` opens the
|
|
254
|
+
focused panel as full output (a selected process shows its live tail, a
|
|
255
|
+
selected file its `git diff`), `Ctrl+C` / `Ctrl+K` send SIGINT / SIGKILL to the
|
|
256
|
+
selected process, and `Esc` / `Ctrl+D` returns to the shell. `/stats` prints
|
|
257
|
+
the same eight panels as plain text.
|
|
258
|
+
|
|
259
|
+
Captured at 110×36 straight from a real run:
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
DASHBOARD · BUILD/anthropic/claude-sonnet-5/misc · ready
|
|
263
|
+
▌PROCESSES ───────────────────────────────────────────│ TOKENS & COST ──────────────────────────────────────
|
|
264
|
+
▌ no tracked processes │ in 0 · out 0 · reason 0
|
|
265
|
+
│ cache 0% · read 0 · write 0
|
|
266
|
+
│ tok ·
|
|
267
|
+
│ cost n/a · saved ~0 tok
|
|
268
|
+
│
|
|
269
|
+
│
|
|
270
|
+
│
|
|
271
|
+
│
|
|
272
|
+
│
|
|
273
|
+
│ FILES ──────────────────────────────────────────────
|
|
274
|
+
│ +0 -0 · 0 changed
|
|
275
|
+
│
|
|
276
|
+
THINKING ────────────────────────────────────────────│
|
|
277
|
+
level 5/20 medium · FAST · ready · idle │
|
|
278
|
+
thinking 0.0s · acting 0.0s · waiting 6.9s │
|
|
279
|
+
turns 0 · last turn · │
|
|
280
|
+
last turn 0.0s · reasoning 0 tok │
|
|
281
|
+
TIMELINE ────────────────────────────────────────────│
|
|
282
|
+
model 0.0s · tools 0.0s · wait 6.9s · wall 6s │
|
|
283
|
+
parallel ×0 · steps 0 · fails 0 │ CTF ────────────────────────────────────────────────
|
|
284
|
+
tok · │ [misc] · tools 0 (0 failed)
|
|
285
|
+
recent: — │ coverage: 0 files · 0 searches · 0 shells
|
|
286
|
+
PLAN ────────────────────────────────────────────────│ flags 0
|
|
287
|
+
no plan yet (update_plan) │ checker: unverified
|
|
288
|
+
│
|
|
289
|
+
│
|
|
290
|
+
│
|
|
291
|
+
│ BUDGET ─────────────────────────────────────────────
|
|
292
|
+
│ ░░░░░░░░░░ $? / n/a (unlimited)
|
|
293
|
+
│ steps 0 (no cap)
|
|
294
|
+
│ wall 6s · flags 0
|
|
295
|
+
│ errors 0 · status ready
|
|
296
|
+
│
|
|
297
|
+
←→ panel · ↑↓/jk select · Enter detail · Ctrl+C/K signal · Esc/Ctrl+D close
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Transcript rows are cells rather than raw lines: each tool shows a one-line
|
|
301
|
+
header with its elapsed time and a preview of the first three and last two
|
|
302
|
+
output lines, failed cells auto-expand with their exit code, nested subagent
|
|
303
|
+
calls draw as a `├─` / `└─` tree, and reasoning opens as a dimmed
|
|
304
|
+
`Thinking… 2.1s` cell that freezes into `Thought n.ns` when the model answers.
|
|
305
|
+
Press `Enter` (or `Ctrl+O`) to expand the cell at the top of the viewport;
|
|
306
|
+
explorations group under a collapsed `▸ Explored 3 files · 1 search` line.
|
|
307
|
+
Every finished task ends with a `── RESULT ─ {status}` block: verification
|
|
308
|
+
state, answer, proof, the model/tools/wait time split, steps, tokens, cache
|
|
309
|
+
rate, and cost.
|
|
310
|
+
|
|
311
|
+
The same dashboard runs outside the TUI:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# Attach to a running session: fresh live.json (≤10s) → /v1/status → offline
|
|
315
|
+
wrosecode dashboard
|
|
316
|
+
|
|
317
|
+
# One plain frame, no ANSI — pipe-friendly
|
|
318
|
+
wrosecode dashboard --once --offline
|
|
319
|
+
|
|
320
|
+
# Headless run whose final JSON is the dashboard snapshot
|
|
321
|
+
wrosecode exec --json 'enumerate the challenge'
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
While idle, a running TUI rewrites `~/.wrosecode/live.json` about once a
|
|
325
|
+
second (schema `wrosecode/live-v1`) — that file is what the attach client
|
|
326
|
+
reads when no web server is up, and `GET /v1/status` serves the identical
|
|
327
|
+
snapshot when the session runs `--web`.
|
|
328
|
+
|
|
329
|
+
## Terminal compatibility
|
|
330
|
+
|
|
331
|
+
The TUI decides how much terminal control to take from the environment instead of
|
|
332
|
+
assuming a full xterm:
|
|
333
|
+
|
|
334
|
+
| Condition | Effect |
|
|
335
|
+
| --- | --- |
|
|
336
|
+
| `TERM` empty or `dumb` | No alternate screen, no mouse capture |
|
|
337
|
+
| VS Code / Cursor integrated terminal (`TERM_PROGRAM=vscode`, `VSCODE_PID`, `VSCODE_CWD`) | Mouse capture stays off so the terminal's native wheel scroll and text selection keep working |
|
|
338
|
+
| Zellij (`ZELLIJ` set) or `WROSECODE_FULL_REDRAW=1` | Every frame repaints fully (multiplexers composite their own grid and keep stale glyphs otherwise) |
|
|
339
|
+
| `WROSECODE_NO_ALT_SCREEN=1` | Run inline instead of switching to the alternate screen |
|
|
340
|
+
| `NO_COLOR` set to a non-empty value, or `--no-color` on the command line | No colour anywhere: the gradient wordmark falls back to plain text and frames emit no colour codes |
|
|
341
|
+
|
|
342
|
+
`mouse_capture` in `config.toml` opts into mouse handling:
|
|
343
|
+
|
|
344
|
+
```toml
|
|
345
|
+
[ui]
|
|
346
|
+
mouse_capture = "off" # "off" (default), "on", or "auto" (off unless WROSECODE_MOUSE=1)
|
|
347
|
+
alert_bell = true # before flag notifications
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The default keeps the terminal's native text selection: drag to select and
|
|
351
|
+
copy with the terminal's own shortcut or menu, with no mouse reporting to
|
|
352
|
+
fight it. Set `mouse_capture = "on"` (or `WROSECODE_MOUSE=1`) only if you
|
|
353
|
+
want click-to-expand cells and wheel scrolling inside the app — every mouse
|
|
354
|
+
gesture already has a keyboard equivalent (`Enter`/`Ctrl+O` to expand,
|
|
355
|
+
`PageUp`/`PageDown`/`Home`/`End` to scroll, arrows + `Enter` in the
|
|
356
|
+
dashboard).
|
|
357
|
+
|
|
358
|
+
## Advanced CTF workflow
|
|
359
|
+
|
|
360
|
+
The detector loads regexes from `config.toml`, checks plain output, ROT13, hexadecimal, and Base64 variants, and uses entropy to select encoded candidates. New flags pass through a checker before optional rate limited CTFd submission. `Ctrl+F` opens flag history and `/writeup` creates a Markdown evidence report.
|
|
361
|
+
While a picker is open, typing fuzzy-filters the list (`pdng` finds
|
|
362
|
+
`flag{padding...}`), `↑`/`↓` move, `Enter` copies the selected flag, and `Esc`
|
|
363
|
+
closes. `Ctrl+R` does the same for prompt history.
|
|
364
|
+
|
|
365
|
+
Long engagements also need to remember what has *not* been tried yet: `/coverage`
|
|
366
|
+
opens a persistent checklist in `.wrosecode/coverage.json` — add scope items,
|
|
367
|
+
tick them off as they are verified, and the `coverage` tool lets the agent
|
|
368
|
+
update the same list while it works. The file survives sessions, so skipped
|
|
369
|
+
areas stay visible instead of quietly falling out of the engagement.
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
# CTFd operations
|
|
373
|
+
wrosecode ctfd list
|
|
374
|
+
wrosecode ctfd scoreboard
|
|
375
|
+
wrosecode ctfd download 42
|
|
376
|
+
wrosecode ctfd submit 42 'flag{answer}'
|
|
377
|
+
wrosecode ctfd verify 42 'flag{answer}' # exit 0 verified, 2 rejected, 1 inconclusive
|
|
378
|
+
|
|
379
|
+
# Race the primary model against other configured models
|
|
380
|
+
wrosecode --provider vyce --model deepseek-v4.1 \
|
|
381
|
+
--race openai:gpt-4o-mini --race ollama:qwen3 \
|
|
382
|
+
'solve this challenge and verify the flag'
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### CTF autopilot
|
|
386
|
+
|
|
387
|
+
`wrosecode ctf <file|dir|url|description>` hands the target to an autonomous
|
|
388
|
+
loop (headless; in the TUI it is `/ctf`, and mentioning a flag, CTF, or
|
|
389
|
+
challenge offers it):
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
# Triage → category hypothesis → playbooks → verify, or a loud budget stop
|
|
393
|
+
wrosecode ctf chal.pcap --budget 60
|
|
394
|
+
wrosecode ctf chal.txt --flag-format 'SECRET\{[^}]+\}' --category forensics
|
|
395
|
+
wrosecode ctf https://challenge.example --remote challenge.example:443 --parallel 2
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Every run starts by printing the allowlist it will work inside
|
|
399
|
+
(`SCOPE files … · hosts …`, `TOOLS installed/missing`, `GUESS <category>`),
|
|
400
|
+
writes `ctf-notes.md` (challenge, scope, triage, assumptions, dead ends —
|
|
401
|
+
dead ends are re-read by the stuck handler so nothing is retried), and loads
|
|
402
|
+
the matching `skills/ctf-*` playbook. Out-of-scope writes are denied with the
|
|
403
|
+
reason handed back to the model instead of executed. Flags are `✔ flag
|
|
404
|
+
verified` only when the value re-derives from an artifact (or a decode chain);
|
|
405
|
+
a model claim alone is `⚠ candidate, unverified`.
|
|
406
|
+
|
|
407
|
+
The stop condition is never silent: hitting the budget prints
|
|
408
|
+
`BUDGET EXHAUSTED (steps|tokens|seconds)` plus `Tried:` / `Learned:` /
|
|
409
|
+
`Next:`, and every run leaves `writeups/<challenge>.md`. Exit codes are `0`
|
|
410
|
+
verified, `2` unsolved (budget or stop), `1` error.
|
|
411
|
+
|
|
412
|
+
Project playbooks load from `skills/`, `.ctf/skills/`, and `~/.wrosecode/skills/`. Delegated tasks accept stable `task_id` values and can resume completed results. Thinking levels allocate runtime concurrency as follows: 0–3 uses one worker, 4–7 uses up to three, 8–12 uses up to five, and 13–20 allows the configured swarm limit.
|
|
413
|
+
|
|
414
|
+
The tool runtime includes MCP, Burp raw request import/export, optional Playwright screenshots, in-memory and Redis result caches, and an optional Qdrant writeup archive. Start the supporting services with `docker compose up -d`.
|
|
415
|
+
|
|
416
|
+
## Web and automation
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
# Local REST API and browser dashboard
|
|
420
|
+
wrosecode --provider vyce --model deepseek-v4.1 --web
|
|
421
|
+
|
|
422
|
+
# Headless automation with machine-readable completion data
|
|
423
|
+
wrosecode --headless --summary json 'analyze the supplied challenge'
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The web server binds to `127.0.0.1:7878` by default. See `API.md` before exposing it through a reverse proxy.
|
|
427
|
+
|
|
428
|
+
`--events PATH` (or `--events -` for stdout) writes a framed NDJSON submission stream for embedding clients: a `start` frame with the session and prompt, `step` frames for tool begins/ends, thinking-level changes, plans, flag hits, and usage, `text` frames carrying streamed tokens, and a final `result` frame with the answer, verification status, usage, and model-turn count. Frames are flushed as they happen and the `result` frame is always last, so a host application can drive and render a session without parsing the transcript:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
wrosecode --headless --events run.jsonl --summary json 'analyze the supplied challenge'
|
|
432
|
+
jq -r 'select(.type == "text") | .delta' run.jsonl # live answer stream
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`src/events.rs` is the formatter; `docs/ref-notes.md` records it against Codex's submission/event protocol.
|
|
436
|
+
|
|
437
|
+
## Shell sandbox
|
|
438
|
+
|
|
439
|
+
`[sandbox]` in `config.toml` decides where `shell` calls run:
|
|
440
|
+
|
|
441
|
+
| `engine` | Behaviour |
|
|
442
|
+
| --- | --- |
|
|
443
|
+
| `none` (default) | Run the command on the host, exactly like before. |
|
|
444
|
+
| `docker` | Run it inside a container. The project directory is bind-mounted at `/workspace`, so file edits made by other tools stay visible. |
|
|
445
|
+
|
|
446
|
+
```toml
|
|
447
|
+
[sandbox]
|
|
448
|
+
engine = "docker"
|
|
449
|
+
image = "wrosecode-sandbox:latest"
|
|
450
|
+
persistent = true # one warm container reused across calls (no cold start)
|
|
451
|
+
name = "wrosecode-sandbox"
|
|
452
|
+
network = "bridge"
|
|
453
|
+
memory = "2g"
|
|
454
|
+
cpus = "2.0"
|
|
455
|
+
auto_build = false # docker build -f Dockerfile.sandbox when the image is missing
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Build the analysis image once:
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
docker build -f Dockerfile.sandbox -t wrosecode-sandbox:latest .
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
`engine = "docker"` never falls back silently: if the docker CLI, the daemon, or
|
|
465
|
+
the image is missing, the tool call fails with a message that tells you which of
|
|
466
|
+
the three to fix. `persistent = true` keeps one `sleep infinity` container alive
|
|
467
|
+
and `docker exec`s into it, so there is no per-call cold start.
|
|
468
|
+
|
|
469
|
+
Whatever the engine, `shell` is resource-boxed: the host path runs the command
|
|
470
|
+
in its own process group, so a timeout or a cancelled turn kills the whole
|
|
471
|
+
group — grandchildren included — stdout and stderr are tail-capped at 256 KiB
|
|
472
|
+
while the command runs so a flooding pipe cannot pin memory, and a background
|
|
473
|
+
daemon cannot hold the result open. The container path adds an in-container
|
|
474
|
+
`timeout -k 5` (skipped automatically on images without coreutils) so killing
|
|
475
|
+
the docker client on timeout cannot strand the workload inside the container.
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
/sandbox # status: engine, container, image
|
|
479
|
+
/sandbox up # warm the container now
|
|
480
|
+
/sandbox down # stop and remove it
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
`auto_build = true` runs `docker build -f Dockerfile.sandbox -t <image> .` the
|
|
484
|
+
first time the image is missing, then falls back to `docker pull`.
|
|
485
|
+
|
|
486
|
+
### Desktop control (opt-in)
|
|
487
|
+
|
|
488
|
+
The `computer` tool drives this machine's desktop with `screenshot`, `click`,
|
|
489
|
+
`type`, `key`, and `scroll`, but it ships disabled — until you opt in, the
|
|
490
|
+
schema is not even advertised to the model. Enable it for one run with
|
|
491
|
+
`--computer`, or permanently with `[tools] computer = true` in `config.toml`.
|
|
492
|
+
Screenshots probe `grim`, `gnome-screenshot`, `scrot`, `import`, and
|
|
493
|
+
`screencapture` in that order; input actions go through `xdotool`. Every call
|
|
494
|
+
is launched as a literal argument vector (never through a shell string),
|
|
495
|
+
screenshot paths are confined to the project root, and approval follows the
|
|
496
|
+
normal mutating-tool policy: `ask` prompts for each call, `auto-safe` and
|
|
497
|
+
`yolo` run it, and plan mode refuses it.
|
|
498
|
+
|
|
499
|
+
## Run the agent in a container
|
|
500
|
+
|
|
501
|
+
Build the image that carries `wrosecode` itself plus the challenge toolchain:
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
docker build -t wrosecode-sandbox .
|
|
505
|
+
docker run --rm -it -v "$PWD:/workspace" \
|
|
506
|
+
-e ANTHROPIC_API_KEY wrosecode-sandbox --permission yolo
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
With an interactive terminal, `wrosecode` opens a full-screen chat UI showing the conversation, tool activity, status, and a persistent input box. Anthropic text appears as it streams; the OpenAI-compatible backend displays its completed response. The UI redraws only changed lines, renders just the visible slice of a long transcript, and pins itself to the bottom while new output arrives (the header shows `· ↑N lines` when you have scrolled away). Type `/` at the start of the input to open the command palette. Keep typing to filter, use the arrow keys to select, press Enter to run, or Esc to close. `Tab` switches between build and read-only plan modes. Use `PageUp`/`PageDown` to scroll, `Up`/`Down` for prompt history. Messages typed while a turn is running are kept: they are queued and sent after the reply, and `Ctrl+C` during a turn cancels just that turn (a second `Ctrl+C` within ~2s exits). One-shot calls and piped input keep plain text output. The UI displays permission requests and accepts `y` or `n`; `Ctrl+C` there denies the request and stays in the session.
|
|
510
|
+
|
|
511
|
+
The `delegate_task` tool can select a different `provider` and `model` for a child task. Supported provider names are `anthropic`, `openai-compat`, `ollama`, and `lm-studio`.
|
|
512
|
+
|
|
513
|
+
Attach an MCP stdio server for one run with `--mcp-bin PATH` and repeated `--mcp-arg ARG` flags. `/mcps add` saves additional servers in `~/.wrosecode/mcps.toml`; they reconnect at startup. The same file takes remote servers over the streamable-HTTP transport: give the entry a `url = "https://…/mcp"` instead of a `bin`, plus an optional `[servers.headers]` table for `Authorization` or API keys — `/mcps add` prompts for the URL when you prefer the TUI. The client posts JSON-RPC, keeps the server's `Mcp-Session-Id`, and accepts either a JSON body or an SSE stream for replies. Tools are exposed to the model as `mcp__SERVER__TOOL`.
|
|
514
|
+
|
|
515
|
+
The standalone `install.sh` and `install.ps1` scripts accept
|
|
516
|
+
`WROSECODE_RELEASE_BASE`; npm users do not need to set it because the npm
|
|
517
|
+
installer derives the versioned GitHub Release URL from package metadata.
|
|
518
|
+
|
|
519
|
+
## Configuration
|
|
520
|
+
|
|
521
|
+
`config.toml` in the project root is the single source of truth for the tunables;
|
|
522
|
+
`config.schema.json` describes it for editor validation. Every section and every
|
|
523
|
+
key is optional — a missing file, a missing section, or a typo falls back to the
|
|
524
|
+
built-in defaults rather than failing to start. CLI flags always win.
|
|
525
|
+
|
|
526
|
+
| Section | Purpose |
|
|
527
|
+
| --- | --- |
|
|
528
|
+
| `[ui]` | Theme, verbosity, alternate screen, `mouse_capture`, `alert_bell`, scroll step. |
|
|
529
|
+
| `[agent]` | Thinking mode (`think`), legacy level (`thinking_level`), worker ceiling, shell timeout, retries, permission tier, budget, planner model for `plan` mode (`planner = "provider/model"`), aider-style auto-commit of edited paths (`auto_commit`, default on). |
|
|
530
|
+
| `[ctf]` | Flag regexes, clipboard copy, CTFd auto-submit. |
|
|
531
|
+
| `[cache]` | `memory` or `redis`, plus Redis and Qdrant URLs. |
|
|
532
|
+
| `[sandbox]` | Where `shell` calls run (`none` or `docker`) and the container settings. |
|
|
533
|
+
| `[lsp]` | Language-server diagnostics appended to file edits (`enabled`, `wait_ms`, per-language `commands`). |
|
|
534
|
+
| `[tools]` | Opt-in extra tool surfaces: `computer` enables desktop control (off by default; the `--computer` flag turns it on for one run). |
|
|
535
|
+
|
|
536
|
+
`cargo test` parses the shipped `config.toml` and asserts every key lands where
|
|
537
|
+
it should, so a section cannot silently stop being read again.
|
|
538
|
+
|
|
539
|
+
## Tests
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
cargo fmt --check
|
|
543
|
+
cargo clippy --all-targets -- -D warnings
|
|
544
|
+
cargo test --all-targets
|
|
545
|
+
cargo test --release benchmarks -- --nocapture # speed budgets (also in ./scripts/check.sh and CI)
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Three layers run in that one command:
|
|
549
|
+
|
|
550
|
+
| Layer | Where | What it proves |
|
|
551
|
+
| --- | --- | --- |
|
|
552
|
+
| Unit | `src/**` modules | Flag detection corpus, metrics/histogram math, terminal profile rules, CTFd verdict classification, config parsing |
|
|
553
|
+
| CLI | `tests/cli.rs` | The argument surface stays documented: `--version`, `--help`, unknown flags, `ctfd verify --help` |
|
|
554
|
+
| End to end | `tests/e2e_headless.rs`, `tests/e2e_ctfd.rs` | A real `wrosecode` process against a local mock OpenAI-compatible server and a mock CTFd: tool round trips, `--summary json` usage numbers, `--until` exit codes, `ctfd verify` retries and exit codes |
|
|
555
|
+
|
|
556
|
+
The end-to-end tests bind `127.0.0.1` on an ephemeral port, point the child
|
|
557
|
+
process at a temporary `HOME`, and never touch the network or a real provider.
|
|
558
|
+
|
|
559
|
+
The release-mode budgets in `src/benchmarks.rs` (output shaping, SSE
|
|
560
|
+
draining, repo-map build) fail on regressions; `./scripts/benchmark.sh`
|
|
561
|
+
records test-suite latency and corpus size to `.ctf/reports/benchmark.json`.
|
|
562
|
+
|
|
563
|
+
## Permissions
|
|
564
|
+
|
|
565
|
+
`--permission ask` prompts for mutating tool calls. `auto-safe` runs them automatically except risky shell commands. `yolo` removes normal prompts. A destructive shell-command blocklist prompts in every tier. `/plan` changes to read-only mode; `/build` returns to edit mode. The shell blocklist is a convenience guard, not an isolation boundary — for real isolation set `[sandbox] engine = "docker"` below, which moves every `shell` call into a container.
|
|
566
|
+
|
|
567
|
+
Tool output, web content, and file text are untrusted data: scope and approval checks inspect structured tool-call arguments before dispatch, so result prose cannot change policy, approvals, or scope. Credential-shaped values and configured provider keys are redacted from displayed and persisted copies — transcript cells, streamed text, dashboard processes, events NDJSON, session JSON/SQLite payloads, crash reports, and memory facts — while the live model context keeps full fidelity.
|
|
568
|
+
|
|
569
|
+
## Skills and memory
|
|
570
|
+
|
|
571
|
+
Skills are `SKILL.md` files with YAML frontmatter:
|
|
572
|
+
|
|
573
|
+
```yaml
|
|
574
|
+
---
|
|
575
|
+
name: my-skill
|
|
576
|
+
description: What it helps with
|
|
577
|
+
fork: true
|
|
578
|
+
keywords: [kubernetes, kubectl]
|
|
579
|
+
---
|
|
580
|
+
Instructions for the agent.
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Skills load from bundled `skills/`, project `skills/`, `~/.wrosecode/skills/`, and `--skills DIR`, with later paths overriding names. Skill packages install from a git URL or a directory — `/skills install <url#ref | path>`, or `--install-skill` headless — into `~/.wrosecode/skills/`, versioned in `~/.wrosecode/packages.json`; `/skills list` and `/skills uninstall <pkg>` manage them. `keywords:` (a YAML list or a comma-separated string) makes a skill fire on its own: a short knowledge snippet loads as soon as one of those whole words appears in the prompt, without the skill name being mentioned — the keyword gate sits below the name match, so a prompt naming the skill still wins. Use `#fact text` for project memory and `#!fact text` for personal memory. `/memory` lists saved facts. Matching facts are recalled automatically. Obvious secret patterns are redacted on save.
|
|
584
|
+
|
|
585
|
+
`/harness minimal|swe|claude` switches prompt style without clearing history. `wrosecode acp` starts a basic ACP stdio agent.
|
|
586
|
+
|
|
587
|
+
## Your own commands and agents
|
|
588
|
+
|
|
589
|
+
Markdown files with YAML frontmatter extend the shell without recompiling anything. Commands load from `.wrosecode/commands/*.md` (project) and `~/.wrosecode/commands/` (personal); the project file wins on a name clash:
|
|
590
|
+
|
|
591
|
+
```markdown
|
|
592
|
+
---
|
|
593
|
+
description: Commit and push the branch
|
|
594
|
+
category: Project
|
|
595
|
+
---
|
|
596
|
+
Review the working tree, write one imperative commit message, commit, and push $ARGUMENTS.
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
`/ship the fix` expands `$ARGUMENTS` and runs the body as a normal turn — in the TUI and in `wrosecode --headless '/ship the fix'`. Your commands appear in `/help` and the `Ctrl+P` palette, and typing `/` completes them.
|
|
600
|
+
|
|
601
|
+
Agents load from `.wrosecode/agents/*.md` and `~/.wrosecode/agents/`:
|
|
602
|
+
|
|
603
|
+
```markdown
|
|
604
|
+
---
|
|
605
|
+
description: Reviews diffs, never edits
|
|
606
|
+
mode: plan
|
|
607
|
+
thinking: deep
|
|
608
|
+
---
|
|
609
|
+
Always quote the file and line you are criticising.
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
`wrosecode --agent reviewer 'review the diff'` activates one for a run; in the TUI it is listed in `/agents`. Its body is injected into every system prompt, `mode:` picks `build`, `plan`, or `general`, and `thinking:` sets the level. `/build`, `/plan`, or `/agents` on a built-in drops the custom agent.
|
|
613
|
+
|
|
614
|
+
## Recipes
|
|
615
|
+
|
|
616
|
+
A recipe is a saved multi-step workflow: a YAML (or Markdown) file with parameters and an ordered list of prompt and command steps, in the spirit of goose's recipes. `.wrosecode/recipes/` holds the project's recipes, `~/.wrosecode/recipes/` the personal ones, and the project file wins a name clash.
|
|
617
|
+
|
|
618
|
+
```yaml
|
|
619
|
+
name: audit
|
|
620
|
+
description: Two-step audit
|
|
621
|
+
version: "1"
|
|
622
|
+
params:
|
|
623
|
+
target:
|
|
624
|
+
description: host to audit
|
|
625
|
+
default: localhost
|
|
626
|
+
steps:
|
|
627
|
+
- prompt: "Recon {{target}} and list open ports."
|
|
628
|
+
- command: "echo ports-open-for-{{target}}"
|
|
629
|
+
- prompt: "Given {{prev}}, write the one-line verdict."
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
`params` may also be a plain list of required names. `{{key}}` placeholders take the resolved parameter values (defaults fill in, an unknown key is an error), `{{prev}}` is the previous step's output, and `{{steps}}` is the accumulated history — bounded, oldest output dropped first. A Markdown recipe uses frontmatter for `name`/`description`/`params` and its body as a single prompt step.
|
|
633
|
+
|
|
634
|
+
```bash
|
|
635
|
+
wrosecode recipe list
|
|
636
|
+
wrosecode recipe run audit --set target=attackme.local
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
`recipe run` behaves like any other headless run: it prints the answer, honours `--summary json`, `--goal`, `--until`, `--until-cmd`, `--events`, and exits `2` when the verification flags say the answer was wrong. Prompt steps run as ordinary turns — full tool use, permissions, and steering apply — while command steps are the recipe author's own shell commands, run with the project's shell timeout. In the TUI, `/recipe` opens a picker, asks for each parameter the recipe does not default, and streams every step; `/recipe NAME` skips the picker.
|
|
640
|
+
|
|
641
|
+
## Slash commands
|
|
642
|
+
|
|
643
|
+
`/help` shows the full command list, generated from the same registry as the palette.
|
|
644
|
+
|
|
645
|
+
| Area | Commands |
|
|
646
|
+
| --- | --- |
|
|
647
|
+
| Agents | `/agents`, `/build`, `/plan`, `/harness`, `/skills` |
|
|
648
|
+
| Providers | `/connect`, `/providers`, `/models`, `/mcps`, `/mcp` |
|
|
649
|
+
| Session | `/new`, `/sessions`, `/resume`, `/fork`, `/history`, `/tree`, `/move`, `/editor`, `/memory`, `/greet`, `/debug`, `/stats`, `/dashboard`, `/verbosity`, `/think`, `/theme`, `/compact`, `/export`, `/help`, `/exit`, `/quit` |
|
|
650
|
+
| Project | `/init`, `/diff`, `/review`, `/commit`, `/issues`, `/rmslop`, `/undo`, `/redo`, `/add`, `/drop`, `/recipe`, `/coverage`, `/flags`, `/writeup`, `/ctf`, `/sandbox` |
|
|
651
|
+
|
|
652
|
+
`/providers` opens a searchable provider screen with the built-in entries and their connection status. Enter configures or selects a provider; `a` adds, `e` edits, `d` deletes any provider (built-ins included — deleting the active one switches to another configured provider first), and `t` tests a connection. `/connect` opens the custom provider form directly; `/connect openai` offers "Sign in with ChatGPT" (browser loopback or device-code OAuth, tokens stored owner-only at `~/.wrosecode/credentials/openai.json`) next to the plain API-key path. The API key field is masked. `/models` lists models across connected providers and switches to the picked one.
|
|
653
|
+
|
|
654
|
+
Provider definitions are saved in `~/.wrosecode/providers.toml` without keys. Keys go to the OS keyring when available, otherwise to `~/.wrosecode/auth.json` with mode `0600`. Environment variables override stored keys. Custom provider keys can use `WROSECODE_NAME_API_KEY` (replace dashes in the name with underscores), or a saved `env:VARIABLE` reference. Custom providers support `openai_compat` and `anthropic` APIs; both support streamed replies and tool calls. Each profile can also set `think` (its default thinking level) and `think_map` (per-level provider values) — see *Thinking levels* above. Providers that need cloud-specific signing or URLs can use an OpenAI-compatible gateway as their base URL.
|
|
655
|
+
|
|
656
|
+
The same registry is available without the TUI:
|
|
657
|
+
|
|
658
|
+
```bash
|
|
659
|
+
wrosecode providers list
|
|
660
|
+
printf '%s\n' "$MY_API_KEY" | wrosecode providers add my-gateway \
|
|
661
|
+
--base-url https://llm.example.com/v1 --stdin --model my-model
|
|
662
|
+
wrosecode providers set-key my-gateway --api-key env:MY_API_KEY
|
|
663
|
+
wrosecode providers edit my-gateway --model other-model
|
|
664
|
+
wrosecode providers test my-gateway
|
|
665
|
+
wrosecode --provider my-gateway 'Explain this project'
|
|
666
|
+
wrosecode providers remove my-gateway
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
OAuth sign-in (ChatGPT-style) is available alongside API keys. It needs a
|
|
670
|
+
client ID from your own OAuth client registration — nothing first-party is
|
|
671
|
+
bundled:
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
wrosecode providers add openai --base-url https://api.openai.com/v1 \
|
|
675
|
+
--model gpt-5 --oauth --oauth-client-id YOUR_CLIENT_ID
|
|
676
|
+
wrosecode providers add openai --base-url https://api.openai.com/v1 \
|
|
677
|
+
--model gpt-5 --oauth --device --oauth-client-id YOUR_CLIENT_ID
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
The browser flow opens the issuer's authorize URL and waits on a short-lived
|
|
681
|
+
`127.0.0.1:1455` callback (paste the redirect URL instead when the browser
|
|
682
|
+
cannot reach this machine); `--device` prints a code to enter at the
|
|
683
|
+
issuer's verification page instead. Tokens land owner-only (`0600`) at
|
|
684
|
+
`~/.wrosecode/credentials/<name>.json`, the profile records
|
|
685
|
+
`auth_method = "oauth"`, and refresh happens automatically before use.
|
|
686
|
+
|
|
687
|
+
Use `--stdin` or `--api-key env:VARIABLE` to keep keys out of shell history. You can add repeated `--header K=V` options for non-secret custom headers.
|
|
688
|
+
|
|
689
|
+
`/new` archives the current conversation to `~/.wrosecode/sessions/` and begins a new one. Long conversations are summarized automatically before the next request once they pass 180 000 characters of history (`WROSECODE_COMPACT_CHARS` changes the threshold), with the newest request carried through the cut verbatim so the ask you just made can never be summarized away; `/compact` runs that summary immediately and reports how much context it freed. `/sessions` (or `/resume`) resumes a saved conversation, and `/move NAME` renames the current session. `/fork [name]` keeps the current session on disk and continues in a copy of it — both sides share the history up to the fork and diverge from there — and `--session ID --fork` does the same from a headless run. `/fork <n> [name]` (and headless `--fork-at N`) branches from an earlier message instead, keeping only the first n messages; `/history` prints every message with the number `/fork <n>` would branch at, and a partial fork rebuilds its visible transcript to match the shortened context. `/tree` draws the resulting forest, indenting each fork under its parent and marking the session you are in. Every run, headless or not, is written to both the SQLite store (`--session`) and the JSON files under `~/.wrosecode/sessions/` that the pickers list. `/editor` opens `$EDITOR` (default `vi`) on a temporary prompt; saving and exiting submits it.
|
|
690
|
+
|
|
691
|
+
`/init` creates `.wrosecode/project.toml` and `WROSECODE.md` in the current repository without replacing existing files. `AGENTS.md` files from the repository root up through its parent directories are loaded with `WROSECODE.md` and injected into every system prompt (8 KB per file, 32 KB per chain). `/diff` includes tracked and untracked changes. `/review` asks the model for findings without applying changes. `/commit` shows the diff, proposes a message, allows edits, and requires an explicit confirmation before staging and committing. `/issues` reads open GitHub or GitLab issues from the `origin` remote; private repositories can use `GITHUB_TOKEN` or `GITLAB_TOKEN`. `/rmslop` lists untracked scratch files and empty directories created by this session's file tool, then requires confirmation before deleting them. Every batch of `write_file`, `edit_file`, and `apply_patch` calls first captures the pre-edit bytes of exactly those paths under `.wrosecode/snapshots/undo/`; `/undo` restores the newest snapshot (removing files the edit created) and keeps the replaced tree on the redo stack, `/redo` reverses that, and a failed turn that changed nothing discards its own snapshot. Git is never touched, so snapshots work in a repository you did not initialise.
|
|
692
|
+
|
|
693
|
+
`/add PATH` pins a project file into every prompt: its contents ride along in the system prompt (capped at 6 000 characters per file and 24 000 in total) so the model never has to re-read it, `/add` alone lists the pins, and `/drop PATH` (or `/drop all`) removes them. The pin list is stored with the session, so `/sessions` and `--session` restore it, and headless runs accept the same thing as a repeatable `--add PATH` flag. For `/`-less prompting the repo map ranks files by query relevance blended with a PageRank over cross-file symbol references and stops at a 1 024-token budget.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const pkg = require('../package.json');
|
|
6
|
+
const lock = require('../package-lock.json');
|
|
7
|
+
const cargoToml = fs.readFileSync(path.resolve(__dirname, '../Cargo.toml'), 'utf8');
|
|
8
|
+
const installer = require('./install.js');
|
|
9
|
+
|
|
10
|
+
const root = path.resolve(__dirname, '..');
|
|
11
|
+
const errors = [];
|
|
12
|
+
const expectedRepo = 'https://github.com/whiterose717/WROSECODE-CLI.git';
|
|
13
|
+
const expectedHomepage = 'https://github.com/whiterose717/WROSECODE-CLI#readme';
|
|
14
|
+
const expectedBugs = 'https://github.com/whiterose717/WROSECODE-CLI/issues';
|
|
15
|
+
const expectedFiles = [
|
|
16
|
+
'npm/wrosecode.js',
|
|
17
|
+
'npm/install.js',
|
|
18
|
+
'npm/check-publish.js',
|
|
19
|
+
'LICENSE',
|
|
20
|
+
'README.md'
|
|
21
|
+
];
|
|
22
|
+
const requiredAssets = [
|
|
23
|
+
['linux', 'x64', 'wrosecode-linux-x86_64'],
|
|
24
|
+
['linux', 'arm64', 'wrosecode-linux-aarch64'],
|
|
25
|
+
['darwin', 'x64', 'wrosecode-darwin-x86_64'],
|
|
26
|
+
['darwin', 'arm64', 'wrosecode-darwin-aarch64'],
|
|
27
|
+
['win32', 'x64', 'wrosecode-windows-x86_64.exe'],
|
|
28
|
+
['win32', 'arm64', 'wrosecode-windows-aarch64.exe']
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function requireCondition(condition, message) {
|
|
32
|
+
if (!condition) errors.push(message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
requireCondition(pkg.name === 'wrosecode', 'package name must be "wrosecode".');
|
|
36
|
+
requireCondition(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(pkg.version || ''), 'package version must use semantic versioning.');
|
|
37
|
+
requireCondition(pkg.version === lock.version && lock.packages?.['']?.version === pkg.version, 'package-lock.json version does not match package.json.');
|
|
38
|
+
requireCondition(new RegExp(`^version\\s*=\\s*"${pkg.version.replaceAll('.', '\\.')}"$`, 'm').test(cargoToml), 'Cargo.toml version does not match package.json.');
|
|
39
|
+
requireCondition(typeof pkg.description === 'string' && pkg.description.trim().length > 0, 'package description is required.');
|
|
40
|
+
requireCondition(pkg.license === 'MIT', 'package license must be MIT.');
|
|
41
|
+
requireCondition(fs.existsSync(path.join(root, 'LICENSE')) && fs.readFileSync(path.join(root, 'LICENSE'), 'utf8').startsWith('MIT License'), 'MIT LICENSE file is missing or invalid.');
|
|
42
|
+
requireCondition(Array.isArray(pkg.keywords) && pkg.keywords.length > 0, 'at least one package keyword is required.');
|
|
43
|
+
requireCondition(pkg.engines?.node === '>=18', 'Node.js engine must be >=18.');
|
|
44
|
+
requireCondition(pkg.bin?.wrosecode === 'npm/wrosecode.js', 'wrosecode bin must point to npm/wrosecode.js.');
|
|
45
|
+
requireCondition(pkg.repository?.type === 'git' && pkg.repository?.url === expectedRepo, `repository.url must be ${expectedRepo}.`);
|
|
46
|
+
requireCondition(pkg.homepage === expectedHomepage, `homepage must be ${expectedHomepage}.`);
|
|
47
|
+
requireCondition(pkg.bugs?.url === expectedBugs, `bugs.url must be ${expectedBugs}.`);
|
|
48
|
+
requireCondition(JSON.stringify(pkg.files) === JSON.stringify(expectedFiles), 'package files allowlist must contain only the five approved npm distribution files.');
|
|
49
|
+
|
|
50
|
+
for (const relativePath of expectedFiles) {
|
|
51
|
+
const file = path.join(root, relativePath);
|
|
52
|
+
requireCondition(fs.existsSync(file) && fs.statSync(file).isFile(), `required package file is missing: ${relativePath}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const launcherPath = path.join(root, 'npm/wrosecode.js');
|
|
56
|
+
if (fs.existsSync(launcherPath)) {
|
|
57
|
+
const launcher = fs.readFileSync(launcherPath, 'utf8');
|
|
58
|
+
requireCondition(launcher.startsWith('#!/usr/bin/env node\n'), 'npm/wrosecode.js must have the Node.js executable shebang.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const installerPath = path.join(root, 'npm/install.js');
|
|
62
|
+
let installerSource = '';
|
|
63
|
+
if (fs.existsSync(installerPath)) {
|
|
64
|
+
installerSource = fs.readFileSync(installerPath, 'utf8');
|
|
65
|
+
for (const [platform, arch, expectedAsset] of requiredAssets) {
|
|
66
|
+
requireCondition(installer.assetName(platform, arch) === expectedAsset, `installer asset mapping is incorrect for ${platform}/${arch}.`);
|
|
67
|
+
}
|
|
68
|
+
const previousOverride = process.env.WROSECODE_RELEASE_BASE;
|
|
69
|
+
delete process.env.WROSECODE_RELEASE_BASE;
|
|
70
|
+
try {
|
|
71
|
+
requireCondition(installer.releaseBase(pkg) === `https://github.com/whiterose717/WROSECODE-CLI/releases/download/v${pkg.version}`, 'installer release URL must resolve from repository metadata and package version.');
|
|
72
|
+
} catch (error) {
|
|
73
|
+
errors.push(`installer cannot derive the release URL: ${error.message}`);
|
|
74
|
+
} finally {
|
|
75
|
+
if (previousOverride === undefined) delete process.env.WROSECODE_RELEASE_BASE;
|
|
76
|
+
else process.env.WROSECODE_RELEASE_BASE = previousOverride;
|
|
77
|
+
}
|
|
78
|
+
requireCondition(installerSource.includes("protocol !== 'https:'") && installerSource.includes("createHash('sha256')") && installerSource.includes('actual !== expected'), 'installer must enforce HTTPS and SHA-256 verification.');
|
|
79
|
+
requireCondition(!/(?:YOUR_GITHUB|YOUR_REPOSITORY|example\.com|placeholder|TODO_RELEASE_URL)/i.test(installerSource), 'installer contains a placeholder release URL or TODO.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const readmePath = path.join(root, 'README.md');
|
|
83
|
+
if (fs.existsSync(readmePath)) {
|
|
84
|
+
const readme = fs.readFileSync(readmePath, 'utf8');
|
|
85
|
+
requireCondition(readme.includes(expectedRepo.replace(/\.git$/, '')), 'README must link to the actual GitHub repository.');
|
|
86
|
+
requireCondition(!/(?:<YOUR_GITHUB_REPOSITORY_URL>|github\.com\/(?:example|your-org|your-user)\/)/i.test(readme), 'README contains a placeholder repository URL.');
|
|
87
|
+
for (const command of [
|
|
88
|
+
'npm install -g wrosecode',
|
|
89
|
+
'wrosecode',
|
|
90
|
+
'npx wrosecode',
|
|
91
|
+
'npm install -g wrosecode@latest',
|
|
92
|
+
'npm uninstall -g wrosecode'
|
|
93
|
+
]) requireCondition(readme.includes(command), `README is missing the npm command: ${command}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Scan only files that npm is explicitly permitted to publish. Never emit a
|
|
97
|
+
// matching line or credential value in diagnostics.
|
|
98
|
+
const credentialPatterns = [
|
|
99
|
+
/-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/,
|
|
100
|
+
/\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{40,}|npm_[A-Za-z0-9]{30,}|AKIA[0-9A-Z]{16})\b/,
|
|
101
|
+
/\bsk-(?:ant|proj|live|svc)?-[A-Za-z0-9_-]{32,}\b/i,
|
|
102
|
+
/\b(?:ANTHROPIC_API_KEY|OPENAI_API_KEY|NPM_TOKEN|GITHUB_TOKEN|CTFD_TOKEN)\s*[:=]\s*["']?([^\s"'#]{24,})/i
|
|
103
|
+
];
|
|
104
|
+
for (const relativePath of expectedFiles) {
|
|
105
|
+
const file = path.join(root, relativePath);
|
|
106
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) continue;
|
|
107
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
108
|
+
if (credentialPatterns.some((pattern) => pattern.test(content))) {
|
|
109
|
+
errors.push(`possible hard-coded credential detected in npm package file: ${relativePath}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function verifyPublishedRelease() {
|
|
114
|
+
const endpoint = `https://api.github.com/repos/whiterose717/WROSECODE-CLI/releases/tags/v${pkg.version}`;
|
|
115
|
+
let response;
|
|
116
|
+
try {
|
|
117
|
+
response = await fetch(endpoint, {
|
|
118
|
+
headers: {
|
|
119
|
+
accept: 'application/vnd.github+json',
|
|
120
|
+
'x-github-api-version': '2022-11-28',
|
|
121
|
+
'user-agent': 'wrosecode-npm-publish-check'
|
|
122
|
+
},
|
|
123
|
+
signal: AbortSignal.timeout(20_000)
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
errors.push(`could not verify GitHub Release v${pkg.version}: ${error.message}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
errors.push(`GitHub Release v${pkg.version} is not publicly available (HTTP ${response.status}); push the matching tag and wait for the release workflow before publishing.`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let release;
|
|
135
|
+
try {
|
|
136
|
+
release = await response.json();
|
|
137
|
+
} catch {
|
|
138
|
+
errors.push('GitHub returned invalid release metadata; cannot verify native download assets.');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
requireCondition(release.tag_name === `v${pkg.version}` && !release.draft && !release.prerelease, `GitHub Release v${pkg.version} must be published (not draft or prerelease).`);
|
|
142
|
+
const availableAssets = new Set((release.assets || []).map((asset) => asset.name));
|
|
143
|
+
const missingAssets = requiredAssets.flatMap(([, , asset]) => [asset, `${asset}.sha256`]).filter((asset) => !availableAssets.has(asset));
|
|
144
|
+
requireCondition(missingAssets.length === 0, `GitHub Release v${pkg.version} is missing assets: ${missingAssets.join(', ')}.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (errors.length) {
|
|
148
|
+
console.error('npm publish preflight failed:');
|
|
149
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
verifyPublishedRelease().then(() => {
|
|
154
|
+
if (errors.length) {
|
|
155
|
+
console.error('npm publish preflight failed:');
|
|
156
|
+
for (const error of errors) console.error(`- ${error}`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.log(`npm publish preflight passed for ${pkg.name}@${pkg.version}; matching public release assets are present.`);
|
|
161
|
+
}).catch((error) => {
|
|
162
|
+
console.error(`npm publish preflight failed: ${error.message}`);
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
});
|
package/npm/install.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs/promises');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
const MAX_BINARY_BYTES = 300 * 1024 * 1024;
|
|
9
|
+
const PLATFORM_ASSETS = {
|
|
10
|
+
'linux:x64': 'wrosecode-linux-x86_64',
|
|
11
|
+
'linux:arm64': 'wrosecode-linux-aarch64',
|
|
12
|
+
'darwin:x64': 'wrosecode-darwin-x86_64',
|
|
13
|
+
'darwin:arm64': 'wrosecode-darwin-aarch64',
|
|
14
|
+
'win32:x64': 'wrosecode-windows-x86_64.exe',
|
|
15
|
+
'win32:arm64': 'wrosecode-windows-aarch64.exe'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function assetName(platform = process.platform, arch = process.arch) {
|
|
19
|
+
const name = PLATFORM_ASSETS[`${platform}:${arch}`];
|
|
20
|
+
if (!name) {
|
|
21
|
+
throw new Error(`Unsupported platform ${platform}/${arch}. Supported targets: Linux, macOS, and Windows on x86_64 or aarch64.`);
|
|
22
|
+
}
|
|
23
|
+
return name;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function releaseBase(pkg) {
|
|
27
|
+
if (process.env.WROSECODE_RELEASE_BASE) return process.env.WROSECODE_RELEASE_BASE.replace(/\/+$/, '');
|
|
28
|
+
|
|
29
|
+
const configured = pkg.repository?.url;
|
|
30
|
+
if (typeof configured !== 'string') throw new Error('package.json is missing repository.url; cannot locate the native release.');
|
|
31
|
+
const normalized = configured.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
32
|
+
let repository;
|
|
33
|
+
try {
|
|
34
|
+
repository = new URL(normalized);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error('package.json repository.url is not a valid HTTPS GitHub URL.');
|
|
37
|
+
}
|
|
38
|
+
if (repository.protocol !== 'https:' || repository.hostname.toLowerCase() !== 'github.com' || repository.pathname.toLowerCase() !== '/whiterose717/wrosecode-cli') {
|
|
39
|
+
throw new Error('package.json repository.url must identify https://github.com/whiterose717/WROSECODE-CLI.git.');
|
|
40
|
+
}
|
|
41
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(pkg.version)) throw new Error(`Invalid package version: ${pkg.version}`);
|
|
42
|
+
return `${repository.origin}${repository.pathname}/releases/download/v${pkg.version}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function userCacheBinaryPath(version, platform = process.platform) {
|
|
46
|
+
const binaryName = platform === 'win32' ? 'wrosecode.exe' : 'wrosecode';
|
|
47
|
+
if (platform === 'win32') {
|
|
48
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
49
|
+
return path.join(localAppData, 'WROSECODE', version, binaryName);
|
|
50
|
+
}
|
|
51
|
+
const cacheHome = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
52
|
+
return path.join(cacheHome, 'wrosecode', version, binaryName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function fetchHttps(url) {
|
|
56
|
+
const parsed = new URL(url);
|
|
57
|
+
if (parsed.protocol !== 'https:') throw new Error('Release downloads must use HTTPS.');
|
|
58
|
+
const response = await fetch(parsed, { signal: AbortSignal.timeout(120_000), redirect: 'follow' });
|
|
59
|
+
if (!response.ok) throw new Error(`Download failed (${response.status} ${response.statusText}): ${parsed}`);
|
|
60
|
+
if (new URL(response.url).protocol !== 'https:') throw new Error('Release download redirected to a non-HTTPS URL.');
|
|
61
|
+
return response;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readLimited(response, limit) {
|
|
65
|
+
if (!response.body) throw new Error('Release server returned an empty response body.');
|
|
66
|
+
const reader = response.body.getReader();
|
|
67
|
+
const chunks = [];
|
|
68
|
+
let size = 0;
|
|
69
|
+
while (true) {
|
|
70
|
+
const { done, value } = await reader.read();
|
|
71
|
+
if (done) break;
|
|
72
|
+
size += value.byteLength;
|
|
73
|
+
if (size > limit) {
|
|
74
|
+
await reader.cancel();
|
|
75
|
+
throw new Error('Downloaded release asset exceeds the allowed size.');
|
|
76
|
+
}
|
|
77
|
+
chunks.push(Buffer.from(value));
|
|
78
|
+
}
|
|
79
|
+
return Buffer.concat(chunks, size);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function installBinary() {
|
|
83
|
+
// Explicit override is intended for local builds and pack/install smoke tests.
|
|
84
|
+
if (process.env.WROSECODE_BINARY) return;
|
|
85
|
+
|
|
86
|
+
const pkg = require('../package.json');
|
|
87
|
+
const base = releaseBase(pkg);
|
|
88
|
+
const name = assetName();
|
|
89
|
+
const assetUrl = `${base}/${name}`;
|
|
90
|
+
|
|
91
|
+
const binary = await readLimited(await fetchHttps(assetUrl), MAX_BINARY_BYTES);
|
|
92
|
+
if (binary.length === 0) throw new Error(`Release asset is empty: ${name}`);
|
|
93
|
+
|
|
94
|
+
const checksumBytes = await readLimited(await fetchHttps(`${assetUrl}.sha256`), 1024);
|
|
95
|
+
const checksumText = checksumBytes.toString('utf8').trim();
|
|
96
|
+
const checksumMatch = /^([a-fA-F0-9]{64})(?:\s+\*?([^\s]+))?$/.exec(checksumText);
|
|
97
|
+
if (!checksumMatch || (checksumMatch[2] && path.basename(checksumMatch[2]) !== name)) {
|
|
98
|
+
throw new Error(`Invalid SHA-256 sidecar for ${name}.`);
|
|
99
|
+
}
|
|
100
|
+
const expected = checksumMatch[1].toLowerCase();
|
|
101
|
+
const actual = crypto.createHash('sha256').update(binary).digest('hex');
|
|
102
|
+
if (actual !== expected) throw new Error(`SHA-256 verification failed for ${name}.`);
|
|
103
|
+
|
|
104
|
+
const binDir = path.join(__dirname, 'bin');
|
|
105
|
+
const destination = path.join(binDir, process.platform === 'win32' ? 'wrosecode.exe' : 'wrosecode');
|
|
106
|
+
try {
|
|
107
|
+
await writeBinary(destination, binary);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Global installs may be root-owned while npm lifecycle scripts are
|
|
110
|
+
// disabled. Keep the executable in the user's cache in that case.
|
|
111
|
+
if (!['EACCES', 'EPERM', 'EROFS'].includes(error.code)) throw error;
|
|
112
|
+
await writeBinary(userCacheBinaryPath(pkg.version), binary);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function writeBinary(destination, binary) {
|
|
117
|
+
const destinationDir = path.dirname(destination);
|
|
118
|
+
const temporary = `${destination}.tmp-${process.pid}`;
|
|
119
|
+
await fs.mkdir(destinationDir, { recursive: true, mode: 0o700 });
|
|
120
|
+
try {
|
|
121
|
+
await fs.writeFile(temporary, binary, { mode: 0o755, flag: 'wx' });
|
|
122
|
+
if (process.platform !== 'win32') await fs.chmod(temporary, 0o755);
|
|
123
|
+
try {
|
|
124
|
+
await fs.rename(temporary, destination);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (process.platform !== 'win32' || !['EEXIST', 'EPERM', 'ENOTEMPTY'].includes(error.code)) throw error;
|
|
127
|
+
await fs.rm(destination, { force: true });
|
|
128
|
+
await fs.rename(temporary, destination);
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
await fs.rm(temporary, { force: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (require.main === module) {
|
|
136
|
+
installBinary().catch((error) => {
|
|
137
|
+
console.error(`wrosecode: native binary installation failed: ${error.message}`);
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { assetName, releaseBase, userCacheBinaryPath };
|
package/npm/wrosecode.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const pkg = require('../package.json');
|
|
10
|
+
const { userCacheBinaryPath } = require('./install.js');
|
|
11
|
+
const packagedBinary = path.join(__dirname, 'bin', process.platform === 'win32' ? 'wrosecode.exe' : 'wrosecode');
|
|
12
|
+
const cachedBinary = userCacheBinaryPath(pkg.version);
|
|
13
|
+
let binary = process.env.WROSECODE_BINARY || [packagedBinary, cachedBinary].find((candidate) => fs.existsSync(candidate)) || null;
|
|
14
|
+
|
|
15
|
+
if (!binary) {
|
|
16
|
+
// npm can be configured to skip lifecycle scripts. Install lazily on first run
|
|
17
|
+
// in that case, but fail instead of launching without a native executable.
|
|
18
|
+
const installer = spawnSync(process.execPath, [path.join(__dirname, 'install.js')], { stdio: 'inherit' });
|
|
19
|
+
if (installer.error) {
|
|
20
|
+
console.error(`wrosecode: could not run native installer: ${installer.error.message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
if (installer.status !== 0) process.exit(installer.status ?? 1);
|
|
24
|
+
binary = [packagedBinary, cachedBinary].find((candidate) => fs.existsSync(candidate)) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!binary) {
|
|
28
|
+
console.error('wrosecode: native executable is missing after installation. Reinstall the package or set WROSECODE_BINARY to an executable path.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const child = spawn(binary, process.argv.slice(2), {
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
windowsHide: true
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let signalForwarded = false;
|
|
38
|
+
const signalsToForward = process.stdin.isTTY && process.stdout.isTTY ? ['SIGTERM'] : ['SIGINT', 'SIGTERM'];
|
|
39
|
+
for (const signal of signalsToForward) {
|
|
40
|
+
process.on(signal, () => {
|
|
41
|
+
if (signalForwarded || child.exitCode !== null || child.signalCode !== null || child.pid === undefined) return;
|
|
42
|
+
signalForwarded = true;
|
|
43
|
+
try {
|
|
44
|
+
child.kill(signal);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code !== 'ESRCH') {
|
|
47
|
+
console.error(`wrosecode: could not forward ${signal} to native process: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let spawnFailed = false;
|
|
54
|
+
child.once('error', (error) => {
|
|
55
|
+
spawnFailed = true;
|
|
56
|
+
console.error(`wrosecode: could not start native executable: ${error.message}`);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
child.once('close', (code, signal) => {
|
|
61
|
+
if (spawnFailed) return;
|
|
62
|
+
if (code !== null) {
|
|
63
|
+
process.exitCode = code;
|
|
64
|
+
} else if (signal) {
|
|
65
|
+
process.exitCode = 128 + (os.constants.signals[signal] || 1);
|
|
66
|
+
} else if (process.exitCode === undefined) {
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
}
|
|
69
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wrosecode",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A terminal coding agent written in Rust",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"cli",
|
|
8
|
+
"coding-agent",
|
|
9
|
+
"developer-tools",
|
|
10
|
+
"terminal",
|
|
11
|
+
"rust"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/whiterose717/WROSECODE-CLI.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/whiterose717/WROSECODE-CLI#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/whiterose717/WROSECODE-CLI/issues"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"wrosecode": "npm/wrosecode.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"npm/wrosecode.js",
|
|
29
|
+
"npm/install.js",
|
|
30
|
+
"npm/check-publish.js",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "cargo build --release --locked",
|
|
36
|
+
"check": "node --check npm/wrosecode.js && node --check npm/install.js && node --check npm/check-publish.js",
|
|
37
|
+
"postinstall": "node npm/install.js",
|
|
38
|
+
"prepack": "npm run check",
|
|
39
|
+
"prepublishOnly": "npm run check && node npm/check-publish.js"
|
|
40
|
+
}
|
|
41
|
+
}
|