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 +120 -42
- package/dist/agent.js +11 -0
- package/dist/commands.js +35 -0
- package/dist/index.js +53 -6
- package/dist/llm.js +374 -12
- package/dist/models.js +111 -1
- package/dist/onboard.js +270 -31
- package/dist/pricing.js +35 -2
- package/dist/prompt.js +94 -26
- package/dist/routes.js +33 -0
- package/dist/sigv4.js +50 -2
- package/dist/usage.js +127 -82
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,28 +1,15 @@
|
|
|
1
1
|
# Faber
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
170
|
+
Setup asks how you want to authenticate, because the choice affects who gets billed.
|
|
147
171
|
|
|
148
|
-
**
|
|
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
|
-
**
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
|
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
|
-
##
|
|
317
|
+
## What happens when things go wrong
|
|
243
318
|
|
|
244
|
-
|
|
|
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,
|
|
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
|
|
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
|
-
|
|
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)
|
package/dist/commands.js
ADDED
|
@@ -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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|