<?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: Ronak Parmar</title>
    <description>The latest articles on DEV Community by Ronak Parmar (@ronak_parmar_033c50d168b5).</description>
    <link>https://dev.to/ronak_parmar_033c50d168b5</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%2F3826925%2Ffbae4883-d4ec-498b-8aeb-697540be41c1.jpg</url>
      <title>DEV Community: Ronak Parmar</title>
      <link>https://dev.to/ronak_parmar_033c50d168b5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ronak_parmar_033c50d168b5"/>
    <language>en</language>
    <item>
      <title>I turned my phone into a remote deck for my Windows laptop — no cloud, no accounts, one npm start</title>
      <dc:creator>Ronak Parmar</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:52:01 +0000</pubDate>
      <link>https://dev.to/ronak_parmar_033c50d168b5/i-turned-my-phone-into-a-remote-deck-for-my-windows-laptop-no-cloud-no-accounts-one-npm-start-34hb</link>
      <guid>https://dev.to/ronak_parmar_033c50d168b5/i-turned-my-phone-into-a-remote-deck-for-my-windows-laptop-no-cloud-no-accounts-one-npm-start-34hb</guid>
      <description>&lt;p&gt;It started with the dumbest problem in computing: I'm in bed, a movie is&lt;br&gt;
playing on my laptop across the room, and pausing it requires &lt;em&gt;physically&lt;br&gt;
getting up&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Every existing fix annoyed me in some way. Remote desktop apps are overkill&lt;br&gt;
and route through someone's cloud. Remote-control apps want accounts,&lt;br&gt;
subscriptions, or a native app install. I just wanted my phone to poke my&lt;br&gt;
laptop over my own Wi-Fi.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/ronak-create/LapDeck" rel="noopener noreferrer"&gt;LapDeck&lt;/a&gt;: one Node.js&lt;br&gt;
process on the laptop, a PWA on the phone. Scan a QR code once and your&lt;br&gt;
phone becomes an app launcher, touchpad, keyboard, live screen viewer, and&lt;br&gt;
media/power remote. MIT licensed, plain JavaScript, no build step.&lt;/p&gt;

&lt;p&gt;This post is about the four problems that turned out to be more interesting&lt;br&gt;
than I expected.&lt;/p&gt;
&lt;h2&gt;
  
  
  The architecture in one line
&lt;/h2&gt;

&lt;p&gt;Phone (PWA) ── WebSocket + MJPEG over Wi-Fi ──► Node.js agent (Windows)&lt;/p&gt;

&lt;p&gt;The agent serves the PWA, exposes a WebSocket for commands, and streams the&lt;br&gt;
screen as MJPEG. The protocol is deliberately dumb JSON envelopes — no&lt;br&gt;
protobuf, no RPC framework — so a native Android client or a CLI script can&lt;br&gt;
speak it in an afternoon. Windows-specific glue (volume, brightness, power,&lt;br&gt;
capture) is isolated in &lt;code&gt;src/win/&lt;/code&gt;, so a macOS/Linux port only has to&lt;br&gt;
reimplement that layer.&lt;/p&gt;
&lt;h2&gt;
  
  
  Problem 1: mobile keyboards lie to you
&lt;/h2&gt;

&lt;p&gt;The obvious way to build a remote keyboard is to listen for &lt;code&gt;keydown&lt;/code&gt; and&lt;br&gt;
forward key codes. On mobile, this collapses immediately: swipe typing,&lt;br&gt;
autocorrect, and IME composition don't emit per-key events. Android will&lt;br&gt;
happily tell you every key is &lt;code&gt;keyCode 229&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix: stop listening to keys entirely. I keep a hidden input, and on&lt;br&gt;
every &lt;code&gt;input&lt;/code&gt; event I &lt;strong&gt;diff the field's value against the last known&lt;br&gt;
state&lt;/strong&gt; — compute the common prefix, then emit "delete N chars, type this&lt;br&gt;
string" to the laptop. Swipe-type a whole word and the laptop receives one&lt;br&gt;
clean text insertion. IME composition, autocorrect rewrites, emoji — all&lt;br&gt;
just become diffs.&lt;/p&gt;

&lt;p&gt;The lesson generalizes: on mobile web, treat the text field as the source of&lt;br&gt;
truth, never the key events.&lt;/p&gt;
&lt;h2&gt;
  
  
  Problem 2: streaming a screen without WebRTC
&lt;/h2&gt;

&lt;p&gt;I wanted live screen view with tap-to-click. WebRTC would be the "correct"&lt;br&gt;
answer, but it drags in a signaling dance and a pile of dependencies for&lt;br&gt;
what is fundamentally "pictures, quickly."&lt;/p&gt;

