<?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 Ran a 284B-Parameter LLM From 3.2GB of RAM — in Plain C</title>
      <dc:creator>Ronak Parmar</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:54:48 +0000</pubDate>
      <link>https://dev.to/ronak_parmar_033c50d168b5/i-ran-a-284b-parameter-llm-from-32gb-of-ram-in-plain-c-cbp</link>
      <guid>https://dev.to/ronak_parmar_033c50d168b5/i-ran-a-284b-parameter-llm-from-32gb-of-ram-in-plain-c-cbp</guid>
      <description>&lt;p&gt;DeepSeek-V4-Flash has 284 billion parameters and takes up about 160GB on disk. My laptop does not have 160GB of RAM. It doesn't even have 32GB.&lt;/p&gt;

&lt;p&gt;It ran the model anyway. Peak memory: &lt;strong&gt;3.23GB&lt;/strong&gt;. With a GPU and a bit more headroom, it generates at &lt;strong&gt;1.6–1.7 seconds per token&lt;/strong&gt;. No quantizing the model down to fit, no renting a multi-GPU box. Just C99, streaming weights off NVMe as they're needed.&lt;/p&gt;

&lt;p&gt;This post is about how &lt;a href="https://github.com/ronak-create/deepseek-v4-in-c" rel="noopener noreferrer"&gt;deepseek-v4-in-c&lt;/a&gt; actually works, and — more usefully — about three bugs I hit building it that taught me more than the parts that went smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick that makes this possible at all
&lt;/h2&gt;

&lt;p&gt;DeepSeek-V4-Flash is a mixture-of-experts model: 256 experts per layer, 43 layers, but only the top 6 experts per layer actually fire on any given token. That's the whole game. You don't need 160GB resident in memory — you need whatever fraction of the checkpoint this specific token's routing decisions touch, which works out to about 3.2GB per forward pass.&lt;/p&gt;

&lt;p&gt;So instead of loading the model, I stream it. Every token, the router decides which experts it needs, and the engine pulls just those off disk into an LRU cache. Give it more RAM and the cache gets bigger and hits more often; give it almost none and it still runs correctly, just slower, re-reading more from disk each time.&lt;/p&gt;

&lt;p&gt;I didn't build the streaming layer from nothing — it's ported from Fareed Khan's &lt;a href="https://github.com/FareedKhan-dev/kimi-k3-in-c" rel="noopener noreferrer"&gt;kimi-k3-in-c&lt;/a&gt;, which does the same thing for Kimi K3. The safetensors reader, the disk streamer, the memory planner, roughly 40% of the codebase — that transfers directly. The other 60% doesn't: DeepSeek-V4 and Kimi K3 don't share any math, so every kernel had to be written fresh against DeepSeek's own reference implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn't trust it until I could prove it
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth about hand-writing inference kernels: a model with a swapped nibble or a misindexed scale factor will still produce text that &lt;em&gt;reads fine&lt;/em&gt;. Fluent output is not evidence of a correct implementation. It just means the bug isn't catastrophic enough to break language modeling entirely.&lt;/p&gt;

&lt;p&gt;So before I believed any of my own benchmark numbers, I checked correctness at three levels against PyTorch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per kernel&lt;/strong&gt; — 14 kernels, checked individually, agreeing to 5e-7&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per block&lt;/strong&gt; — three layer types, 46 positions, same tolerance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end&lt;/strong&gt; — a real (tiny) &lt;code&gt;deepseek_v4&lt;/code&gt; checkpoint run through the actual C loader and actual inference path, matching PyTorch to &lt;strong&gt;2.9e-6&lt;/strong&gt; with identical argmax at every single position
The PyTorch side is a from-scratch reimplementation of DeepSeek's &lt;code&gt;inference/model.py&lt;/code&gt;, not derived from my C code. If I'd built the reference from my own implementation, both could quietly agree on the same misunderstanding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I went one step further for the C code itself: &lt;strong&gt;bit-exactness&lt;/strong&gt; between the scalar path, the OpenMP path, and the AVX2 path. Not "close enough" — byte-identical, enforced with a fixed 16-accumulator reduction tree and &lt;code&gt;-ffp-contract=off&lt;/code&gt;, checked at runtime by literally running both paths on the same input and diffing the bits. The GPU can't join that club — warp-level reduction order isn't deterministic the same way — so it gets held to relative error plus argmax match instead, and the CPU stays the ground truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The optimization that was actually silent corruption
&lt;/h2&gt;

&lt;p&gt;This is the bug I'm most annoyed I shipped, and most glad I caught.&lt;/p&gt;

&lt;p&gt;Reading experts off disk uses &lt;code&gt;O_DIRECT&lt;/code&gt;, which demands the file offset, read length, and destination buffer all land on 4096-byte boundaries. The checkpoint's tensors don't naturally align that way, so my first version widened every read to the nearest aligned window, landed it in a staging buffer, then &lt;code&gt;memcpy&lt;/code&gt;'d the actual payload into the cache slot. A 12.75MB copy, roughly 5,000 times per run. I'd written it off as cheap — "~1ms against the ~10ms the unbuffered read saves" — without actually measuring it.&lt;/p&gt;

&lt;p&gt;I measured it. The read itself was 2.81ms. The copy was costing 3.60ms total per expert. &lt;strong&gt;The copy wasn't a rounding error, it was 22% of the cost.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix seemed simple: allocate cache slots 4096-aligned with a little slack, and place each read at the offset whose alignment residue matches the actual tensor, so the widened window lands exactly where the data belongs — no copy needed. My first attempt at that fix was wrong. It placed each read at the next offset with a matching residue, without accounting for the fact that an aligned window can begin up to 4095 bytes &lt;em&gt;before&lt;/em&gt; the actual payload starts. So a second tensor's "aligned" read could reach backward and silently overwrite bytes belonging to the tensor before it.&lt;/p&gt;

&lt;p&gt;Every existing test passed. Every one. Here's why: my correctness gates compared the cache against &lt;em&gt;itself&lt;/em&gt; — serial mode against concurrent mode — and both were corrupting the data identically, so the comparison found nothing wrong. Worse, the corrupted version looked like a huge win: cache hit rate jumped from 52.6% to 95.5%, because the corruption had collapsed routing onto a tiny handful of experts. Disk reads dropped from 61GB to 5.76GB. It looked like I'd found a 5x speedup. The only thing that gave it away was that the generated token IDs were wrong.&lt;/p&gt;

