<?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: Arpan Shah</title>
    <description>The latest articles on DEV Community by Arpan Shah (@arpanvgm).</description>
    <link>https://dev.to/arpanvgm</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%2F4039376%2F852d75c4-c78b-481e-80e8-b5f3a967a444.png</url>
      <title>DEV Community: Arpan Shah</title>
      <link>https://dev.to/arpanvgm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arpanvgm"/>
    <language>en</language>
    <item>
      <title>How I Ran a Containerized Playwright Scraper on a Free VM — and What Actually Broke</title>
      <dc:creator>Arpan Shah</dc:creator>
      <pubDate>Tue, 28 Jul 2026 05:43:38 +0000</pubDate>
      <link>https://dev.to/arpanvgm/how-i-ran-a-containerized-playwright-scraper-on-a-free-vm-and-what-actually-broke-24ik</link>
      <guid>https://dev.to/arpanvgm/how-i-ran-a-containerized-playwright-scraper-on-a-free-vm-and-what-actually-broke-24ik</guid>
      <description>&lt;p&gt;The goal was simple: download data on a schedule, automatically, with no local machine involved. No manual triggers, no keeping a laptop on overnight. Just a container, a free VM, and a cron job.&lt;/p&gt;

&lt;p&gt;Getting there took longer than expected. Here's what actually broke and how I fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Oracle Cloud Always Free&lt;/strong&gt; AMD micro VM (Ubuntu 24.04, x86_64, ~954MB RAM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playwright&lt;/strong&gt; running Microsoft Edge (headless, via Xvfb) inside a Docker container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two .NET apps&lt;/strong&gt; — a Playwright downloader and a CSV consolidator — combined into one multi-stage image&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GHCR&lt;/strong&gt; as the image registry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron&lt;/strong&gt; for scheduling on the VM itself — no CI, no external orchestrator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The build and push happen from a local WSL machine via VS Code tasks. The VM just pulls and runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why containerize at all?
&lt;/h2&gt;

&lt;p&gt;The alternative was installing the .NET SDK directly on the VM and running the app with &lt;code&gt;dotnet run&lt;/code&gt;. That works, but it ties the VM to a specific runtime version, makes the setup hard to reproduce, and diverges from how the rest of the project is deployed.&lt;/p&gt;

&lt;p&gt;Containerizing means the VM needs exactly two things: Docker and a cron job. Everything else — the SDK, Xvfb, Edge, both apps — lives inside the image. If the VM is rebuilt or replaced, setup is: install Docker, add the cron entry, pull the image, done.&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%2Fwb0fdpsj0pb6517h4n00.jpg" 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%2Fwb0fdpsj0pb6517h4n00.jpg" alt="Zero host dependencies via multi-stage Docker — build stage holds the SDK and compiles both apps, runtime image contains only Xvfb, Edge, and the published artifacts" width="798" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The first real problem: OOM kills
&lt;/h2&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%2Fwezul3yjhdfvm98v7y50.jpg" 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%2Fwezul3yjhdfvm98v7y50.jpg" alt="Taming the OOM Killer on 954MB RAM — Oracle Cloud Always Free, AMD x86_64" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The AMD Always Free shape has ~954MB RAM and &lt;strong&gt;zero swap by default&lt;/strong&gt;. Playwright launching a real browser under load hits memory hard. Without swap, the OOM killer terminates the browser process outright — no graceful degradation, no error message in the logs, just a hard kill.&lt;/p&gt;

&lt;p&gt;The fix is a 2GB swap file, persisted to &lt;code&gt;/etc/fstab&lt;/code&gt; so it survives reboots:&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="nb"&gt;sudo &lt;/span&gt;fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 2G /swapfile
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkswap /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;swapon /swapfile
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/swapfile none swap sw 0 0'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fpemenn3h3mbob3ksz71w.jpg" 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%2Fpemenn3h3mbob3ksz71w.jpg" alt="RAM at 954MB overflows into a 2GB swap file — exit code 0, 78 files generated, OOMKilled: false" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this, sustained load across a full run completed with &lt;code&gt;OOMKilled: false&lt;/code&gt;. This isn't a one-time workaround — it's a permanent property of the VM shape.&lt;/p&gt;

