@trybullet/cli 1.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +366 -0
  2. package/bin/bullet.js +20 -0
  3. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,366 @@
1
+ # Bullet ⁍
2
+
3
+ **The fastest coding agent.** A minimalist macOS desktop app: open a folder and
4
+ Bullet plans, edits files, runs commands, verifies its work, and shows you
5
+ everything live. Zero setup — runs on-device with no key.
6
+
7
+ ## How it works
8
+
9
+ - **Agent loop** (custom, ~200 lines — no framework): plan → tool calls
10
+ (read / edit / write / delete / run) → observations → verify → done. Hardened
11
+ with failure-retry directives, stuck detection, duplicate suppression, a 45s
12
+ wall-clock cap, and a definition-of-done verification gate.
13
+ - **Router:** a nano-model semantic classifier (hard 4s cap — routing is never
14
+ the wait) rates each prompt on five difficulty levels. No key → everything
15
+ runs on-device (Qwen2.5-Coder via Metal, warm KV-cache sessions). With a key
16
+ → each level buys a deeper model *and* more thinking effort: quick asks run
17
+ on fast models (Haiku / Codex-low), harder work escalates (Sonnet 5 / Codex
18
+ at rising effort), and *extreme* tasks go to Fable 5 / Sol at full effort
19
+ (say "use fable", "use sol", or "think hard" to force the level). The debug
20
+ badge shows the exact model, tier, and effort used.
21
+ - **Plan gate:** asking to plan ("help me plan X", "let's figure out the
22
+ approach first") is its own classifier answer — those turns draft a plan and
23
+ stop instead of working. Difficulty alone never triggers it: a big task you
24
+ told Bullet to *do* just gets done.
25
+ - **Providers:** Bring a ChatGPT/Codex or Claude Code subscription, or use an
26
+ Anthropic, OpenAI, or xAI API key. Switch methods and providers in Settings
27
+ (⌘,). OAuth credentials are encrypted locally; keys stay on this Mac.
28
+ - **Tools:** read / write / edit / delete, `search_code`, `find_files`,
29
+ `run_command`, background processes, web search + fetch, and a real browser
30
+ (below). File tools reach any absolute path; commands run from the workspace.
31
+ - **Code search that lands.** `search_code` is extended-regex, so `(a|b)`, `\d`
32
+ and `+` mean what the model thinks they mean — grep's default dialect treats
33
+ them as literal characters, which silently answered "no matches" for code that
34
+ was right there. A pattern that finds nothing is retried as literal text
35
+ before that answer is allowed, matches inside minified files are windowed with
36
+ a column instead of dumping a 400 KB line, `*.{ts,tsx}` is expanded, and a
37
+ directory you explicitly point at is never excluded as noise.
38
+ - **Eyes.** `open_page` → `screenshot` → `read_page` → `page_action` /
39
+ `page_eval` drive an offscreen Chromium (no Playwright, no extra dependency).
40
+ The screenshot goes to the model *and* into the transcript, so a visual change
41
+ is verified by looking at it. `read_page` always reports console errors and
42
+ failed requests, every load clears the cache, and `page_action` includes
43
+ relative mouse-look for pointer-locked pages. Older captures are pruned from
44
+ the context so a long visual session doesn't pay for stale pixels.
45
+ - **Speed levers:** warm KV sessions (no re-processing between turns),
46
+ patch-based edits (fewer output tokens), prompt caching on Anthropic,
47
+ parallel tool execution on cloud, flash attention on Metal, idle model
48
+ unload (frees ~2 GB after 5 min). One dev server per command: re-running
49
+ `npm run dev` returns the server already running with its URL rather than
50
+ starting a seventh one on a seventh port, and `stop_process` ends it.
51
+
52
+ ## UI
53
+
54
+ - **Sidebar grouped by folder** — chats are bound to their folder; agents in
55
+ different folders run concurrently (busy dot), and agents in the *same*
56
+ folder are told about each other's recent edits.
57
+ - **Live activity**: plan checklist ticks in real time, action rows
58
+ (`✓ Edited app.js +10 −3`, click to expand output), a status pulse with an
59
+ elapsed counter through every silent reasoning stretch (never looks frozen;
60
+ dead connections error out via stream watchdogs), and a stop button (Esc).
61
+ - **Resizable panels**: drag the sidebar and Activity/Preview edges; widths persist.
62
+ - **Activity panel** (right): running/finished commands + per-file diffs.
63
+ - **Plan panel**: a plan-first request drafts markdown (goal, approach, risks,
64
+ steps) into the side panel and *waits* — reply with feedback to iterate, ask
65
+ questions and the plan stays put, edit it directly, then **Run plan** (or
66
+ "run it") executes. Steps tick as the agent works; mid-run edits steer it.
67
+ The plan lives in the panel, never in the chat. Discard, or just move on to
68
+ other work, exits plan mode.
69
+ - **LaTeX**: `$$…$$`, `\[…\]`, `\(…\)`, and math-y `$…$` render via KaTeX
70
+ (bundled, offline); dollar amounts in prose stay literal.
71
+ - **Preview panel**: in-app browser for whatever the agent is serving. It
72
+ opens itself when the agent starts a dev server, and auto-reloads (cache
73
+ cleared) after each edit burst — toggleable via the `auto` button.
74
+ - **Prompt history**: ↑/↓ in the composer cycles your previous prompts,
75
+ terminal-style; your in-progress draft is preserved.
76
+ - **Attachments**: 📎, drag in, or **paste** code/text, images & screenshots
77
+ (sent as vision to cloud models), and PDF / Word / Excel / PowerPoint
78
+ (auto-extracted to text — works with the local model too).
79
+ **Rewind/fork**: hover any of your messages —
80
+ rewinding confirms first, then also reverts the file edits made after that
81
+ point (command side-effects are not undone). **Two themes**: Midnight / Paper.
82
+
83
+ ## CLI
84
+
85
+ The terminal front end. Not a port — it loads `electron/router.js` and every
86
+ module under it, so routing, tools, skills, sub-agents, the rewind ledger and
87
+ the provider auth are byte-for-byte the same code the app runs.
88
+
89
+ ```bash
90
+ npm link # puts `bullet` on your PATH
91
+ bullet auth # Claude / ChatGPT subscription, or an API key
92
+ bullet # interactive session in this folder
93
+ bullet "fix the failing tests" # run one task, then keep talking
94
+ bullet -p "what does app.js do" # print and exit — pipeable
95
+ git diff | bullet -p "review this"
96
+ ```
97
+
98
+ Inside a session: `enter` sends, `ctrl+j` newlines, `esc` stops a running turn,
99
+ `tab` completes `/commands` and `@files`, `↑` walks history. `/help` lists the
100
+ rest — `/model`, `/cost`, `/diff`, `/undo`, `/resume`, `/agents`.
101
+
102
+ Sign-in reuses whatever is already on the machine: an existing Claude Code or
103
+ Codex login is imported directly, no browser round-trip. Credentials are
104
+ encrypted with the OS keychain (macOS Keychain, `secret-tool` on Linux) and kept
105
+ in `~/.bullet`, separate from the app's own store.
106
+
107
+ In scripts, `-p` exits `0` when it answered, `1` when the turn errored, `130`
108
+ when you interrupted it, and `2` on bad arguments — so `bullet -p … || handle-it`
109
+ does what it looks like. stdout carries only the answer; routing notes and
110
+ warnings go to stderr.
111
+
112
+ Two capabilities do not cross over. The agent's browser — `open_page`,
113
+ `screenshot`, `read_page` — needs the offscreen Chromium only Electron can host,
114
+ so those tools are removed from the kit and the system prompt is told, rather
115
+ than left to fail mid-turn. And with no provider connected the CLI falls back to
116
+ the same on-device model the app uses; that is a one-time ~2 GB download, so it
117
+ asks first, and `-p` refuses rather than pulling 2 GB in a script nobody is
118
+ watching.
119
+
120
+ How it stays one codebase: `cli/shim.js` registers itself as the `electron`
121
+ module before anything under `electron/` is required, and supplies the four
122
+ things the shared core actually asks Electron for — a user-data directory,
123
+ `safeStorage`, `shell.openExternal`, and a `BrowserWindow` that explains why it
124
+ isn't there. Everything in `cli/` above that line is presentation.
125
+
126
+ ## Run
127
+
128
+ ```bash
129
+ npm install
130
+ npm start # dev
131
+ npm test # UI liveness + feature regressions (offline, no key)
132
+ npm run cli # the CLI, from source
133
+ npm run test:cli # CLI suites (plain node, offline, no key)
134
+ npm run dist # local unsigned build (right-click → Open)
135
+ ```
136
+
137
+ `npm run eval:classifier` additionally checks routing quality — plan intent and
138
+ plan-approval verdicts — against a live nano model, so it needs a provider key.
139
+
140
+ ## Releasing
141
+
142
+ Installed copies update themselves: the app checks
143
+ `https://api.davidhf.com/bullet/v1/updates/mac/latest-mac.yml` on launch and
144
+ every six hours, downloads in the background, and installs on quit (Settings ›
145
+ Updates has a manual check and a *Restart to update* button). Nobody has to
146
+ delete and reinstall.
147
+
148
+ **This only works for signed builds.** macOS applies updates through
149
+ Squirrel.Mac, which requires the downloaded bundle to satisfy the running app's
150
+ code-signing requirement — an ad-hoc signed build can never update itself, and
151
+ publishing one strands every user who installs it on that version.
152
+ `scripts/publish-update.sh` refuses to publish an unsigned build for that reason.
153
+
154
+ One-time setup on the build Mac:
155
+
156
+ 1. In Xcode › Settings › Accounts, sign in with the Apple Developer account, then
157
+ **Manage Certificates › + › Developer ID Application**. Confirm with
158
+ `security find-identity -v -p codesigning` — you need a line reading
159
+ `Developer ID Application: … (TEAMID)`, not just `Apple Development`.
160
+ 2. Create an app-specific password at <https://appleid.apple.com> › Sign-In and
161
+ Security › App-Specific Passwords — notarization rejects the account
162
+ password — and put the credentials in `.env.notarize` at the repo root:
163
+
164
+ ```bash
165
+ APPLE_ID="you@example.com"
166
+ APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
167
+ APPLE_TEAM_ID="YOURTEAMID" # Membership details on developer.apple.com
168
+ ```
169
+
170
+ `npm run release` loads that file itself. It is gitignored, it must stay
171
+ that way, and the Team ID in it has to match the team that issued the
172
+ signing certificate — the script checks and stops if they disagree, because
173
+ Apple rejects the notarization otherwise.
174
+
175
+ Then each release:
176
+
177
+ ```bash
178
+ npm version patch # or edit "version" — the feed compares against it
179
+ npm run release # signs + notarizes, writes release/latest-mac.yml
180
+ npm run release:publish # uploads to the update feed on the API box
181
+ ```
182
+
183
+ `npm run release` takes a few extra minutes the first time — notarization is a
184
+ round trip to Apple. Running clients pick the new version up within six hours,
185
+ or immediately from Settings › Updates.
186
+
187
+ ### Signing on someone else's Mac
188
+
189
+ **Bullet is signed on one specific Mac, and that never changes.** Squirrel.Mac
190
+ only applies an update whose signature satisfies the running app's requirement,
191
+ so every release must be signed by the same Developer ID / Team ID as the one
192
+ before it. Signing a release under a different team strands every installed
193
+ copy — those users would each need a manual reinstall to get updates back.
194
+ Keep a `.p12` export of that certificate (and its password) somewhere safe
195
+ off that machine: losing access to it means losing the ability to ship updates
196
+ to everyone already running Bullet.
197
+
198
+ Whoever signs builds from this repo rather than re-signing someone else's `.app`
199
+ — electron-builder rebuilds the native modules and signs the unpacked
200
+ `node-llama-cpp` binaries as part of packaging, and any hand-editing of a signed
201
+ bundle invalidates both the signature and the sha512 in `latest-mac.yml`.
202
+
203
+ On their Mac: Xcode command line tools, Node, the **Developer ID Application**
204
+ certificate in the login keychain, then
205
+
206
+ ```bash
207
+ git clone … && cd bullet && npm install
208
+ # write .env.notarize with the Apple ID, app-specific password, and the Team ID
209
+ # that issued the certificate on this Mac (see step 2 above)
210
+ npm run release
211
+ ```
212
+
213
+ (`CSC_NAME="Developer ID Application: Name (TEAMID)"` if the keychain holds more
214
+ than one Developer ID certificate.) Leave `build/entitlements.mac.plist` alone —
215
+ JIT, unsigned executable memory, and library validation are all off-limits to
216
+ strip, because the on-device model needs them under the hardened runtime.
217
+
218
+ Checks before handing the build over:
219
+
220
+ ```bash
221
+ codesign -dv --verbose=2 release/mac-arm64/Bullet.app # Authority=Developer ID Application: … (TEAMID)
222
+ spctl -a -vv release/mac-arm64/Bullet.app # accepted, source=Notarized Developer ID
223
+ xcrun stapler validate release/Bullet-*-arm64.dmg
224
+ ```
225
+
226
+ Then hand over `release/` intact — the `.zip` (electron-builder's, never a
227
+ Finder-made one, which mangles symlinks and the signature), its `.blockmap`, the
228
+ `.dmg`, and the `latest-mac.yml` from that same build, whose hashes must match
229
+ the files beside it. `npm run release:publish` does the upload and needs the VM
230
+ SSH key, so it is easiest for whoever holds that key to run it on the received
231
+ `release/` directory.
232
+
233
+ ## SWE-bench Verified
234
+
235
+ `scripts/swe/` runs Bullet's real agent over SWE-bench Verified and grades every
236
+ patch with the official Docker scorer — see
237
+ [scripts/swe/README.md](scripts/swe/README.md).
238
+
239
+ **All 500 instances, officially scored: 479/500 = 95.8%.** Native x86, one attempt
240
+ per instance, network on, `gpt-5.6-sol`, `swebench.harness.run_evaluation`. Zero
241
+ empty patches, zero worker errors, every instance graded.
242
+
243
+ | | resolved | round trips¹ | latency | cost/inst |
244
+ |---|---|---|---|---|
245
+ | **Bullet — all 500** | **479/500 = 95.8%** | **15.0** | **119s** | **$0.73** |
246
+ | mini-swe-agent + sol@max — all 500 | 421/500 = 84.2% | 12.6 | 91s | $0.29 |
247
+
248
+ Resolve rate by repository, so it is clear no single project is carrying it:
249
+ django 96.5% (223/231), sympy 97.3%, sphinx 95.5%, matplotlib 94.1%,
250
+ scikit-learn 100%, xarray 95.5%, astropy 81.8%, pytest 100%, pylint 90%,
251
+ requests 87.5%.
252
+
253
+ **The 17-point asterisk.** Both rows above have network access, which is the
254
+ reference's default and — on the evidence of its cost and latency — the published
255
+ leaderboards' too. That matters enormously: these instances are public GitHub PRs,
256
+ so an agent with a route out can fetch the merged upstream fix, and the reference
257
+ does exactly that on 23% of instances (`curl raw.githubusercontent.com/<the very
258
+ file>`, `api.github.com/search/issues?q=<the bug>`). Measured directly on the
259
+ random 120, same configuration, only the network changed:
260
+
261
+ | | resolved |
262
+ |---|---|
263
+ | network on | 113/120 = 94.2% |
264
+ | `--network none`, no web tools | 92/120 = 76.7% |
265
+
266
+ **So 95.8% is the leaderboard-comparable number and ~77% is the from-the-repository
267
+ -alone number.** Offline is the default here; `SWE_NETWORK=1` is the switch.
268
+
269
+ ¹Round trips — model calls per instance — is the only speed figure worth quoting.
270
+ Wall clock is provider-load dependent: the *same* configuration on the *same*
271
+ instances measured 131s and 248s nine hours apart with the machine idle. Turns
272
+ don't drift. `profile.js` reports both.
273
+
274
+ **What the turn-efficiency work bought**, paired on the same instances: 17.2 → 14.4
275
+ round trips and $0.93 → $0.68 an instance, for an accuracy change that is not
276
+ statistically separable (0 gained, 2 lost, McNemar p = 0.50 — a 1-2 point cost is
277
+ possible and unproven). Against the reference on the shared 120, Bullet resolved
278
+ every instance it resolved and 17 more, nothing the other way (**p < 0.001**).
279
+
280
+ A gold control on this machine resolves 117/120 reference patches on the random
281
+ 120; the three exceptions (a live HTTP call, an env-sensitive header test, an
282
+ extension-version test) cannot pass whatever the patch does. So roughly 2.5% of any
283
+ raw rate here is the machine, not the agent — `ceiling.js` reports the corrected
284
+ figure. No gold control was run over all 500, so the 95.8% above is raw.
285
+
286
+ **What moved it.** Routing: the deep model resolves 62.1% at 138s against the fast
287
+ model's 36.8% at 152s, so the best model leads at every level instead of being an
288
+ escalation. Reliability: an earlier run submitted 13 blank patches from dead
289
+ workers, and there are none now. Two defects found by measurement rather than by
290
+ reading:
291
+
292
+ - **`search_code` ran in grep's BRE dialect**, where `(`, `|` and `+` are literal
293
+ characters — so `function (alpha|beta)` reported "no matches" for code plainly
294
+ present. Searches are 40% of all tool calls, so this cost both accuracy and a
295
+ great many wasted round-trips.
296
+ - **The shared git mirror had a creation race.** `git clone` makes its directory
297
+ immediately and fills it over minutes, so a worker that only tested existence
298
+ cloned from a half-written mirror and lost the instance one second in. Six of 120,
299
+ all in the repositories eight workers wanted at once; all six resolved on re-run.
300
+
301
+ A third defect, found by a one-instance smoke test after the runs above: an
302
+ undeclared timer variable threw at the first phase write, silently aborting the tail
303
+ of every attempt — losing the reproduction it was meant to collect, which is what
304
+ the candidate selector runs on. The patch survived because the error path recovers
305
+ it from disk, which is exactly why it went unnoticed.
306
+
307
+ Two other levers were paired, graded and landed within noise: the methodology brief
308
+ versus bare environment facts, and structured tools versus a bare shell.
309
+
310
+ **Latency is round-trip count and nothing else.** Instrumented per tool over the
311
+ 500: an instance is 120s, of which **checkout 0.3s, container start 0.7s, collect
312
+ 0.5s, tool execution 14s (12%, four fifths of it real test suites) — and 104s of
313
+ model thinking**, i.e. 15 round trips at 7s each. There is nothing to win in the
314
+ scaffolding; the only lever is needing fewer turns. Bullet's 15 against the
315
+ reference's 12.6 is the entire speed difference between them. Beware comparing
316
+ *tool calls* instead: Bullet issues ~38 of those, but batches 2.5 per turn, and
317
+ confusing the two overstated the gap threefold in an earlier draft of this file.
318
+ `profile.js` reports both.
319
+
320
+ Two things carry the result, and a third had to be taken back out:
321
+
322
+ - **A testbed the agent can run.** The attempt works in the instance's prepared
323
+ container, so it can actually reproduce the bug and run the project's tests
324
+ instead of reasoning about code it cannot execute. File tools keep editing a
325
+ local checkout (that is where the diff comes from) and edits are pushed across
326
+ before every command.
327
+ - **The best model, at full depth, first.** Measured over 86 graded attempts, the
328
+ deep model resolved 62.1% at 138s against the fast model's 36.8% at 152s — better
329
+ *and* quicker, because it needs far fewer steps. Escalation is right for chat and
330
+ wrong here, so there is no ladder: one attempt, strongest model, highest effort.
331
+ - **The ensemble is off by default, and the reason is not yet settled.** Several
332
+ attempts under different lenses, each leaving a reproduction, every reproduction
333
+ run against every candidate in fresh containers, a reviewer to break ties — that
334
+ configuration scored 104/120 against a single deep attempt's 113/120, losing nine
335
+ instances and winning none (p = 0.004). But that comparison is confounded: an
336
+ undeclared variable in the phase stopwatch was throwing partway through every
337
+ attempt, and the statement it aborted was the one that collected the attempt's
338
+ reproduction. So the selector — whose whole job is weighing reproductions — was
339
+ running on almost none, and fell back to preferring the first, shortest candidate
340
+ on 97 of 114 instances. The bug is fixed; the experiment has not been repeated.
341
+ One attempt is the default because it is the best *measured* configuration, not
342
+ because the ensemble is known to be worse. An oracle over the pair scored
343
+ 105/114, so the headroom is real and `SWE_K` still turns it back on.
344
+
345
+ The benchmark's answer key — test patch, reference patch,
346
+ `FAIL_TO_PASS`/`PASS_TO_PASS` — never reaches inference; `export-instances.py`
347
+ asserts those fields out of the data the agent sees. `SWE_NETWORK=1` is the switch
348
+ behind the two regimes in the table; without it the web tools are removed and every
349
+ container runs `--network none`.
350
+
351
+ First prompt downloads the on-device model once (~2 GB), then it's local.
352
+ Apple Silicon only.
353
+
354
+ ## Layout
355
+
356
+ ```
357
+ electron/ main.js (window/IPC) · preload.js · router.js (routing+context)
358
+ engine.js (local agent loop) · cloud.js (cloud loops) · tools.js
359
+ src/ index.html · styles.css · app.js
360
+ cli/ bin.js · shim.js (fake `electron` for plain node) · main.js (repl)
361
+ session.js (host wiring) · render.js (events → transcript) · ui/
362
+ scripts/ icon/logo generators · ui-test.js / feature-test.js (offline suites)
363
+ cli-test.js (CLI suites) · classifier-eval.js
364
+ eval.js / swebench.js / polyglot.js (live evals)
365
+ BENCHMARK.md one-shot website benchmark vs Claude Code / Codex
366
+ ```
package/bin/bullet.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Dispatcher only. Every line of real Bullet logic lives in the compiled binary
4
+ // shipped by the matching @trybullet/cli-<platform> package; this file just execs it.
5
+ const { spawnSync } = require('child_process');
6
+ const arch = process.arch;
7
+ const platform = process.platform;
8
+ const ext = platform === 'win32' ? '.exe' : '';
9
+ const pkg = platform + '-' + arch;
10
+ let bin;
11
+ try {
12
+ bin = require.resolve('@trybullet/cli-' + pkg + '/bin/bullet' + ext);
13
+ } catch {
14
+ console.error('bullet: no prebuilt binary for ' + pkg + '.');
15
+ console.error('Supported: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64.');
16
+ process.exit(1);
17
+ }
18
+ const r = spawnSync(bin, process.argv.slice(2), { stdio: 'inherit' });
19
+ if (r.error) { console.error('bullet: failed to launch (' + r.error.message + ')'); process.exit(1); }
20
+ process.exit(r.status == null ? 1 : r.status);
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@trybullet/cli",
3
+ "version": "1.3.12",
4
+ "description": "The fastest coding agent — terminal edition. Same router, tools and agent loop as the Bullet desktop app, in your shell.",
5
+ "license": "UNLICENSED",
6
+ "homepage": "https://usebullet.com",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/trybullet/bullet.git"
10
+ },
11
+ "keywords": [
12
+ "cli",
13
+ "coding-agent",
14
+ "ai",
15
+ "agent",
16
+ "claude",
17
+ "codex"
18
+ ],
19
+ "author": "Bullet",
20
+ "bin": {
21
+ "bullet": "bin/bullet.js"
22
+ },
23
+ "files": [
24
+ "bin/bullet.js"
25
+ ],
26
+ "optionalDependencies": {
27
+ "@trybullet/cli-darwin-arm64": "1.3.12",
28
+ "@trybullet/cli-darwin-x64": "1.3.12",
29
+ "@trybullet/cli-linux-x64": "1.3.12",
30
+ "@trybullet/cli-linux-arm64": "1.3.12",
31
+ "@trybullet/cli-win32-x64": "1.3.12"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }