From CLI to Web App: When Prototypes Meet Real Workflows
May 29, 2025
The command-line prototype was working! Well, “working” in the sense that a paper airplane “works” as an aircraft. Sure, it could answer questions about our documentation, but try explaining to your team why they need to open a terminal, activate a virtual environment, and type commands like it’s 1985.
I could run github_agent.py and see a successful login:
% python github_agent.py
/Users/xian/Library/Python/3.9/lib/python/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
warnings.warn(
2025-07-10 05:27:47,782 - pm_agent_app - INFO - Logging configured successfully.
2025-07-10 05:27:48,120 - pm_agent_app - INFO - ✅ Connected to GitHub as: mediajunkie
2025-07-10 05:27:48,120 - pm_agent_app - INFO -
📚 Your repositories:
2025-07-10 05:27:48,647 - pm_agent_app - INFO - - 7cups-testbot 🌍
2025-07-10 05:27:48,647 - pm_agent_app - INFO - - christiancrumlish.com 🌍
2025-07-10 05:27:48,648 - pm_agent_app - INFO - - civictech.club 🌍
2025-07-10 05:27:48,648 - pm_agent_app - INFO - - crumlish.me 🌍
2025-07-10 05:27:48,648 - pm_agent_app - INFO - - designinproduct 🌍Or I could run a separate command-line app github_reviewer.py to give it the URL of an issue to review. That was about it, and it’s not like anyone else was going to be able to access this from my machine.
The CLI comfort zone
There’s something beautifully simple about command-line interfaces. No buttons to align, no CSS to fight with, no JavaScript framework wars. Just you, a terminal, and the naive belief that everyone loves typing commands as much as you do.
Some people believe we are currently in the CLI era with these LLM-powered AI thingies. I guess maybe I like this period when it’s still all about typing and words?
My CLI prototype was elegant in its constraints:
- Input: Type question
- Output: Get answer
- Error handling: Stack trace (that’s helpful, right?)
- User experience: What’s that?
This worked great for an audience of one (me). For an audience of more than one? Time to enter the wonderful world of web development, where everything takes four times longer than expected and nothing works in Internet Explorer (yes, Myron from Finance still uses it).
The great migration: OpenAI to Claude
Before tackling the web interface, I made one “small” change: switching from OpenAI to Claude. How hard could swapping LLM providers be?
Narrator: Actually not that hard!
The technical migration took less than 30 minutes thanks to my accidentally-brilliant adapter pattern. The prompt engineering (which was embedded in the code and not something I had paid any close attention to) carried over as is.
Why did I switch? Mainly due to a recommendation from my boss, the idea that Anthropic was the most advanced LLM out there these days, and some discomfort with OpenAI as an org. The truth is, though, the code is designed to swap out LLMs like rented mules. In fact, just recently I hit a token limit on Claude that broke the app, at which point we put in a silent fall-back to switch over to OpenAI when that happens, but I’m getting ahead of myself.
Enter Streamlit: The “simple” web framework
Claude recommended we built the UI with Streamlit (new to me). Streamlit promised I could build a web UI with “just Python!” No JavaScript! No CSS! No problems!
Ron Howard voice: There were problems.
Sure, Streamlit made it easy to create a basic interface. But “basic” is doing a lot of heavy lifting there. Want custom styling? Write CSS. Need real-time updates? Hello, JavaScript. Want to handle more than one user at a time? Welcome to the wonderful world of session state management.
The Streamlit app looks… fine?

GitHub integration: The API that launched a thousand errors
Adding GitHub integration seemed straightforward. The GitHub API is well-documented! There are Python libraries! What could go wrong?
Everything. Everything could go wrong.
Authentication Adventures: “Just use a personal access token!” they said. “It’ll be easy!” they said. They didn’t mention “don’t accidentally make it a read-only token.
The Repository Name Game: First I setup a repo called piper-morgan for the project. Then I realized I didn’t want to clutter up the real repo with test issues so I made one called test-piper-morgan. Then I mistakenly told the code it was called piper-morgan-test and hilarity ensued.
The Plain Language Dance: Users don’t think in GitHub’s owner/repo format. They type things like “the mobile repo” or “the design in product repo.” Building a repository resolver felt like teaching a GPS to understand directions like “turn left at where the old gas station used to be.”
Context is everything (and everything is context)
The biggest shock moving from CLI to web + GitHub was how much context matters. In the CLI, I could assume I (the only user) knew what I meant. In production, I would need to to handle inputs like:
“Users are complaining they can’t upload files bigger than 20 meg”
From this, the system needed to infer that this is a bug or an engancement request, that it involves the file upload feature on some product we’ve likely been discussing, and that there is a limit in place that is too strict. The extra work to go from that request to a well qualified ticket involves some leaps of logic.
This required building what I generously called an “intent resolution system” and what was actually at first some clever regex that looked for the hallmarks of different types of utterances (greetings, request to make a ticket, request to analyze a ticket, uh… it didn’t really do much else yet.
Integration hell: A circle for every API
By the time I had GitHub working, I’d created a integration hydra:
GitHub API: Authentication, rate limiting, webhook handling, permissions, repository management, issue templates, label management…
Claude API: Prompt optimization, token counting, response streaming, fallback handling, cost tracking (💸)…
Vector Database: Document chunking, embedding generation, similarity search, index management, backup strategies…
Web Framework: Session state, concurrent users, WebSocket connections, file uploads, error boundaries…
Each integration point was a potential failure mode. The combinations were exponential. My error handling code grew longer than my actual application code.
What we actually made
After that seemingly simple request (“Can we get a web UI on this thing), we now had made:
- A web interface that didn’t require a computer science degree to use
- GitHub integration that created issues engineers actually wanted to read
- Context resolution that right 80% of the time (and asked for clarification the other 20%)
- A loading experience that didn’t make people think their browser crashed

It wasn’t perfect. But it was usable. Theoretically someday even by people who weren’t me. That felt like a miracle.
Lessons learned (the hard way)
Integration complexity grows exponentially: Two systems = 2x complexity. Three systems = 9x complexity. Four systems = maybe reconsider your life choices.
Error messages are user experience: Right now we had error handling only a robot could love.
Quality beats speed for professional tools: Fast and wrong is worse than slow and right. Your users are professionals; treat them like it.
Also, it’s likely that Piper isn’t up on Monty Python.

The accidental platform
By tackling the “simple” problem of letting people create GitHub issues through a web interface, I’d accidentally started building a platform:
- Async job processing (much harder than it sounds)
- External API orchestration (harder than async job processing)
- Context resolution engine (I still don’t fully understand how it works)
- And every time I asked for something else, Claude agreed and we got to work bolting it on.
This wasn’t the plan. The plan was “make CLI tool work on web.” The reality was building infrastructure for something much bigger.
The real product lesson
Moving from CLI to web-based GitHub integration taught me the most valuable product lesson of all: The interface is not the product. The workflow is the product.
Users don’t want to create GitHub issues. They want their problems solved. They don’t want to query documentation. They want answers to their questions. They don’t want AI. They want their job to be easier.
Every technical decision should serve that goal.
