<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harshana Jayasinghe</title>
    <description>The latest articles on DEV Community by Harshana Jayasinghe (@harshana_jay).</description>
    <link>https://dev.to/harshana_jay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4212554%2Fc1d01a1d-fe42-468c-bfac-6564632ff7c6.jpg</url>
      <title>DEV Community: Harshana Jayasinghe</title>
      <link>https://dev.to/harshana_jay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harshana_jay"/>
    <language>en</language>
    <item>
      <title>I built a CLI that explains terminal errors with a local LLM</title>
      <dc:creator>Harshana Jayasinghe</dc:creator>
      <pubDate>Wed, 28 Oct 2026 09:06:34 +0000</pubDate>
      <link>https://dev.to/harshana_jay/i-built-a-cli-that-explains-terminal-errors-with-a-local-llm-dof</link>
      <guid>https://dev.to/harshana_jay/i-built-a-cli-that-explains-terminal-errors-with-a-local-llm-dof</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbx8wr97dkwly8u7xp9o2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbx8wr97dkwly8u7xp9o2.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;Every developer has a muscle-memory routine for errors: copy the stack trace, open a browser, paste it into a search engine or a chatbot, read, switch back, and try to remember what you were doing.&lt;/p&gt;

&lt;p&gt;I wanted to cut that loop out. So I built errbuddy: you put it in front of any command, and when something breaks, it explains the error right under it, in your terminal.&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
&lt;code&gt;npm install -g error-solution-ai-buddy&lt;br&gt;
errbuddy node app.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1jm4vl2948cqyhe5ukhf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1jm4vl2948cqyhe5ukhf.png" alt=" " width="800" height="692"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this post I'll walk through how it works, the decisions behind it, and a few lessons from turning a side project into a package other people can install.&lt;/p&gt;

&lt;p&gt;How it works&lt;/p&gt;

&lt;p&gt;The idea is simple: errbuddy runs your command as a child process and watches what it prints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;errbuddy node app.js
      │
      ▼
Runs your command ──► stdout passes straight through
      │               stderr is shown AND captured
      ▼
Error detector ──► language, error type, file and line
      ▼
Sanitizer ──► strips file paths and anything that looks like a secret
      ▼
AI explains it (Ollama or Gemini) ── or ── built-in pattern explanation
      ▼
Prints WHAT / WHY / FIX / EXAMPLE, then exits with your program's exit code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details turned out to matter more than I expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output is streamed, not buffered&lt;/strong&gt;. errbuddy uses spawn() instead of exec(), so you see your program's output live, exactly as if errbuddy weren't there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It returns your program's exit code&lt;/strong&gt;. If your script fails with exit code 1, so does errbuddy. That means it doesn't break scripts or CI steps that check for failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It only steps in when there's an error&lt;/strong&gt;. Normal output passes through untouched.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Local first: why Ollama is the default
&lt;/h2&gt;

&lt;p&gt;Pasting a stack trace into a chatbot means sending bits of your code and file paths to someone else's server. For a lot of developers (and a lot of employers) that's not OK.&lt;/p&gt;

&lt;p&gt;So errbuddy's default provider is Ollama, which runs the model on your own machine. The default model is &lt;code&gt;qwen2.5:0.5b&lt;/code&gt;. It's about 400 MB and runs on low-RAM laptops, which matters because that's exactly the kind of machine people learning to code tend to have.&lt;/p&gt;