&lt;p&gt;I fixed it two ways: a new gate that compares the fast path against a plain buffered &lt;code&gt;pread&lt;/code&gt; sharing no code with it (so it can't fail the same way), and a test fixture that actually mirrors how the real checkpoint splits an expert's tensors — three scale tensors in one disk region, three weight tensors ~341MB away at a different alignment residue. My synthetic fixture had been storing everything contiguously, which meant the bug's trigger condition literally never came up in testing. Real fix, properly verified: disk time down 16%, wall clock down 12%.&lt;/p&gt;

&lt;p&gt;Lesson I'm keeping: &lt;strong&gt;if two things you're comparing can fail identically, your test proves nothing.&lt;/strong&gt; I needed a reference path with zero shared code.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 30x slowdown with the wrong explanation
&lt;/h2&gt;

&lt;p&gt;Turning on &lt;code&gt;--gpu&lt;/code&gt; moves the dense trunk's FP8 matrices into VRAM. Naively, that should be free for anything still running on CPU — the GPU does its thing, the CPU does its thing. It wasn't. I benchmarked a CPU-only FP4 matmul while the GPU was busy and watched throughput fall from 119 GF/s to &lt;strong&gt;3.9 GF/s&lt;/strong&gt;. A 30x collapse.&lt;/p&gt;

&lt;p&gt;My first theory: CUDA's default sync mode spins the calling thread instead of sleeping it, so that thread competes for CPU cycles with my OpenMP workers. I switched to &lt;code&gt;cudaDeviceScheduleBlockingSync&lt;/code&gt;, which sleeps instead of spinning. It should have fixed it.&lt;/p&gt;

&lt;p&gt;It didn't, really — 5.0 GF/s instead of 3.9. Still a 25x collapse.&lt;/p&gt;

&lt;p&gt;What actually fixed it: reserving one CPU core for the thread driving the GPU, and letting OpenMP use the rest. That removed the collapse completely, in either sync mode. My best guess now is DMA traffic from the GPU contending with a memory-bandwidth-bound kernel for DRAM bandwidth — but I haven't measured that, so I'm not claiming it in the README as fact. The fix works. The explanation I originally reached for was wrong, and I'd rather say that than pretend I nailed it on the first guess.&lt;/p&gt;

&lt;p&gt;One caveat I want to be honest about: this collapse is much bigger in the microbenchmark than in the real model. The synthetic benchmark keeps the GPU saturated back-to-back; the actual model only touches it a few times per layer. On real generation, holding back a core is worth 10–15%, not 3x. An earlier draft of my README overstated this based on a heat-soaked benchmarking session, and I want to flag that explicitly rather than let an inflated number stick around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thermal state matters more than I expected
&lt;/h2&gt;

&lt;p&gt;Speaking of that heat-soaked session — this is the correction I'm least proud of needing to make, and the most useful thing in this whole post if you benchmark anything disk-bound.&lt;/p&gt;

&lt;p&gt;I re-ran every number in the README on a freshly rebooted, idle machine on AC power. The gap versus numbers taken after 24+ hours of sustained load was not the ±20% I'd assumed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;config&lt;/th&gt;
&lt;th&gt;heat-soaked&lt;/th&gt;
&lt;th&gt;cold&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;--budget 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;13.7 s/tok&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.64 s/tok&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.95x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--budget 16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.65 s/tok&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.21 s/tok&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.20x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--budget 16 --gpu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.81 s/tok&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.74 s/tok&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.04x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern makes sense in hindsight: the more disk-bound a configuration is, the more it suffers when the machine is hot, because sequential O_DIRECT throughput itself drops under thermal load (5.3 GB/s cold vs 4.4 GB/s heat-soaked, measured directly). Low-&lt;code&gt;--budget&lt;/code&gt; runs are almost pure disk I/O, so they took the worst of it.&lt;/p&gt;

&lt;p&gt;I now treat any single timing as ±20% at a fixed thermal state and up to 3x across states — and I resolve actual kernel changes with a dedicated microbenchmark (&lt;code&gt;bench/matmul_bw.c&lt;/code&gt;) rather than by timing a full generation run, because a 70-second end-to-end run just can't resolve a 12% kernel improvement through that much noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;--budget&lt;/code&gt; flag is the one that actually matters
&lt;/h2&gt;

&lt;p&gt;If you try this yourself, the flag to understand is &lt;code&gt;--budget&lt;/code&gt; — it sets total RAM for the trunk plus the expert cache, and getting it wrong doesn't just make things slow, it can make the cache mathematically incapable of ever hitting.&lt;/p&gt;

&lt;p&gt;One forward pass touches 258 experts across all layers — about 3.21GB. Below that, an LRU cache evicts every entry before its layer comes back around, so it can &lt;em&gt;never&lt;/em&gt; hit. I measured this directly at an old default: 10,320 requests, 0 hits, 128GB pulled from disk for one run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--budget    expert cache    hit rate    disk read
8 GB        1.6 GB          0%          128 GB
12 GB       5.6 GB          ~40%        ~80 GB
16 GB       9.6 GB          53%         61 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine now prints a warning naming the threshold if you're under it, because I hit this myself before I understood why my "optimized" cache was doing nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make            &lt;span class="c"&gt;# CPU-only build&lt;/span&gt;
make &lt;span class="nb"&gt;test&lt;/span&gt;       &lt;span class="c"&gt;# 20 gates, no model weights required&lt;/span&gt;

&lt;span class="c"&gt;# pack the checkpoint once&lt;/span&gt;
python3 tools/pack_trunk.py ~/models/dsv4-flash ~/dsv4-trunk
python3 tools/pack_tokenizer.py ~/models/dsv4-flash ~/dsv4_tok.bin

./bin/dsv4 ~/models/dsv4-flash &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--trunk&lt;/span&gt; ~/dsv4-trunk &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tok&lt;/span&gt;   ~/dsv4_tok.bin &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"The capital of France is"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--gen&lt;/span&gt; 25 &lt;span class="nt"&gt;--budget&lt;/span&gt; 16 &lt;span class="nt"&gt;--gpu&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--chat&lt;/code&gt; applies DeepSeek-V4's real prompt format — I had to pull it from the checkpoint's own &lt;code&gt;encoding_dsv4.py&lt;/code&gt;, because it isn't where most tokenizer tooling looks (&lt;code&gt;tokenizer_config.json&lt;/code&gt;). &lt;code&gt;--think&lt;/code&gt; opens a reasoning block before the model answers. CUDA is auto-detected; without &lt;code&gt;nvcc&lt;/code&gt;, the build still succeeds and &lt;code&gt;--gpu&lt;/code&gt; just reports no device found instead of failing.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DeepSeek-V4-Pro&lt;/strong&gt; (61 layers, 384 experts, ~865GB checkpoint) is gated and planned in the test suite but never actually run — I don't have the disk for it. The planning gate already caught a real bug: a shape-validation rule that happened to compute the right answer for Flash by coincidence (both sides of the equation collapse to 4096) but would silently fail on Pro's dimensions. A Flash-only test suite would never have caught that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batched prefill&lt;/strong&gt; doesn't exist yet. Prompt tokens go through one at a time, same as generated ones, at roughly 1 second each — so a long prompt dominates latency far more than generation speed does. I've measured the expert-reuse ceiling that batching would unlock (up to ~8x weight reuse at 200+ tokens of prompt), but implementing the batched kernels is future work, not something I've shipped.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tool-calling loop.&lt;/strong&gt; The model correctly emits its tool-call format, but actually driving an agent loop on top of it is a layer above this CLI that I haven't built.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I'd tell someone doing something similar
&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fluent output from a hand-written kernel is not proof it's correct. Get an independently-written reference and check bit-level agreement, not vibes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If your test compares two things that can fail the same way, it proves nothing. I needed a reference path with zero shared code to catch my worst bug.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Measure the thing you think is expensive before you "optimize" it. My memcpy was 22% of cost, not the 1ms I'd assumed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Report the thermal state your benchmarks ran under, or don't trust them.&lt;br&gt;
Code's here, Apache-2.0: &lt;a href="https://github.com/ronak-create/deepseek-v4-in-c" rel="noopener noreferrer"&gt;github.com/ronak-create/deepseek-v4-in-c&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>c</category>
      <category>ai</category>
      <category>opensource</category>
      <category>performance</category>
    </item>
    <item>
      <title>We tested "tokenize before you compress" against 452 configurations, and it mostly held up</title>
      <dc:creator>Ronak Parmar</dc:creator>
      <pubDate>Fri, 21 Aug 2026 01:37:16 +0000</pubDate>
      <link>https://dev.to/ronak_parmar_033c50d168b5/we-tested-tokenize-before-you-compress-against-452-configurations-and-it-mostly-held-up-4m6p</link>
      <guid>https://dev.to/ronak_parmar_033c50d168b5/we-tested-tokenize-before-you-compress-against-452-configurations-and-it-mostly-held-up-4m6p</guid>
      <description>&lt;p&gt;A few weeks ago my friend &lt;a href="https://github.com/u84u" rel="noopener noreferrer"&gt;@u84u&lt;/a&gt; and I (&lt;a href="https://github.com/ronak-create" rel="noopener noreferrer"&gt;@ronak-create&lt;/a&gt;) had a simple, slightly annoying question: if LLMs get 30-45% smaller representations of text by tokenizing it into subwords instead of raw bytes, why don't we tokenize text &lt;em&gt;before&lt;/em&gt; handing it to a byte-level compressor like LZMA or zstd?&lt;/p&gt;

&lt;p&gt;It felt like the kind of idea someone must have already tried and quietly dropped. So instead of writing a blog post about the idea, we built the harness to actually test it, and the result is &lt;strong&gt;&lt;a href="https://github.com/shallowbyte/parmar" rel="noopener noreferrer"&gt;parmar&lt;/a&gt;&lt;/strong&gt; — a subword-tokenization pre-filter for byte-level compressors, plus a fairly paranoid benchmarking rig to check whether the idea holds up at scale.&lt;/p&gt;

&lt;p&gt;Short version: &lt;strong&gt;it works, but for a narrower reason than we expected&lt;/strong&gt;, and the harness told us that just as clearly as it told us the headline number.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hypothesis
&lt;/h2&gt;

&lt;p&gt;Byte-level compressors like LZMA2 and zstd find repeated patterns inside a fixed-size sliding "dictionary window," measured in bytes. Our premise: if you replace UTF-8 prose with BPE token IDs — the same tokenization used to feed LLMs — before compressing, the token stream is roughly 45% smaller than the source text. A 64 MiB dictionary window that normally covers ~64 MB of prose can now cover roughly twice as much prose once that prose has been pre-shrunk.&lt;/p&gt;

&lt;p&gt;The catch is that this only becomes testable once your corpus is bigger than the compressor's window. On a 5 MB file, everything already fits inside the window, so there's no expansion to measure and pre-tokenization buys you basically nothing. That's why a single ratio number on a small test file is close to meaningless here — what actually matters is the &lt;em&gt;curve&lt;/em&gt; of (parmar ratio − raw-byte ratio) as corpus size grows, and whether that curve keeps climbing.&lt;/p&gt;

&lt;p&gt;So that became the deliverable: not "does it compress smaller," but "does the advantage widen with scale, and if so, why."&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually built
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;parmar&lt;/code&gt; is two things bolted together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The pipeline&lt;/strong&gt; (&lt;code&gt;parmar_core.py&lt;/code&gt;, &lt;code&gt;parmar.py&lt;/code&gt;) — tokenize text with &lt;code&gt;tiktoken&lt;/code&gt;, pack the token IDs (LEB128 or a fixed 2-byte width), and stream the result straight into an &lt;code&gt;xz&lt;/code&gt;/&lt;code&gt;zstd&lt;/code&gt;/&lt;code&gt;gzip&lt;/code&gt;/&lt;code&gt;bzip2&lt;/code&gt; subprocess. Nothing is ever fully materialized in memory — chunks are read, tokenized, packed, and piped into the compressor's stdin, and its stdout &lt;em&gt;is&lt;/em&gt; the archive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The harness&lt;/strong&gt; (&lt;code&gt;matrix.py&lt;/code&gt;, &lt;code&gt;run_cell.py&lt;/code&gt;, &lt;code&gt;analyze.py&lt;/code&gt;) — a matrix runner that sweeps tokenizer × packing × backend × transport × chunking × threading, on a real corpus, across four size tiers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The corpus is PG-19 (Rae et al., 2019's long-range-sequence-modelling dataset), built out to four tiers from 64 MB to 4 GB, about 10,600 documents. A literal cartesian product of every axis is on the order of 8,000+ valid cells per tier — weeks of runtime — so we split it into a 51-cell "ratio grid" that isolates the axes that actually move compression ratio, and a separate one-factor-at-a-time sweep for the axes that should only affect speed. Ratio is still logged on every OFAT cell too, so if a "speed-only" axis quietly moves the ratio, that shows up as a contradiction instead of getting averaged away.&lt;/p&gt;