&lt;p&gt;One other thing worth noting: the free ARM64 shape (Ampere A1) has more RAM and would avoid the swap requirement entirely — but Microsoft Edge has no native Linux ARM64 build. Staying on AMD x86_64 was the only viable path for this stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second problem: &lt;code&gt;xvfb-run&lt;/code&gt; hangs silently
&lt;/h2&gt;

&lt;p&gt;Playwright needs a display. The standard approach is &lt;code&gt;xvfb-run&lt;/code&gt;, but in a minimal container image it hangs indefinitely with zero output.&lt;/p&gt;

&lt;p&gt;The reason: &lt;code&gt;xvfb-run&lt;/code&gt; uses &lt;code&gt;xdpyinfo&lt;/code&gt; (from &lt;code&gt;x11-utils&lt;/code&gt;) to confirm the virtual display is ready before handing off. That package isn't installed by default. Xvfb itself starts fine — the wrapper just never moves past its own readiness check, silently.&lt;/p&gt;

&lt;p&gt;The fix: start Xvfb manually in the entrypoint, poll for the actual X11 socket file, then export &lt;code&gt;DISPLAY&lt;/code&gt; and exec the app directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Xvfb :99 &lt;span class="nt"&gt;-screen&lt;/span&gt; 0 1280x800x24 &amp;amp;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-S&lt;/span&gt; /tmp/.X11-unix/X99 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;0.1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DISPLAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;:99
&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No dependency on &lt;code&gt;xdpyinfo&lt;/code&gt;, no silent hang.&lt;/p&gt;

&lt;h2&gt;
  
  
  The third problem: output vanishing silently
&lt;/h2&gt;

&lt;p&gt;The apps write files to paths configured in &lt;code&gt;appsettings.json&lt;/code&gt;. Those paths are fine on a dev machine. Inside a container, the app creates those paths on its own ephemeral filesystem — and when the container is removed, the files go with it. Logs still report success. Only running &lt;code&gt;ls&lt;/code&gt; on the host mount folder reveals the problem.&lt;/p&gt;

&lt;p&gt;The fix: both apps call &lt;code&gt;.AddEnvironmentVariables()&lt;/code&gt; after &lt;code&gt;.AddJsonFile(...)&lt;/code&gt; so environment variables override config file values. The Dockerfile sets container-native ENV defaults for all output paths. The bind mounts in &lt;code&gt;docker run&lt;/code&gt; wire the container paths to real host folders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-pipeline &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /opt/myapp/downloads:/app/downloads &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /opt/myapp/diagnostics:/app/diagnostics &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /opt/myapp/logs:/app/logs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /opt/myapp/consolidated:/app/consolidated &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /opt/myapp/archive:/app/archive &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/username/my-pipeline:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One subtle catch: Docker won't auto-create bind-mount source directories reliably. Create them explicitly with &lt;code&gt;mkdir -p&lt;/code&gt; before the first run, or output silently goes to the container's own filesystem. This is now part of the run task — the directories are created automatically before &lt;code&gt;docker run&lt;/code&gt; fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fourth problem: the entrypoint script encoding traps
&lt;/h2&gt;