&lt;p&gt;Even with a local model, errbuddy cleans the error first, before it reaches any model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function sanitizeError(errorText) {
  let sanitized = errorText;

  // Remove Windows-style absolute paths (C:\Users\...)
  sanitized = sanitized.replace(/[A-Za-z]:\\(?:[^\\/:*?"&amp;lt;&amp;gt;|\r\n]+\\)*/g, '[path]\\');

  // Remove Unix-style absolute paths (/home/user/...)
  sanitized = sanitized.replace(/\/(?:home|Users|var|usr|opt)\/[^\s:]+/g, '[path]');

  // Remove common secret patterns
  sanitized = sanitized.replace(/(?:api[_-]?key|secret|password|token)\s*[:=]\s*['"]?[^'"\s]+['"]?/gi, '[REDACTED]');

  return sanitized;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When you can't run a local model
&lt;/h2&gt;

&lt;p&gt;Then I hit a problem myself: I didn't have the disk space to run a model comfortably. If I couldn't, plenty of users couldn't either, and without a model they only got the basic pattern-matching explanations.&lt;/p&gt;

&lt;p&gt;So version 1.1 added an &lt;strong&gt;opt-in cloud provider: Gemini&lt;/strong&gt;, which anyone can use with a free API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export GEMINI_API_KEY="your-key"
errbuddy --provider gemini node app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules kept this honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cloud is never used unless you ask. Ollama stays the default, and Gemini only runs with &lt;code&gt;--provider gemini&lt;/code&gt; (or &lt;code&gt;ERRBUDDY_PROVIDER=gemini&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The key never goes in a URL or a command-line argument. It's read from an environment variable and sent in a header, so it doesn't end up in your shell history or in logs:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const response = await fetch(`${this.baseUrl}/models/${this.model}:generateContent`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-goog-api-key': this.apiKey,
  },
  // ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;});&lt;/p&gt;

&lt;p&gt;To keep both providers consistent, I moved the prompt and the response parser into one shared module. Each provider only has to do two things: &lt;code&gt;checkConnection()&lt;/code&gt; and &lt;code&gt;explainError()&lt;/code&gt;. A better prompt now improves both providers at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a reliable answer format out of an LLM
&lt;/h2&gt;

&lt;p&gt;For the output to look good in a terminal, every answer needs the same shape. The system prompt insists on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Always respond in this EXACT format:
  WHAT: [Name the specific error and what it means]
  WHY: [Explain specifically why this code causes that error]
  FIX: [Clear steps to fix the error]
  EXAMPLE: [Short code example if helpful]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also has rules that came from real bad answers, like "&lt;em&gt;Do NOT assume frameworks (like Spring, React) unless the error explicitly mentions them&lt;/em&gt;" and "&lt;em&gt;only suggest installing a package if the error is explicitly Module/Package not found&lt;/em&gt;". Small models love to recommend installing things.&lt;/p&gt;

&lt;p&gt;The parser then pulls out each section. And when the model ignores the format anyway (it happens), the parser falls back to splitting the answer into sentences, so the user still gets something useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bugs real usage found
&lt;/h2&gt;

&lt;p&gt;Two bugs only showed up once Gemini, a stronger model, started giving longer, nicely formatted answers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Numbered steps were mangled.&lt;/strong&gt; My text wrapper split on spaces only, so a "word" like &lt;code&gt;array.\n2&lt;/code&gt;. counted as one long word and lines broke in strange places. The fix was to wrap each line separately so the model's own line breaks survive. The small local model rarely wrote numbered lists, which is why tests with Ollama never caught it.&lt;/p&gt;

&lt;p&gt;Markdown leaked into the terminal. The model wrapped its examples in&lt;br&gt;
&lt;br&gt;
 ```&lt;code&gt;javascript&lt;/code&gt; fences, which terminals print literally. Now they're stripped before printing.&lt;/p&gt;

&lt;p&gt;Both fixes came with tests that reproduce the exact output that broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping it properly
&lt;/h2&gt;

&lt;p&gt;The code was the easy half. Making it something strangers can install took more work than I expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The repo had about 14,000 committed &lt;code&gt;**node_modules**&lt;/code&gt; files. Removing them and adding a &lt;code&gt;.gitignore&lt;/code&gt; took the repo from 14,207 files to 43. The tests still passed from a clean install, which proved nothing depended on them.&lt;/li&gt;
&lt;li&gt;The npm package ships only what the CLI needs. A &lt;code&gt;files&lt;/code&gt; list in &lt;code&gt;package.json&lt;/code&gt; keeps it to around 20 kB, instead of shipping the VS Code extension, tests and setup scripts too.&lt;/li&gt;
&lt;li&gt;Tests gate every publish. &lt;code&gt;prepublishOnly&lt;/code&gt; runs the full suite (86 tests, on Node's built-in test runner), so a broken build can't reach npm.&lt;/li&gt;
&lt;li&gt;Releases are automated. I keep a &lt;code&gt;CHANGELOG.md&lt;/code&gt;. Pushing a version tag triggers a GitHub Actions workflow that runs the tests, checks the tag matches &lt;code&gt;package.json&lt;/code&gt;, and publishes a GitHub Release using that version's changelog section as the notes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

npm install -g error-solution-ai-buddy
errbuddy node app.js


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It supports JavaScript/Node.js, Java and C#/.NET errors today. Python is next on the list.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="//github.com/sithija-jayasinghe/errbuddy"&gt;github.com/sithija-jayasinghe/errbuddy&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/error-solution-ai-buddy" rel="noopener noreferrer"&gt;error-solution-ai-buddy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it explains an error badly, please open an issue with the error text. Those reports are how the prompts get better.&lt;/p&gt;

&lt;p&gt;Thanks for reading! I'm a full-stack developer in Colombo, Sri Lanka, and I'd love to hear how you deal with errors in your own workflow.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>node</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