&lt;p&gt;Every single decompression is actually executed and checked against a sha256 written into the archive footer. If that check fails, the cell's ratio is thrown out and reported separately, not folded into the averages. Across 452 matrix cells: 452 verified round trips, zero failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The gap does widen with corpus size — but only for backends with a big enough window, and it plateaus.&lt;/strong&gt; On &lt;code&gt;gzip&lt;/code&gt;'s tiny 32 KiB window, the +15% advantage is completely flat from 64 MB to 4 GB, because the window is already saturated at every tier — that's pure "denser representation," no window effect at all. On LZMA and zstd variants with multi-megabyte windows, the gap climbs from 64 MB up through roughly 1 GB and then flattens out once the corpus is well past the dictionary size. One backend, &lt;code&gt;zstd --long&lt;/code&gt; with its 2 GiB window, was still climbing at the 4 GB tier — because 4 GB is only 2x its window, so it hadn't left that regime yet.&lt;/p&gt;

&lt;p&gt;Having &lt;code&gt;gzip&lt;/code&gt; in the matrix as a tiny-window control turned out to be the thing that separated two effects we'd originally lumped together: raw representation density (tokens are just a denser way to write English than UTF-8) versus window expansion (a smaller stream lets the same window cover more source text). Without that control we'd have credited window expansion for gains that were actually just density.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On &lt;code&gt;bzip2&lt;/code&gt;, pre-tokenization is a flat loss (about -3.9%), at every corpus size.&lt;/strong&gt; bzip2's Burrows-Wheeler transform is exploiting byte-level structure in the prose that tokenization destroys, so it's simply the wrong backend to pair this with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is usually not a size-for-speed tradeoff.&lt;/strong&gt; On 5 of the 7 backends tested, &lt;code&gt;parmar&lt;/code&gt; is &lt;em&gt;smaller and faster&lt;/em&gt; than compressing raw bytes, at every tier, simultaneously — because the compressor is handed ~45% fewer bytes, and the time saved compressing them outweighs the time spent tokenizing. The two exceptions are informative rather than just noise: &lt;code&gt;zstd&lt;/code&gt; at its fastest level is quick enough that tokenization itself becomes the bottleneck above a couple hundred MB, and &lt;code&gt;bzip2&lt;/code&gt; loses on both size and speed for the structural reason above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multithreading interacts with this in a way we hadn't anticipated.&lt;/strong&gt; &lt;code&gt;xz -T&lt;/code&gt; splits input into independent blocks once the stream is at least twice the dictionary size, and speedup tracks the resulting block count almost 1:1 until you run out of cores. Because pre-tokenization shrinks the stream fed to &lt;code&gt;xz&lt;/code&gt;, it also shrinks the block count at a fixed block size — on a 1 GB corpus, raw bytes got 8 blocks and a 7.5x speedup from threading, while the tokenized version got 5 blocks and only 5x. So &lt;code&gt;parmar&lt;/code&gt; was quietly giving up close to a third of the available multithreaded speedup in exchange for its ratio win, in the regime where block count sits below the core count. Interestingly, &lt;code&gt;zstd&lt;/code&gt;'s multithreading doesn't cost any ratio at all — it doesn't reset its window between jobs the way &lt;code&gt;xz&lt;/code&gt;'s independent blocks do, which is a concrete reason to prefer &lt;code&gt;zstd&lt;/code&gt; if you need both threading and no ratio penalty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One negative result we were happy to get:&lt;/strong&gt; we'd built a hand-rolled worker pool (&lt;code&gt;manual_pool&lt;/code&gt;) on the theory it might beat &lt;code&gt;tiktoken&lt;/code&gt;'s built-in batch tokenization. It won in exactly 8 of 16 comparable configurations — chance — with swings from -6.8% to +44% and no pattern by thread count, backend, or corpus size. Digging in, &lt;code&gt;tiktoken&lt;/code&gt;'s batch encode already releases the GIL and parallelizes internally in Rust, so there's no headroom left for Python-side coordination to claw back, and the swings we saw were mostly measurement noise on a step that only takes 1-3 seconds. The honest conclusion: the extra complexity isn't worth it, and we kept the negative result in the writeup instead of quietly deleting the code path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is actually useful
&lt;/h2&gt;

