meowcode
v0.1.6
Published
MeowCode — a Claude Code–style agentic coding CLI (framework skeleton).
Maintainers
Readme
MeowCode
A Claude Code–style agentic coding CLI. This is the framework skeleton — the terminal UI, streaming loop, slash-command system, and a pluggable provider layer are all in place. Real agent capabilities (tools, file edits, planning) are meant to be layered on next.
Quick start
npm install
npm run dev # interactive session (mock provider, no API key needed)Build a standalone CLI:
npm run build # bundles to dist/cli.js with a shebang
node dist/cli.js # or: npm link → then run `meowcode`Using a real model
export ANTHROPIC_API_KEY=sk-ant-...
npm run dev # provider auto-switches to `anthropic`The Anthropic provider streams via the Messages API using the built-in fetch
(no SDK dependency).
Non-interactive / scripting
meowcode -p "explain this repo"
echo "write a haiku about tps reports" | meowcodeCommands
/help · /model <id> · /provider <mock|anthropic> · /config · /entry · /clear · /version · /exit
Config location
All user state lives in ~/.meowcode/ — settings.json, sessions/,
memory/, credentials.json, history.json, and entries/.
This tool was called AnyCode before; its config directory was ~/.anycode.
That directory is no longer read. On the next interactive start, if ~/.anycode
still holds data and ~/.meowcode is empty, MeowCode asks whether to merge it
in — a dialog with the same keys and mouse support as the rest of the UI
(↑↓/wheel to move, 1–2 to jump, ↵/click to confirm, y/n to answer
directly, esc to skip). The merge is additive and never deletes the old
directory, so you can check the result and remove ~/.anycode yourself. To
skip the prompt:
mv ~/.anycode ~/.meowcode # or merge by handNon-interactive runs (print mode, pipes, CI) print a one-line notice with the
same mv hint instead of prompting.
Entries (front-ends)
An entry is a complete front-end of its own — its own UI and wiring, driven
by the same agent — not a per-user profile. A plugin can ship one that replaces
the whole terminal UI (a web front-end, a tray app); the built-in terminal UI is
itself just an entry, named tui, materialized as ~/.meowcode/entries/tui/ on
first start.
meowcode # start the default entry, or the built-in tui
meowcode tui # ...explicitly, the built-in terminal UI
meowcode meowui # start a specific entry (argv shorthand)
meowcode --entry meowui # same, as an explicit flag
meowcode entry install <pkg> # install one (the only way an entry appears)
meowcode entry use meowui # make it the default (~/.meowcode/entry.json)/entry opens an interactive menu over the installed set — pick an entry, then
set it as the startup default, show its details, or remove it. The subcommands
stay for scripting: /entry list, /entry current, /entry default [name|off],
/entry remove <name> [--force].
An entry cannot be hand-authored, and there is no "new entry" anywhere in the UI:
the list is a readout of what you have installed, because what you can install is
decided by the plugins you have. meowcode entry install <source> is the only way
one appears, and entry new refuses with that pointer.
Entries bind at startup (no hot-switching), so every message says a restart is needed. Sessions and memory are shared across entries — switching front-ends does not fork your history.
~/.meowcode/
settings.json ← global base config
sessions/ memory/ projects/ history.json ← shared by every entry
entry.json ← { "default": "<name>" }; absent ⇒ tui
entries/
tui/ entry.json settings.json ← built-in, materialized at startup
meowui/ entry.json settings.json skills/ commands/ agents/Each entry dir holds:
entry.json— manifest and highest-precedence config overrides (description,model,provider,theme,settings,hooks,mcpServers,permissions,customProviders,launcher).settings.json— the entry's saved config. Layering, low → high: built-in defaults → global~/.meowcode/settings.json→ entrysettings.json→ entryentry.json.skills/,commands/,agents/— per-entry content, same lookup order.
Credentials, usage stats, and the update cache stay machine-global. The API key
always comes from the environment (ANTHROPIC_API_KEY), never from an entry.
Installing shared entries
meowcode entry install creates an entry from a template shipped in an npm
package, a git URL, or a local path:
meowcode entry install <npm-package>
meowcode entry install <git-url>
meowcode entry install <local-path>
meowcode entry remove <name> # delete an entry's directoryA template package declares its entry in package.json under
"meowcode.entry"; install never overwrites existing files, so reinstalling
keeps your local edits.
{
"name": "my-meowcode-entry",
"meowcode": {
"entry": {
"description": "A Web front-end for the same agent",
"model": "claude-opus-4-8",
"settings": { "effort": "high" },
"content": {
"skills": { "review": "# Review\n..." },
"commands": { "standup.md": "Summarize what changed since yesterday." },
"agents": { "explorer": "You are a code exploration agent." }
}
}
}
}Entries with a launcher (has-entry plugins)
Most entries are just config + content for the normal TUI. An entry can also
change how MeowCode starts: its entry.json may declare a launcher
front-end (e.g. a web UI). When that entry is active, meowcode skips the Ink
TUI entirely and spawns the launcher instead — the plugin drives the agent
through a JSON-RPC bridge, and the transcript lands in the shared sessions/
(/resume-compatible, same as in the TUI).
The plugin is the JSON-RPC client and MeowCode the server, over the
child's stdin/stdout (newline-delimited JSON-RPC 2.0 — same framing as MCP
servers, opposite direction). On startup the host sends one initialize
request — { protocolVersion: "2025-meowcode-launcher-1", serverInfo: { name,
version } } — which the plugin answers (any result value), then:
| Direction | Method | Payload → result |
|---|---|---|
| plugin → host | agent/turn | { prompt } → { turn }: runs one full agent turn (tool loop included), streaming progress as agent/event notifications |
| host → plugin | agent/event | { turn, event }: one AgentEvent (text / thinking / tool_use / tool_result / usage / error / workflow / agent) |
| plugin → host | agent/abort | {} → { aborted }: stops the in-flight turn (the TUI's esc); aborted: false when nothing is running |
| plugin → host | tools/list | {} → { tools } (Anthropic-format schemas) |
| plugin → host | tools/call | { name, input } → the ToolResult |
| plugin → host | config/get | {} → the effective AppConfig (apiKey stripped) |
| plugin → host | session/state | {} → { sessionId, messages, usage } |
| plugin → host | session/reset | {} → { sessionId }: drops the transcript (like /clear) |
Rules of the road:
entry.jsonform:"launcher": { "command": "node", "args": ["launcher.js"] }. A relativecommand/argsstring that names a file in the entry dir is anchored there — that is where the installer puts it — while anything else (a barenode/bunonPATH, a flag, a path for the launched program itself) is passed through untouched.cwddefaults to the entry dir; a relativecwdis written through as-is, andspawnresolves it against the parent's cwd, so"cwd": "."is how a manifest asks for the session's own directory. Optionalenvadds variables.meowcode entry installcopies the files referenced by relativelauncher.argsinto the entry, as it does for relativemcpServersargs.- One turn at a time — a second
agent/turnwhile one streams is rejected.agent/abortstops the running one: the pendingagent/turnstill resolves with{ turn }, the partial answer is committed withmeta.interrupted, and no⚠error row is added — so a plugin learns the turn is over from the turn's own resolution and needs no abort event. The bridge reads lines one at a time, so an abort sent beforeagent/turnhas installed its controller finds nothing to stop and answers{ aborted: false }. - Headless: turns run with
bypassPermissions(the plugin owns interaction and gates before calling), but the entry'spermissionsdeny/allow rules still bind. Hooks fire when the entry brings them; there is nobody to answerrequestUserInput, so theask_usertool reports that by itself. - Session state lives in the host, so the plugin can stay stateless. Print mode
(
-p/--print) bypasses the launcher and uses the normal non-interactive path. - Entry
mcpServersstart with the host (the TUI never mounts in launcher mode). Discovery is async — a server'smcp__*tools appear intools/listonce its handshake completes (same "next turn" semantics as the TUI), so a plugin should re-list before its firstagent/turnif it needs them. /entryandmeowcode entry listmark these entries[launcher].
Known gaps, for plugin authors who need to plan around them: the host never
reports the session cwd (declare "cwd": "." in launcher — a relative cwd
resolves against the parent's, so that yields the session's own directory; omit
it and process.cwd() is the entry dir), there is no interactive permission
prompt (an ask decision falls through to allow), abort produces no event
(only the turn's resolution), and there is no fs/* method — read what the
host's tools would, or read the filesystem directly.
Minimal plugin sketch (node) — answer initialize, run one turn, exit (the
host saves the transcript):
// launcher.js — toy front-end: one turn, log events to stderr, exit
const readline = require('node:readline');
let id = 1;
const pending = new Map();
const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
readline.createInterface({ input: process.stdin }).on('line', (line) => {
const msg = JSON.parse(line);
if (msg.id && !msg.method) pending.get(msg.id)?.(msg.result); // host reply
else if (msg.method === 'agent/event') process.stderr.write(`[${msg.params.turn}] ${msg.params.event.type}\n`);
else if (msg.id) send({ jsonrpc: '2.0', id: msg.id, result: {} }); // initialize
});
const call = (method, params) => new Promise((res) => {
const i = id++; pending.set(i, res);
send({ jsonrpc: '2.0', id: i, method, params });
});
(async () => {
await call('config/get', {});
const { turn } = await call('agent/turn', { prompt: 'Say hi in one line.' });
const state = await call('session/state', {});
process.stderr.write(`turn ${turn}: ${state.messages.at(-1)?.content}\n`);
process.exit(0);
})();Layout
src/
cli.tsx entry point (arg parsing, print mode, render)
app.tsx Ink app: layout, global keys, elapsed timer
theme.ts colors + symbols
types.ts Message / Provider / Command contracts
config.ts ~/.meowcode/settings.json load/save
hooks/useChat.ts conversation state machine + streaming
components/ Banner · Message · StatusLine · PromptInput
commands/ slash-command registry
providers/ mock (offline) + anthropic (real, fetch/SSE)
lib/ markdown rendering · spinner frames · token estimateExtending
- Add a command: push a
SlashCommandontoregistryinsrc/commands/index.ts. - Add a provider: implement the
Providerinterface and register it insrc/providers/index.ts. - Add tools / file edits: give a provider access to a tool loop and render
toolmessages insrc/components/Message.tsx(the role already exists).