&lt;p&gt;The entrypoint is a shell script. Copying it from a host file into the image hit two separate encoding issues, both producing the same error — &lt;code&gt;exec format error&lt;/code&gt; (exit 255):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CRLF line endings&lt;/strong&gt; — Windows editors save files with &lt;code&gt;\r\n&lt;/code&gt;. The &lt;code&gt;\r&lt;/code&gt; becomes part of the interpreter name in the shebang line, breaking it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UTF-8 BOM&lt;/strong&gt; — some editors prepend a BOM before the shebang. The kernel sees the BOM first and can't identify the interpreter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The standard fix for CRLF is &lt;code&gt;sed 's/\r$//'&lt;/code&gt; — but this does not fix a BOM. A BOM-aware sed pattern can also silently fail under a UTF-8 locale. Both issues produce the same exit code, making it easy to think you've fixed one when the other is still present.&lt;/p&gt;

&lt;p&gt;The actual fix: write the entrypoint directly in the Dockerfile via a heredoc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;&amp;lt;'EOF' /app/entrypoint.sh&lt;/span&gt;
&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
Xvfb :99 -screen 0 1280x800x24 &amp;amp;
while [ ! -S /tmp/.X11-unix/X99 ]; do sleep 0.1; done
export DISPLAY=:99
exec "$@"
EOF
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /app/entrypoint.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The heredoc guarantees LF line endings and no BOM regardless of host OS or editor. The encoding problem simply can't occur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduling: cron on the VM itself
&lt;/h2&gt;

&lt;p&gt;No CI runner, no GitHub Actions schedule, no external trigger — cron calls &lt;code&gt;docker run&lt;/code&gt; directly on the VM. The local machine doesn't need to be on.&lt;/p&gt;

&lt;p&gt;One thing to know: cron is not installed by default on minimal Ubuntu cloud images. It needs to be installed and enabled explicitly:&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="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; cron
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;cron
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start cron
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cron entry cleans up any leftover container from a previous run before starting a new one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;30 1 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 2-6 docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; my-pipeline 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; docker run &lt;span class="nt"&gt;--name&lt;/span&gt; my-pipeline &lt;span class="o"&gt;[&lt;/span&gt;mounts and flags] &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /opt/myapp/logs/cron.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All container output is appended to a cron log — one place to check after any scheduled run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's proven
&lt;/h2&gt;

&lt;p&gt;After a full run across all configured targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exit code &lt;code&gt;0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;78 files saved, 0 failed, 0 skipped&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OOMKilled: false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 2GB swap held under sustained load. The pipeline now runs on schedule with no local machine involvement.&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%2F0aoupj8071rb0a7e6u8c.jpg" 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%2F0aoupj8071rb0a7e6u8c.jpg" alt="The law of free-tier computing — what you save in compute, you pay for in architectural precision" width="799" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still open
&lt;/h2&gt;

&lt;p&gt;A few things are deliberately deferred:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Off-site backup&lt;/strong&gt; — currently data lives only on the VM's local disk. A Google Drive backup via service account is planned but not started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnostics under real failure&lt;/strong&gt; — the screenshot/HTML capture on Playwright startup failure is wired up but hasn't been verified end-to-end under a real failure yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these block the automated pipeline. They're next.&lt;/p&gt;




&lt;p&gt;If you've hit any of these in your own container/VM setup — especially the Xvfb hang or the BOM trap — I'd be curious what your fix looked like.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>playwright</category>
      <category>devops</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a CLI to Use Free Web-Based AI Chatbots for Real Development Work — No API Keys, No Extensions</title>
      <dc:creator>Arpan Shah</dc:creator>
      <pubDate>Wed, 22 Jul 2026 18:42:09 +0000</pubDate>
      <link>https://dev.to/arpanvgm/i-built-a-cli-to-use-free-web-based-ai-chatbots-for-real-development-work-no-api-keys-no-jlf</link>
      <guid>https://dev.to/arpanvgm/i-built-a-cli-to-use-free-web-based-ai-chatbots-for-real-development-work-no-api-keys-no-jlf</guid>
      <description>&lt;p&gt;I wanted to use web-based AI chatbots — Claude, Gemini, ChatGPT, Qwen — for actual development work, not just Q&amp;amp;A. The free tiers are generous, and I didn't want to be locked into a single coding agent or pay for API access just to get an assistant to touch my code. But the moment you try to actually use a web chat for real dev work, you hit the same wall every time: you either paste in your whole codebase manually every session, or you give up and reach for a paid extension with an API key behind it.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/arpanvgm/ai-bridge" rel="noopener noreferrer"&gt;AI Bridge&lt;/a&gt;&lt;/strong&gt; — a CLI tool that bridges a local codebase and any browser-based AI chatbot. No API keys, no extensions running in the background, no vendor lock-in. You pack your code, paste it into whichever AI chat you're using, and apply the changes back with a command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use Copilot, Cursor, or an API key?
