Releases: moshcoder/moshcode
Release list
v0.59.0 — what the herd is costing you
The other question you have with six agents running
moshcode ps answers "which one is blocked". It has never answered "what is
this costing me", and with a herd running overnight that is the question you
wake up to.
session engine model in out cache cost age
api claude claude-opus-5 1.2k 27k 10.5M $9.91~ 42m
audit codex gpt-5.6-sol 400 200 600 — 12m
total $9.91~ 1.6k in · 27k out · 10.5M cached
~ estimated from published rates; unmarked figures are the engine's own.
⚠ no rate for gpt-5.6-sol — tokens counted, cost omitted.
Nothing is instrumented, wrapped or proxied to produce that. Every engine
already writes down what it used, because it needs the numbers itself — Claude
Code keeps a per-message usage block in ~/.claude/projects/**/*.jsonl, Codex
emits cumulative token_count events into ~/.codex/sessions/…, opencode and
privacycode store a cost they computed themselves in SQLite, and aider prints
a running session total into .aider.chat.history.md beside the code. This
reads what is on disk and lines it up against the herd.
moshcode cost # per session, last 24h
moshcode cost api # one session, with its engine runs
moshcode cost --all --since 7d # every engine session on the box, herd or not
moshcode cost --watch # re-read every 10s — the slope is the point
moshcode cost --json # for a scriptAlso moshcode herd cost, /cost in the pit, and cost() in moshscript.
--all needs no herd at all, so a foreground moshcode agents claude is
covered the same way.
A ~ is an estimate and an unmarked figure is not
opencode and aider price their own messages, and that price is reported
untouched — they know which model actually served the request. Claude Code and
Codex record tokens and nothing else, which is the honest state of things on a
subscription where the marginal request costs nothing extra, so those get
multiplied by a rate card and marked. The mark is the whole point: one of those
numbers is a measurement and the other is "what this would have cost on the
API". --json carries it as costSource.
A model nobody has priced shows its tokens and no cost, rather than a
convincing-looking $0. Only Anthropic rates ship, because a Codex or Kimi
model id (gpt-5.6-sol is what a rollout actually records) does not appear on
any public price list and inventing one would be worse than a blank. Your own
rates go in ~/.moshcode/pricing.json and win over the shipped table:
{ "gpt-5.6-sol": { "input": 1.25, "output": 10 } }Cache tokens get a column of their own for the same reason. On a long agent
session they are most of the traffic and a tenth of the price — fold them into
in and a $3 session reads like a $60 one, which is the opposite of the point.
Attribution is engine + directory + "started before this run did". That is right
for the ordinary case of one agent per directory and can be wrong when two
sessions of the same engine share one, so --json carries the raw run list
rather than implying an answer, and a run that matches no session is its own
line instead of being absorbed into a neighbour's total. gemini, kimi, qwen,
deepseek and openagents keep no readable usage log, so they report no cost —
which is not the same as zero cost, and the report says so.
A name can publish, not just point
target proxies a name at a web server its owner already runs. That is the
right primitive and the wrong first step: most people who claim a name have
something to publish and nowhere to publish it from, so the commonest thing in
the registry was a name that never became a site.
A name can now carry a feed — paste an RSS or Atom URL next to any name you
hold and /n/<name> renders it, as a blog with dates and excerpts or as a
podcast with cover art, running times and a player. The layout is inferred from
whether the entries carry audio enclosures, and can be overridden when the guess
is wrong.
The other half is somewhere to put writing that does not exist anywhere yet:
POST /api/moshpit/sites/blue.eggs/content
{"kind":"link","title":"Worth reading","url":"https://example.com/post"}
Eight kinds — section and page for navigation, then text, link, image, gallery,
video and embed — which is the set a link aggregator has because it is the set
people actually post. It upserts on the slug, because the caller is a webhook
and anything firing over HTTP retries: a publish endpoint that makes a second
copy on every redelivery is one you cannot point automation at.
And GET /api/moshpit/log finally exposes the allocation record. "The directory
can be mirrored and served by anyone" was true of the schema and false of the
product, because nothing could read the log; now a second copy of the registry
is something a stranger can build.
moshscript catches up with the CLI
The vocabulary had drifted to 43 verbs against 54 commands, with no way for a
script to authenticate, reach the pit's aliases, or read a tool's output as
anything but an exit code. Account verbs (login, whoami, requireLogin,
logout) close that, with identity() in src/auth.mjs as the one place that
decides what "logged in" means so the CLI and a script can never disagree.
The paddle is on the game's clock
Pong and breakout tick at 60Hz and move the ball on that clock; the paddle — the
one object you are actually steering — was still being moved by the keypress
itself, one jump per key. A terminal has no key-up, only auto-repeat, so that is
two problems and neither of them is "the paddle is slow". Both paddles now run
on the same clock as everything else on the board.
Housekeeping
The ThreatCrush pack moves to 1.7.0, replacing a local scoping fix, and its PR
comment is scoped to the files a PR actually changes.
v0.58.0 — a silent hand-off says so
A hand-off that runs nothing no longer looks like a clean session
/agents deepseek printed this, and it read as success:
· opening deepseek autonomously — hand-off to its CLI, exit it to come back…
· deepseek exited (code 0). back in the pit.
The engine had not run a single line. "exited (code 0)" is what the pit says
when somebody opens an agent and quits it, so the failure was invisible and the
search for the bug started in the wrong program.
It is a real failure mode rather than a hypothetical. @serjm/deepseek-code
0.5.0 decides whether it is the entrypoint by comparing
resolve(process.argv[1]) against import.meta.url, and npm installs every
global bin as a symlink — so the comparison never matches, the program falls off
the end having done nothing, and exits 0. Anyone who installed it the documented
way got a CLI that silently does nothing. (A fix is open upstream.)
The child owns the terminal while it runs, so its output is not the pit's to
inspect. The clock is enough: an exit of 0 in under a second and a half did not
host a session anybody used, and the pit now says so —
· deepseek exited (code 0). back in the pit.
⚠ deepseek exited instantly without running — that usually means a broken
install, not a clean session. check it directly with `deepseek-code
--version`, and reinstall with /install deepseek if that prints nothing.
Only for silent success. A non-zero exit already says something went wrong, and
the engine has usually printed why; adding "that was fast" on top would be
noise. Workflow-tool hand-offs get the same treatment, because they fail the
same way.
v0.57.0 — smallweb stops being a catalogue
smallweb stops being a catalogue
/rss and /news find were carrying Kagi's Small Web as its catalogue: a 5MB
OPML of 33,000 personal blogs, plus a 1.1MB plain-text fork of the same list.
Both were marked search-only, which kept them out of a subscription but not out
of a search — and find loads every published list on every search, so each
search parsed 33,000 outlines. That is what took the reader down.
Kagi publishes those same blogs already merged. kagi.com/smallweb/feed is one
Atom document, newest first, several thousand recent posts with titles and
summaries, and that is what smallweb is now: one feed wearing a list's name.
/news add smallweb subscribes to it without fetching anything, /news rm smallweb takes it back out, and it reads on its own with
/news --feed kagi-small-web.
A catalogue can no longer be subscribed to by any route
Search-only guarded the list name, so add smallweb was refused while
add https://kagi.com/smallweb/opml quietly wrote 32,969 outlines into
news.opml — after which every /rss tried to fetch all of them. A URL is not
a smaller promise than a name, so the limit now sits on the number of feeds an
add subscribes to, whatever the source: named list, URL, or local file. It is
set well above every published list and well below a catalogue.
If you already have one in your subscriptions, ~/.moshcode/news.opml is the
file to look at — an import by URL lands untagged, so /news rm <list> cannot
find it.
Aggregated feeds no longer crowd out everyone else
Two guards that apply to every feed, not just this one. A feed read stops at 2MB
and parses what arrived, rather than failing outright at the document cap — a
10MB firehose used to be refused entirely when its newest few hundred entries
were exactly what was wanted. And no single feed may put more than 200 items
into a merged listing, because otherwise subscribing to an aggregator is the
same as unsubscribing from everything else: its same-day entries sort above
every other publisher's headlines.
v0.56.0 — MCPJam joins the tool registry
MCPJam joins the tool registry
moshcode install mcpjam now installs MCPJam — test, debug, and validate MCP servers from the terminal or CI: health checks, OAuth conformance, tool-surface diffing, and structured triage.
moshcode install mcpjam # npm i -g @mcpjam/cli
moshcode mcpjam --help # straight through to the native CLIIt is the companion to moshcode mcp. That command registers an MCP server across your engines and fans the config out; this one tells you whether the server is worth registering in the first place — and, once it is registered, whether it still answers the way it did last week.
Like every other workflow tool, /mcpjam passes arguments, stdin, stdout, stderr, and the native exit code straight through — moshcode conducts the CLI, it does not reimplement it. mcpjam(args…) is available in .mosh scripts, and the name is in shell completions, /help, and moshcode tools alongside the rest of the registry.
MCPJam is published as an ordinary global npm package, and npm install -g is idempotent, so re-running the install is also the upgrade: moshcode upgrade mcpjam moves it to the current version with no separate updater involved.
Full changelog: v0.55.0...v0.56.0
v0.55.0 — aliases in, job control out
A version-only release
No code changes since v0.53.0 — the tree is identical, as it was for v0.54.0. Cut deliberately, so moshcode upgrade and npm both have a fresh version to move to.
What these releases actually carry, if you are coming from v0.51.0 or earlier:
Your shell aliases work in the pit
The pit runs what you type through $SHELL, and it used to do that with -c. A shell started with -c is non-interactive, and a non-interactive shell does not read ~/.zshrc or ~/.bashrc — so a /alias that named a shell alias failed with command not found while the identical word worked when typed at a prompt. Naming a shell command is most of what /alias is for, so that was the common case, not an edge one.
Pit commands now load your rc file, and your aliases and functions resolve.
…without the pit suspending itself
Asking for an interactive shell brings job control along, unasked, and that had a sharp edge. An interactive shell makes itself a process group leader and claims the terminal, then on exit hands it back to what it believes the shell before it was. The pit is not a shell and does not play that game, so the terminal could be left belonging to a process group that had already exited — and the pit's very next write took SIGTTOU:
· shell exited (code 0). back in the pit.
[1] + 3034615 suspended (tty output) moshcode
Pit commands now run with +m, which keeps the rc file and drops job control. Nothing is given up: job control is for managing several jobs at a prompt, and this shell runs one command and exits. It also restores exactly the signal behaviour of the plain -c it replaced.
bash is left alone on purpose — an interactive bash turns job control back on regardless of +m, so passing it there would advertise a protection that is not real.
The root password is asked for before the work
Some installers escalate on their own, partway through their own work: tailscale's goes through your distro's package manager, so it asks for a password only after it has already spent time refreshing package lists. Tolerable on its own, and not inside moshcode update, which walks a plan of moshcode + every installed engine + every tool. On a machine with tailscale installed that prompt landed at step 17 of 18 — long after anyone was still watching.
The password is now asked for once, before the first hand-off. It stays quiet when no step in the plan needs root, when a credential is already cached or the rule is NOPASSWD, when there is no terminal to type into, when the box has no sudo/doas, and when you are already root. macOS is exempt for tailscale, where the App Store does its own authorisation. A declined prompt is never fatal — the installer still prompts for itself if it turns out to need to.
Full changelog: v0.54.0...v0.55.0
v0.54.0 — aliases in, job control out
A version-only release
No code changes since v0.53.0 — the tree is identical. Cut deliberately, so that moshcode upgrade and npm both have a fresh version to move to.
If you are coming from 0.51.0 or earlier, the two releases just behind this one are the ones that matter, and they are best read together.
Your shell aliases work in the pit
The pit runs what you type through $SHELL, and it used to do that with -c. A shell started with -c is non-interactive, and a non-interactive shell does not read ~/.zshrc or ~/.bashrc — so a /alias that named a shell alias failed with command not found while the identical word worked when typed at a prompt. Naming a shell command is most of what /alias is for, so that was the common case rather than an edge one.
Pit commands now load your rc file, and your aliases and functions resolve.
…without the pit suspending itself
Asking for an interactive shell brings job control with it, unasked, and that had a sharp edge: an interactive shell makes itself a process group leader and claims the terminal, then on exit hands it back to what it believes the shell before it was. The pit is not a shell and does not play that game, so the terminal could be left belonging to a process group that had already exited — and the pit's very next write took SIGTTOU:
· shell exited (code 0). back in the pit.
[1] + 3034615 suspended (tty output) moshcode
Pit commands now run with +m, which keeps the rc file and drops job control. Nothing is given up for it: job control is for managing several jobs at a prompt, and this shell runs one command and exits. It also restores exactly the signal behaviour of the plain -c it replaced.
bash is left alone on purpose — an interactive bash turns job control back on regardless of +m, so passing it there would advertise a protection that is not real.
The root password is asked for before the work
Some installers escalate on their own, partway through their own work: tailscale's goes through your distro's package manager, so it calls for a password only after it has spent time refreshing package lists. Tolerable on its own, and not inside moshcode update, which walks a plan of moshcode + every installed engine + every tool. On a machine with tailscale installed that prompt landed at step 17 of 18 — long after anyone was still watching.
The password is now asked for once, before the first hand-off. It stays quiet when no step in the plan needs root, when a credential is already cached or the rule is NOPASSWD, when there is no terminal to type into, when the box has no sudo/doas, and when you are already root. macOS is exempt for tailscale, where the App Store does its own authorisation. A declined prompt is never fatal — the installer still prompts for itself if it turns out to need to.
Full changelog: v0.53.0...v0.54.0
v0.53.0 — the pit stops suspending itself
The pit stops suspending itself
v0.52.0 taught pit commands to load your shell's rc file so /alias could name a shell alias. It worked, and it also brought job control along with it — which could take the pit's terminal away and not give it back:
mosh ▸ /gh-prs-merge-all
· zsh -ic gh-prs-merge-all --apply
────────────────────────────────────────
Summary: ready=1 readied=0 merged=1 skipped=0 failed=0
────────────────────────────────────────
· shell exited (code 0). back in the pit.
[1] + 3034615 suspended (tty output) moshcode
An interactive shell makes itself a process group leader and claims the terminal, then on exit hands it back to what it believes the shell before it was. The pit is not a shell and does not play that game, so the terminal could be left belonging to a process group that had already exited — and the pit's very next write took SIGTTOU.
Measured under a real terminal: zsh -c has job control off and cannot see your aliases, zsh -ic sees them and has job control on, and zsh +m -ic sees them with job control off. So pit commands now run with +m.
Nothing is given up for it. Job control is for managing several jobs at a prompt, and this shell runs one command and exits; +m also restores exactly the signal behaviour of the plain -c that v0.51.0 used, where the command shared the pit's process group.
bash is left alone on purpose: an interactive bash turns job control back on regardless of +m, so passing it there would advertise a protection that is not real.
If you are on 0.52.0, this is the release that supersedes it.
The root password is asked for before the work, not partway through
Some installers escalate on their own, partway through their own work — tailscale's goes through your distro's package manager, so it calls for a password after it has already spent time refreshing package lists. That is tolerable when it is the only thing running. Inside moshcode update it was not, because that walks a plan: moshcode itself, then every installed engine, then every tool. On a machine with tailscale installed the prompt landed at step 17 of 18 — long after anyone was still watching, and sometimes after the escalation helper's own timeout had already failed the step.
MoshCode now asks first, before the first hand-off. The credential is cached against the terminal that every installer inherits, so the escalation they do later finds it already there and never asks.
It stays quiet unless it has something to ask. Nothing happens when no step in the plan needs root — the ordinary moshcode update never prompts — nor when a credential is already cached or the rule is NOPASSWD, nor when there is no terminal to type into, nor when there is no sudo/doas on the box, nor when you are already root. macOS is spared too: there the tailscale script delegates to the App Store, which does its own authorisation.
A declined prompt is never fatal. The installer prompts for itself if it turns out to need to, which is exactly the old behaviour — being unable to ask early is a missed convenience, not a reason to refuse to install.
Full changelog: v0.52.0...v0.53.0
v0.52.0 — your shell aliases work in the pit
Your shell aliases work in the pit
An alias that names a shell command used to fail with command not found while the identical word worked when typed at a prompt:
mosh ▸ /prs
▸ !gh-prs-all
· zsh -c gh-prs-all
zsh:1: command not found: gh-prs-all
The pit ran everything through $SHELL -c, and zsh -c and bash -c are non-interactive shells — they do not read ~/.zshrc or ~/.bashrc, so the aliases and functions defined there were simply not present. Having ~/.zshrc source your ~/.zsh_aliases made no difference, because the rc file itself was never read.
That was the common case rather than an edge one. Naming a shell command is most of what /alias is for, and the commands people name are the ones they already named once in their rc file.
Shell commands now ask for an interactive shell, so your rc file loads and your aliases and functions resolve. It applies everywhere the pit reaches a shell — !cmd, /shell, a shell-valued /alias, and moshscript's shell() — all through one place, so the line the pit echoes is built from the invocation it actually runs:
mosh ▸ /prs
▸ !gh-prs-all
· zsh -ic gh-prs-all
Nothing that worked before stops working: anything already on PATH resolved then and resolves now. What changed is that aliases and functions resolve too.
The scope is deliberately narrow. Only bash and zsh are asked for an interactive shell — fish sources config.fish however it was started, sh and dash have no rc file to miss, and a shell we have not heard of keeps the old behaviour rather than getting a guessed flag. It is also gated on a terminal being attached, because an interactive bash without one prints cannot set terminal process group and no job control in this shell to stderr before running anything, which would wrap every cron, CI, and piped run in noise. Set MOSHCODE_SHELL_NO_RC=1 to buy back a plain -c.
Full changelog: v0.51.0...v0.52.0
v0.51.0 — Coral joins the tool registry
Coral joins the tool registry
moshcode install coral now installs Coral — a read-only SQL layer over your APIs, databases, and internal systems, so an agent can query across sources through one connection.
moshcode install coral # runs the vendor's official install script
moshcode coral sql "SELECT * FROM github.pulls LIMIT 5"Like every other workflow tool, /coral passes arguments, stdin, stdout, stderr, and the native exit code straight through — moshcode conducts the CLI, it does not reimplement it.
The installer resolves the latest GitHub release, verifies its sha256 checksum, and drops the binary in ~/.local/bin. Coral ships no self-updater and documents re-running the installer as the upgrade path, so moshcode upgrade coral does exactly that.
Full changelog: v0.50.0...v0.51.0
v0.50.0 — /save works again, and openagents joins the registry
/save failed with 502 every time unless you passed --force, and the cause was one missing clause. The precondition that makes a save refuse to clobber a newer revision was expressed as a HAVING on an implicit single-group aggregate — no GROUP BY. SQLite treats the whole result as one group and runs that happily; Turso's parser rejects it outright, near HAVING, "None": syntax error. The route threw, the platform returned 502, and the CLI reported exactly what it saw. The retries added in #380 could never help: an unparseable statement fails the same way on every attempt.
What kept it alive is that the tests could not see it. They run against file: — a different engine from the deployment — and the precondition is genuinely covered there, a stale revision refused as 409 and a current one accepted. That suite passed throughout, because on SQLite the statement is valid. GROUP BY user_id makes it parse on both engines and cannot change the answer, since the caller passes a null precondition when the account has no current revision, so there is always a row to group. It was verified against the real database rather than only locally. The new guard is static rather than behavioural for the same reason the bug survived: only reading the SQL can catch this, so sql-portability.test.mjs now fails on any uppercase HAVING in src/ with no GROUP BY, comments stripped, because "having" is also an English word and four files that contain no SQL say it in prose.
The other half of this release is a new engine, and it is the first entry in that registry that is not a coding agent. moshcode install openagents runs the vendor's own installer, and OpenAgents launches and supervises the engines around it rather than answering prompts itself. That shapes every field: there are no approvals to bypass and no yolo flag, so its autonomous-mode arguments are empty, and a bare launch opens the dashboard — which is its agent list, so agents openagents and start openagents land in the same place instead of one of them pretending to be an agent view. It stays out of the ai() headless map for the same reason; asking it to answer a prompt names it in the error rather than silently picking it as your engine.
The installer unpacks to ~/.openagents/nodejs and only appends that directory to a shell rc, so PATH will not see the binary until your next shell — including in the moshcode session that just ran the install. It declares that directory the same way kimi and opencode already do, so the engine reads as installed immediately. One thing worth knowing before you run it: the installer ends by offering to pair the machine with a workspace and waits for a real answer. It only asks when stderr is a terminal, and Enter skips it — but moshcode install hands the child the terminal, so that prompt is yours to answer. Run it somewhere you can reach a keyboard.
Full changelog: v0.49.0...v0.50.0