DEV Community

Cover image for I built a desktop app for Meta's Muse Code CLI, and the billing is the whole story
Harjot Rana
Harjot Rana

Posted on Originally published at harjotrana.com

I built a desktop app for Meta's Muse Code CLI, and the billing is the whole story

Meta shipped Muse Code in August: a coding agent that lives in your terminal, with a subscription starting at five dollars a month. The model is good. The harness runs subagents in parallel and keeps an append-only event log you can replay.

Two things about it bothered me enough to spend a fortnight on them.

It is terminal-only. And on Windows there is no native build at all, so several comparison guides now tell readers that if they are on Windows or want a desktop app, they should pick a competitor.

So I built Helicon: an open-source desktop and web client for the actual Muse CLI. This post is about the one design decision that shaped everything else, because it is the part I got wrong first and the part most people get wrong when they wrap a CLI.

The mistake I made first: scraping the terminal

The obvious way to put a UI on a CLI is to spawn it in a pty, parse what comes out, and render that.

I did this. It took an afternoon and it worked, in the way that a demo works.

Then I tried to implement approvals. The agent wants to run npm test, and the user needs to allow or reject it. In a scraped terminal, "the agent is asking for approval" is a string you pattern-match out of ANSI escape codes, and your answer is keystrokes you write back into a pty. You are inferring a security decision from formatted text. Every output tweak upstream becomes a correctness bug in your client, and some of those bugs run commands nobody approved.

I deleted it.

What replaced it

Meta publishes the Muse Code SDK, MIT licensed, for programmatically driving Muse over the Muse Session Protocol. The CLI itself exposes muse serve, which speaks that protocol over stdio.

So Helicon's daemon spawns one muse serve host per workspace and talks MSP to it through the official SDK. Muse is the agent. Helicon is a client.

That means approvals arrive as protocol events, not as parsed text. Diffs arrive as structured data. Sessions that were started in the terminal show up in the sidebar with their history, because the CLI already wrote them to disk in a format the protocol can read back. Nothing is inferred from what the screen happened to look like.

The rule I would give anyone wrapping a CLI: if the vendor documents a protocol, the protocol is the only surface that stays where you left it. Output formatting is not an API, no matter how stable it looks today.

The part that decides your bill

Here is the thing I did not expect to be the most important feature.

Muse Code subscriptions only bill through Muse's own harness. If you point a generic OpenAI-compatible client at the API, you are on pay-as-you-go rates instead. Same model, different bill.

I have watched people work this out the hard way in public and reach the wrong conclusion. One thread has a user stating, twice, that a Muse Code subscription "can not be used in any kind of GUI harness" and that you have to go pay-as-you-go to use it anywhere but the terminal. Another person in the same thread confirms the symptom: using the vendor's harness deducts from the plan, and every other option lands on the API.

The observation is correct. The conclusion is not.

The subscription is not terminal-locked. It is harness-locked. Once you stop trying to replace the harness and start driving it instead, a GUI costs nothing extra, because the work is still going through Muse with your own muse login.

That is the entire argument for this architecture, and it is worth more than any UI feature I could build. If your wrapper reimplements the agent loop against the raw model API, you have quietly moved your users onto a second bill for a model they already pay for.

Windows, honestly

Muse has no Windows binary. That is not something a client can fix.

What Helicon does is more boring than "Muse Code on Windows" sounds. The daemon runs natively on Windows, and when it needs the agent it runs:

wsl -d Ubuntu -- muse serve
Enter fullscreen mode Exit fullscreen mode

Then it translates paths in both directions, because the daemon thinks in C:\Users\you\project and the agent thinks in /mnt/c/Users/you/project. Get that wrong and every file operation lands somewhere plausible and wrong.

export function toWindowsPath(wslPath: string): string {
  const match = wslPath.match(/^\/mnt\/([a-z])\/(.*)$/);
  if (!match) {
    throw new Error(`Cannot map to Windows: not a /mnt/<drive> path: ${wslPath}.`);
  }
  const drive = match[1].toUpperCase();
  const rest = match[2].replace(/\//g, "\\");
  return `${drive}:\\${rest}`;
}
Enter fullscreen mode Exit fullscreen mode

Note the throw. An unmappable path is a bug, not a thing to guess at.

So WSL2 is still required, and I say that on the landing page, in the install steps and in the FAQ. What changes is not the requirement. It is that you stop living in an Ubuntu terminal on your own machine to use a tool you pay for. The installer is signed and auto-updates, which on Windows matters more than people from other platforms expect.

Bundling the runtime

The daemon is Node. Until last night, that meant a user had to install Node 22 or newer before Helicon would start, and on Windows they had to install it on the Windows side rather than inside WSL, which is exactly the kind of instruction that loses people.

The fix is a Tauri sidecar. A build script downloads a pinned Node build, verifies its SHA256 against the published checksums, and drops it in src-tauri/binaries/node-<target-triple>. For universal macOS builds, lipo merges the two architectures. At boot the app prefers the binary next to its own executable and only falls back to a system Node if that is missing.

It costs about 30MB on the Windows installer. It removes an entire step from the install instructions, and an entire category of "it doesn't launch" issue. Worth it.

One detail worth copying: the bundled runtime's folder is deliberately left off the PATH handed to subprocesses. Users run shell commands through the app, and their node should be their node, not mine.

What it looks like

Projects grouped by folder, worktrees included. Threads with inline diffs where the edit happened. Approvals surfaced the moment they arrive, never batched, never bypassed. A cost view that shows what each thread would have cost at published API rates, so "is this plan worth it" becomes a number instead of a feeling. Command palette, slash commands, full keyboard operation.

The same React UI ships twice: as a Tauri desktop app, and as a web app pointed at a remote daemon. One codebase, two shells.

Honest gaps

The macOS builds are not Apple-notarized yet, so the first launch needs a right-click and Open. Linux has no packaged build, only a source path. The SDK is a developer preview, so the protocol can move under me. And Helicon is not the only GUI in this space — there are several, including ACP adapters for Zed and JetBrains, and an unofficial VS Code extension. If you want Muse inside your editor, use those. Helicon is for people who want a standalone workspace.

It is free, MIT licensed, and unofficial: not made, sponsored or endorsed by Meta.

github.com/HarjjotSinghh/helicon · helicon.sh

If you use Muse Code on Windows, I would like to know what breaks. That is the platform I have tested most and can verify least.

Top comments (0)