faberwright 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
 
@@ -34,7 +21,7 @@ faber # start the REPL (approval mode is on by default)
34
21
  1. **Give it a real task.** Try `add a comment explaining what the main entry file does`
35
22
  2. **Approve the diff** with the arrow keys and Enter. Cursor starts on Yes; "Always this session" grants trust. Shell commands ask too.
36
23
  3. **See the safety net.** `/history` lists every task with the files it touched.
37
- 4. **Undo it.** `/undo` reverts the task, and if you change your mind changed your mind? `/redo` brings it back. Nothing is ever lost in either direction.
24
+ 4. **Undo it.** `/undo` reverts the task, and `/redo` brings it back. Nothing is ever lost in either direction.
38
25
  5. **Come back tomorrow.** `faber --resume` continues the conversation, or just ask *"what did we do last time?"* — it searches past sessions itself.
39
26
 
40
27
  That loop of task, approve, inspect, revert is the whole trust model. Everything else is detail.
@@ -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, your own AWS account, or Microsoft Foundry on Azure. OpenAI through both of its APIs — chat completions and the Responses API that the codex family requires — directly or through your Azure subscription. 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
 
@@ -53,11 +42,43 @@ That loop of task, approve, inspect, revert is the whole trust model. Everything
53
42
 
54
43
  ## Upgrading from Codewright
55
44
 
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/`.
45
+ Faber is Codewright renamed, after the npm name was taken between building and releasing. Existing `.codewright/` state directories are adopted automatically; `CW_*` environment variables still work alongside `FABER_*`.
57
46
 
58
47
  ## Requirements & install
59
48
 
60
- 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.
49
+ Faber needs **Node.js 22.5 or newer**. It 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.
50
+
51
+ Check your version:
52
+
53
+ ```bash
54
+ node --version
55
+ ```
56
+
57
+ If it prints `v22.5.0` or higher, skip ahead to Install.
58
+
59
+ ### Installing Node.js
60
+
61
+ The recommended way is **nvm**, which lets you switch versions per project without touching your system:
62
+
63
+ ```bash
64
+ # macOS / Linux
65
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
66
+ # restart your terminal, or: source ~/.zshrc
67
+ nvm install 22
68
+ nvm use 22
69
+ ```
70
+
71
+ Alternatives:
72
+
73
+ ```bash
74
+ brew install node # macOS, Homebrew
75
+ winget install OpenJS.NodeJS # Windows
76
+ sudo apt install nodejs npm # Debian/Ubuntu — often ships an older version; prefer nvm
77
+ ```
78
+
79
+ Or download an installer from [nodejs.org](https://nodejs.org). Already on Node but below 22.5? `nvm install 22 && nvm use 22`, or `brew upgrade node`.
80
+
81
+ ### Install Faber
61
82
 
62
83
  ```bash
63
84
  npm install -g faberwright
@@ -113,6 +134,7 @@ Setup is skipped entirely when there's no terminal attached, so scripts and CI n
113
134
  | **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
135
  | **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
136
  | **Approval by default** | Arrow-key menu on every file edit (colored diff) and every shell command. `--auto` / `/auto` / `FABER_APPROVAL=auto` opts into autonomy. |
137
+ | **Two OpenAI APIs** | Chat completions and the Responses API, chosen per model from the endpoint each one publishes. The codex family works without configuration. |
116
138
  | **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
139
  | **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
140
  | **Interactive choices** | Genuinely ambiguous request? The agent presents 2–4 options plus "Chat more about this instead" before writing code. |
@@ -137,22 +159,26 @@ Faber separates three choices, so you can change one without redoing the others:
137
159
  |---|---|---|
138
160
  | **Vendor** | who makes the model | `/route` |
139
161
  | **Route** | how you reach it and who owns auth | `/route` |
140
- | **Model** | which model on that route | `/model` |
162
+ | **Model** | which model on that route | `/model` (searchable model options) |
141
163
 
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.
164
+ Routes available today: **Anthropic API**, **Amazon Bedrock**, **Microsoft Foundry**, **OpenAI API**, **Azure OpenAI**, **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.
165
+
166
+ 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
167
 
144
168
  ### Amazon Bedrock
145
169
 
146
- Faber authenticates two ways, and tries them in this order.
170
+ Setup asks how you want to authenticate, because the choice affects who gets billed.
147
171
 