&lt;p&gt;MJPEG turned out to be embarrassingly good for this. The agent runs a&lt;br&gt;
capture loop (screenshots via nut.js, resize + JPEG encode via sharp) and&lt;br&gt;
pushes frames as &lt;code&gt;multipart/x-mixed-replace&lt;/code&gt; — a &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag on the phone&lt;br&gt;
renders it natively, zero client code. Quality presets just change fps,&lt;br&gt;
width, and JPEG quality.&lt;/p&gt;

&lt;p&gt;Latency is fine for "tap where you want to click," but a raw MJPEG stream&lt;br&gt;
makes the &lt;em&gt;cursor&lt;/em&gt; feel laggy. So the phone renders its own client-predicted&lt;br&gt;
cursor overlay on top of the stream — your finger drives a local cursor&lt;br&gt;
instantly, and the real cursor catches up underneath. The perceived latency&lt;br&gt;
of the whole feature is the latency of that overlay, not the stream.&lt;/p&gt;
&lt;h2&gt;
  
  
  Problem 3: the security model for "my phone can shut down my laptop"
&lt;/h2&gt;

&lt;p&gt;Giving a web page on the network the power to inject input and kill your&lt;br&gt;
machine deserves paranoia. The rules I settled on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 256-bit random token, generated on first run, embedded in the QR
link.&lt;/strong&gt; You scan once; the phone stores it. Every WebSocket command and
the MJPEG stream require it. Comparison is constant-time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First WebSocket message must authenticate within 3 seconds&lt;/strong&gt; or the
socket drops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature switches are enforced server-side.&lt;/strong&gt; If you disable screen view
in settings, the agent &lt;em&gt;refuses&lt;/em&gt; those commands — it doesn't just hide
the button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destructive actions are opt-in and abortable.&lt;/strong&gt; Sleep/shutdown/restart
each need to be enabled in settings, require an explicit confirm flag,
and shutdown/restart honor a grace period during which one tap aborts.
(I have fat-fingered shutdown. The abort window has paid for itself.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Transport is plain HTTP/WS, which is fine for a home LAN you trust — and&lt;br&gt;
explicitly &lt;em&gt;not&lt;/em&gt; fine for the open internet. Which leads to:&lt;/p&gt;
&lt;h2&gt;
  
  
  Problem 4: remote access without running a server
&lt;/h2&gt;

&lt;p&gt;"Control your laptop from anywhere" usually means either port forwarding&lt;br&gt;
(no) or somebody's relay cloud (also no). The answer that costs nothing and&lt;br&gt;
required surprisingly little code: &lt;strong&gt;detect Tailscale&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the agent sees a Tailscale interface, it prints a second "remote" QR and&lt;br&gt;
the UI offers a one-tap switch to the tailnet URL. WireGuard gives you&lt;br&gt;
end-to-end encryption, MagicDNS + &lt;code&gt;tailscale cert&lt;/code&gt; gives you a real HTTPS&lt;br&gt;
URL (which also makes the PWA installable from anywhere), and I run zero&lt;br&gt;
infrastructure. Any VPN that puts your phone on your home network works —&lt;br&gt;
Tailscale is just the zero-config path.&lt;/p&gt;
&lt;h2&gt;
  
  
  Things I deliberately didn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No TypeScript, no build step.&lt;/strong&gt; Plain ESM on Node ≥ 20. Clone, install,
run. Contributors can read every line that executes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Electron, no tray app.&lt;/strong&gt; It's a console process; a PowerShell script
installs a hidden autostart launcher into the Startup folder if you want
it permanent (no admin needed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cloud component, period.&lt;/strong&gt; There is nothing to sign up for, nothing
that phones home, and the whole thing works with Wi-Fi and no internet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Windows 10/11, Node ≥ 20, phone on the same Wi-Fi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ronak-create/LapDeck.git
&lt;span class="nb"&gt;cd &lt;/span&gt;LapDeck
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scan the QR, add to home screen, done.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/ronak-create/LapDeck" rel="noopener noreferrer"&gt;https://github.com/ronak-create/LapDeck&lt;/a&gt; — the WebSocket protocol&lt;br&gt;
is documented in docs/PROTOCOL.md if you want to build your own client.&lt;br&gt;
macOS/Linux ports, multi-monitor support, and code roasts all welcome.&lt;/p&gt;

&lt;p&gt;What's the laziest problem you've ever over-engineered a solution for?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>node</category>
      <category>opensource</category>
    </item>
    <item>
      <title>pretty interesting stuff!!</title>
      <dc:creator>Ronak Parmar</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:07:37 +0000</pubDate>
      <link>https://dev.to/ronak_parmar_033c50d168b5/pretty-interesting-stuff-3963</link>
      <guid>https://dev.to/ronak_parmar_033c50d168b5/pretty-interesting-stuff-3963</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/u84u/i-built-a-cli-so-id-stop-typing-imagemagick-flags-wrong-e61" class="crayons-story__hidden-navigation-link"&gt;I built a CLI so I'd stop typing ImageMagick flags wrong.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/u84u" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F3887397%2Fd31d0a3b-3646-4e6d-a0af-f388523f40e2.jpeg" alt="u84u profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/u84u" class="crayons-story__secondary fw-medium m:hidden"&gt;
              shekhar
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                shekhar
                
              
              &lt;div id="story-author-preview-content-4206702" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/u84u" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F3887397%2Fd31d0a3b-3646-4e6d-a0af-f388523f40e2.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;shekhar&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/u84u/i-built-a-cli-so-id-stop-typing-imagemagick-flags-wrong-e61" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/u84u/i-built-a-cli-so-id-stop-typing-imagemagick-flags-wrong-e61" id="article-link-4206702"&gt;
          I built a CLI so I'd stop typing ImageMagick flags wrong.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cli"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cli&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/u84u/i-built-a-cli-so-id-stop-typing-imagemagick-flags-wrong-e61" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;12&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/u84u/i-built-a-cli-so-id-stop-typing-imagemagick-flags-wrong-e61#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>The project file is the interface: letting AI agents drive a video editor</title>
      <dc:creator>Ronak Parmar</dc:creator>
      <pubDate>Thu, 09 Jul 2026 15:54:17 +0000</pubDate>
      <link>https://dev.to/ronak_parmar_033c50d168b5/the-project-file-is-the-interface-letting-ai-agents-drive-a-video-editor-58hd</link>
      <guid>https://dev.to/ronak_parmar_033c50d168b5/the-project-file-is-the-interface-letting-ai-agents-drive-a-video-editor-58hd</guid>
      <description>&lt;p&gt;Last week I open sourced &lt;a href="https://github.com/ronak-create/FableCut" rel="noopener noreferrer"&gt;FableCut&lt;/a&gt;,&lt;br&gt;
a Premiere-style video editor that runs in the browser and that AI agents can&lt;br&gt;
operate. It hit the front page of Hacker News&lt;br&gt;
(&lt;a href="https://news.ycombinator.com/item?id=48845422" rel="noopener noreferrer"&gt;thread&lt;/a&gt;), and the questions&lt;br&gt;
there made me realize the interesting part isn't the editor. It's one design&lt;br&gt;
decision: &lt;strong&gt;the project file is the interface.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The usual way, and why I flipped it
&lt;/h2&gt;

&lt;p&gt;Most AI video tools hide the edit behind an API. You call &lt;code&gt;addClip()&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;applyFilter()&lt;/code&gt;, and the tool owns the state. If you want a human to touch the&lt;br&gt;
result, you build a whole collaboration layer.&lt;/p&gt;

&lt;p&gt;FableCut does the opposite. The entire timeline lives in one JSON document,&lt;br&gt;
&lt;code&gt;project.json&lt;/code&gt;: media, clips, tracks, keyframes, transitions, markers. The&lt;br&gt;
editor UI reads it. The export renders it. And anything that can write JSON&lt;br&gt;
can edit video: Claude Code through MCP, a Python script, &lt;code&gt;jq&lt;/code&gt;, or you with a&lt;br&gt;
text editor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c_title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"track"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"V3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"props"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HANDMADE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"font"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bebas Neue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="nl"&gt;"glow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"textAnim"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"letter-pop"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That clip is a glowing kinetic caption. There is no API call that creates it.&lt;br&gt;
Writing it into the file IS creating it.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSE as a doorbell, not a data channel
&lt;/h2&gt;

&lt;p&gt;The first question on HN was "what's the benefit of SSE here?" Fair question,&lt;br&gt;
because the SSE channel does almost nothing, and that's the point.&lt;/p&gt;

&lt;p&gt;The server watches the project file with &lt;code&gt;fs.watch&lt;/code&gt;, debounces 150ms, and&lt;br&gt;
pushes the literal string &lt;code&gt;change&lt;/code&gt; to the browser. No payload. The browser&lt;br&gt;
re-fetches the project and re-renders. The whole mechanism is about 15 lines&lt;br&gt;
on a bare &lt;code&gt;node:http&lt;/code&gt; server.&lt;/p&gt;

&lt;p&gt;Why not WebSockets? Because the data only flows one way. Everything that&lt;br&gt;
writes (the UI, an agent, a shell script) goes through REST or the&lt;br&gt;
filesystem. The browser only ever needs to hear "something changed, go look."&lt;br&gt;
An event with no payload can't arrive out of order, and a missed event costs&lt;br&gt;
nothing because the next fetch has the latest state anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The revision counter, or: how a human and an agent share a timeline
&lt;/h2&gt;

&lt;p&gt;The file carries an integer revision. Every write must bump it. If a write&lt;br&gt;
arrives with a &lt;code&gt;revision&lt;/code&gt; that isn't newer than what's on disk, the server&lt;br&gt;
rejects it with a 409.&lt;/p&gt;

&lt;p&gt;This one integer is the entire concurrency model. If I drag a clip in the UI&lt;br&gt;
while an agent is mid-edit, the agent's stale write bounces, it re-reads,&lt;br&gt;
re-applies its change on top of mine, and writes again. No operational&lt;br&gt;
transforms, no CRDTs, no lock files. It works because edits are coarse&lt;br&gt;
(a whole document) and rare (human speed), so last-writer-wins with a&lt;br&gt;
staleness check is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick I'm proudest of: frame-accurate CSS animations
&lt;/h2&gt;

&lt;p&gt;FableCut has animated SVG overlays (lower thirds, confetti, sparkles) that&lt;br&gt;
are plain &lt;code&gt;.svg&lt;/code&gt; files animated with CSS &lt;code&gt;@keyframes&lt;/code&gt;. The problem: a video&lt;br&gt;
compositor needs to render the animation state at an exact time, and export&lt;br&gt;
isn't realtime. You can't just let the animation play.&lt;/p&gt;

&lt;p&gt;The solution: pause every animation and drive time by hand. The compositor&lt;br&gt;
sets &lt;code&gt;animation-delay: calc(var(--d, 0s) - t)&lt;/code&gt; where &lt;code&gt;t&lt;/code&gt; is the clip's local&lt;br&gt;
time. A negative delay means "you started in the past," so a paused animation&lt;br&gt;
with delay &lt;code&gt;-1.3s&lt;/code&gt; displays exactly its 1.3 second frame. Deterministic,&lt;br&gt;
scrubbable, identical in preview and export. The only rule for SVG authors is&lt;br&gt;
to never hardcode &lt;code&gt;animation-delay&lt;/code&gt; and use the &lt;code&gt;--d&lt;/code&gt; custom property for&lt;br&gt;
staggering instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  "You can just give Claude access to ffmpeg"
&lt;/h2&gt;

&lt;p&gt;Someone said this on HN and it deserves a straight answer. For trims,&lt;br&gt;
concats, and batch transcodes: yes, absolutely, do that.&lt;/p&gt;

&lt;p&gt;The difference is the creative loop. ffmpeg is write-only. The agent builds a&lt;br&gt;
filter graph, renders for minutes, and cannot see what it made. You give&lt;br&gt;
feedback, everything re-renders. In FableCut an edit is a JSON diff, the open&lt;br&gt;
browser updates in 150ms, and the timeline stays editable instead of being&lt;br&gt;
baked into a filter string. It's not a replacement for ffmpeg anyway: the&lt;br&gt;
export pipeline renders frames in the browser and pipes them to ffmpeg for&lt;br&gt;
encoding. FableCut is the state and preview layer between the agent and&lt;br&gt;
ffmpeg.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;The compositor is the browser, so export needs a browser open (headless&lt;br&gt;
export is not there yet). It's Chromium-first. And an AI can misjudge a cut&lt;br&gt;
just fine, which is why the human-in-the-loop part matters more than the AI&lt;br&gt;
part: the agent does the labor, you do the taste.&lt;/p&gt;

&lt;p&gt;Full disclosure since HN asked: Claude helped write the README, and large&lt;br&gt;
parts of the editor were built in collaboration with it. That felt fitting&lt;br&gt;
for a tool whose primary user is an AI agent, but the architecture decisions&lt;br&gt;
above are the ones I'd defend in person.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/ronak-create/FableCut" rel="noopener noreferrer"&gt;https://github.com/ronak-create/FableCut&lt;/a&gt;. It's MIT, zero dependencies,&lt;br&gt;
one &lt;code&gt;node server.js&lt;/code&gt;. If you build something weird with it, I want to see it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Why I Reimplemented 22 Unix Tools in Go for AI Agents</title>
      <dc:creator>Ronak Parmar</dc:creator>
      <pubDate>Mon, 06 Apr 2026 14:12:20 +0000</pubDate>
      <link>https://dev.to/ronak_parmar_033c50d168b5/why-i-reimplemented-22-unix-tools-in-go-for-ai-agents-21f0</link>
      <guid>https://dev.to/ronak_parmar_033c50d168b5/why-i-reimplemented-22-unix-tools-in-go-for-ai-agents-21f0</guid>
      <description>&lt;p&gt;I spent three weeks rebuilding &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;stat&lt;/code&gt;, &lt;code&gt;diff&lt;/code&gt;, and 16 other Unix coreutils in Go. Not because the originals are broken — they're masterpieces of systems programming that have survived decades of use. I rebuilt them because AI coding agents are terrible at reading their output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Talked About
&lt;/h2&gt;

&lt;p&gt;Every time an AI agent runs &lt;code&gt;ls src/&lt;/code&gt;, it receives something like this:&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="nt"&gt;-rw-r--r--&lt;/span&gt;  1 user  staff  2048 Apr  6 12:00 main.go
drwxr-xr-x  3 user  staff    96 Apr  6 11:00 internal
lrwxr-xr-x  1 user  staff    12 Apr  6 10:00 &lt;span class="nb"&gt;link&lt;/span&gt; -&amp;gt; main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent has to figure out which column is the filename. Which is the size. Whether that &lt;code&gt;d&lt;/code&gt; at the start means directory. Whether &lt;code&gt;Apr  6&lt;/code&gt; means this year or last year. It guesses. Sometimes it guesses wrong. And every wrong guess costs tokens, introduces errors, and degrades the quality of the code it writes.&lt;/p&gt;

&lt;p&gt;Now multiply that by every &lt;code&gt;grep&lt;/code&gt;, every &lt;code&gt;cat&lt;/code&gt;, every &lt;code&gt;find&lt;/code&gt; the agent runs in a single session. The token waste is staggering. The parsing fragility is a constant source of subtle bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Insight
&lt;/h2&gt;

&lt;p&gt;AI agents don't need pretty terminal output. They need structured data. They need to know that &lt;code&gt;main.go&lt;/code&gt; is 2048 bytes, was modified 3600 seconds ago, is written in Go, has MIME type &lt;code&gt;text/x-go&lt;/code&gt;, and is not a binary file. They need this information labeled, unambiguous, and machine-readable.&lt;/p&gt;

&lt;p&gt;So I asked: what if &lt;code&gt;ls&lt;/code&gt; returned XML?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ls&lt;/span&gt; &lt;span class="na"&gt;timestamp=&lt;/span&gt;&lt;span class="s"&gt;"1712404800"&lt;/span&gt; &lt;span class="na"&gt;total_entries=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;file&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"main.go"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"src/main.go"&lt;/span&gt; &lt;span class="na"&gt;absolute=&lt;/span&gt;&lt;span class="s"&gt;"/project/src/main.go"&lt;/span&gt;
        &lt;span class="na"&gt;size_bytes=&lt;/span&gt;&lt;span class="s"&gt;"2048"&lt;/span&gt; &lt;span class="na"&gt;size_human=&lt;/span&gt;&lt;span class="s"&gt;"2.0 KiB"&lt;/span&gt;
        &lt;span class="na"&gt;modified=&lt;/span&gt;&lt;span class="s"&gt;"1712404800"&lt;/span&gt; &lt;span class="na"&gt;modified_ago_s=&lt;/span&gt;&lt;span class="s"&gt;"3600"&lt;/span&gt;
        &lt;span class="na"&gt;language=&lt;/span&gt;&lt;span class="s"&gt;"go"&lt;/span&gt; &lt;span class="na"&gt;mime=&lt;/span&gt;&lt;span class="s"&gt;"text/x-go"&lt;/span&gt; &lt;span class="na"&gt;binary=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;directory&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"internal"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"src/internal"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;symlink&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"link"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"main.go"&lt;/span&gt; &lt;span class="na"&gt;broken=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ls&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero ambiguity. Zero parsing. The agent reads the attributes and knows exactly what it's looking at. No regex to extract filenames from column-aligned text. No heuristic to determine if something is a directory. No guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why XML and Not JSON?
&lt;/h2&gt;

&lt;p&gt;Good question. JSON is the lingua franca of APIs. But XML has a structural advantage that matters for AI context windows: &lt;strong&gt;attributes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Compare these two representations of the same file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;file&lt;/span&gt; &lt;span class="na"&gt;size_bytes=&lt;/span&gt;&lt;span class="s"&gt;"2048"&lt;/span&gt; &lt;span class="na"&gt;language=&lt;/span&gt;&lt;span class="s"&gt;"go"&lt;/span&gt; &lt;span class="na"&gt;mime=&lt;/span&gt;&lt;span class="s"&gt;"text/x-go"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"size_bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"go"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text/x-go"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The XML version is 40 characters. The JSON version is 60. That's a 33% difference. When you're listing 1,000 files, that's tens of thousands of tokens saved. AI context windows are expensive and limited. Every character counts.&lt;/p&gt;

&lt;p&gt;That said, &lt;code&gt;aict&lt;/code&gt; supports &lt;code&gt;--json&lt;/code&gt; for every tool. The schema is identical. Use whatever your pipeline prefers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Go?
&lt;/h2&gt;

&lt;p&gt;Three reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single binary.&lt;/strong&gt; Go compiles to a static binary with zero runtime dependencies. &lt;code&gt;aict&lt;/code&gt; is one file you drop on a system and it works. No &lt;code&gt;pip install&lt;/code&gt;, no &lt;code&gt;npm install&lt;/code&gt;, no shared libraries to manage. For a tool that's supposed to replace coreutils, this is non-negotiable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standard library only.&lt;/strong&gt; Every feature — regex matching, MIME detection, filesystem walking, XML encoding — uses Go's standard library. Zero external dependencies means zero supply chain risk, zero version conflicts, and the ability to audit the entire codebase in an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance is good enough.&lt;/strong&gt; Yes, &lt;code&gt;aict grep&lt;/code&gt; is slower than &lt;code&gt;ripgrep&lt;/code&gt;. Yes, &lt;code&gt;aict ls&lt;/code&gt; is slower than &lt;code&gt;eza&lt;/code&gt;. But we're talking 15ms vs 2ms for listing 1,000 files. The overhead comes from language detection, MIME sniffing, and structured output — features that are the entire point of the project. For normal codebases, the difference is imperceptible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Twenty-two tools across five categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File inspection&lt;/strong&gt;: &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;stat&lt;/code&gt;, &lt;code&gt;wc&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directory &amp;amp; search&lt;/strong&gt;: &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;diff&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path utilities&lt;/strong&gt;: &lt;code&gt;realpath&lt;/code&gt;, &lt;code&gt;basename&lt;/code&gt;, &lt;code&gt;dirname&lt;/code&gt;, &lt;code&gt;pwd&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text processing&lt;/strong&gt;: &lt;code&gt;sort&lt;/code&gt;, &lt;code&gt;uniq&lt;/code&gt;, &lt;code&gt;cut&lt;/code&gt;, &lt;code&gt;tr&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System &amp;amp; environment&lt;/strong&gt;: &lt;code&gt;env&lt;/code&gt;, &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;ps&lt;/code&gt;, &lt;code&gt;df&lt;/code&gt;, &lt;code&gt;du&lt;/code&gt;, &lt;code&gt;checksums&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus a &lt;code&gt;git&lt;/code&gt; subcommand suite (&lt;code&gt;status&lt;/code&gt;, &lt;code&gt;diff&lt;/code&gt;, &lt;code&gt;log&lt;/code&gt;, &lt;code&gt;ls-files&lt;/code&gt;, &lt;code&gt;blame&lt;/code&gt;) and an MCP server that exposes every tool as a callable function to AI assistants like Claude and Cursor.&lt;/p&gt;

&lt;p&gt;Every tool supports three output modes: XML, JSON, and plain text. Every error is structured data in stdout, never stderr. Every path is absolute. Every timestamp is a Unix epoch integer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP Server
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. &lt;code&gt;aict&lt;/code&gt; ships with an MCP (Model Context Protocol) server binary called &lt;code&gt;aict-mcp&lt;/code&gt;. You configure it in Claude Desktop or Cursor, and suddenly every tool becomes a typed, callable function.&lt;/p&gt;

&lt;p&gt;The AI agent doesn't shell out to run &lt;code&gt;aict ls src/&lt;/code&gt;. It calls the &lt;code&gt;ls&lt;/code&gt; function with &lt;code&gt;{path: "src/"}&lt;/code&gt; and receives structured JSON. No shell spawning. No output parsing. No ambiguity.&lt;/p&gt;

&lt;p&gt;This is the future of how AI agents interact with filesystems. Not by typing commands into a terminal and reading the output like a human would. By calling typed functions and receiving typed responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Didn't Build
&lt;/h2&gt;

&lt;p&gt;I intentionally excluded write operations: &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;mkdir&lt;/code&gt;, &lt;code&gt;chmod&lt;/code&gt;, &lt;code&gt;chown&lt;/code&gt;. These are dangerous when called by AI agents without human confirmation. &lt;code&gt;aict&lt;/code&gt; is a read-only tool. It observes, it doesn't modify.&lt;/p&gt;

&lt;p&gt;I also didn't try to match GNU coreutils flag-for-flag. Where a flag made sense for the AI use case, I added it. Where it didn't, I skipped it. The goal is not compatibility — it's utility for AI agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Benchmark
&lt;/h2&gt;

&lt;p&gt;I benchmarked &lt;code&gt;aict&lt;/code&gt; against GNU coreutils. Here are the results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;GNU&lt;/th&gt;
&lt;th&gt;aict&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ls&lt;/code&gt; (1,000 files)&lt;/td&gt;
&lt;td&gt;~2ms&lt;/td&gt;
&lt;td&gt;~15ms&lt;/td&gt;
&lt;td&gt;7×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;grep&lt;/code&gt; (100k lines)&lt;/td&gt;
&lt;td&gt;~1ms&lt;/td&gt;
&lt;td&gt;~100ms&lt;/td&gt;
&lt;td&gt;100×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;find&lt;/code&gt; (deep tree)&lt;/td&gt;
&lt;td&gt;~2ms&lt;/td&gt;
&lt;td&gt;~9ms&lt;/td&gt;
&lt;td&gt;5×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cat&lt;/code&gt; (100k lines)&lt;/td&gt;
&lt;td&gt;~1ms&lt;/td&gt;
&lt;td&gt;~23ms&lt;/td&gt;
&lt;td&gt;17×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;diff&lt;/code&gt; (1,000 lines)&lt;/td&gt;
&lt;td&gt;~1ms&lt;/td&gt;
&lt;td&gt;~10ms&lt;/td&gt;
&lt;td&gt;10×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;cat&lt;/code&gt; are slow because every file is MIME-typed and language-detected. Use &lt;code&gt;--plain&lt;/code&gt; to skip enrichment when you only need content. The trade-off is intentional: more tokens spent on parsing vs. more semantic information returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is This Actually Useful?
&lt;/h2&gt;

&lt;p&gt;I've been using &lt;code&gt;aict&lt;/code&gt; with Claude and Cursor for two months. The difference is noticeable. The agent makes fewer mistakes about file types. It doesn't confuse directories with files. It correctly identifies binary files before trying to read them. It understands the structure of a codebase faster.&lt;/p&gt;

&lt;p&gt;The token savings are real. A directory listing that used to cost 2,000 tokens in plain text now costs 800 in XML with three times the information density. Over a typical coding session with dozens of tool calls, that adds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Source
&lt;/h2&gt;

&lt;p&gt;The project is MIT licensed and on GitHub. It's written in Go with zero external dependencies. You can audit the entire codebase in an afternoon. I'd love contributions — new tools, performance improvements, bug fixes.&lt;/p&gt;

&lt;p&gt;If you build AI agents that interact with codebases, give it a try. Your agent will thank you. And if it doesn't work for your use case, that's fine too. GNU coreutils aren't going anywhere.&lt;/p&gt;

&lt;p&gt;The repo is at &lt;a href="https://github.com/synseqack/aict" rel="noopener noreferrer"&gt;github.com/synseqack/aict&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>agentaichallenge</category>
      <category>claude</category>
    </item>
    <item>
      <title>I got tired of configuring the same stack over and over, so I built a modular project assembler</title>
      <dc:creator>Ronak Parmar</dc:creator>
      <pubDate>Wed, 25 Mar 2026 13:26:48 +0000</pubDate>
      <link>https://dev.to/ronak_parmar_033c50d168b5/i-got-tired-of-configuring-the-same-stack-over-and-over-so-i-built-a-modular-project-assembler-e1m</link>
      <guid>https://dev.to/ronak_parmar_033c50d168b5/i-got-tired-of-configuring-the-same-stack-over-and-over-so-i-built-a-modular-project-assembler-e1m</guid>
      <description>&lt;p&gt;Every side project I started followed the same ritual.&lt;/p&gt;

&lt;p&gt;Find a starter. Clone it. Realize it doesn't have the exact &lt;br&gt;
combination I need — say, Next.js with Express as a separate &lt;br&gt;
backend, PostgreSQL, JWT auth, and Tailwind. Start adding things. &lt;br&gt;
Watch the tsconfig break. Spend 45 minutes on Stack Overflow. &lt;br&gt;
Finally start actually building at 11pm, mentally exhausted.&lt;/p&gt;

&lt;p&gt;I did this enough times that I started keeping a folder of &lt;br&gt;
"my personal starters." Which worked until I had six of them and &lt;br&gt;
they were all slightly different and none of them had the latest &lt;br&gt;
versions of anything.&lt;/p&gt;

&lt;p&gt;So I built Foundation CLI instead.&lt;/p&gt;


&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Foundation CLI is a dependency-aware project assembler. You describe &lt;br&gt;
your stack — frontend, backend, database, auth, UI, deployment — and &lt;br&gt;
instead of copying static template files, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resolves the full module dependency graph&lt;/li&gt;
&lt;li&gt;Detects and handles conflicts automatically&lt;/li&gt;
&lt;li&gt;Merges configs intelligently (deep merge for package.json, 
key-dedup for .env, semver intersection for requirements.txt)&lt;/li&gt;
&lt;li&gt;Commits everything atomically — or not at all&lt;/li&gt;
&lt;/ol&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%2Fraw.githubusercontent.com%2Fronak-create%2FFoundation-Cli%2Fmain%2Fpublic%2Fdemo.gif" 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%2Fraw.githubusercontent.com%2Fronak-create%2FFoundation-Cli%2Fmain%2Fpublic%2Fdemo.gif" alt="Demo" width="400" height="225"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @systemlabs/foundation-cli create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The part that was interesting to build
&lt;/h2&gt;

&lt;p&gt;The dependency resolver was the core challenge. Modules don't &lt;br&gt;
conflict by name — they conflict by capability. "Auth JWT" and &lt;br&gt;
"Auth OAuth" are different implementations of the same capability. &lt;br&gt;
The resolver uses capability tokens, not module IDs, to detect this.&lt;/p&gt;

&lt;p&gt;Then it runs Kahn's algorithm to build a topological sort of the &lt;br&gt;
module execution order — because if Tailwind needs to run after &lt;br&gt;
Next.js (to patch the config correctly), that dependency needs to be &lt;br&gt;
explicit and enforced.&lt;/p&gt;

&lt;p&gt;Conflicts come in two tiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard conflicts&lt;/strong&gt; — two auth modules, two frontends. Block the 
entire scaffold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advisory conflicts&lt;/strong&gt; — Express + Cloudflare Workers (Express 
doesn't run on the edge). Warn, but don't block.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The config merge problem
&lt;/h2&gt;

&lt;p&gt;This is the part that template copiers get wrong. When you add &lt;br&gt;
Tailwind to a Next.js + Express project, three different files need &lt;br&gt;
to change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;package.json&lt;/code&gt; — new devDependencies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tailwind.config.js&lt;/code&gt; — new file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;globals.css&lt;/code&gt; — Tailwind directives&lt;/li&gt;
&lt;li&gt;Possibly &lt;code&gt;next.config.mjs&lt;/code&gt; — PostCSS config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A static template can't do this. You either have a template per &lt;br&gt;
combination (exponential), or you have a module system that knows &lt;br&gt;
how to merge.&lt;/p&gt;

&lt;p&gt;Foundation CLI's merge engine handles this per file type. JSON files &lt;br&gt;
get deep-merged with conflict detection. &lt;code&gt;.env&lt;/code&gt; files get &lt;br&gt;
key-deduplicated. &lt;code&gt;docker-compose.yml&lt;/code&gt; gets service-merged. &lt;br&gt;
&lt;code&gt;requirements.txt&lt;/code&gt; gets semver-intersected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zero partial scaffolds
&lt;/h2&gt;

&lt;p&gt;Every write goes through a FileTransaction. Files are staged to a &lt;br&gt;
temp directory. If anything fails — a template render error, a &lt;br&gt;
missing dependency, a hook failure — the temp dir is deleted and &lt;br&gt;
the output directory is left completely untouched.&lt;/p&gt;

&lt;p&gt;You either get a complete, working project or you get nothing. No &lt;br&gt;
mystery half-generated directories.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;28 built-in modules across 7 categories&lt;/li&gt;
&lt;li&gt;8 project archetypes (SaaS, AI App, E-commerce, API Backend, etc.) 
with pre-filled smart defaults&lt;/li&gt;
&lt;li&gt;Plugin SDK — third-party modules publish to npm with 
&lt;code&gt;foundation-plugin&lt;/code&gt; keyword&lt;/li&gt;
&lt;li&gt;TypeScript throughout, strict mode, all generated projects 
pass &lt;code&gt;tsc --noEmit&lt;/code&gt;
&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 shell"&gt;&lt;code&gt;npx @systemlabs/foundation-cli create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/ronak-create/Foundation-Cli" rel="noopener noreferrer"&gt;https://github.com/ronak-create/Foundation-Cli&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love feedback — especially if you've built something similar &lt;br&gt;
and solved the config merge problem differently. The current &lt;br&gt;
approach works but I'm not convinced it's the best design.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>node</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Open-sourcing Foundation CLI — a dependency-aware project scaffolding tool</title>
      <dc:creator>Ronak Parmar</dc:creator>
      <pubDate>Mon, 16 Mar 2026 10:06:11 +0000</pubDate>
      <link>https://dev.to/ronak_parmar_033c50d168b5/open-sourcing-foundation-cli-a-dependency-aware-project-scaffolding-tool-pan</link>
      <guid>https://dev.to/ronak_parmar_033c50d168b5/open-sourcing-foundation-cli-a-dependency-aware-project-scaffolding-tool-pan</guid>
      <description>&lt;p&gt;I’ve been experimenting with a CLI tool for my own workflow that tries to simplify starting new projects.&lt;/p&gt;

&lt;p&gt;The idea is that instead of manually wiring frameworks, databases, auth, etc., you describe the stack you want and the CLI generates the project structure.&lt;/p&gt;

&lt;p&gt;For example something like:&lt;/p&gt;

&lt;p&gt;"I want a SaaS with Next.js, Express, PostgreSQL and JWT auth"&lt;/p&gt;

&lt;p&gt;The tool resolves dependencies, merges configs, and sets up the integration code automatically.&lt;/p&gt;

&lt;p&gt;I'm curious if people here actually find tools like this useful, or if most devs prefer starting projects manually.&lt;/p&gt;

&lt;p&gt;Would love to hear how others approach bootstrapping new projects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ronak-create/Foundation-Cli" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Fj5ey69ttzx3jdd1uyzyl.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.amazonaws.com%2Fuploads%2Farticles%2Fj5ey69ttzx3jdd1uyzyl.png" alt="Tool Preview" width="800" height="745"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>devops</category>
      <category>discuss</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
