diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7639f62..c93a507 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ # Desktop installers → GitHub Release. -# CLI ships separately as npm package @solvyx/molt (see npm run pack:cli). +# CLI is built from source at the same tag (not on npm; see README, CLI / TUI). name: release on: @@ -116,7 +116,7 @@ jobs: body: | **A coding agent that cannot say "done" without proving it.** - This release ships the **desktop app** (macOS, Windows, Linux) and matches the **CLI** published as `@solvyx/molt` (binary `molt`) at the same version. + This release ships the **desktop app** (macOS, Windows, Linux) and the **CLI** (commands `maat` and `molt`) at the same version. The CLI is not on npm; build it from source. ### Desktop downloads - **macOS:** `.dmg` (arm64 and x64). Unsigned on purpose — first open: right-click the app → Open → Open. @@ -125,9 +125,14 @@ jobs: ### CLI ``` - npm i -g @solvyx/molt + git clone --branch ${{ steps.tag.outputs.name }} https://github.com/solvyxtech/molt.git maat + cd maat + npm ci + npm run pack:cli + npm i -g ./out-cli + maat --version ``` - Do **not** install unscoped `molt` or `molt-cli` from npm — those are other projects. Use `@solvyx/molt`. + Or run it in place with `npm run build && node dist/cli.js`. See the README's CLI / TUI section. Do **not** install `molt` or `molt-cli` from npm; those are other projects. ### After install Pick a provider, add a key, point molt at a project. Completions are claims; `.molt/done.yml` has to pass against the real disk before "done" sticks. Every attempt gets a receipt. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b3b646..7023c6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -149,6 +149,26 @@ ### Fixed +- **Anthropic prices are per model version, checked 2026-10-08.** The table + priced by family: Opus 5.5 was billed at $5 / $25 per MTok instead of + $4 / $20, Sonnet 5 and 5.5 at $3 / $15 instead of $2 / $10, Opus 4 / 4.1 at + $5 / $25 instead of $15 / $75, and Haiku 5.5 at Haiku 4.5's $1 / $5. Haiku + 5.5 is tiered by prompt length ($0.10 / $0.50 up to 100k tokens, $0.50 / + $2.50 over); the meter prices session totals, so it uses the over-100k + rates, because over-counting is the safe direction for a budget + (`/price 0.1 0.5 0.01` sets the lower tier). Cache reads now use each model's + published rate rather than a flat tenth of input (Fable/Mythos 5.1 $0.25, + Opus 5.5 $0.20, Sonnet 5.5 $0.10, Haiku 5.5 $0.05). The `claude-opus-4-0` + alias gets Opus 4's price instead of falling through to $5 / $25, and + Claude 3.5 Haiku ids are priced too. Tests pin Haiku 5.5 and each row. +- **README install line.** `npm i -g @solvyx/molt` and `npx @solvyx/molt` + pointed at a package that was unpublished on 2026-10-07. The README now + builds the CLI from source (clone, `npm ci`, `npm run build`, + `node dist/cli.js`, or `npm run pack:cli && npm i -g ./out-cli` for the + `maat` / `molt` commands; re-run both after pulling), the npm badges are + gone, and docs/versioning.md, docs/acp-server.md, CONTRIBUTING.md and the + release notes in release.yml say the same. The staged `out-cli/` package is + now `"private": true`, so an accidental `npm publish` fails. - **Two lean-session defects (lean-sessions study, PR #50).** When the kept user turns are mostly Maat's notes, a shed cut on user turns must now free at least 25% of the history, or it cuts on recent messages instead: Maat's own notes (acceptance criteria, bar refusals) are diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3635c5..ca87efa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,9 @@ queue. ## Versioning -Desktop and CLI ship the same semver from root `package.json`. The publishable -CLI is staged with `npm run pack:cli` as `@solvyx/molt` (binary `molt`). See -[docs/versioning.md](docs/versioning.md). Do not publish without COO say-so. +Desktop and CLI ship the same semver from root `package.json`. The CLI is +staged with `npm run pack:cli` under `out-cli/` (commands `maat` and `molt`) +and installed from source with `npm i -g ./out-cli`. It is not published to npm: +the staged package is marked `"private": true`, so `npm publish` refuses it. +See [docs/versioning.md](docs/versioning.md). diff --git a/README.md b/README.md index bd89378..442a5f5 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,10 @@ > Maat (MAH-aht), the Egyptian goddess of truth: in the Weighing of the Heart, a > claim is weighed against her feather before it may pass. Maat Agent weighs a > model's "done" against checks set before the work began, on the real disk, and -> writes the verdict down. It is built on the **molt** engine; the npm package is -> still `@solvyx/molt` and both the `maat` and `molt` commands work. +> writes the verdict down. It is built on the **molt** engine; both the `maat` +> and `molt` commands work. [![check](https://github.com/solvyxtech/molt/actions/workflows/check.yml/badge.svg)](https://github.com/solvyxtech/molt/actions/workflows/check.yml) -[![npm](https://img.shields.io/npm/v/@solvyx/molt.svg)](https://www.npmjs.com/package/@solvyx/molt) -[![npm downloads](https://img.shields.io/npm/dm/@solvyx/molt.svg)](https://www.npmjs.com/package/@solvyx/molt) [![licence: Apache 2.0](https://img.shields.io/badge/licence-Apache%202.0-blue.svg)](LICENSE) Maat Agent is an open source coding agent for developers. Terminal CLI and Electron desktop. Written in TypeScript. It runs any OpenAI compatible model and Anthropic’s native API. Then it refuses to accept done until every check in your project’s `.maat/done.yml` passes (projects that already have `.molt/` keep using it) against the real state on disk. A completion is a claim. Maat checks the claim and writes a receipt either way. @@ -144,15 +142,31 @@ macOS builds are unsigned on purpose. On first open, right-click the app, choose ### CLI / TUI +The CLI is not on npm (the `@solvyx/molt` package was unpublished on +2026-10-07); build it from source. Needs Node 20.11 or later and git. + +```sh +git clone https://github.com/solvyxtech/molt.git maat +cd maat +npm ci +npm run build # compiles the CLI to dist/cli.js +node dist/cli.js # interactive TUI (same as: npm start) +node dist/cli.js run "…" --yes # headless +``` + +To get `maat` and `molt` commands on your PATH, stage the CLI package and +install it from the local folder (nothing is downloaded from the registry): + ```sh -npm i -g @solvyx/molt -molt # interactive TUI -molt run "…" --yes # headless +npm run pack:cli # builds, then stages the CLI package under out-cli/ +npm i -g ./out-cli # installs the maat and molt commands from that folder +maat --version ``` -Or without installing: `npx @solvyx/molt`. +After pulling, re-run both commands to update the installed CLI. -**Do not** `npm i -g molt` or `npm i -g molt-cli` — those are unrelated packages on the registry. The binary name is still `molt`; the package name is `@solvyx/molt`. +**Do not** `npm i -g molt` or `npm i -g molt-cli` — those are unrelated +packages on the registry. Desktop and CLI share one version. See [docs/versioning.md](docs/versioning.md). @@ -161,10 +175,10 @@ Desktop and CLI share one version. See [docs/versioning.md](docs/versioning.md). For contributors working in this repository: ```sh -npm install +npm ci npm run app # the desktop window -npm start # the terminal UI (node dist/cli.js) -npm run pack:cli # stage publishable @solvyx/molt under out-cli/ (do not publish without COO) +npm start # the terminal UI (node dist/cli.js, after npm run build) +npm run pack:cli # stage the CLI package under out-cli/ (not published to npm) ``` First run: `/login`, pick a provider, paste a key, `/model`, go. Keys live in diff --git a/docs/acp-server.md b/docs/acp-server.md index 3df118f..ef3380c 100644 --- a/docs/acp-server.md +++ b/docs/acp-server.md @@ -44,7 +44,7 @@ Build it once with `npm run build`, then add this to Zed's `settings.json` from the Dock does not have your shell's PATH, so a bare `node` may not be found. - `--acp` works in place of `acp`, for launchers that want a flag. -- If the CLI package is installed (`npm run pack:cli`, `@solvyx/molt`), +- If the CLI is installed on your PATH (`npm run pack:cli && npm i -g ./out-cli`), `"command": "molt", "args": ["acp"]` is the same thing. - `default_config_options` sets what each new thread starts with — see [The pickers](#the-pickers) for the ids and values. Zed only applies a diff --git a/docs/versioning.md b/docs/versioning.md index 2a17adb..7300f8a 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -4,7 +4,7 @@ Desktop and CLI share one engine and one semver. - Root `package.json` (`molt-desktop`, private) is the source of truth for the version string. - `molt --version` reads that field from the package that shipped the binary. -- Publishable CLI package `@solvyx/molt` is staged by `npm run pack:cli`, which copies the same version into `out-cli/package.json`. -- GitHub Release tags (`v0.2.0`, `v0.3.0`, …) name both surfaces: desktop installers on the release, CLI via `npm i -g @solvyx/molt` at that version. +- The CLI package is staged by `npm run pack:cli`, which copies the same version into `out-cli/package.json`. It is not published to npm (unpublished 2026-10-07); install it from source with `npm i -g ./out-cli`. +- GitHub Release tags (`v0.2.0`, `v0.3.0`, …) name both surfaces: desktop installers on the release, CLI built from source at that tag (see the README's CLI / TUI section). Do not start an independent CLI major while the engine is the same. A CLI-only packaging fix can be a patch (`0.2.1`) with clear notes. diff --git a/package.cli.json b/package.cli.json index 13e3396..7778085 100644 --- a/package.cli.json +++ b/package.cli.json @@ -1,5 +1,6 @@ { "name": "@solvyx/molt", + "private": true, "version": "0.2.2", "description": "Maat Agent \u2014 a coding agent that can't say done without proving it. Built on the molt engine. Terminal CLI and TUI. OpenAI-compatible and Anthropic APIs; uses Grok Build via ACP.", "license": "Apache-2.0", @@ -44,8 +45,5 @@ "ink": "^5.0.1", "react": "^18.3.1", "yaml": "^2.5.1" - }, - "publishConfig": { - "access": "public" } } diff --git a/scripts/prepare-cli-package.mjs b/scripts/prepare-cli-package.mjs index d8ae069..be2cf7f 100755 --- a/scripts/prepare-cli-package.mjs +++ b/scripts/prepare-cli-package.mjs @@ -4,7 +4,9 @@ * * Root package.json stays private for electron-builder. This script builds * dist/, then copies the CLI manifest + allowlisted files into out-cli/ so - * `npm publish ./out-cli` ships the terminal binary (bin: molt) only. + * `npm i -g ./out-cli` installs the terminal binary (bin: maat, molt) only. + * The CLI is not published to npm (unpublished 2026-10-07), so the staged + * manifest is always private and `npm publish` refuses it. */ import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync, chmodSync, existsSync } from "node:fs"; import { join, dirname } from "node:path"; @@ -39,6 +41,9 @@ const manifest = JSON.parse(readFileSync(join(root, "package.cli.json"), "utf8") const rootPkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); // Unified versioning: CLI --version and npm tag follow root package.json. manifest.version = rootPkg.version; +// Fail safe: an accidental `npm publish ./out-cli` must not bring the package back. +manifest.private = true; +delete manifest.publishConfig; writeFileSync(join(out, "package.json"), JSON.stringify(manifest, null, 2) + "\n"); for (const name of ["dist", "examples", "README.md", "LICENSE", "NOTICE"]) { @@ -56,6 +61,6 @@ process.stdout.write( `staged ${out}\n` + ` name: ${manifest.name}@${manifest.version}\n` + ` bin: maat, molt → dist/cli.js\n` + - `next: npm pack ./out-cli # dry-run\n` + - ` npm publish ./out-cli --access public # COO only\n`, + `next: npm i -g ./out-cli # install the maat and molt commands\n` + + ` (private: true, not published to npm)\n`, ); diff --git a/src/providers.ts b/src/providers.ts index bd9928f..49de73f 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -276,14 +276,40 @@ export function openrouterPricing(json: unknown, model: string): Pricing | null * too late. Over-counting stops you early, which is the safe direction for a * ceiling. `/price` overrides both numbers when you want the real ones. * - * Cache reads bill at a tenth of the input rate — which is the whole reason - * the native protocol was worth writing. + * Rows are per model version, not per family: prices differ inside a family + * (Haiku 5.5 is a tenth of Haiku 4.5, Opus 5.5 is cheaper than Opus 5), and a + * family-wide row overstated Haiku 5.5 tenfold. First match wins, so the + * specific versions come before the family fallbacks. `cached` is the + * cache-hit rate, which is not always a tenth of input (Fable/Mythos 5.1 are + * 0.025x, Opus/Sonnet 5.5 are 0.05x). Cache writes have no column here: the + * meter bills written tokens at the base input rate. + * + * Checked against https://platform.claude.com/docs/en/about-claude/pricing + * on 2026-10-08. Haiku 5.5 is priced by prompt length: $0.10 / $0.50 / $0.01 + * for prompts up to 100k tokens, $0.50 / $2.50 / $0.05 over 100k. The meter + * prices session totals, not each request, so it cannot tell the tiers apart; + * the row uses the over-100k rates, because a coding session regularly sends + * long prompts and the cheaper tier would under-count those fivefold. For + * short-prompt work, `/price 0.1 0.5 0.01` sets the lower rates. */ -const ANTHROPIC_PRICES: { match: RegExp; in: number; out: number }[] = [ - { match: /^claude-(fable|mythos)-5/, in: 10, out: 50 }, - { match: /^claude-opus-/, in: 5, out: 25 }, - { match: /^claude-sonnet-/, in: 3, out: 15 }, - { match: /^claude-haiku-/, in: 1, out: 5 }, +const ANTHROPIC_PRICES: { match: RegExp; in: number; out: number; cached: number }[] = [ + { match: /^claude-(fable|mythos)-5[-.]1(?!\d)/, in: 10, out: 50, cached: 0.25 }, + { match: /^claude-(fable|mythos)-5/, in: 10, out: 50, cached: 1 }, + { match: /^claude-opus-5[-.]5(?!\d)/, in: 4, out: 20, cached: 0.2 }, + { match: /^claude-opus-4[-.]1(?!\d)/, in: 15, out: 75, cached: 1.5 }, + { match: /^claude-opus-4(?:-0)?(?:-\d{8})?$/, in: 15, out: 75, cached: 1.5 }, + // Opus 5 and 4.5-4.8, and any Opus not listed above. + { match: /^claude-opus-/, in: 5, out: 25, cached: 0.5 }, + { match: /^claude-sonnet-5[-.]5(?!\d)/, in: 2, out: 10, cached: 0.1 }, + // Sonnet 5's $2/$10 launch price became its standard price (no Sept 2026 rise). + { match: /^claude-sonnet-5(?![-.]?\d)|^claude-sonnet-5-\d{8}/, in: 2, out: 10, cached: 0.2 }, + // Sonnet 4.x, and any Sonnet not listed above. + { match: /^claude-sonnet-/, in: 3, out: 15, cached: 0.3 }, + // Over-100k tier on purpose: see above. + { match: /^claude-haiku-5[-.]5(?!\d)/, in: 0.5, out: 2.5, cached: 0.05 }, + { match: /^claude-3[-.]5-haiku/, in: 0.8, out: 4, cached: 0.08 }, + // Haiku 4.5, and any Haiku not listed above. + { match: /^claude-haiku-/, in: 1, out: 5, cached: 0.1 }, ]; /** The published rate for an Anthropic model, or null if it is not one. */ @@ -294,8 +320,7 @@ export function anthropicPricing(model: string): Pricing | null { return { in: row.in, out: row.out, - // A tenth of input, per Anthropic's published cache-read rate. - cached: row.in / 10, + cached: row.cached, source: "published rates (standard, not introductory) — /price to override", }; } diff --git a/test/anthropic.test.ts b/test/anthropic.test.ts index 8685c36..4d51d14 100644 --- a/test/anthropic.test.ts +++ b/test/anthropic.test.ts @@ -304,20 +304,60 @@ describe("what an Anthropic session costs", () => { // Every other provider molt talks to publishes a price list. Anthropic does // not, so the meter read "no price for this model" on precisely the // endpoint where caching does the most work. - const sonnet = anthropicPricing("claude-sonnet-5")!; + const sonnet = anthropicPricing("claude-sonnet-4-6")!; assert.equal(sonnet.in, 3); assert.equal(sonnet.out, 15); - // A cache read is a tenth of input — the whole reason the native protocol - // was worth writing. + // A cache read is a tenth of input on most models — the whole reason the + // native protocol was worth writing. assert.equal(sonnet.cached, 0.3); assert.equal(anthropicPricing("claude-opus-5")!.in, 5); assert.equal(anthropicPricing("claude-haiku-4-5")!.out, 5); assert.equal(anthropicPricing("claude-fable-5")!.in, 10); // Reached through a router, the model is still the model. - assert.equal(anthropicPricing("anthropic/claude-sonnet-5")!.in, 3); + assert.equal(anthropicPricing("anthropic/claude-sonnet-5")!.in, 2); // And nothing else is guessed at. assert.equal(anthropicPricing("grok-4.6"), null); assert.equal(anthropicPricing("gpt-5"), null); }); + + // Checked against platform.claude.com/docs/en/about-claude/pricing on + // 2026-10-08. The table used to price by family, so Haiku 5.5 billed at + // Haiku 4.5's $1/$5. Haiku 5.5 is tiered by prompt length ($0.10/$0.50 up + // to 100k tokens, $0.50/$2.50 over); the meter cannot tell the tiers apart, + // so it uses the higher one: over-counting stops a budget early, which is + // the safe direction. + it("prices Haiku 5.5 at its own over-100k rate, not the Haiku family's", () => { + for (const id of ["claude-haiku-5-5", "claude-haiku-5.5", "anthropic/claude-haiku-5-5"]) { + const p = anthropicPricing(id)!; + assert.equal(p.in, 0.5, id); + assert.equal(p.out, 2.5, id); + assert.equal(p.cached, 0.05, id); + } + // The older Haiku keeps its own price. + assert.deepEqual( + [anthropicPricing("claude-haiku-4-5")!.in, anthropicPricing("claude-haiku-4-5")!.out], + [1, 5], + ); + }); + + it("prices each version where the family shares no single rate", () => { + const rate = (id: string) => { + const p = anthropicPricing(id)!; + return [p.in, p.out, p.cached]; + }; + assert.deepEqual(rate("claude-opus-5-5"), [4, 20, 0.2]); + assert.deepEqual(rate("claude-opus-5"), [5, 25, 0.5]); + assert.deepEqual(rate("claude-opus-4-8"), [5, 25, 0.5]); + assert.deepEqual(rate("claude-opus-4-1-20250805"), [15, 75, 1.5]); + assert.deepEqual(rate("claude-opus-4-20250514"), [15, 75, 1.5]); + assert.deepEqual(rate("claude-opus-4-0"), [15, 75, 1.5]); + assert.deepEqual(rate("claude-opus-4"), [15, 75, 1.5]); + assert.deepEqual(rate("claude-sonnet-5-5"), [2, 10, 0.1]); + assert.deepEqual(rate("claude-sonnet-5"), [2, 10, 0.2]); + assert.deepEqual(rate("claude-sonnet-4-5"), [3, 15, 0.3]); + assert.deepEqual(rate("claude-fable-5-1"), [10, 50, 0.25]); + assert.deepEqual(rate("claude-mythos-5"), [10, 50, 1]); + assert.deepEqual(rate("claude-3-5-haiku-20241022"), [0.8, 4, 0.08]); + }); });