148
- **With an IAM role, which needs no key at all.** In SageMaker Studio, on EC2, in ECS or Lambda, or on a laptop where you've run `aws configure`, Faber finds your AWS credentials the same way the SDKs do and signs each request with SigV4. Pick Amazon Bedrock during setup, give it a region, and that's the whole configuration. The signing is implemented directly against Node's crypto module rather than pulling in the AWS SDK, so the zero-dependency install still holds, and it's verified against the signature AWS publishes for its own worked example.
172
+ **AWS credentials (IAM role)** needs no key at all. In SageMaker Studio, on EC2, in ECS or Lambda, or on a laptop where you've run `aws configure`, Faber finds your credentials the same way the SDKs do and signs each request with SigV4. The region comes from `AWS_REGION` or `~/.aws/config` when it's there, so usually nothing is asked at all. Signing is implemented directly against Node's crypto module rather than pulling in the AWS SDK, and is verified against the signature AWS publishes for its own worked example.
149
173
 
150
- **With a Bedrock API key**, if you'd rather use one:
174
+ **A Bedrock API key** bills through that key instead:
151
175
 
152
176
  ```bash
153
177
  export BEDROCK_API_KEY=...
154
178
  ```
155
179
 
180
+ Either path must complete: pick the role with no credentials present, or the key without entering one, and setup saves nothing and starts over next time.
181
+
156
182
  Model ids on Bedrock differ by region and deployment, so pin them per alias in your profile instead of relying on the built-in names:
157
183
 
158
184
  ```json
@@ -161,19 +187,66 @@ Model ids on Bedrock differ by region and deployment, so pin them per alias in y
161
187
  "apiKeyEnv": "BEDROCK_API_KEY" }
162
188
  ```
163
189
 
190
+ ### Claude on Azure (Microsoft Foundry)
191
+
192
+ Claude has been generally available in Microsoft Foundry since June 2026, hosted on Azure with your organisation's own authentication, billing and governance — usage appears on the Azure invoice and can count toward a Microsoft Azure Consumption Commitment.
193
+
194
+ It serves the same Messages API as the direct route, so for Faber this is a base URL and an auth header rather than a new protocol: prompt caching, extended thinking and tool streaming all work unchanged. Setup asks for your resource name and takes either a subscription key or a token minted from Entra ID, recognising which you gave it.
195
+
196
+ ```bash
197
+ export AZURE_FOUNDRY_API_KEY=...
198
+ ```
199
+
200
+ A Foundry resource is separate from an Azure OpenAI one and has its own key, so the two never share a credential.
201
+
202
+ ### Azure OpenAI
203
+
204
+ The answer for anyone who wants OpenAI's coding models billed through their employer rather than a personal card: AWS doesn't host OpenAI models, so Bedrock isn't the route for them — Azure is.
205
+
206
+ Setup asks for your resource name rather than a URL, since that's the part people know from the portal, and builds the endpoint from it. Azure addresses **deployments** rather than model ids: you call `my-codex-deployment`, a name someone in your organisation chose, and the deployment decides which model runs. Faber lists the deployments your subscription has created and shows the model behind each one.
207
+
208
+ ```bash
209
+ export AZURE_OPENAI_API_KEY=...
210
+ ```
211
+
164
212
  ### Costs
165
213
 
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.
214
+ `/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.
215
+
216
+ `/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.
217
+
218
+ 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.
219
+
220
+ 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.
221
+
222
+ **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.
223
+
224
+ 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.
225
+
226
+ 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.
227
+
228
+ 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.
229
+
230
+ **What gets recorded.** One row per task in `<project>/.faber/usage.db`:
167
231
 
168
232
  ```
169
- faber> /usage --refresh-prices # pull current rates now
233
+ ts 1785979475047 when it ran
234
+ input 27000 fresh input tokens
235
+ cache_read 46000 served from cache
236
+ cache_write 500 stored into the cache for reuse
237
+ output 3600 generated
238
+ calls 3 API round-trips inside that task
239
+ model gpt-5.3-codex which model ran it
240
+ cost 0.0412 dollars, frozen at run time
241
+ saved 0.0231 what caching avoided
170
242
  ```
171
243
 
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.
244
+ <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" />
245
+
173
246
 
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.
247
+ 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
248
 
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.
249
+ 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
250
 
178
251
  Settings live in `~/.faber/settings.json` as named **profiles**:
179
252
 
