<?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: Todd Sullivan</title>
    <description>The latest articles on DEV Community by Todd Sullivan (@toddsullivan).</description>
    <link>https://dev.to/toddsullivan</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3895637%2Fc957b85c-53f5-4505-8b53-7e62e06088e9.jpeg</url>
      <title>DEV Community: Todd Sullivan</title>
      <link>https://dev.to/toddsullivan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toddsullivan"/>
    <language>en</language>
    <item>
      <title>I Built a Persistent AI Assistant That Runs on My Mac</title>
      <dc:creator>Todd Sullivan</dc:creator>
      <pubDate>Fri, 24 Apr 2026 09:05:25 +0000</pubDate>
      <link>https://dev.to/toddsullivan/i-built-a-persistent-ai-assistant-that-runs-on-my-mac-44n</link>
      <guid>https://dev.to/toddsullivan/i-built-a-persistent-ai-assistant-that-runs-on-my-mac-44n</guid>
      <description>&lt;p&gt;I got tired of AI assistants that forget everything the moment a session ends. So I built one that doesn't.&lt;/p&gt;

&lt;p&gt;It runs 24/7 on my Mac, has access to my files, GitHub, iMessage, email, and calendar. It knows who I am, what I'm working on, and what I said to it last week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core problem with stateless AI
&lt;/h2&gt;

&lt;p&gt;Every time you open a new Claude or ChatGPT session, you start from zero. You re-explain your context. You re-establish what you're working on. You paste in the same background info.&lt;/p&gt;

&lt;p&gt;This is fine for one-off tasks. It's terrible for an ongoing working relationship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The memory architecture
&lt;/h2&gt;

&lt;p&gt;Instead of in-context memory, I use files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MEMORY.md&lt;/code&gt; — long-term curated knowledge. What matters, distilled.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory/YYYY-MM-DD.md&lt;/code&gt; — daily logs. What happened, decisions made, things to remember.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USER.md&lt;/code&gt; — who I am, my stack, my communication style.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TOOLS.md&lt;/code&gt; — local setup specifics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every session, the agent reads the relevant files before doing anything. This is the continuity layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP for real-world access
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol (MCP) is what lets the agent actually &lt;em&gt;do&lt;/em&gt; things — not just talk about them.&lt;/p&gt;

&lt;p&gt;I use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apple Mail, Calendar, Messages via a local MCP server&lt;/li&gt;
&lt;li&gt;GitHub via &lt;code&gt;gh&lt;/code&gt; CLI&lt;/li&gt;
&lt;li&gt;File system access&lt;/li&gt;
&lt;li&gt;Browser automation (Puppeteer via Chrome DevTools Protocol)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;It's not a chatbot. It's closer to a part-time assistant who's always available and never forgets anything. The most useful thing isn't any single capability — it's that context persists.&lt;/p&gt;

&lt;p&gt;I can say "remember the JWT issue from last week" and it actually knows what I mean.&lt;/p&gt;




&lt;p&gt;The hardest part isn't the AI. It's designing the memory and context system that makes it feel coherent over time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>macos</category>
      <category>programming</category>
    </item>
    <item>
      <title>On-Device AI: What Nobody Tells You About the Tradeoffs</title>
      <dc:creator>Todd Sullivan</dc:creator>
      <pubDate>Fri, 24 Apr 2026 08:59:40 +0000</pubDate>
      <link>https://dev.to/toddsullivan/on-device-ai-what-nobody-tells-you-about-the-tradeoffs-126k</link>
      <guid>https://dev.to/toddsullivan/on-device-ai-what-nobody-tells-you-about-the-tradeoffs-126k</guid>
      <description>&lt;p&gt;Everyone's building cloud AI. I've been building AI that runs with no internet, on a phone, in real-world conditions.&lt;/p&gt;

&lt;p&gt;Here's what I've learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model size vs accuracy in the wild
&lt;/h2&gt;

&lt;p&gt;In the lab, your model hits 94% accuracy. In production, it's handling variable lighting, partial occlusion, camera shake, and phones that haven't been updated since 2021. Your 94% becomes something lower.&lt;/p&gt;

&lt;p&gt;The instinct is to make the model bigger and more accurate. The problem: bigger models are slower, and on-device speed matters a lot when a person is standing in a room waiting for a result.&lt;/p&gt;

&lt;p&gt;The real answer is usually: &lt;strong&gt;accept a lower accuracy threshold and design your UX to handle uncertainty gracefully.&lt;/strong&gt; A confidence score + "tap to confirm" beats a slow high-confidence answer that times out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Category-level beats object-level at scale
&lt;/h2&gt;

&lt;p&gt;If you're doing object detection across thousands of SKUs, training a model to identify every individual product is a losing strategy. Too many classes, too many edge cases, constant retraining as products change.&lt;/p&gt;

&lt;p&gt;Category-level detection — "this is a drinks product, this is a snack, this is a cleaning product" — is dramatically simpler and more stable. You can add object-level identification on top for high-value cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feedback loop problem
&lt;/h2&gt;

&lt;p&gt;On-device models don't automatically improve. You need a pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User makes a correction (override the model's output)&lt;/li&gt;
&lt;li&gt;Correction is logged with context (lighting, device, conditions)&lt;/li&gt;
&lt;li&gt;Flagged for review&lt;/li&gt;
&lt;li&gt;Feeds the next training cycle&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without this, your model is frozen the moment you ship it. With it, field conditions become training data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data pipeline is harder than the model
&lt;/h2&gt;

