Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/acp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 1 addition & 3 deletions package.cli.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -44,8 +45,5 @@
"ink": "^5.0.1",
"react": "^18.3.1",
"yaml": "^2.5.1"
},
"publishConfig": {
"access": "public"
}
}
11 changes: 8 additions & 3 deletions scripts/prepare-cli-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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"]) {
Expand All @@ -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`,
);
43 changes: 34 additions & 9 deletions src/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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",
};
}
Expand Down
48 changes: 44 additions & 4 deletions test/anthropic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});
Loading