A minimal CLI coding agent for quick prototypes, learning, and hackability.
p90 is like Codex, Claude Code, and Roo/Cline, but is roughly ~1,000 lines of code and is meant to demonstrate the core components of a CLI coding agent.
It is built on ink, react, and node.js
- Node > 22
- Linux, MacOS, or WSL
yarn
yarn dev(Or using the package manager of your choice)
📦 p90.src
┣ 📂components: pure react components
┣ 📂hooks: common hooks
┣ 📂models: core app types
┣ 📂reducers: core app state + management
┣ 📂tools: agent accessible tools
┣ 📂utils: general functionality
┗ cli.tsx: entry point
cli.tsx is the entry point that maps command line args into state and functionality. It is the top level orchestrator that facilitates the intersection of view, state, and action (similar to MVC).
cli.tsx should generally be kept thin of any logic itself, instead delegating those responsibilities to components, the reducer store, or a hook.
📂components is responsible for view. Each component should be concerned with mapping state to view, and generally be side effect and state free. They may hold some minor view state for optimization purposes.
📂hooks holds the global react hooks. Hooks are generally for actions with side effects, async work, or the like. For example, useHandlers makes async https requests and writes to logs. It then uses the reducer dispatch to change app state. This is preferable to making the reducer impure.
📂models holds the global app types. These can be referenced and imported anywhere (view, state, etc) and give a core foundation to build the rest of the app around. This CLI app is built around an array of Messages, which are standard LLM API objects.
📂reducers is responsible for app state. It uses the standard reducer pattern and should be kept pure.
📂tools is a set of modules meant to be accessed and used by the LLM to achieve user tasks. As you can see, giving the LLM access to the usual shell commands gives plenty of search/read/edit power. dispatcher.ts is the tool orchestrator.
📂utils is for all general JavaScript utilities that don't fit into any of the above areas.
At the root, this is all a wrapper around an array of system-user-assistant-tool messages:
[
{ type: system, prompt: "You are a coding assistant [...]" },
{ type: user, message: "Write a README.md" },
{ type: assistant, message: "Sure I can do that!" },
{ type: tool, message: "ls -l" },
]When a user enters text into the CLI, a user message is appended onto the list and the chat function in agent.ts gets an assistant response. If this assistant response has tool calls, we turn those into tool messages and append them. We loop until the assistant decides it is done.
Everything else in the app is simply UI around this core loop.
If you're running in WSL, you can launch the project in vscode by running code . in the project root. It will properly handle the file interop.
Remember to run yarn in WSL and not in Windows.
