faberwright 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,28 +1,15 @@
1
1
  # Faber
2
2
 
3
- **An agentic AI coding assistant for your terminal.** Give it a task in plain English; it explores your repository, edits files with your approval, runs your tests, and remembers your project across sessions.
3
+ **A cost and performance efficient agentic AI coding assistant for your terminal.** Give it a task in plain English; it explores your repository, edits files with your approval, runs your tests, and remembers your project across sessions.
4
4
 
5
5
  *Faber — Latin for craftsman. Yours lives at `faber`.*
6
+ * Read: [Faber Medium](https://medium.com/@jshial25/why-should-an-ai-coding-agent-read-hundreds-of-files-to-answer-one-question-d6369d29dfa5?postPublishedType=repub)
7
+ ## Code Graph
8
+ <img width="636" height="573" alt="Screenshot 2026-08-09 at 2 58 30 PM" src="https://github.com/user-attachments/assets/176e5093-8e29-452f-a749-d06a561c583a" />
9
+
10
+ ## Demo
11
+ <img width="1240" height="700" alt="terminal_demo_compressed" src="https://github.com/user-attachments/assets/f78a8f68-130e-4f37-90f2-7a7089fa2653" />
6
12
 
7
- ```
8
- $ faber "add input validation to the signup endpoint and cover it with tests"
9
- ⚙ trace_path (from=main, to=handleSignup)
10
- ⚙ read_file (path=app/routes/auth.ts)
11
-
12
- Proposed change to app/routes/auth.ts:
13
- @@ -12,6 +12,9 @@
14
- + if (!isEmail(req.body.email)) return res.status(400)...
15
- Apply? ↑/↓ then Enter
16
- ❯ Yes
17
- No
18
- Always this session
19
-
20
- ⚙ run_shell (command=npm test)
21
- ─ result ────────────────────────────
22
- Added email/password validation to /signup, verified with 4 new passing tests.
23
- ─────────────────────────────────────
24
- tokens: 31.2k in (78% cached) / 1.9k out · 7 calls
25
- ```
26
13
 
27
14
  ## Your first session (2 minutes)
28
15
 
@@ -43,7 +30,9 @@ That loop of task, approve, inspect, revert is the whole trust model. Everything
43
30
 
44
31
  **It has a map, not just eyes.** The code graph stores call and import *edges*, incrementally updated in milliseconds. One ~50-token query (`trace_path(main, saveUser)` → `main → startServer → handleSignup → saveUser`) replaces reading thousands of tokens of files. `/map main` prints the call tree — the "trace it from main" ritual every programmer does, automated.
45
32
 
46
- **It's honest about money.** Prompt caching marks the stable prefix of every request, so repeat loop iterations pay ~10% for tokens already sent. And after every task you see exactly what happened: `tokens: 31.2k in (78% cached) / 1.9k out · 7 calls`.
33
+ **Every model you can reach, including the coding ones.** Claude through the Anthropic API or your own AWS account, OpenAI through both of its APIs — chat completions and the Responses API that the codex family requires — plus anything OpenAI-compatible, and local models through Ollama for no key and no cost. Faber reads which endpoint each model needs and routes there itself, so `gpt-5.3-codex` and `claude-opus-5` are both just entries in the same list, priced side by side.
34
+
35
+ **It's honest about money.** Prompt caching marks the stable prefix of every request, so repeat loop iterations pay ~10% for tokens already sent. Every task is costed and recorded as it runs, so `/usage` is a ledger of what you actually spent rather than an estimate — broken down by time window and by model, with the cheapest option always visible next to the one you're using.
47
36
 
48
37
  **Nothing is irreversible.** Every task is checkpointed before it touches a file. Undo is undoable. Approvals gate both edits *and* shell commands. Rejecting a change tells the model to change course, not retry.
49
38
 
@@ -51,10 +40,6 @@ That loop of task, approve, inspect, revert is the whole trust model. Everything
51
40
 
52
41
  **You can steer it mid-flight.** See it going the wrong way? Just type `use TypeScript, not JavaScript` — and your guidance is injected at the next loop iteration. No cancelling, no wasted tokens.
53
42
 
54
- ## Upgrading from Codewright
55
-
56
- Faber is Codewright renamed, after the npm name was taken between building and releasing. Existing projects keep working untouched: an existing `.codewright/` state directory is adopted as-is, so memory, code graph, sessions, and usage history all survive. `CW_*` environment variables still work alongside the canonical `FABER_*` names. New projects get `.faber/`.
57
-
58
43
  ## Requirements & install
59
44
 
60
45
  You need Node.js 22.5 or newer. Faber uses Node's built-in SQLite, so there are no native dependencies to compile and installs don't fail on a missing toolchain. The only runtime dependencies are two small pure-JS packages.
@@ -108,17 +93,10 @@ Setup is skipped entirely when there's no terminal attached, so scripts and CI n
108
93
 
109
94
  ## Features
110
95
 
111
- | | |
96
+ | Feature|Description |
112
97
  |---|---|
113
98
  | **Streaming agent loop** | Text renders as generated, with a live heartbeat (`✳ Working… 14s`) that never interleaves with output and ends in `✳ Worked for 14s`. Plan → tool → observe → adjust, until done. Hard iteration cap. |
114
99
  | **Code graph** | Symbols *and* call/import edges, incrementally maintained (only changed files re-parse). Agent tools: `who_calls` (blast radius), `calls_from` (dependencies), `trace_path` (workflow chain). A compact repo map of the most-connected symbols orients every task. Edges are static hints — dynamic dispatch/DI/events aren't captured; the agent reads code where precision matters. |
115
- | **Approval by default** | Arrow-key menu on every file edit (colored diff) and every shell command. `--auto` / `/auto` / `FABER_APPROVAL=auto` opts into autonomy. |
116
- | **Guided setup** | First run walks through vendor, route, credential and model, then remembers it. Later launches verify the setup can reach a model before opening the prompt. Nothing is saved until setup finishes, so an interrupted run leaves no half-configured state. |
117
- | **Credentials** | Keys live in `~/.faber/credentials.json`, owner-only, outside every repository. Faber checks that what you paste looks like a key before storing it, hides it as you type, and never prints more than a masked fragment. Environment variables keep working and take precedence. |
118
- | **Interactive choices** | Genuinely ambiguous request? The agent presents 2–4 options plus "Chat more about this instead" before writing code. |
119
- | **Mid-task steering** | Type while it works; guidance is injected at the next loop boundary. If the model finishes while steering is queued, the task continues instead. Steering markers carry a per-task nonce, so hostile file contents can't impersonate you. |
120
- | **Paste as chips** | Raw-mode composer: pasting a 50-line block renders only a chip — `[pasted #1 +50 lines]` — never the code itself, while the full text is expanded into the message on Enter. Paste the same block again to expand it visibly. Paste, type, paste again: one submission. Full line editing: arrows, Home/End, forward-delete, Ctrl-A/E/K/U, Up/Down history; long drafts wrap across rows with exact cursor tracking — backspace and arrows travel across wrap boundaries. Chips are atomic — one arrow step, one backspace. Works at the prompt and while steering. |
121
- | **Reversible history** | `/history` lists tasks with files touched; `/restore <id>` jumps anywhere; `/undo` / `/redo` — restores are never destructive. |
122
100
  | **Git-aware** | Warns about uncommitted changes at startup. Optional `FABER_GIT=commit`: one commit per completed *task* (never per edit). The agent never commits by default. |
123
101
  | **Two-layer memory** | Short-term: token-budgeted window, auto-summarized past 60k (tool pairs never split). Long-term: SQLite+FTS5 facts/decisions/gotchas + per-file notes, with full lifecycle (`/forget`, `/archive`, `/prune`). |
124
102
  | **Sessions** | Crash-safe JSONL transcripts; `--resume`; last-session digest injected at startup; `recall_sessions` keyword search across history. |
@@ -128,6 +106,11 @@ Setup is skipped entirely when there's no terminal attached, so scripts and CI n
128
106
  | **Usage dashboard** | `/usage` shows a persistent ledger: tasks, tokens, cache rate, cost, and cache *savings* — for this session, today, and all time, plus totals across every project on the machine. Costs use each model's own rates, including its exact cache read/write prices, so a history spanning a model switch stays accurate. Prices ship built in and update with `/usage --refresh-prices`. Records live in `.faber/usage.db` per project and survive restarts. |
129
107
  | **Error recovery** | Transient API errors: backoff + jitter, honors Retry-After. Tool errors return to the model to self-correct. Identical call failing twice → warning; three times → clean abort. Ctrl-C aborts streams *and* running commands; checkpoints survive. |
130
108
  | **Safety rails** | Symlink-resolved path jail, shell denylist, timeouts, output truncation, atomic writes (temp+rename), stale-edit guard. Guardrails, not a sandbox — use a container for untrusted code. |
109
+ | **Approval by default** | Arrow-key menu on every file edit (colored diff) and every shell command. `--auto` / `/auto` / `FABER_APPROVAL=auto` opts into autonomy. |
110
+ | **Guided setup** | First run walks through vendor, route, credential and model, then remembers it. Later launches verify the setup can reach a model before opening the prompt. Nothing is saved until setup finishes, so an interrupted run leaves no half-configured state. |
111
+ | **Credentials** | Keys live in `~/.faber/credentials.json`, owner-only, outside every repository. Faber checks that what you paste looks like a key before storing it, hides it as you type, and never prints more than a masked fragment. Environment variables keep working and take precedence. |
112
+ | **Mid-task steering** | Type while it works; guidance is injected at the next loop boundary. If the model finishes while steering is queued, the task continues instead. Steering markers carry a per-task nonce, so hostile file contents can't impersonate you. |
113
+ | **Reversible history** | `/history` lists tasks with files touched; `/restore <id>` jumps anywhere; `/undo` / `/redo` — restores are never destructive. |
131
114
 
132
115
  ## Vendors, routes, and models
133
116
 
@@ -137,9 +120,11 @@ Faber separates three choices, so you can change one without redoing the others:
137
120
  |---|---|---|
138
121
  | **Vendor** | who makes the model | `/route` |
139
122
  | **Route** | how you reach it and who owns auth | `/route` |
140
- | **Model** | which model on that route | `/model` |
123
+ | **Model** | which model on that route | `/model` (searchable model options) |
124
+
125
+ Routes available today: **Anthropic API**, **Amazon Bedrock**, **OpenAI API**, **Ollama** (local, no key and no cost), and any **OpenAI-compatible endpoint** (OpenRouter, Groq, Together, vLLM, a gateway). Google Vertex is defined but not yet wired up, and `/route` says so rather than failing at request time.
141
126
 
142
- Routes available today: **Anthropic API**, **Amazon Bedrock**, **OpenAI API**, **Ollama** (local, no key and no cost), and any **OpenAI-compatible endpoint** (OpenRouter, Groq, Together, vLLM, a gateway). Google Vertex is defined but not yet wired up `/route` says so rather than failing at request time.
127
+ On OpenAI, Faber speaks both APIs. Chat completions for most models, and the **Responses API** for the ones that require it, which includes the codex family the coding-tuned models a coding agent actually wants. You don't choose: each model's endpoint is recorded in the price dataset Faber already downloads, so `gpt-5.3-codex` routes to `/v1/responses` and `gpt-5.5` to `/v1/chat/completions` automatically. If a model is too new to appear in that dataset and the API says it belongs elsewhere, Faber retries on the endpoint it names rather than failing.
143
128
 
144
129
  ### Amazon Bedrock
145
130
 
@@ -163,17 +148,43 @@ Model ids on Bedrock differ by region and deployment, so pin them per alias in y
163
148
 
164
149
  ### Costs
165
150
 
166
- `/usage` shows real dollars without any configuration. Prices come from the best source available, in this order: whatever you set explicitly, then the provider's own published rates (OpenRouter publishes per-token prices including cache reads and writes, and needs no key for it), then a community dataset covering everyone else, then a small table built into Faber so it still works offline. Anthropic and OpenAI don't publish prices through their APIs at all, which is why the last two exist.
151
+ `/usage` is a ledger, not an estimate. Every task's cost is worked out and written down when the task runs, at the rates in effect then, and never recalculated. If a vendor raises prices next month, last month's tasks still show what they actually cost.
152
+
153
+ `/usage` breaks spend down across seven rolling windows — today, 7 days, 14 days, 30 days, 3 months, 6 months, all time — each showing tasks, tokens, cache rate, cost and savings, with a per-model table underneath showing where the money actually goes.
154
+
155
+ Windows are rolling rather than calendar, so "last 30 days" always means thirty days instead of resetting to one on the first of the month. All seven are shown even when they hold identical numbers: a missing row would read as "no data" when it actually means "nothing new since," and that distinction is the whole point for someone coming back after a break.
156
+
157
+ The two columns nobody can interpret unaided are defined in the panel itself. **Cached** is the share of input served from a cache rather than billed at full price. **Saved** is the counterfactual: what those same tasks would have cost without prompt caching. The by-model table is the actionable part, since switching routine work to a cheaper model is usually the largest saving available.
158
+
159
+ **Where prices come from**, best source first: whatever you set explicitly, then the provider's own published rates (OpenRouter publishes per-token prices including cache reads and writes, and needs no key for it), then a community dataset covering everyone else, then a small table built into Faber so it works offline. Anthropic and OpenAI don't publish prices through their APIs at all, which is why the last two exist.
160
+
161
+ Keeping those rates current matters more than it sounds, precisely because costs are frozen when recorded: a stale table would bake a wrong number into your history permanently. So Faber fetches prices during setup and re-checks on every launch with a conditional request. When nothing has changed the server answers `304` with no body — about 70ms in the background — so there's no window where an out-of-date rate can enter your records. The full download happens only when the list actually changes, and after a failed attempt Faber waits a day before retrying so an offline machine isn't making a doomed request on every start.
162
+
163
+ Turn the automatic refresh off with `FABER_AUTO_PRICES=0` or `"autoRefreshPrices": false` in your profile. This is the only network request Faber makes that isn't an inference call; it fetches a public price list and sends nothing about you.
164
+
165
+ Costs use each model's own input, output, cache-read and cache-write rates rather than a flat multiplier, and cloud model ids like `us.anthropic.claude-sonnet-5` normalise onto the same entry as the direct one. `FABER_PRICE_IN` and `FABER_PRICE_OUT`, or `priceIn` and `priceOut` in a profile, override everything. A model with no known price shows a dash rather than a guess.
166
+
167
+ **What gets recorded.** One row per task in `<project>/.faber/usage.db`:
167
168
 
168
169
  ```
169
- faber> /usage --refresh-prices # pull current rates now
170
+ ts 1785979475047 when it ran
171
+ input 27000 fresh input tokens
172
+ cache_read 46000 served from cache
173
+ cache_write 500 stored into the cache for reuse
174
+ output 3600 generated
175
+ calls 3 API round-trips inside that task
176
+ model gpt-5.3-codex which model ran it
177
+ cost 0.0412 dollars, frozen at run time
178
+ saved 0.0231 what caching avoided
170
179
  ```
180
+ **Cost metrics `/usage`:
181
+
182
+ <img width="636" height="388" alt="Screenshot 2026-08-10 at 12 25 57 AM" src="https://github.com/user-attachments/assets/33ba06ce-69ad-4afe-afcc-6119a897e67a" />
171
183
 
172
- Keeping those rates current matters more than it might sound, because a task's cost is written down when the task runs and never recalculated. If a vendor raises prices next month, last month's tasks still show what they actually cost. That's the point of a ledger. But it also means a stale price table would bake a wrong number into your history permanently, so Faber fetches prices once during setup and re-checks on every launch using a conditional request. When nothing has changed the server answers with a 304 and no body, which costs about 70ms in the background, so there's no window where an out-of-date rate can slip into your records. The full download only happens when the price list actually changes, and after a failed attempt Faber waits a day before trying again so an offline machine isn't making a doomed request every time you start.
173
184
 
174
- You can turn the automatic refresh off with `FABER_AUTO_PRICES=0` or `"autoRefreshPrices": false` in your profile, in which case Faber falls back to its built-in rates and mentions at startup when they're getting old. This is the only network request Faber makes that isn't an inference call. It fetches a public price list and sends nothing about you.
185
+ Counters and a timestamp. No prompts, no code, no file contents. Around 80 bytes a row, so a year of heavy use is roughly a megabyte, and nothing is ever pruned.
175
186
 
176
- Costs use each model's own input, output, cache-read and cache-write rates rather than a flat multiplier, and cloud model ids like `us.anthropic.claude-sonnet-5` normalize onto the same entry as the direct one. Setting `FABER_PRICE_IN` and `FABER_PRICE_OUT`, or `priceIn` and `priceOut` in a profile, overrides everything. A model with no known price shows a dash rather than a guess.
187
+ Keeping the raw rows rather than rolling them into running totals is deliberate: any question you think of later can still be answered about the past. Spend for a particular month, which model a given week ran on, cost per task before and after a prompt change. It's a plain SQLite file, so `sqlite3 .faber/usage.db` will answer anything Faber doesn't print including an expense report of your own shape.
177
188
 
178
189
  Settings live in `~/.faber/settings.json` as named **profiles**:
179
190
 
@@ -196,10 +207,10 @@ Settings resolve in this order, highest first: environment variables, then a pro
196
207
  ```
197
208
  /setup run setup again (vendor, route, credential, model)
198
209
  /route choose a vendor and route
199
- /model [alias] switch model; lists what your key can actually use
210
+ /model [id] switch model; lists what your key can use, with prices
200
211
  /key [set|rm] show, save or remove an API key
201
212
  /profile [name] list or switch saved profiles
202
- /usage tokens, cost and cache savings (--refresh-prices)
213
+ /usage cost ledger by time window and model (--refresh-prices)
203
214
  /index rebuild the code graph (symbols + call edges)
204
215
  /map <symbol> print the call tree from any entry point
205
216
  /memory [archived] long-term memories (+ file notes)
@@ -239,9 +250,9 @@ Faber keeps three files. `~/.faber/settings.json` holds your profiles, which is
239
250
 
240
251
  Commit protection happens on its own. When Faber starts inside a git repo it writes `.faber/` into `.git/info/exclude`, a local ignore that produces no diff and is never committed, so project state can't reach GitHub even if you forget your `.gitignore`. If `.faber/` was already committed at some point in the past, you get a loud warning with the exact `git rm --cached` command to fix it. This matters because sessions and checkpoints can contain the contents of files the agent read. If teammates will use Faber too, add `.faber/` to the shared `.gitignore` so they're covered from their first run.
241
252
 
242
- ## Failure-mode matrix
253
+ ## What happens when things go wrong
243
254
 
244
- | Failure | Behavior |
255
+ | Situation | What Faber does |
245
256
  |---|---|
246
257
  | API 429/5xx/network drop | retry with backoff+jitter; Retry-After honored; clear fatal after N attempts |
247
258
  | Bad API key | immediate fatal naming the env var to set |
@@ -266,7 +277,7 @@ npm test # offline test suite, no API key needed
266
277
  node selftest.mjs # installation self-check + live agent verification (report file to share)
267
278
  ```
268
279
 
269
- The suite deliberately covers the awkward cases rather than the easy ones: undo and redo round-trips, checkpoint ids colliding within the same millisecond, session files torn mid-write, cyclic call graphs, surgical edits through CRLF and emoji, cancellation mid-stream, steering messages keeping the API's role alternation valid, paste markers split across stream chunks, AWS signatures checked against the vector AWS publishes, credential files landing with owner-only permissions, and a recorded cost staying put when prices later change. Several tests drive the real agent loop against a mock streaming server, and one drives the actual CLI binary.
280
+ The suite deliberately covers the awkward cases rather than the easy ones: undo and redo round-trips, checkpoint ids colliding within the same millisecond, session files torn mid-write, cyclic call graphs, surgical edits through CRLF and emoji, cancellation mid-stream, steering messages keeping the API's role alternation valid, paste markers split across stream chunks, AWS signatures checked against the vector AWS publishes, credential files landing with owner-only permissions, a recorded cost staying put when prices later change, Responses-API tool calls assembled from their argument deltas, and usage windows that put a 45-day-old task in "last 3 months" but not "last 30 days". Several tests drive the real agent loop against a mock streaming server, and one drives the actual CLI binary.
270
281
 
271
282
  CI runs on Ubuntu, macOS and Windows across Node 22 and 24: tests, build, and CLI smoke on every push.
272
283
 
@@ -277,18 +288,21 @@ CLI/REPL (index.ts) streaming render · arrow-key approvals · steering captur
277
288
 
278
289
  Agent loop (agent.ts) recall memory + repo map → [LLM ⇄ tools] → verify → summarize
279
290
  │ doom-loop breaker · iteration cap · compaction · session log · steering drain
280
- ├─ LLM (llm.ts) Anthropic + OpenAI-compatible · SSE streaming · prompt caching · usage · retry
291
+ ├─ LLM (llm.ts) three wires: Anthropic · OpenAI chat · OpenAI Responses · SSE · caching · retry
292
+ ├─ Routes (routes.ts) vendor → route → model · SigV4 for Bedrock (sigv4.ts) · live model discovery
293
+ ├─ Pricing (pricing.ts) per-model rates, modes and endpoints · conditional refresh · frozen at spend
281
294
  ├─ Tools (tools/) validated dispatch · mutations staged as diffs · shell gated by approval
282
295
  ├─ Graph (indexer.ts) symbols + call/import edges · incremental · who_calls / trace_path / map
283
296
  ├─ Memory (memory/) shortTerm (window+compaction) · longTerm (SQLite+FTS) · sessions (JSONL)
284
297
  └─ Checkpoints snapshot-before-write · /history · reversible restore
285
298
 
286
- All state in <repo>/.faber/ (memory.db, index.db, sessions/, checkpoints/)
299
+ Per project <repo>/.faber/ memory.db · index.db · usage.db · sessions/ · checkpoints/
300
+ Per machine ~/.faber/ settings.json (profiles) · credentials.json (0600) · cache/
287
301
  ```
288
302
 
289
303
  ## Roadmap
290
304
 
291
- Google Vertex is defined but not yet wired up, since its OAuth flow is a bigger piece than Bedrock turned out to be. Beyond that: tree-sitter for compiler-grade graph edges, evicting stale tool results from long conversations, redacting secrets in session logs, team-shared project settings that can live in a repo, embedding-based recall, a branch-per-task git mode, and a VS Code extension built on this core.
305
+ Google Vertex is defined but not yet wired up, since its OAuth flow is a bigger piece than Bedrock turned out to be. Beyond that: tree-sitter for compiler-grade graph edges, evicting stale tool results from long conversations, redacting secrets in session logs, team-shared project settings that can live in a repo, embedding-based recall, a branch-per-task git mode, a `/usage --all` view comparing every project on the machine, and a VS Code extension built on this core.
292
306
 
293
307
  ## License
294
308
 
package/dist/agent.js CHANGED
@@ -81,6 +81,17 @@ export class Agent {
81
81
  this.model = id;
82
82
  this.llm.setModel(id);
83
83
  }
84
+ /**
85
+ * Adopt a new configuration in the running session, after /setup or /route
86
+ * changed the route. Without this the session keeps talking to the old
87
+ * provider while the settings file says otherwise, so /model would list
88
+ * models for a route the user thought they had left.
89
+ */
90
+ reconfigure(next) {
91
+ this.config = next;
92
+ this.model = next.model;
93
+ this.llm = new LLMClient(next);
94
+ }
84
95
  steer(text) {
85
96
  const t = text.trim();
86
97
  if (t)
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ import { loadSettings, saveSettings, updateActive, settingsPath } from "./settin
43
43
  import { needsOnboarding, interactive, runOnboarding, reportSetup, setupComplete } from "./onboard.js";
44
44
  import { saveCredential, deleteCredential, listCredentialNames, getCredential, maskCredential, credentialsPath, resolveCredential, looksLikeKey, } from "./credentials.js";
45
45
  import { readCache, writeCache, clearCache, buildPicker } from "./models.js";
46
- import { refreshPrices, priceFor } from "./pricing.js";
46
+ import { refreshPrices, priceFor, readPriceCache } from "./pricing.js";
47
47
  /** Version comes from package.json — one source of truth for banner and --version. */
48
48
  const VERSION = (() => {
49
49
  try {
@@ -396,6 +396,15 @@ async function main() {
396
396
  console.log(pc.dim(" /model --save make it the profile default"));
397
397
  break;
398
398
  }
399
+ // With no price data at all — first run, or the cache was discarded
400
+ // because an older Faber wrote it — wait for the fetch rather than
401
+ // rendering a menu that says "price unknown" for everything. The
402
+ // background refresh is fine when we already have rates to show.
403
+ if (!readPriceCache()) {
404
+ process.stdout.write(pc.dim(" fetching prices… "));
405
+ await refreshPrices(undefined, { baseUrl: config.baseUrl });
406
+ process.stdout.write("\r\x1b[2K");
407
+ }
399
408
  // Ask the provider what this key can actually use; cached for a day.
400
409
  if (args[0] === "--refresh")
401
410
  clearCache(route.id);
@@ -412,8 +421,15 @@ async function main() {
412
421
  console.log(`No model list available for ${route.label}. Set one with: /model <id>`);
413
422
  break;
414
423
  }
424
+ // Show the rate here too. /model is where people switch models to save
425
+ // money, so hiding the price is exactly backwards.
415
426
  const width = Math.min(34, Math.max(...entries.map((e) => e.label.length)) + 2);
416
- const labels = entries.map((e) => `${e.label.padEnd(width)}${pc.dim(e.blurb)}${e.live ? pc.dim(" ·live") : ""}`);
427
+ const labels = entries.map((e) => {
428
+ const id = resolveModel(e.value, route, config.modelPins);
429
+ const p = priceFor(id, { in: config.priceIn, out: config.priceOut });
430
+ const cost = p ? `$${p.in}/$${p.out} per Mtok` : "price unknown";
431
+ return `${e.label.padEnd(width)}${pc.dim(cost.padEnd(22))}${pc.dim(e.blurb)}`;
432
+ });
417
433
  const curIdx = entries.findIndex((e) => e.value === agent.model || resolveModel(e.value, route, config.modelPins) === agent.model);
418
434
  const pick = await select(rl, "Select model (this session)", labels, curIdx < 0 ? 0 : curIdx);
419
435
  const chosen = entries[pick];
@@ -464,7 +480,14 @@ async function main() {
464
480
  case "/setup": {
465
481
  const setup = await runOnboarding(rl);
466
482
  reportSetup(setup);
467
- console.log(pc.dim(" restart faber to apply"));
483
+ if (!setup.aborted) {
484
+ // Apply in this session. Telling the user to restart left the
485
+ // running agent on the old route, so /model would then list models
486
+ // for the provider they had just switched away from.
487
+ config = loadConfig(config.workspace);
488
+ agent.reconfigure(config);
489
+ console.log(pc.dim(` now using ${describeModel(config.model, getRoute(config.route) ?? ROUTES[0], config.modelPins)} on ${getRoute(config.route)?.label}`));
490
+ }
468
491
  break;
469
492
  }
470
493
  case "/route": {
@@ -506,7 +529,9 @@ async function main() {
506
529
  if (chosen.aliasesArePinned) {
507
530
  console.log(pc.dim(" model ids differ on this route — set one with: /model <id>"));
508
531
  }
509
- console.log(pc.dim(" restart faber to apply"));
532
+ config = loadConfig(config.workspace);
533
+ agent.reconfigure(config);
534
+ console.log(pc.dim(` now on ${getRoute(config.route)?.label}`));
510
535
  break;
511
536
  }
512
537
  case "/profile": {
@@ -527,7 +552,9 @@ async function main() {
527
552
  }
528
553
  st.activeProfile = args[0];
529
554
  saveSettings(st);
530
- console.log(`Active profile: ${args[0]}. Restart faber to apply.`);
555
+ config = loadConfig(config.workspace);
556
+ agent.reconfigure(config);
557
+ console.log(`Active profile: ${args[0]} — ${getRoute(config.route)?.label}, ${config.model}.`);
531
558
  break;
532
559
  }
533
560
  case "/usage": {