&lt;p&gt;Getting inference results off the device and into your backend — with context, without data loss, without requiring constant connectivity — is the actual hard problem. The model is the easy part.&lt;/p&gt;

&lt;p&gt;Offline-first sync, conflict resolution, context preservation across sessions. That's where the real engineering lives.&lt;/p&gt;




&lt;p&gt;On-device AI is genuinely exciting but it's a different discipline from cloud inference. The constraints are real and they change the design of everything.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>mobile</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Zero-Config Test Runner: JWT Auto-Gen and No Setup Docs</title>
      <dc:creator>Todd Sullivan</dc:creator>
      <pubDate>Fri, 24 Apr 2026 08:59:39 +0000</pubDate>
      <link>https://dev.to/toddsullivan/zero-config-test-runner-jwt-auto-gen-and-no-setup-docs-4066</link>
      <guid>https://dev.to/toddsullivan/zero-config-test-runner-jwt-auto-gen-and-no-setup-docs-4066</guid>
      <description>&lt;p&gt;Here's a thing I've built more than once: a test automation runner that works perfectly on my machine and is a complete mystery to everyone else.&lt;/p&gt;

&lt;p&gt;The usual failure mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Private key has to live in a specific path&lt;/li&gt;
&lt;li&gt;Three env vars need to be exported&lt;/li&gt;
&lt;li&gt;You need to know which bundle and cloud flag to pass&lt;/li&gt;
&lt;li&gt;There's a doc somewhere explaining this, probably out of date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I just fixed all of that. Here's the new interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./run-test.sh smoke-tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Optional args for tag, bundle, and cloud target if you need them. Otherwise: just works.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;JWT generated at runtime.&lt;/strong&gt; The script finds &lt;code&gt;jwt-private-key.pem&lt;/code&gt; in the repo root (RS256, no expiry needed for test runs). No env var. No "where do I put this file" question.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;node tools/generate-jwt.js&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Firebase key resolved automatically.&lt;/strong&gt; Checks the repo root first, falls back to &lt;code&gt;~/.ssh/&lt;/code&gt;. Works the same on a fresh clone as it does on my laptop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./firebase-key.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./firebase-key.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;homedir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.ssh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firebase-key.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One optional arg.&lt;/strong&gt; Need to target a specific bundle or cloud env? Pass it. Otherwise the defaults are sane.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Claude catch
&lt;/h2&gt;

&lt;p&gt;Built this pairing with Claude Opus. It caught something I'd missed: my JWT generation had an expiry set that would silently break test runs longer than the token lifetime. Not a crash — just stale auth, failing tests, no obvious error.&lt;/p&gt;

&lt;p&gt;That's the kind of thing that only shows up at 2am. Fixed it before it shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;Any engineer can clone the repo and run tests. No setup doc. No "ask Todd what the env var is called." No onboarding friction.&lt;/p&gt;

&lt;p&gt;Zero-config should be the default, not the goal you work toward.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Claude is in My Commit History</title>
      <dc:creator>Todd Sullivan</dc:creator>
      <pubDate>Fri, 24 Apr 2026 08:58:33 +0000</pubDate>
      <link>https://dev.to/toddsullivan/claude-is-in-my-commit-history-3i6f</link>
      <guid>https://dev.to/toddsullivan/claude-is-in-my-commit-history-3i6f</guid>
      <description>&lt;p&gt;My recent commits have a new co-author:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Co-Authored-By: Claude Opus 4.6 (1M context) &amp;lt;noreply@anthropic.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It started as accurate bookkeeping. Now it's just how I work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm actually using it for
&lt;/h2&gt;

&lt;p&gt;I build a lot of tooling — test runners, CI pipelines, API integrations. Claude doesn't help me write README files or summarise meetings. It's in the code, catching the things I miss when I'm moving fast.&lt;/p&gt;

&lt;p&gt;A few recent examples:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JWT expiry logic.&lt;/strong&gt; I was generating tokens for a test runner. Claude flagged that my expiry config would silently break long test runs — the kind of failure that only shows up at 2am. Fixed before it shipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firebase key resolution.&lt;/strong&gt; I wanted the tool to find credentials automatically. Claude suggested the fallback chain: check the repo root first, then &lt;code&gt;~/.ssh/&lt;/code&gt;. Obvious in hindsight. I'd have hardcoded the path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API endpoint shape differences.&lt;/strong&gt; Caught a mismatch between dev and staging environments before it became a test failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the pattern looks like
&lt;/h2&gt;

&lt;p&gt;I describe the problem. Claude asks questions — usually good ones. We work it out together. Then I write the code, or we write it together if it's straightforward.&lt;/p&gt;

&lt;p&gt;The "AI writes your code" framing is wrong. It's closer to: pair programming with someone who's read everything, never has meetings, and doesn't care about tabs vs spaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I credit it in commits
&lt;/h2&gt;

&lt;p&gt;Because it's accurate. If a human pair programmer caught the JWT bug, I'd mention them. Same logic applies. It also makes the history honest when I'm reviewing it six months later.&lt;/p&gt;




&lt;p&gt;The commit history doesn't lie. If Claude is in it, it earned the attribution.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>engineering</category>
      <category>claude</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