&lt;/h2&gt;

&lt;p&gt;Coding agent apps and API-based tools work, but they come with tradeoffs I wanted to avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web-based AI chat plans are usually more generous on the free tier than API usage&lt;/li&gt;
&lt;li&gt;I'm not locked into one provider — I can switch models mid-project depending on which one is handling a task better&lt;/li&gt;
&lt;li&gt;There's no background agent or extension — it's just a CLI and whatever chat tab I already have open&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is that browser chats don't have direct filesystem access. AI Bridge closes that gap without turning it into full manual copy-pasting.&lt;/p&gt;

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

&lt;p&gt;There are two modes, depending on project size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple Mode&lt;/strong&gt; is for projects small enough to fit in a single prompt. You pack the codebase, upload it along with a couple of prompt templates, and apply the AI's response back to your files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; Tools.AIBridge
&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/your-project
ai-bridge init
ai-bridge pack
&lt;span class="c"&gt;# Upload ai-bridge/1-SimpleMode/*.md + the generated context files to your AI&lt;/span&gt;
&lt;span class="c"&gt;# Copy the AI's response, then:&lt;/span&gt;
ai-bridge apply &lt;span class="nt"&gt;--paste&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advanced Mode&lt;/strong&gt; is for larger codebases, where uploading everything every time burns tokens and adds noise. Instead, you generate a one-time &lt;code&gt;index.xml&lt;/code&gt; map of the project. From then on, the AI requests only the specific files it needs for a given task, and AI Bridge gathers and bundles just those into context — so you're not re-uploading the whole repo on every prompt.&lt;/p&gt;

&lt;p&gt;It respects &lt;code&gt;.gitignore&lt;/code&gt; automatically (via &lt;code&gt;git ls-files&lt;/code&gt;), and there's an &lt;code&gt;.aiignore&lt;/code&gt; file for excluding things like test fixtures or generated code that the AI doesn't need to see.&lt;/p&gt;

&lt;p&gt;It's also not tied to .NET specifically — it detects your ecosystem (&lt;code&gt;.csproj&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt;, &lt;code&gt;Cargo.toml&lt;/code&gt;) and groups context files accordingly, so it works with Node.js, Python, Go, and Rust projects too.&lt;/p&gt;

&lt;p&gt;Applying changes back works the same way regardless of mode — copy the AI's XML response and run &lt;code&gt;ai-bridge apply --paste&lt;/code&gt;, which reads straight from your clipboard on Windows, macOS, and Linux (X11/Wayland), with a stdin fallback for headless/SSH setups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://youtu.be/9u7JEi7WwUg" rel="noopener noreferrer"&gt;Here's a short demo of Simple Mode in action&lt;/a&gt;.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; Tools.AIBridge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires the &lt;a href="https://dotnet.microsoft.com/download" rel="noopener noreferrer"&gt;.NET 10 SDK&lt;/a&gt; and Git on your PATH. Full setup and command reference are in the &lt;a href="https://github.com/arpanvgm/ai-bridge" rel="noopener noreferrer"&gt;repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I built this mainly to solve my own workflow friction, but I'm curious whether it's useful outside that — if you try it, I'd genuinely like to know whether it fits how you work, or where it breaks down. It's MIT licensed, so issues and PRs are welcome too.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>dotnet</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