@@ -196,10 +269,10 @@ Settings resolve in this order, highest first: environment variables, then a pro
196
269
  ```
197
270
  /setup run setup again (vendor, route, credential, model)
198
271
  /route choose a vendor and route
199
- /model [alias] switch model; lists what your key can actually use
272
+ /model [id] switch model; lists what your key can use, with prices
200
273
  /key [set|rm] show, save or remove an API key
201
274
  /profile [name] list or switch saved profiles
202
- /usage tokens, cost and cache savings (--refresh-prices)
275
+ /usage cost ledger by time window and model (--refresh-prices)
203
276
  /index rebuild the code graph (symbols + call edges)
204
277
  /map <symbol> print the call tree from any entry point
205
278
  /memory [archived] long-term memories (+ file notes)
@@ -233,15 +306,17 @@ Flags: `faber [task] [--workspace|-w <dir>] [--resume] [--ask|--auto] [--version
233
306
  | `FABER_ROUTE`, `FABER_REGION` | route id and region, overriding the profile |
234
307
  | `FABER_AUTO_PRICES` | `0` disables the background price refresh |
235
308
  | `BEDROCK_API_KEY` | Bedrock, when you'd rather use a key than an IAM role |
309
+ | `AZURE_OPENAI_API_KEY` | Azure OpenAI, for GPT deployments in your subscription |
310
+ | `AZURE_FOUNDRY_API_KEY` | Microsoft Foundry, for Claude on Azure (a separate resource) |
236
311
  | `AWS_ACCESS_KEY_ID` etc. | picked up automatically for Bedrock's SigV4 signing |
237
312
 
238
313
  Faber keeps three files. `~/.faber/settings.json` holds your profiles, which is the route, model and which environment variable a credential comes from. `~/.faber/credentials.json` holds the keys themselves, owner-only and outside every repository. A project can override the profile with its own `.faber/config.json`, which is useful when one repo should use a cheaper model than the rest.
239
314
 
240
315
  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
316
 
242
- ## Failure-mode matrix
317
+ ## What happens when things go wrong
243
318
 
244
- | Failure | Behavior |
319
+ | Situation | What Faber does |
245
320
  |---|---|
246
321
  | API 429/5xx/network drop | retry with backoff+jitter; Retry-After honored; clear fatal after N attempts |
247
322
  | Bad API key | immediate fatal naming the env var to set |
@@ -266,7 +341,7 @@ npm test # offline test suite, no API key needed
266
341
  node selftest.mjs # installation self-check + live agent verification (report file to share)
267
342
  ```
268
343
 
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.
344
+ 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
345
 
271
346
  CI runs on Ubuntu, macOS and Windows across Node 22 and 24: tests, build, and CLI smoke on every push.
272
347
 
@@ -277,18 +352,21 @@ CLI/REPL (index.ts) streaming render · arrow-key approvals · steering captur
277
352
 
278
353
  Agent loop (agent.ts) recall memory + repo map → [LLM ⇄ tools] → verify → summarize
279
354
  │ doom-loop breaker · iteration cap · compaction · session log · steering drain
280
- ├─ LLM (llm.ts) Anthropic + OpenAI-compatible · SSE streaming · prompt caching · usage · retry
355
+ ├─ LLM (llm.ts) three wires: Anthropic · OpenAI chat · OpenAI Responses · SSE · caching · retry
356
+ ├─ Routes (routes.ts) vendor → route → model · SigV4 for Bedrock (sigv4.ts) · live model discovery
357
+ ├─ Pricing (pricing.ts) per-model rates, modes and endpoints · conditional refresh · frozen at spend
281
358
  ├─ Tools (tools/) validated dispatch · mutations staged as diffs · shell gated by approval
282
359
  ├─ Graph (indexer.ts) symbols + call/import edges · incremental · who_calls / trace_path / map
283
360
  ├─ Memory (memory/) shortTerm (window+compaction) · longTerm (SQLite+FTS) · sessions (JSONL)
284
361
  └─ Checkpoints snapshot-before-write · /history · reversible restore
285
362
 
286
- All state in <repo>/.faber/ (memory.db, index.db, sessions/, checkpoints/)
363
+ Per project <repo>/.faber/ memory.db · index.db · usage.db · sessions/ · checkpoints/
364
+ Per machine ~/.faber/ settings.json (profiles) · credentials.json (0600) · cache/
287
365
  ```
288
366
 
289
367
  ## Roadmap
290
368
 
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.
369
+ 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
370
 
293
371
  ## License
294
372
 
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)
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Deciding whether a line is a command.
3
+ *
4
+ * Kept in its own module because index.ts starts the CLI when imported, so a
5
+ * test that reaches for this function would otherwise boot the whole program
6
+ * and sit at a prompt forever.
7
+ */
8
+ /**
9
+ * How many arguments each command takes. Anything with more is prose.
10
+ *
11
+ * The old rule was "starts with a slash", which read only the first word — so
12
+ * "/undo and /redo how does it work?" reverted the user's files while they
13
+ * were asking a question about them. A command has to match the WHOLE line.
14
+ */
15
+ const COMMAND_ARITY = {
16
+ "/help": 0, "/exit": 0, "/quit": 0, "/undo": 0, "/redo": 0, "/clear": 0,
17
+ "/compact": 0, "/sessions": 0, "/history": 0, "/index": 0, "/setup": 0,
18
+ "/route": 0, "/ask": 0, "/auto": 0, "/verbose": 0, "/concise": 0,
19
+ "/usage": 1, // --refresh-prices
20
+ "/model": 1, // an id, --save or --refresh
21
+ "/profile": 1, // a profile name
22
+ "/map": 1, "/restore": 1, "/forget": 1, "/archive": 1, "/unarchive": 1,
23
+ "/prune": 1, "/memory": 1,
24
+ "/key": 2, // set|rm plus a variable name
25
+ };
26
+ export function looksLikeCommand(line) {
27
+ // Trim first: a pasted or indented line is still a command.
28
+ const parts = line.trim().split(/\s+/);
29
+ if (!parts[0]?.startsWith("/"))
30
+ return false;
31
+ const arity = COMMAND_ARITY[parts[0].toLowerCase()];
32
+ if (arity === undefined)
33
+ return false; // unknown: let /help catch it
34
+ return parts.length - 1 <= arity;
35
+ }
package/dist/index.js CHANGED
@@ -38,12 +38,13 @@ import { Composer } from "./editor.js";
38
38
  import { StatusLine } from "./status.js";