&lt;p&gt;Grounded in the measurements rather than vibes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Archiving large prose corpora&lt;/strong&gt; with LZMA or high-level zstd — smaller archive, shorter compression run, no downside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything stuck on gzip/deflate&lt;/strong&gt; — the +15% gain shows up at every corpus size, including small ones, because gzip's window is always saturated. If you can't swap the compressor but can change what feeds it, this is the cleanest win in the whole project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text that's getting tokenized anyway&lt;/strong&gt; — LLM training shards, eval sets, retrieval corpora — since a reader can skip re-tokenization on the way back out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold storage, write-once/read-rarely&lt;/strong&gt; — decompression carries a detokenization cost that compression doesn't, so the asymmetry favors data you don't read often.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And where it explicitly isn't a fit: it's untested on non-prose (code, JSON, logs), it's not a general-purpose archive format (the archive stores the tokenizer's &lt;em&gt;name&lt;/em&gt;, not its vocabulary, so you need the exact same &lt;code&gt;tiktoken&lt;/code&gt; encoding available to decompress), and it has no encryption or authentication — the sha256 in the footer is an integrity check, not a MAC.&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;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
./venv/Scripts/python.exe &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;tiktoken numpy zstandard psutil matplotlib pandas

&lt;span class="c"&gt;# smoke test&lt;/span&gt;
python matrix.py smoke &lt;span class="nt"&gt;--corpus&lt;/span&gt; ./corpus/pg19_64mb.txt

&lt;span class="c"&gt;# or run the whole programme end to end&lt;/span&gt;
bash run_all.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full results, the matrix-generation logic, and &lt;code&gt;FINDINGS.md&lt;/code&gt; (a running list of everything that turned out to be wrong once the code could actually be run — including a fun one about the PG-19 dataset no longer being loadable the documented way) are all in the repo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/shallowbyte/parmar" rel="noopener noreferrer"&gt;github.com/shallowbyte/parmar&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's also an auto-generated code walkthrough on &lt;a href="https://deepwiki.com/shallowbyte/parmar" rel="noopener noreferrer"&gt;DeepWiki&lt;/a&gt; if you want to navigate the codebase rather than the results.&lt;/p&gt;

&lt;p&gt;Open questions we'd like help on: sweeping dictionary size directly instead of corpus size (should isolate the plateau mechanism much more cheaply), frequency-remapping token IDs before packing to close the gap between LEB128 and the fixed-width packing on large-vocabulary tokenizers, and testing this on source code instead of prose. Issues and PRs welcome.&lt;/p&gt;

</description>
      <category>compression</category>
      <category>python</category>
      <category>opensource</category>
      <category>benchmark</category>
    </item>
    <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" width="460" height="460"&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="" width="460" height="460"&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/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;14&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;/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/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/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>