39
39
  import { renderMarkdown, StreamRenderer } from "./markdown.js";
40
40
  import { renderUsagePanel, UsageLedger } from "./usage.js";
41
+ import { looksLikeCommand } from "./commands.js";
41
42
  import { ROUTES, getRoute, describeModel, resolveModel, vendors, baseUrlFor, DEFAULT_REGION } from "./routes.js";
42
43
  import { loadSettings, saveSettings, updateActive, settingsPath } from "./settings.js";
43
44
  import { needsOnboarding, interactive, runOnboarding, reportSetup, setupComplete } from "./onboard.js";
44
45
  import { saveCredential, deleteCredential, listCredentialNames, getCredential, maskCredential, credentialsPath, resolveCredential, looksLikeKey, } from "./credentials.js";
45
46
  import { readCache, writeCache, clearCache, buildPicker } from "./models.js";
46
- import { refreshPrices, priceFor } from "./pricing.js";
47
+ import { refreshPrices, priceFor, readPriceCache } from "./pricing.js";
47
48
  /** Version comes from package.json — one source of truth for banner and --version. */
48
49
  const VERSION = (() => {
49
50
  try {
@@ -60,6 +61,18 @@ function renderDiff(diff) {
60
61
  : l.startsWith("@@") ? pc.cyan(l)
61
62
  : pc.dim(l)).join("\n");
62
63
  }
64
+ /**
65
+ * Is this line a command, or a question that happens to mention one?
66
+ *
67
+ * Matching on the first word alone meant "/undo and /redo how does it work?"
68
+ * ran /undo and reverted the user's files. A destructive command must be the
69
+ * whole line — anything trailing means the person is talking, not commanding.
70
+ */
71
+ const NO_ARG_COMMANDS = new Set([
72
+ "/undo", "/redo", "/history", "/clear", "/compact", "/sessions", "/help",
73
+ "/exit", "/quit", "/index", "/memory", "/ask", "/auto", "/verbose",
74
+ "/concise", "/setup", "/route",
75
+ ]);
63
76
  async function main() {
64
77
  const argv = process.argv.slice(2);
65
78
  const flags = new Set(argv.filter((a) => a.startsWith("-")));
@@ -396,6 +409,15 @@ async function main() {
396
409
  console.log(pc.dim(" /model --save make it the profile default"));
397
410
  break;
398
411
  }
412
+ // With no price data at all — first run, or the cache was discarded
413
+ // because an older Faber wrote it — wait for the fetch rather than
414
+ // rendering a menu that says "price unknown" for everything. The
415
+ // background refresh is fine when we already have rates to show.
416
+ if (!readPriceCache()) {
417
+ process.stdout.write(pc.dim(" fetching prices… "));
418
+ await refreshPrices(undefined, { baseUrl: config.baseUrl });
419
+ process.stdout.write("\r\x1b[2K");
420
+ }
399
421
  // Ask the provider what this key can actually use; cached for a day.
400
422
  if (args[0] === "--refresh")
401
423
  clearCache(route.id);
@@ -412,8 +434,15 @@ async function main() {
412
434
  console.log(`No model list available for ${route.label}. Set one with: /model <id>`);
413
435
  break;
414
436
  }
437
+ // Show the rate here too. /model is where people switch models to save
438
+ // money, so hiding the price is exactly backwards.
415
439
  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") : ""}`);
440
+ const labels = entries.map((e) => {
441
+ const id = resolveModel(e.value, route, config.modelPins);
442
+ const p = priceFor(id, { in: config.priceIn, out: config.priceOut });
443
+ const cost = p ? `$${p.in}/$${p.out} per Mtok` : "price unknown";
444
+ return `${e.label.padEnd(width)}${pc.dim(cost.padEnd(22))}${pc.dim(e.blurb)}`;
445
+ });
417
446
  const curIdx = entries.findIndex((e) => e.value === agent.model || resolveModel(e.value, route, config.modelPins) === agent.model);
418
447
  const pick = await select(rl, "Select model (this session)", labels, curIdx < 0 ? 0 : curIdx);
419
448
  const chosen = entries[pick];
@@ -464,7 +493,14 @@ async function main() {
464
493
  case "/setup": {
465
494
  const setup = await runOnboarding(rl);
466
495
  reportSetup(setup);
467
- console.log(pc.dim(" restart faber to apply"));
496
+ if (!setup.aborted) {
497
+ // Apply in this session. Telling the user to restart left the
498
+ // running agent on the old route, so /model would then list models
499
+ // for the provider they had just switched away from.
500
+ config = loadConfig(config.workspace);
501
+ agent.reconfigure(config);
502
+ console.log(pc.dim(` now using ${describeModel(config.model, getRoute(config.route) ?? ROUTES[0], config.modelPins)} on ${getRoute(config.route)?.label}`));
503
+ }
468
504
  break;
469
505
  }
470
506
  case "/route": {
@@ -506,7 +542,9 @@ async function main() {
506
542
  if (chosen.aliasesArePinned) {
507
543
  console.log(pc.dim(" model ids differ on this route — set one with: /model <id>"));
508
544
  }
509
- console.log(pc.dim(" restart faber to apply"));
545
+ config = loadConfig(config.workspace);
546
+ agent.reconfigure(config);
547
+ console.log(pc.dim(` now on ${getRoute(config.route)?.label}`));
510
548
  break;
511
549
  }
512
550
  case "/profile": {
@@ -527,7 +565,9 @@ async function main() {
527
565
  }
528
566
  st.activeProfile = args[0];
529
567
  saveSettings(st);
530
- console.log(`Active profile: ${args[0]}. Restart faber to apply.`);
568
+ config = loadConfig(config.workspace);
569
+ agent.reconfigure(config);
570
+ console.log(`Active profile: ${args[0]} — ${getRoute(config.route)?.label}, ${config.model}.`);
531
571
  break;
532
572
  }
533
573
  case "/usage": {
@@ -611,7 +651,7 @@ async function main() {
611
651
  }
612
652
  if (!line)
613
653
  continue;
614
- if (line.startsWith("/")) {
654
+ if (line.startsWith("/") && looksLikeCommand(line)) {
615
655
  if (!(await command(line)))
616
656
  break;
617
657
  continue;
@@ -626,6 +666,13 @@ async function main() {
626
666
  }
627
667
  }
628
668
  const HELP = `
669
+ /setup set up again: vendor, route, credential, model
670
+ /route choose a vendor and how to reach it
671
+ /model [id] switch model; lists what your key can use, with prices
672
+ /key [set|rm] show, save or remove an API key (~/.faber, readable only by you)
673
+ /profile [name] list or switch saved profiles
674
+ /usage cost ledger by time window and model (--refresh-prices)
675
+ /verbose | /concise full-depth answers, or short ones (default concise)
629
676
  /index rebuild the code graph (symbols + call edges)
630
677
  /map <symbol> print the call tree from any entry point (e.g. /map main)
631
678
  /memory show active long-term memories (+ file notes)