<?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: Naveen Gopalakrishna</title>
    <description>The latest articles on DEV Community by Naveen Gopalakrishna (@naveenneog).</description>
    <link>https://dev.to/naveenneog</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%2F4023553%2Fdba2483f-64d2-4c6d-8481-753abf1b2fe4.jpg</url>
      <title>DEV Community: Naveen Gopalakrishna</title>
      <link>https://dev.to/naveenneog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naveenneog"/>
    <language>en</language>
    <item>
      <title>SpendLens: Private, On-Device Bank-Statement Analysis — No Login, No Cloud, No LLM</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Thu, 30 Jul 2026 07:13:08 +0000</pubDate>
      <link>https://dev.to/naveenneog/spendlens-private-on-device-bank-statement-analysis-no-login-no-cloud-no-llm-bnp</link>
      <guid>https://dev.to/naveenneog/spendlens-private-on-device-bank-statement-analysis-no-login-no-cloud-no-llm-bnp</guid>
      <description>&lt;p&gt;&lt;a href="https://naveenneog.github.io/SpendLens/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3u3iqbqq5pk6jmam46b9.png" alt="SpendLens — a bank statement analyzed entirely on-device: reconciled totals, spend by category, and a daily-spend chart, marked Verified" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Day 17 of #AI4Good.&lt;/strong&gt; I used an AI coding agent to build a personal-finance app — and the single most important design decision was to keep AI, the cloud, and the network completely &lt;em&gt;out&lt;/em&gt; of it. Your bank statement is read, parsed, and reconciled on your own device. Nothing is uploaded. Nothing is stored anywhere but your phone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm &lt;strong&gt;&lt;a href="https://naveenneog.github.io/AI4Good/about/" rel="noopener noreferrer"&gt;Naveen Gopalakrishna&lt;/a&gt;&lt;/strong&gt; — an AI Global Black Belt at Microsoft, a compulsive builder after hours. Most #AI4Good builds put AI &lt;em&gt;in&lt;/em&gt; the product. &lt;strong&gt;SpendLens&lt;/strong&gt; is the opposite, on purpose: the "good" here is financial privacy, so the intelligence lives in the parser, not in a model, and your money never leaves the device.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Try it in the browser (nothing retained):&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/SpendLens/" rel="noopener noreferrer"&gt;naveenneog.github.io/SpendLens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/SpendLens/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/SpendLens/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/SpendLens" rel="noopener noreferrer"&gt;github.com/naveenneog/SpendLens&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;SpendLens reads a bank-statement PDF and turns it into an honest picture of where your money went — &lt;strong&gt;without a bank login, an OTP, a screen-scrape, or an upload&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Import a PDF&lt;/strong&gt; through the native Android/iOS document picker (or drag one into the browser version). Password-protected statements are supported; the password is used in memory and never kept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciled summary&lt;/strong&gt; — total spent, total received, opening and closing balance, and date range, in Indian digit grouping with tabular figures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend by category, daily-spend trend, and largest spends&lt;/strong&gt;, each with an accessible table/list equivalent and labels that never rely on colour alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Searchable, filterable transactions&lt;/strong&gt; with editable categories. Correct a category once and SpendLens remembers the merchant rule for next time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saved statement history&lt;/strong&gt; on-device, and a one-tap &lt;strong&gt;delete everything&lt;/strong&gt; that really deletes everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detection for 78 RBI scheduled retail banks&lt;/strong&gt; — public-sector, private, small-finance, payments, and regional-rural — by official name, known aliases, and IFSC prefixes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cover image is the browser version analysing a fully synthetic Axis sample: an &lt;code&gt;Axis Bank Ltd.&lt;/code&gt; statement, marked &lt;strong&gt;Verified&lt;/strong&gt;, &lt;code&gt;Active only in this tab&lt;/code&gt;, spend broken out by category, and a daily-spend chart — all rendered without a single network request leaving the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The whole app is &lt;strong&gt;Flutter (Dart)&lt;/strong&gt; with a thin layer of native platform code, and it was built with &lt;strong&gt;GitHub Copilot CLI in autopilot mode&lt;/strong&gt; — the agent researched Indian statement formats, wrote the parser and tests, ran the builds, and drove an Android emulator smoke test end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-device PDF text extraction.&lt;/strong&gt; Rather than ship a commercial PDF SDK, SpendLens calls the platform's own engine on a worker thread: &lt;strong&gt;PDFBox-Android (Kotlin)&lt;/strong&gt; on Android and &lt;strong&gt;PDFKit (Swift)&lt;/strong&gt; on iOS, over a method channel. Image-only / scanned PDFs are rejected explicitly (on-device OCR is a future module) instead of pretending they parsed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A deterministic parser — no ML, no LLM.&lt;/strong&gt; This is the heart of the trust model. The generic parser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recovers multiline transaction descriptions and handles the common Indian date formats;&lt;/li&gt;
&lt;li&gt;infers debit vs credit from the direction of consecutive running balances;&lt;/li&gt;
&lt;li&gt;checks the &lt;strong&gt;opening + credits − debits = closing&lt;/strong&gt; equation, and, when the statement declares its own debit/credit totals, validates those too — in the detected column order;&lt;/li&gt;
&lt;li&gt;preserves &lt;code&gt;DR&lt;/code&gt;, leading-minus, and zero-crossing overdraft balances;&lt;/li&gt;
&lt;li&gt;and returns an explicit &lt;strong&gt;Verified&lt;/strong&gt;, &lt;strong&gt;Needs review&lt;/strong&gt;, or &lt;strong&gt;Incomplete&lt;/strong&gt; verdict with the diagnostic attached.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It never converts a parse error into a success-shaped empty dashboard. If the numbers don't reconcile, the UI says so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Money is integer paise, never floating point.&lt;/strong&gt; Every amount is stored and summed as an integer so ₹0.01 rounding drift can't creep into a total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explainable categorization.&lt;/strong&gt; Categories come from transparent keyword rules with &lt;strong&gt;token-boundary matching&lt;/strong&gt;, so "Zuber" doesn't get filed under the ride brand "Uber," and your own merchant corrections always override the built-ins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local persistence.&lt;/strong&gt; Statements, edited categories, merchant rules, and settings live in &lt;strong&gt;SQLite&lt;/strong&gt; (analyses stored as JSON documents for schema agility). Category-rule updates and full data deletion are atomic transactions, and concurrent first-access is serialised through a single database-open future so startup can't race.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A custom native document picker.&lt;/strong&gt; &lt;code&gt;file_picker&lt;/code&gt; didn't compile cleanly against Flutter 3.44 / AGP 9, so SpendLens ships its own Storage Access Framework / &lt;code&gt;UIDocumentPickerViewController&lt;/code&gt; picker — which also avoids asking for broad storage permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A zero-retention web twin.&lt;/strong&gt; The GitHub Pages version does the same parsing in the browser with &lt;strong&gt;self-hosted PDF.js&lt;/strong&gt; and a Content-Security-Policy of &lt;code&gt;connect-src 'none'&lt;/code&gt; — no cookies, local storage, IndexedDB, service worker, analytics, or upload endpoint. A static &lt;code&gt;verify_web_privacy&lt;/code&gt; check runs in CI to keep it that way, and the analysis auto-clears after 15 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verified before shipping.&lt;/strong&gt; 40 automated tests (parser, bank registry, categorization, SQLite, controller, privacy, concurrency, widgets), a clean &lt;code&gt;flutter analyze&lt;/code&gt;, the web-privacy verifier, and a scripted Android emulator smoke that imports a synthetic statement, checks reconciliation, and confirms that "delete everything" survives a relaunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;Most "see where your money goes" apps ask you to connect your bank — handing a third party your credentials, or trusting an aggregator to hold your transaction history. SpendLens is a bet that you shouldn't have to. &lt;strong&gt;Everything happens on the device you already trust with the statement.&lt;/strong&gt; No account, no analytics SDK, no remote parser, no generative-AI dependency; Android backup/transfer is disabled and the iOS database is excluded from backups.&lt;/p&gt;

&lt;p&gt;And it's built for &lt;strong&gt;Indian&lt;/strong&gt; statements specifically — the date formats, the digit grouping, and coverage across 78 banks including the small-finance and regional-rural banks that mainstream tools ignore. When a layout is unfamiliar it degrades honestly to &lt;strong&gt;Needs review&lt;/strong&gt; rather than inventing certainty.&lt;/p&gt;

&lt;p&gt;The meta-point is the one I like most: an AI agent built a finance tool whose entire value proposition is that &lt;strong&gt;AI and the network never touch your money.&lt;/strong&gt; That's a version of #AI4Good worth keeping in the toolkit — using the agent to ship privacy, not to harvest it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live (zero-retention browser analyzer):&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/SpendLens/" rel="noopener noreferrer"&gt;naveenneog.github.io/SpendLens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/SpendLens/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/SpendLens/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/SpendLens" rel="noopener noreferrer"&gt;github.com/naveenneog/SpendLens&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;
        naveenneog
      &lt;/a&gt; / &lt;a href="https://github.com/naveenneog/SpendLens" rel="noopener noreferrer"&gt;
        SpendLens
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Privacy-first Flutter app that turns Indian bank-statement PDFs into on-device spending insights - no bank login, cloud upload, analytics, or LLMs.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;SpendLens&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;SpendLens is a privacy-first Flutter app that turns Indian bank-statement PDFs into understandable spending summaries without bank logins, cloud uploads, or LLMs.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Version 0.2&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;On-device PDF text extraction and deterministic parsing&lt;/li&gt;
&lt;li&gt;Detection registry for 78 RBI scheduled retail banks&lt;/li&gt;
&lt;li&gt;Reconciliation and partial-parse diagnostics&lt;/li&gt;
&lt;li&gt;Editable categories and remembered merchant rules&lt;/li&gt;
&lt;li&gt;Category, daily-spend, and large-spend views&lt;/li&gt;
&lt;li&gt;Searchable transactions and saved statement history&lt;/li&gt;
&lt;li&gt;Adaptive Android/iOS layouts, light/dark themes, and accessible chart alternatives&lt;/li&gt;
&lt;li&gt;Native Android and iOS document pickers with no broad storage permission&lt;/li&gt;
&lt;li&gt;Password-protected PDF handling&lt;/li&gt;
&lt;li&gt;Local SQLite persistence and complete data deletion&lt;/li&gt;
&lt;li&gt;Zero-retention GitHub Pages analyzer in &lt;code&gt;docs/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Self-hosted PDF.js with a CSP that forbids authored network connections&lt;/li&gt;
&lt;li&gt;Explicit browser-memory clear and 15-minute inactivity clearing&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Development&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Flutter is installed at &lt;code&gt;C:\Users\navg\flutter&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-powershell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;C:\Users\navg\flutter\bin\flutter.bat&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; pub get
&lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;C:\Users\navg\flutter\bin\flutter.bat&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; analyze
&lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;C:\Users\navg\flutter\bin\flutter.bat&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; test
&lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;C:\Users\navg\flutter\bin\flutter.bat&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; run&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The current sideload artifact is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;artifacts\SpendLens-v0.2.0.apk&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It follows…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/naveenneog/SpendLens" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Part of the &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/10/ai4good-an-app-a-day/" rel="noopener noreferrer"&gt;#AI4Good&lt;/a&gt; series — one build at a time, from a single laptop, with the "who does this help?" chosen first. &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/naveen-gopalakrishna-99863040/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. #AI4Good&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>flutter</category>
      <category>privacy</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Overdrive: An Open-Source Skill That Makes Your Coding Agent Stop Settling for Correct</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:51:47 +0000</pubDate>
      <link>https://dev.to/naveenneog/overdrive-an-open-source-skill-that-makes-your-coding-agent-stop-settling-for-correct-1kdl</link>
      <guid>https://dev.to/naveenneog/overdrive-an-open-source-skill-that-makes-your-coding-agent-stop-settling-for-correct-1kdl</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/naveenneog/overdrive" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fon59z1kbi982tb37f42z.png" alt="Overdrive — a capability amplifier for coding agents, installed with a single npx command" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Most of the gap between a mediocre answer and an exceptional one from the same model isn't capability. It's &lt;strong&gt;standards&lt;/strong&gt;. Overdrive is the standards, written down and installed.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx github:naveenneog/overdrive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm &lt;strong&gt;&lt;a href="https://naveenneog.github.io/AI4Good/about/" rel="noopener noreferrer"&gt;Naveen Gopalakrishna&lt;/a&gt;&lt;/strong&gt; — an AI Global Black Belt at Microsoft, a compulsive builder after hours. This #AI4Good release is a tool rather than an app: an open-source &lt;strong&gt;agent skill&lt;/strong&gt; that changes how hard your coding agent works.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💻 &lt;strong&gt;Source (MIT):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/overdrive" rel="noopener noreferrer"&gt;github.com/naveenneog/overdrive&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;npx github:naveenneog/overdrive&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;Works with:&lt;/strong&gt; GitHub Copilot CLI · Claude Code&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where it came from
&lt;/h2&gt;

&lt;p&gt;I'd collected a handful of those "show me the absolute ceiling of what you can do" prompts that get used to evaluate frontier models against each other. Five of them, in five completely unrelated domains:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Advanced web design&lt;/li&gt;
&lt;li&gt;Real-time 3D&lt;/li&gt;
&lt;li&gt;Motion graphics&lt;/li&gt;
&lt;li&gt;A playable game&lt;/li&gt;
&lt;li&gt;A private-wealth investment deck&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A deck and a shader have nothing in common. But reading the five side by side, the &lt;em&gt;shape&lt;/em&gt; of them was &lt;em&gt;identical&lt;/em&gt;. Every one of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;framed &lt;strong&gt;the ceiling&lt;/strong&gt;, not the requirement — "the best thing you can produce", explicitly compared against other models, explicitly credited to you by name&lt;/li&gt;
&lt;li&gt;told the model &lt;strong&gt;to decide and commit&lt;/strong&gt; rather than ask which direction you'd prefer, and said the ambition of that choice was itself part of the score&lt;/li&gt;
&lt;li&gt;named the &lt;strong&gt;boring, obvious, median answer&lt;/strong&gt; up front so it could be ruled out&lt;/li&gt;
&lt;li&gt;demanded &lt;strong&gt;research&lt;/strong&gt;, not memory&lt;/li&gt;
&lt;li&gt;offered a &lt;strong&gt;non-exhaustive buffet&lt;/strong&gt; of advanced techniques and asked for several, integrated&lt;/li&gt;
&lt;li&gt;held &lt;strong&gt;one dominant success criterion&lt;/strong&gt; above everything else — fun in thirty seconds; timing; 60fps; taste; defensibility&lt;/li&gt;
&lt;li&gt;required &lt;strong&gt;original assets&lt;/strong&gt; rather than borrowed ones&lt;/li&gt;
&lt;li&gt;ended with &lt;em&gt;"QA it by actually using it"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's not five prompts. That's one prompt wearing five costumes. So I extracted the skeleton and made it a skill.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Overdrive is a judgement layer, not a domain skill.&lt;/strong&gt; It doesn't know how to build a game or write a deck. It changes &lt;em&gt;how hard and how well&lt;/em&gt; the agent attacks whatever it was already going to do.&lt;/p&gt;

&lt;p&gt;Ten rules override the agent's normal defaults when it's on. The two that change the most behaviour:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Decide and commit — never ask.&lt;/strong&gt; Where the task leaves a choice open — concept, subject, genre, stack, angle — &lt;em&gt;you choose&lt;/em&gt;. The ambition of that choice is part of the score. Asking "which would you like?" is a wasted turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QA by actually using it.&lt;/strong&gt; Run it, play it, scroll it, watch it, read it end to end, stress it. An unrun deliverable is an unfinished deliverable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rest: frame the ceiling, name the median answer and refuse it, research instead of recalling, stack techniques rather than bolting one on, hold one dominant criterion, generate rather than borrow, survive inspection past the first screen, and red-team your own work then &lt;em&gt;fix what the attack found&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Seven phases
&lt;/h3&gt;

&lt;p&gt;The skill runs the work through: &lt;strong&gt;set the bar&lt;/strong&gt; (write &lt;code&gt;DOMAIN / CEILING / DOMINANT&lt;/code&gt; in three lines) → &lt;strong&gt;commit to a concept&lt;/strong&gt; in one sentence → &lt;strong&gt;research&lt;/strong&gt; → &lt;strong&gt;choose amplifiers&lt;/strong&gt; → &lt;strong&gt;build with the dominant criterion in front&lt;/strong&gt; → &lt;strong&gt;red-team&lt;/strong&gt; → &lt;strong&gt;QA for real&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  A rubric it has to report
&lt;/h3&gt;

&lt;p&gt;Before declaring done, the agent scores itself 1–5 and &lt;strong&gt;shows you the scores&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;5 =&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ambition of the choice&lt;/td&gt;
&lt;td&gt;The concept is impressive before a line is written&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dominant criterion&lt;/td&gt;
&lt;td&gt;The one thing that matters is genuinely nailed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technical depth&lt;/td&gt;
&lt;td&gt;Multiple advanced techniques, composed — not bolted on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Craft / finish&lt;/td&gt;
&lt;td&gt;Nothing default; deliberate typography, timing, naming, structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coherence&lt;/td&gt;
&lt;td&gt;One clear point of view carried through every part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Originality&lt;/td&gt;
&lt;td&gt;Assets, analysis and structure are yours, not borrowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robustness&lt;/td&gt;
&lt;td&gt;Runs clean: no errors, no stutter, no overflow, no dead link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Honesty&lt;/td&gt;
&lt;td&gt;Claims are sourced; estimates are labelled; limits are stated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anything scoring ≤3 gets another pass. Scoring in the open is a forcing function — it's uncomfortable to hand someone a 3 you've written down yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The contract, the seven phases, the rubric, the failure modes, and when &lt;em&gt;not&lt;/em&gt; to use it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/amplifiers.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nine domain packs — web, real-time 3D, motion graphics, games, audio, research &amp;amp; decks, systems, data viz, writing. Each with its dominant criterion, technique buffet, library shortlist and QA moves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/red-team.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Five questions, an inversion pass, a contrast pass, how to delegate the red team to a sub-agent, and the bar for what must get fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/qa-protocol.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-domain "actually use it" QA, a browser harness, and the ship checklist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/research-protocol.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What always needs looking up, sourcing rules, and how to label an estimate as an estimate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/prompt-templates.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Paste-ready Overdrive prompts for when &lt;em&gt;you&lt;/em&gt; are the one prompting a model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scripts/qa_web.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Playwright harness — console/page errors, failed requests, horizontal overflow per viewport, FPS sampling, screenshots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Installing it
&lt;/h2&gt;

&lt;p&gt;One command, zero runtime dependencies — the installer is plain Node and copies seven files:&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="c"&gt;# everywhere it finds an agent&lt;/span&gt;
npx github:naveenneog/overdrive

&lt;span class="c"&gt;# or pick one&lt;/span&gt;
npx github:naveenneog/overdrive &lt;span class="nt"&gt;--claude&lt;/span&gt;
npx github:naveenneog/overdrive &lt;span class="nt"&gt;--copilot&lt;/span&gt;

&lt;span class="c"&gt;# per-repo instead of your home directory&lt;/span&gt;
npx github:naveenneog/overdrive &lt;span class="nt"&gt;--project&lt;/span&gt;

&lt;span class="c"&gt;# what's detected, what's already installed&lt;/span&gt;
npx github:naveenneog/overdrive &lt;span class="nt"&gt;--list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It lands in &lt;code&gt;~/.copilot/skills/overdrive&lt;/code&gt; and &lt;code&gt;~/.claude/skills/overdrive&lt;/code&gt;. There's &lt;code&gt;--force&lt;/code&gt;, &lt;code&gt;--dry-run&lt;/code&gt;, &lt;code&gt;--dir &amp;lt;path&amp;gt;&lt;/code&gt; and &lt;code&gt;--uninstall&lt;/code&gt; too.&lt;/p&gt;

&lt;p&gt;Then start a &lt;strong&gt;new&lt;/strong&gt; agent session and say &lt;strong&gt;"overdrive"&lt;/strong&gt; — or &lt;em&gt;"go all out"&lt;/em&gt;, &lt;em&gt;"show me the ceiling"&lt;/em&gt;, &lt;em&gt;"don't hold back"&lt;/em&gt;, &lt;em&gt;"make it a portfolio piece"&lt;/em&gt;, &lt;em&gt;"this is going public"&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Does it actually do anything?
&lt;/h2&gt;

&lt;p&gt;Fair question, and the honest answer is that a prompt-shaped tool is easy to fool yourself about. So here's the concrete case.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://naveenneog.github.io/Antikythera/" rel="noopener noreferrer"&gt;&lt;strong&gt;ANTIKYTHERA&lt;/strong&gt;&lt;/a&gt; title sequence — &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/26/antikythera-generative-title-sequence/" rel="noopener noreferrer"&gt;written up here&lt;/a&gt; — was built under Overdrive from a single prompt. A 29-second documentary title sequence in one self-contained HTML file: procedural bronze gear geometry at authentic Antikythera tooth counts, a hand-written post-processing chain, a fully synthesised Web Audio score, no assets of any kind.&lt;/p&gt;

&lt;p&gt;But the output isn't the interesting part. &lt;strong&gt;Phase 7 is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;QA by actually using it&lt;/em&gt; is trivial for a human and structurally impossible for an agent — it can't watch a film. So it built itself an eye: installed Playwright, exposed a transport API from the piece, captured 90 frames across the runtime, assembled them into a contact sheet, and looked at the whole edit as one image.&lt;/p&gt;

&lt;p&gt;That caught seven defects that reading the code never would have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a wrapper &lt;code&gt;div&lt;/code&gt;'s &lt;code&gt;z-index&lt;/code&gt; created a stacking context that &lt;strong&gt;isolated &lt;code&gt;mix-blend-mode&lt;/code&gt; from the canvas&lt;/strong&gt; — film grain rendered as raw grey static over everything&lt;/li&gt;
&lt;li&gt;GSAP read a CSS &lt;code&gt;translateY(112%)&lt;/code&gt; back as resolved &lt;em&gt;pixels&lt;/em&gt; into its &lt;code&gt;y&lt;/code&gt; cache, so the later &lt;code&gt;yPercent&lt;/code&gt; tween animated a different axis and &lt;strong&gt;the title never appeared&lt;/strong&gt; — while the tween reported &lt;code&gt;progress: 1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the eclipsing moon's circles sat at SVG origin instead of the orrery centre, so &lt;strong&gt;totality never happened&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;the title wrapped onto two lines mid-animation&lt;/li&gt;
&lt;li&gt;flash frames held 0.35s and read as an error rather than a cut&lt;/li&gt;
&lt;li&gt;the corona quad's square edge was visible against the starfield&lt;/li&gt;
&lt;li&gt;point-light intensities were in physical units on a renderer defaulting to legacy lighting, blowing an entire act to white&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is invisible in source and obvious in a screenshot. Without the "actually use it" clause, that piece ships broken and &lt;em&gt;looks&lt;/em&gt; finished in the diff.&lt;/p&gt;

&lt;p&gt;That's the mechanism. Not better code generation — &lt;strong&gt;better standards, and a refusal to declare done without evidence.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  When not to use it
&lt;/h2&gt;

&lt;p&gt;The skill says this itself, which I think matters more than the feature list. Overdrive costs time and tokens, and it explicitly tells the agent &lt;strong&gt;not&lt;/strong&gt; to apply it to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trivial mechanical edits — renames, version bumps, typos&lt;/li&gt;
&lt;li&gt;anything you've said you want quick, minimal or throwaway&lt;/li&gt;
&lt;li&gt;hotfixes under time pressure, where correctness and reversibility beat ambition&lt;/li&gt;
&lt;li&gt;work with a hard external spec — an API contract, a compliance form — where invention is the enemy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A capability amplifier that fires on everything is just an expensive way to over-engineer a typo fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;Skills are the most underrated part of the current agent tooling, and they're just &lt;strong&gt;markdown in a folder&lt;/strong&gt;. No SDK, no plugin API, no build. Which means the thing that separates a good agent session from a great one is largely &lt;em&gt;writing down what "great" means&lt;/em&gt; — and that's a skill anyone can author, read, disagree with, and fork.&lt;/p&gt;

&lt;p&gt;Overdrive is MIT. If your standards differ from mine, rewrite the rubric — that's the point. The interesting artifact isn't my eight dimensions. It's that eight dimensions written down at all beats infinite capability with no bar.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;⚡ &lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;npx github:naveenneog/overdrive&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source (MIT):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/overdrive" rel="noopener noreferrer"&gt;github.com/naveenneog/overdrive&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎬 &lt;strong&gt;Built with it:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/Antikythera/" rel="noopener noreferrer"&gt;ANTIKYTHERA&lt;/a&gt; · &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/26/antikythera-generative-title-sequence/" rel="noopener noreferrer"&gt;the write-up&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
    <item>
      <title>ANTIKYTHERA: A 29-Second Title Sequence in One HTML File — No Assets, No Build Step</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Sun, 26 Jul 2026 14:56:43 +0000</pubDate>
      <link>https://dev.to/naveenneog/antikythera-a-29-second-title-sequence-in-one-html-file-no-assets-no-build-step-4fj8</link>
      <guid>https://dev.to/naveenneog/antikythera-a-29-second-title-sequence-in-one-html-file-no-assets-no-build-step-4fj8</guid>
      <description>&lt;p&gt;&lt;a href="https://naveenneog.github.io/Antikythera/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fusfhweuhrnjr18pnvlxf.png" alt="ANTIKYTHERA — four frames from the title sequence: the bronze gear train, the X-ray scan, the total eclipse, and the title card"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One prompt. No assets. No build step. No server. One &lt;code&gt;index.html&lt;/code&gt; you can open by double-clicking it — and 29 seconds of motion design comes out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm &lt;strong&gt;&lt;a href="https://naveenneog.github.io/AI4Good/about/" rel="noopener noreferrer"&gt;Naveen Gopalakrishna&lt;/a&gt;&lt;/strong&gt; — an AI Global Black Belt at Microsoft, a compulsive builder after hours. This #AI4Good build is a little different from the app-a-day entries: it isn't an app at all. It's a &lt;strong&gt;film&lt;/strong&gt;. And it's the most interesting capability test I've run at an AI coding agent so far.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Watch it:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/Antikythera/" rel="noopener noreferrer"&gt;naveenneog.github.io/Antikythera&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source (MIT):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/Antikythera" rel="noopener noreferrer"&gt;github.com/naveenneog/Antikythera&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The prompt
&lt;/h2&gt;

&lt;p&gt;This is the whole brief, verbatim. Nothing else was specified — not the subject, not the concept, not the look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build one motion graphics piece that demonstrates the absolute ceiling of your
motion design capability. You choose the subject and the concept. That decision
is part of what's being evaluated.

Context you should know: this is going into a video comparing you against other
frontier models, shown to a large audience, and your work will be credited to you
by name. Every model gets this exact prompt. So go nuts and show people what
you're actually capable of.

Requirements:

1. You pick it. A title sequence, a product spot, a data story, an explainer, a
   brand identity in motion, a piece of pure abstract choreography — anything.
   Decide and commit. The ambition of the choice is part of the score.

2. It plays automatically on load, runs roughly 15-30 seconds, and holds on a
   final frame. Add a replay control. It must read as a finished, edited piece —
   not a looping animation demo.

3. Choreography is the whole test. Pull from whatever best showcases you —
   including but not limited to: kinetic typography and per-character animation,
   variable font animation, staggered reveal systems, easing with real authored
   weight, overshoot and anticipation, masking and reveal wipes, SVG path drawing
   and morphing, shape transitions, mix-blend-mode compositing, particle and
   physics-driven motion, 3D layers and camera moves, depth and parallax,
   shader-based transitions, displacement and distortion, grain and optical
   treatment, match cuts between scenes, sound-design-driven timing.

4. You have free reign to look up how to use any of these, and to pull in open
   source animation libraries freely — GSAP, anime.js, Three.js, Lottie, shader
   libraries, anything on a CDN. Add any other technique you think demonstrates
   more. The list is a starting point, not a boundary.

5. Timing is what separates real motion design from animation. Beats should land
   with intent, moments should breathe, and the piece should build. If everything
   moves at the same speed with the same easing, it has failed.

6. Single self-contained index.html. Any library via CDN. No build step, no server,
   no API keys.

7. No hotlinked video or stock footage. Everything generated in-browser.

8. QA it by watching it. Play it start to finish several times. Check that beats
   don't collide, nothing pops in unstyled, timing reads, and it holds 60fps
   throughout.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note requirement 1: &lt;strong&gt;the choice of subject is part of the score.&lt;/strong&gt; That's the genuinely interesting part of the test. Anyone can animate a logo. Picking &lt;em&gt;what deserves 29 seconds&lt;/em&gt; is a design judgement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Antikythera mechanism
&lt;/h2&gt;

&lt;p&gt;Copilot picked the &lt;strong&gt;Antikythera mechanism&lt;/strong&gt; — the bronze geared analog computer built around 150 BCE, lost in a shipwreck off the island of Antikythera around 60 BCE, and dredged up by sponge divers in 1901. It predicted eclipses, tracked the Metonic and Saros cycles, and modelled the motion of the sun and moon. Nothing of comparable mechanical complexity appears again in the archaeological record for &lt;strong&gt;over a thousand years&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reasoning it gave for the choice is the bit I actually care about, because it's a &lt;em&gt;motion design&lt;/em&gt; argument rather than a "cool topic" argument:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It hands you two opposing motion vocabularies to choreograph against each other.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Register&lt;/th&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Organic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;slow, sine-driven drift, long eases, nothing ever stops hard&lt;/td&gt;
&lt;td&gt;the sea floor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mechanical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;quantised, staccato, hard &lt;code&gt;expo&lt;/code&gt; landings, overshoot and settle&lt;/td&gt;
&lt;td&gt;the machine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The edit then cuts &lt;em&gt;between&lt;/em&gt; those two registers. The subject isn't decoration — it generates the grammar of the piece. That's the difference between choosing a topic and choosing a concept.&lt;/p&gt;




&lt;h2&gt;
  
  
  The edit
&lt;/h2&gt;

&lt;p&gt;Five acts, ~29 seconds, then it holds on the final frame.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;th&gt;Act&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Dark&lt;/td&gt;
&lt;td&gt;The frame closes to 2.39:1. Marine snow drifts through three parallax depth bands, light shafts sway, coordinates type on in the corner.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;A raking light travels across frame and &lt;strong&gt;uncovers&lt;/strong&gt; a corroded bronze fragment — the light and the reveal mask are driven by the same value, so the light is physically the thing revealing the object.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5.55s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Find&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Hard cut.&lt;/strong&gt; Register change to cold cyan. Corner reticles snap in with overshoot, data readouts stagger, a counter races to 82.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;6.5s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;A scan bar crosses the artifact and the gear line-art draws in &lt;strong&gt;exactly where the bar has already passed&lt;/strong&gt;. Greek inscription glyphs flicker on around the rim in random order.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;9.45s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Machine&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Hard cut.&lt;/strong&gt; Eight bronze gears fly forward out of the dark and land hard. A beat of anticipation — a tiny backwards creep — and the train starts to turn.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;11.2s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Four cycle cards cut over the mechanism: &lt;strong&gt;METONIC · SAROS · CALLIPPIC · EXELIGMOS&lt;/strong&gt;, each with a &lt;em&gt;different&lt;/em&gt; entrance, each with its number counting up.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;15.7s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Everything accelerates, then &lt;strong&gt;slams to a dead stop&lt;/strong&gt; with a prismatic radial smear.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;16.5s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(silence)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cut to black. 0.36 seconds of nothing.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;16.85s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Sky&lt;/td&gt;
&lt;td&gt;The great gear's rim becomes the outer orbit. An orrery runs at real relative periods.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;20.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;The moon crosses the sun. Time decelerates. The light goes out, the stars come up — as they really do at totality.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;22.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Totality.&lt;/strong&gt; The piece stops moving for about a second.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;23.85s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Title&lt;/td&gt;
&lt;td&gt;The corona ring is squashed on one axis until it &lt;em&gt;is&lt;/em&gt; the rule under the title. &lt;strong&gt;ANTIKYTHERA&lt;/strong&gt; reveals per character.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspjgfy89rkbcqozfgtyl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspjgfy89rkbcqozfgtyl.png" alt="The raking light uncovering the bronze fragment on the sea floor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbz16qfm4l9f6qi9r9aur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbz16qfm4l9f6qi9r9aur.png" alt="The X-ray scan revealing the gear line-art inside the fragment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Three match cuts carry the structure
&lt;/h3&gt;

&lt;p&gt;This is the part I'd point at if someone asked what "editing" means in a piece with no footage.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The artifact becomes its own X-ray.&lt;/strong&gt; Same SVG path, same position, same size — only the render register changes. The eye reads continuity, the brain reads &lt;em&gt;a new way of seeing the same object&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The great gear's rim becomes the outer orbit.&lt;/strong&gt; The camera lands at exactly &lt;code&gt;z = 43.7&lt;/code&gt; so the 223-tooth wheel's rim measures ~298px on screen; the outer orbit circle is 300px. For most of a second &lt;em&gt;both are on screen at once&lt;/em&gt; while the machine dissolves out from under the circle. The machine doesn't cut to the sky — it &lt;strong&gt;becomes&lt;/strong&gt; the sky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The corona ring becomes the title rule.&lt;/strong&gt; One property: &lt;code&gt;scaleY → 0.0035&lt;/code&gt;. A circle with no height is a line. That line then slides down and is the rule under the title.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One circle, three meanings, across three acts.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the graphics were made
&lt;/h2&gt;

&lt;p&gt;Nothing is loaded. No image files, no video, no models, no HDRIs, no textures. Here's what that actually required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gear geometry from real tooth counts
&lt;/h3&gt;

&lt;p&gt;The gears aren't decorative cog shapes. Each is a &lt;code&gt;THREE.Shape&lt;/code&gt; whose outline is generated tooth by tooth, then extruded with &lt;code&gt;THREE.ExtrudeGeometry&lt;/code&gt; and bevelled. The tooth counts are the &lt;strong&gt;real surviving Antikythera counts&lt;/strong&gt; — 223 (the Saros wheel), 127 (Metonic), 64, 53, 48, 38, 32, 27 — laid out on a &lt;strong&gt;shared module&lt;/strong&gt;, so every pitch radius is &lt;code&gt;m · teeth / 2&lt;/code&gt; and the gears genuinely mesh when placed tangentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GEAR_SPEC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;223&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;th&lt;/span&gt;&lt;span class="p"&gt;:.&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// the great Saros wheel&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;th&lt;/span&gt;&lt;span class="p"&gt;:.&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// Metonic, on a layer behind&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;th&lt;/span&gt;&lt;span class="p"&gt;:.&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;th&lt;/span&gt;&lt;span class="p"&gt;:.&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// meshing distance is just the sum of the two pitch radii&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rP&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;px&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mesh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Angular velocity is then &lt;code&gt;ω · (223 / teeth)&lt;/code&gt; with alternating direction — so the train turns at &lt;strong&gt;true ratios&lt;/strong&gt;. The little 27-tooth gear whirs; the great wheel barely creeps. You can feel the gearing without being told about it. Spoke cutouts are &lt;code&gt;THREE.Path&lt;/code&gt; holes punched into the same shape, which is why you can see the Metonic wheel turning &lt;em&gt;through&lt;/em&gt; the Saros wheel behind it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw6maaidf7gmropkv2uwg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw6maaidf7gmropkv2uwg.png" alt="The full mechanism — the 223-tooth Saros wheel with the 127-tooth Metonic wheel layered behind it"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The bronze surface, computed in JavaScript
&lt;/h3&gt;

&lt;p&gt;There's no texture file, so the corrosion is a &lt;strong&gt;value-noise heightfield&lt;/strong&gt; evaluated in JS at load:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An integer hash → smooth-interpolated value noise → 5-octave fBm heightfield.&lt;/li&gt;
&lt;li&gt;Screen-space normals taken from the heightfield's neighbours.&lt;/li&gt;
&lt;li&gt;A Lambert term against a fixed key direction, plus a tight specular power.&lt;/li&gt;
&lt;li&gt;A second low-frequency noise field distributes &lt;strong&gt;verdigris&lt;/strong&gt; patches (bronze → green patina).&lt;/li&gt;
&lt;li&gt;A third, higher-frequency field carves &lt;strong&gt;pits&lt;/strong&gt;, which also self-shadow — that's what sells the relief rather than making it look like printed noise.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's rasterised once into three canvases and reused three ways: as an &lt;strong&gt;SVG &lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt; fill&lt;/strong&gt; for the artifact, and as the &lt;strong&gt;&lt;code&gt;map&lt;/code&gt; + &lt;code&gt;roughnessMap&lt;/code&gt; + &lt;code&gt;bumpMap&lt;/code&gt;&lt;/strong&gt; on the gear material. One noise pass, one look, two rendering technologies.&lt;/p&gt;

&lt;h3&gt;
  
  
  An environment map with nothing to load
&lt;/h3&gt;

&lt;p&gt;Metal needs reflections, and hotlinking an HDRI was off the table. So the environment is &lt;em&gt;painted&lt;/em&gt;: a canvas gradient with a warm key blob and a cold cyan kicker, marked as an equirectangular texture and run through &lt;code&gt;PMREMGenerator&lt;/code&gt;. That's where the bronze's specular life comes from.&lt;/p&gt;

&lt;h3&gt;
  
  
  A hand-written post-processing chain
&lt;/h3&gt;

&lt;p&gt;No &lt;code&gt;EffectComposer&lt;/code&gt; — the whole chain is written out, which keeps the cost controllable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scene → half-float render target
      → bright pass (threshold 1.15)
      → two-scale separable gaussian bloom (¼ and 1/14 res, ping-pong)
      → one composite pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single composite pass does &lt;strong&gt;barrel distortion, prismatic radial smear, chromatic aberration, colour grade, ACES tonemap, sRGB encode and vignette&lt;/strong&gt;. The expensive smear/aberration path — 8 taps × 3 channels — sits behind a &lt;em&gt;uniform&lt;/em&gt; branch, so it costs nothing except on the two or three beats that actually use it (the slam, the cuts).&lt;/p&gt;

&lt;h3&gt;
  
  
  Optical treatment: grain and gate weave
&lt;/h3&gt;

&lt;p&gt;Two details that do a disproportionate amount of work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Film grain&lt;/strong&gt; is eight pre-baked frames of &lt;em&gt;gaussian&lt;/em&gt; noise (three uniforms averaged — flat &lt;code&gt;Math.random()&lt;/code&gt; reads as video noise, not film) cycled at &lt;strong&gt;24fps, not 60&lt;/strong&gt;, with a randomised offset each swap, composited in &lt;code&gt;mix-blend-mode: overlay&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate weave&lt;/strong&gt; — sub-pixel translation and a fraction of a degree of rotation applied to the whole frame, because projected film never sits perfectly still. It's almost subliminal and the piece feels dead without it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Three layers, one frame
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#gl    &amp;lt;canvas&amp;gt;  Three.js — particles, gears, starfield, corona, post chain
#vec   &amp;lt;svg&amp;gt;     line art — artifact, X-ray gears, orbits, the title rule
#type  &amp;lt;div&amp;gt;     DOM      — all typography
       grain / scanlines / halation / vignette / flash / letterbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typography deliberately stays in the &lt;strong&gt;DOM&lt;/strong&gt;: real font rendering, real variable-font axes, and it stays razor sharp regardless of the WebGL render scale. The title animates &lt;code&gt;font-variation-settings&lt;/code&gt; from &lt;code&gt;'wght' 900&lt;/code&gt; down to &lt;code&gt;420&lt;/code&gt; as the letters land — the letterforms visibly &lt;em&gt;cool&lt;/em&gt; from black to book weight while tracking in. That's a variable font doing something a static font simply cannot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnrvmkp11i2rc9embqi08.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnrvmkp11i2rc9embqi08.png" alt="The final title card — Bodoni Moda tracking in over the rule, with a gear still turning behind"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How the audio was made
&lt;/h2&gt;

&lt;p&gt;Also no files. Every sound is synthesised with the &lt;strong&gt;Web Audio API&lt;/strong&gt; at runtime.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cue&lt;/th&gt;
&lt;th&gt;How it's built&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Sub drone&lt;/strong&gt; (the bed)&lt;/td&gt;
&lt;td&gt;Two detuned sawtooth oscillators at 40 / 40.35 Hz plus two sines, through a lowpass whose cutoff sweeps 110 → 340 → 150 Hz across the whole piece. A 0.09 Hz LFO on the gain gives it a slow tape-like breathing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Impacts&lt;/strong&gt; (the cuts)&lt;/td&gt;
&lt;td&gt;A sine with a pitch envelope from 150 Hz down to 32 Hz in 0.34s, plus a band-passed noise transient for the click.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gear ticks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50ms noise bursts through a narrow band-pass (Q = 9) at 2.4 kHz, pitch-scaled per gear so the eight gears landing sound like eight &lt;em&gt;different&lt;/em&gt; pieces of metal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data blips&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Short square-wave pings through a lowpass, one per readout row.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Riser&lt;/strong&gt; (into the slam)&lt;/td&gt;
&lt;td&gt;White noise through a band-pass sweeping 300 Hz → 7 kHz exponentially, doubled by a sawtooth sweeping 70 → 430 Hz.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Celestial pad&lt;/strong&gt; (the sky)&lt;/td&gt;
&lt;td&gt;Five sine partials on a minor voicing, each doubled and detuned by 0.4%, with a long 1.9s fade in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Reverse swell&lt;/strong&gt; (into the title)&lt;/td&gt;
&lt;td&gt;Band-passed noise with an &lt;em&gt;exponentially rising&lt;/em&gt; gain envelope, so it sounds like a reversed cymbal.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reverb is a &lt;strong&gt;synthesised impulse response&lt;/strong&gt; — 3.4 seconds of stereo noise shaped by an exponential decay curve, fed into a &lt;code&gt;ConvolverNode&lt;/code&gt;. A plate reverb with no plate and no sample.&lt;/p&gt;

&lt;p&gt;The important part isn't the synthesis, though. It's that &lt;strong&gt;every cue is fired from the same GSAP timeline that drives the picture&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;SND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;A3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.050&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// the impact IS the cut&lt;/span&gt;
&lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;SND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;riser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;A3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;5.30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// riser into the slam&lt;/span&gt;
&lt;span class="nx"&gt;GEARS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;SND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.55&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mf"&gt;0.16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// per gear landing&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sound and picture can't drift, because there's only one clock. Sound is &lt;strong&gt;off by default&lt;/strong&gt;, and the piece says so rather than hiding it: a speaker icon sits in the corner from the first frame, reading &lt;em&gt;turn on sound — headphones recommended&lt;/em&gt;. That click is the gesture that unlocks the &lt;code&gt;AudioContext&lt;/code&gt;, so no autoplay policy is ever fought. Turn it on part-way through and the drone &lt;strong&gt;joins at the point in its envelope the picture has already reached&lt;/strong&gt;, rather than restarting its five-second fade underneath Act III. The invitation steps back to a bare toggle after about eleven seconds, so it isn't sitting over the film for all 29 of them.&lt;/p&gt;

&lt;p&gt;It's still designed to read perfectly &lt;strong&gt;silent&lt;/strong&gt; — the sound is a layer on top of timing that already works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Timing: the actual test
&lt;/h2&gt;

&lt;p&gt;Requirement 5 said it plainly: &lt;em&gt;if everything moves at the same speed with the same easing, it has failed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;nothing in the piece uses a default ease.&lt;/strong&gt; There are ten hand-authored &lt;code&gt;CustomEase&lt;/code&gt; bezier curves, each with a job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;CE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cinema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.62,0 0.05,1 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// long filmic settle&lt;/span&gt;
&lt;span class="nx"&gt;CE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.04,0.92 0.09,1 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// hard mechanical stop&lt;/span&gt;
&lt;span class="nx"&gt;CE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.12,0.86 0.18,1.045 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// land + micro overshoot&lt;/span&gt;
&lt;span class="nx"&gt;CE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;punch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.02,1.18 0.28,0.99 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// big overshoot&lt;/span&gt;
&lt;span class="nx"&gt;CE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;antic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.34,-0.30 0.18,1 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// pull back, then go&lt;/span&gt;
&lt;span class="nx"&gt;CE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;choke&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.86,0.02 0.98,0.62 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// slow start, late rush&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;antic&lt;/code&gt; is the one I like best: it dips &lt;em&gt;below zero&lt;/em&gt; before moving, so when the gear train starts turning it creeps backwards a few frames first. Classic animation anticipation, expressed as a bezier control point at &lt;code&gt;y = -0.30&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And the single most effective beat in the whole piece costs nothing at all: after the mechanism slams to a stop, there is a &lt;strong&gt;hard cut to black and 0.36 seconds of absolutely nothing&lt;/strong&gt; before the sky fades up. Silence and stillness, used as a beat. That's an editing decision, not an animation one.&lt;/p&gt;

&lt;p&gt;Everything is placed on one &lt;code&gt;gsap.timeline()&lt;/code&gt; at &lt;strong&gt;absolute&lt;/strong&gt; times, so cut points are authored rather than accumulated. GSAP writes to a plain state object; the renderer only ever &lt;em&gt;reads&lt;/em&gt; it. Picture logic and draw logic never touch each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it was actually built — and what it says about capability
&lt;/h2&gt;

&lt;p&gt;The whole thing was built in a single session with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt; in autopilot mode. The genuinely notable part wasn't the code generation. It was requirement 8: &lt;em&gt;QA it by watching it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An agent can't watch. So it built itself an eye:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Installed Playwright, exposed a small transport API (&lt;code&gt;SEQ.seek()&lt;/code&gt;, &lt;code&gt;SEQ.play()&lt;/code&gt;) from the piece.&lt;/li&gt;
&lt;li&gt;Captured &lt;strong&gt;90 frames&lt;/strong&gt; across the runtime — every 0.5s, plus extra samples clustered on the cut points.&lt;/li&gt;
&lt;li&gt;Assembled them into a &lt;strong&gt;contact sheet&lt;/strong&gt; and looked at the whole edit as one image.&lt;/li&gt;
&lt;li&gt;Measured frame pacing across a full real-time run (p50/p95/p99).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That loop caught seven real defects that reading the code would never have surfaced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug&lt;/th&gt;
&lt;th&gt;Why it mattered&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A wrapper &lt;code&gt;div&lt;/code&gt;'s &lt;code&gt;z-index&lt;/code&gt; created a stacking context that &lt;strong&gt;isolated &lt;code&gt;mix-blend-mode&lt;/code&gt; from the canvas&lt;/strong&gt; — the film grain rendered as raw grey static over everything.&lt;/td&gt;
&lt;td&gt;Structurally invisible in code. Obvious in one screenshot.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSAP read a CSS &lt;code&gt;transform: translateY(112%)&lt;/code&gt; back as &lt;strong&gt;resolved pixels&lt;/strong&gt; into its &lt;code&gt;y&lt;/code&gt; cache, so the later &lt;code&gt;yPercent&lt;/code&gt; tween animated a &lt;em&gt;different axis&lt;/em&gt; — &lt;strong&gt;the title never appeared at all.&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;The tween existed, was correctly positioned, and reported &lt;code&gt;progress: 1&lt;/code&gt;. Only the picture showed it was wrong.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The eclipsing moon's circles sat at SVG origin instead of the orrery centre — it flew off-screen and &lt;strong&gt;totality never happened&lt;/strong&gt;.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The title &lt;strong&gt;wrapped onto two lines&lt;/strong&gt; mid-track-in.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flash frames held ~0.35s — read as an error, not a cut. Now 2–3 frames.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The corona quad's &lt;strong&gt;square edge was visible&lt;/strong&gt; against the starfield.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Point-light intensities were in physical units on a renderer defaulting to legacy lighting — &lt;strong&gt;the entire third act blew out to white.&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those were in the QA harness rather than the piece, which is its own lesson: &lt;code&gt;waitForFunction&lt;/code&gt;'s rAF polling stalls under a software rasterizer, and GSAP's &lt;code&gt;seek()&lt;/code&gt; &lt;strong&gt;suppresses &lt;code&gt;onUpdate&lt;/code&gt; by default&lt;/strong&gt;, so scrub-based screenshots silently missed every &lt;code&gt;onUpdate&lt;/code&gt;-driven write. Both were diagnosed by instrumenting rather than guessing.&lt;/p&gt;

&lt;p&gt;Then, because the environment had no GPU at all (a software rasterizer — 15fps), it added &lt;strong&gt;adaptive resolution&lt;/strong&gt;: if rolling frame time exceeds 21.5ms the render scale steps down, at most three times, never changing composition, timing or colour. That doubled throughput on the bad machine and stays at full resolution on real hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the capability worth pointing at.&lt;/strong&gt; Not "it wrote some Three.js." It made a design decision with a defensible rationale, built its own verification instrument when the requirement demanded one it didn't have, believed the evidence over its own code, and fixed things it could not have known were broken without looking.&lt;/p&gt;




&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;Two things, honestly.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;access&lt;/strong&gt;. A 29-second title sequence of this kind is normally After Effects, Cinema 4D, a plugin stack, licences, and a render farm's worth of patience. This is one text file, three CDN links, and a browser. A student anywhere can open it, read every line, change the tooth counts, and watch what happens. There's no pipeline standing between curiosity and the thing.&lt;/p&gt;

&lt;p&gt;The second is the &lt;strong&gt;subject&lt;/strong&gt;. The Antikythera mechanism is a reminder that the ancient world was not simple, and that knowledge is genuinely fragile — this machine existed, worked, and then the entire idea of it vanished for a millennium. If a few thousand people watch 29 seconds and go and read about the Saros cycle afterwards, that's a good use of a title sequence.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Watch it:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/Antikythera/" rel="noopener noreferrer"&gt;naveenneog.github.io/Antikythera&lt;/a&gt; — plays on load, ~29s; click the speaker to turn the score on (headphones recommended)&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source (MIT, one file):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/Antikythera" rel="noopener noreferrer"&gt;github.com/naveenneog/Antikythera&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🛠 &lt;strong&gt;Built with:&lt;/strong&gt; &lt;a href="https://threejs.org" rel="noopener noreferrer"&gt;Three.js r149&lt;/a&gt; · &lt;a href="https://gsap.com" rel="noopener noreferrer"&gt;GSAP + CustomEase&lt;/a&gt; · Web Audio API · Bodoni Moda / Archivo / JetBrains Mono · &lt;a href="https://github.com/features/copilot/cli" rel="noopener noreferrer"&gt;GitHub Copilot CLI&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One note if you go poking: it pins &lt;strong&gt;Three.js r149&lt;/strong&gt; on purpose. r149 is the last release that ships a real UMD &lt;code&gt;build/three.min.js&lt;/code&gt;. Everything after it is ES-module only, and module CORS blocks &lt;code&gt;file://&lt;/code&gt; — which would have quietly broken the "no build step, no server" constraint the moment anyone double-clicked the file.&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>threejs</category>
      <category>javascript</category>
      <category>creativecoding</category>
    </item>
    <item>
      <title>Cast in Bidri Metal: Four Ancient Karnataka Board Games, Reborn in 3D</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Tue, 21 Jul 2026 17:27:06 +0000</pubDate>
      <link>https://dev.to/naveenneog/cast-in-bidri-metal-four-ancient-karnataka-board-games-reborn-in-3d-4khg</link>
      <guid>https://dev.to/naveenneog/cast-in-bidri-metal-four-ancient-karnataka-board-games-reborn-in-3d-4khg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flj68n7ospuqkbcgt6l2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flj68n7ospuqkbcgt6l2x.png" alt="Four Karnataka board games rendered as Bidri metalwork app icons" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before Ludo, before Snakes &amp;amp; Ladders left the subcontinent and came back Anglicised, our grandmothers drew boards in rice-flour on the courtyard floor and played games older than most kingdoms. This is an attempt to give four of Karnataka's own games a home that honours where they came from — &lt;strong&gt;and&lt;/strong&gt; looks good enough to keep on your phone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm &lt;strong&gt;Naveen Gopalakrishna&lt;/strong&gt; — an AI Global Black Belt at Microsoft, a compulsive builder after hours. This #AI4Good build is four games shipped together, each its own free web app and Android APK:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Game&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Play&lt;/th&gt;
&lt;th&gt;Get the APK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ಸಾಲು ಮನೆ ಆಟ · Saalu Mane Ata&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nine Men's Morris — align three, break one&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/SaaluManeAta/" rel="noopener noreferrer"&gt;play&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/naveenneog/SaaluManeAta/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ಆಡು ಹುಲಿ · Aadu Huli&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Goats &amp;amp; Tigers — the asymmetric hunt&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/AaduHuli/" rel="noopener noreferrer"&gt;play&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/naveenneog/AaduHuli/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ಚೌಕಾ ಬಾರಾ · Chowka Bara&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The cowrie-and-tokens race&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/ChowkaBara/" rel="noopener noreferrer"&gt;play&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/naveenneog/ChowkaBara/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ಅಳಗುಳಿ ಮನೆ · Alaguli Mane&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pallanguzhi — the sowing &amp;amp; counting game&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/AlaguliMane/" rel="noopener noreferrer"&gt;play&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/naveenneog/AlaguliMane/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The games, and where they come from
&lt;/h2&gt;

&lt;p&gt;None of these are inventions. They're games with centuries — sometimes millennia — of continuous play across South India.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Saalu Mane Ata&lt;/strong&gt; (Nine Men's Morris) is a &lt;em&gt;mill&lt;/em&gt; game — three nested squares, nine seeds a side. Form a connected line of three ("a mill") and you break a rival seed. Pure strategy, no dice: the same family of game scratched into the stone floors of temples at &lt;strong&gt;Aihole&lt;/strong&gt;. A game of foresight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aadu Huli&lt;/strong&gt; (Goats &amp;amp; Tigers) is an &lt;em&gt;asymmetric hunt&lt;/em&gt; on a triangular board — a few tigers leap to capture goats, while a herd of goats tries to hem the tigers in until they cannot move. The South Indian cousin of &lt;em&gt;Bagh-Chal&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chowka Bara&lt;/strong&gt; (Ashta Chamma) is a &lt;strong&gt;race&lt;/strong&gt; game played with &lt;strong&gt;cowrie shells&lt;/strong&gt; for dice on a cross-and-circle board, with safe squares. The ancestor Ludo forgot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alaguli Mane&lt;/strong&gt; (Pallanguzhi) is a &lt;strong&gt;mancala&lt;/strong&gt; — two rows of pits, seeds sown counter-clockwise, pits of four captured. No luck, just counting ahead. The carved wooden board is a genuine heirloom in Kannada and Tamil homes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each game ships with &lt;strong&gt;multiple "worlds"&lt;/strong&gt; — a &lt;em&gt;Realistic&lt;/em&gt; heritage board, an &lt;em&gt;Original&lt;/em&gt; glowing-3D reading, a &lt;em&gt;Modern&lt;/em&gt; interpretation, sometimes a &lt;em&gt;Fable&lt;/em&gt; world — and the one this post is really about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fig6jjzid0wdsnj3pi3ho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fig6jjzid0wdsnj3pi3ho.png" alt="The Kannada-first setup screen: pick a world, an opponent, a difficulty" width="800" height="978"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bidri world — the reason this build exists
&lt;/h2&gt;

&lt;p&gt;The spark for the whole project was a single question: &lt;em&gt;what would these games look like if a master craftsman of Bidar made them?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bidriware&lt;/strong&gt; is a 14th-century metal craft from &lt;strong&gt;Bidar, Karnataka&lt;/strong&gt;. A zinc-and-copper alloy is cast, engraved with floral and geometric &lt;em&gt;tarkashi&lt;/em&gt; patterns, inlaid with &lt;strong&gt;pure silver&lt;/strong&gt; wire, then blackened with a special soil from the Bidar fort — a soil that darkens the alloy to deep matte black but leaves the silver brilliant. The result is unmistakable: &lt;strong&gt;jet-black metal, glowing silver inlay.&lt;/strong&gt; A GI-tagged craft, and one of the most striking in the world.&lt;/p&gt;

&lt;p&gt;So every game got a &lt;strong&gt;bespoke Bidri board&lt;/strong&gt; — not a generic skin, but a mat whose inlay is designed around &lt;em&gt;that game's&lt;/em&gt; geometry:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fquwzc43arjoitt1jc7gk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fquwzc43arjoitt1jc7gk.jpg" alt="Bidri Saalu Mane Ata board" width="800" height="800"&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F17gh4p2b2s05ludgg5rx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F17gh4p2b2s05ludgg5rx.jpg" alt="Bidri Aadu Huli board" width="800" height="800"&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdjw4xz8736bnob0o12w.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdjw4xz8736bnob0o12w.jpg" alt="Bidri Chowka Bara board" width="800" height="800"&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm5hrq0mqw72enlm6it1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm5hrq0mqw72enlm6it1.jpg" alt="Bidri Alaguli Mane board" width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How we matched real artisan work
&lt;/h3&gt;

&lt;p&gt;Getting AI-generated art to actually &lt;em&gt;read as Bidri&lt;/em&gt; — and not as "generic ornate black-and-gold" — took a tight loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Study first.&lt;/strong&gt; Real Bidri is &lt;strong&gt;matte black, not glossy&lt;/strong&gt;, the inlay is &lt;strong&gt;silver, not gold&lt;/strong&gt;, the motifs are &lt;strong&gt;floral tarkashi and geometric bands&lt;/strong&gt;, and the craft lives in the &lt;em&gt;contrast&lt;/em&gt;. Those became hard constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt to the constraints.&lt;/strong&gt; Every render was pushed toward &lt;em&gt;"near-black blackened-brass ground, brilliant pure-silver inlay, delicate floral tarkashi engraving, museum-quality metalwork, soft studio light with rich reflections"&lt;/em&gt; using &lt;strong&gt;Azure &lt;code&gt;gpt-image-2&lt;/code&gt;&lt;/strong&gt;. Gold only as a thin rim highlight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Align the board to the mat.&lt;/strong&gt; The generated ornament had to sit &lt;em&gt;around&lt;/em&gt; the playable geometry — central design centred on the board, surrounding &lt;em&gt;tarkashi&lt;/em&gt; filling the field, the way a craftsman composes a real tray. The mat and a &lt;strong&gt;seamless table tile&lt;/strong&gt; were rendered as a set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate on light &amp;amp; reflection.&lt;/strong&gt; Several passes went purely into how the silver catches light and how the black holds shadow, until each board looked photographed, not textured.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same pipeline produced each game's &lt;strong&gt;app icon&lt;/strong&gt; — this game's own board, cast in Bidri. The Bidri work was distilled into a reusable &lt;strong&gt;"Bidri style" skill&lt;/strong&gt; so the look stays consistent across future builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The whole suite is deliberately &lt;strong&gt;boring where it counts&lt;/strong&gt; so it can live forever for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pure static, mobile-first, Kannada-first.&lt;/strong&gt; No backend, no accounts, no tracking. Each game is a folder of HTML/JS/assets on &lt;strong&gt;GitHub Pages&lt;/strong&gt;. Kannada is the first language, English second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3D in the browser.&lt;/strong&gt; A shared &lt;strong&gt;Three.js&lt;/strong&gt; core (&lt;code&gt;game3d&lt;/code&gt;) renders boards, pieces and worlds, tuned for a mid-range phone. Each game is a thin ruleset on top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four games, one drift-locked core.&lt;/strong&gt; ~36 shared modules stay &lt;strong&gt;byte-identical&lt;/strong&gt; across all four, enforced by a custom &lt;strong&gt;&lt;code&gt;rc-gate&lt;/code&gt;&lt;/strong&gt; that fails the build on any drift and runs the full &lt;strong&gt;unit-test matrix (668 tests: AH 206 · AM 161 · CB 152 · SMA 149)&lt;/strong&gt;, i18n coverage, JSON-schema contracts, language/voice-pack freshness, and an &lt;strong&gt;asset budget&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Six languages, packaged.&lt;/strong&gt; Kannada + English core; four more ship as &lt;strong&gt;downloadable text &amp;amp; voice packs&lt;/strong&gt; (an "asset diet" with a default-core build that fetches optional packs on demand).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narration&lt;/strong&gt; via &lt;strong&gt;Azure Speech&lt;/strong&gt; neural voices, later repacked to cut size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APKs from the same web build.&lt;/strong&gt; &lt;strong&gt;Capacitor&lt;/strong&gt; wraps each &lt;code&gt;web/&lt;/code&gt; folder; &lt;strong&gt;GitHub Actions&lt;/strong&gt; builds a debug APK on every release (JDK 21, &lt;code&gt;cap add android&lt;/code&gt;, &lt;code&gt;gradlew assembleDebug&lt;/code&gt;) and attaches it automatically. Push to &lt;code&gt;main&lt;/code&gt; → Pages redeploys; publish a release → the APK builds itself.&lt;/li&gt;
&lt;li&gt;Art &amp;amp; icons: Azure &lt;code&gt;gpt-image-2&lt;/code&gt;. &lt;strong&gt;Everything is open source.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Constraints make AI art authentic.&lt;/strong&gt; "Ornate Indian" gets you tourist-shop clip-art. &lt;em&gt;"Matte-black blackened alloy, pure-silver tarkashi, no gold except a rim highlight"&lt;/em&gt; gets you something a Bidar artisan would recognise. Research the craft, then encode it as non-negotiables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A drift gate is worth more than a test suite alone&lt;/strong&gt; when shipping four near-identical apps. The most valuable script in the repo is the one that refuses to let the four codebases diverge silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship the culture first, the translation second.&lt;/strong&gt; Kannada-first (not "English with a toggle") changed how the whole UI reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static + Capacitor is a superpower for heritage software.&lt;/strong&gt; One &lt;code&gt;web/&lt;/code&gt; folder becomes a free website &lt;em&gt;and&lt;/em&gt; an installable app, zero servers — exactly right for something meant to outlast any hosting bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tiny asset budget forces good decisions.&lt;/strong&gt; Packing optional languages/voices and fetching on demand kept the base app small without cutting features.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Free, offline, no account, open source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Saalu Mane Ata&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/SaaluManeAta/" rel="noopener noreferrer"&gt;play&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/SaaluManeAta/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/SaaluManeAta" rel="noopener noreferrer"&gt;source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aadu Huli&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/AaduHuli/" rel="noopener noreferrer"&gt;play&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/AaduHuli/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/AaduHuli" rel="noopener noreferrer"&gt;source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chowka Bara&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/ChowkaBara/" rel="noopener noreferrer"&gt;play&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/ChowkaBara/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/ChowkaBara" rel="noopener noreferrer"&gt;source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alaguli Mane&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/AlaguliMane/" rel="noopener noreferrer"&gt;play&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/AlaguliMane/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/AlaguliMane" rel="noopener noreferrer"&gt;source&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have grandparents who played these on a courtyard floor, show them the Bidri board. That reaction is the whole point.&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>gamedev</category>
      <category>threejs</category>
      <category>opensource</category>
    </item>
    <item>
      <title>cordless v0.8: Tab Groups, Custom Launchers &amp; Bring-Your-Own Copilot</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Mon, 13 Jul 2026 14:51:24 +0000</pubDate>
      <link>https://dev.to/naveenneog/cordless-v08-tab-groups-custom-launchers-bring-your-own-copilot-2i0b</link>
      <guid>https://dev.to/naveenneog/cordless-v08-tab-groups-custom-launchers-bring-your-own-copilot-2i0b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;cordless&lt;/a&gt; is a CLI-first tool that manages your remote terminal / coding-agent sessions and puts them on your phone. &lt;strong&gt;v0.8&lt;/strong&gt; is about &lt;em&gt;organising&lt;/em&gt; the swarm: &lt;strong&gt;Chrome-mobile-style tab groups&lt;/strong&gt;, &lt;strong&gt;custom launchers&lt;/strong&gt; (bring any command as a profile), a built-in &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt; profile, &lt;strong&gt;renameable tabs&lt;/strong&gt;, and &lt;strong&gt;persisted scrollback history&lt;/strong&gt; so a reopened session after a reboot shows what it was doing. There's also a proper &lt;strong&gt;&lt;code&gt;cordless setup&lt;/code&gt;&lt;/strong&gt; installer. Six features, each on its own branch — designed with &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt;, built with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftcwzc7t70e6lleusv1uv.png" alt="cordless v0.8 — tab groups, custom launchers, Copilot" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;v0.7 taught cordless to tell me &lt;em&gt;which&lt;/em&gt; of my eight agent sessions needs me. v0.8 is the next problem: once you're running that many, they stop fitting on the screen. You need to &lt;strong&gt;organise&lt;/strong&gt; them — like browser tabs, because that was the whole metaphor from day one. So this release is six features that make a big session swarm manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tab groups
&lt;/h2&gt;

&lt;p&gt;The headline. Sessions can now live in &lt;strong&gt;named, colored groups&lt;/strong&gt; — think "API migration", "Website", "Flaky tests" — exactly like Chrome mobile's tab groups.&lt;/p&gt;

&lt;p&gt;In the terminal dashboard, groups render as &lt;strong&gt;collapsible headers&lt;/strong&gt; with a live per-group waiting/session count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;── Sessions (6) ──
▼ API migration          2 waiting · 3 sessions
  ! claude   build pipeline            waiting
  ● codex    api tests                 working
  ○ shell    api ~/src/api             idle
▶ Website                              2 sessions
▼ Ungrouped                            1 session
    ○ copilot scratch                  idle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;g&lt;/code&gt; for the group menu (new / assign / ungroup / collapse / rename / delete), and &lt;code&gt;f&lt;/code&gt; to cycle a &lt;strong&gt;smart-view filter&lt;/strong&gt; — &lt;code&gt;All · Attention · Claude · Codex · Copilot · Shell&lt;/code&gt;. Those filters are &lt;em&gt;views&lt;/em&gt;, not groups; collapse state is per-device. On the &lt;strong&gt;phone&lt;/strong&gt; you get the same idea as a chip strip above the tabs (All · Unread · one chip per group, with counts and color dots) plus rename/move in the details sheet.&lt;/p&gt;

&lt;p&gt;The design decision I kept going back and forth on — and where Sol was firm — was &lt;strong&gt;manual groups over continuous auto-grouping&lt;/strong&gt;. Auto-sorting your tabs by directory sounds clever until a session changes cwd and silently jumps groups. So groups are something you &lt;em&gt;choose&lt;/em&gt;; deleting one never kills its sessions, it just ungroups them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom launchers (and a built-in Copilot)
&lt;/h2&gt;

&lt;p&gt;Until now cordless had three profiles: &lt;code&gt;shell&lt;/code&gt;, &lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;codex&lt;/code&gt;. v0.8 makes profiles &lt;strong&gt;yours&lt;/strong&gt;. Drop this in &lt;code&gt;~/.cordless/config.json&lt;/code&gt;:&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="nl"&gt;"profiles"&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;"api"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pwsh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"-NoLogo"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&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:/src/api"&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;"notes"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nvim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"~/notes.md"&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="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;…and &lt;code&gt;cordless new api&lt;/code&gt; spawns it directly (resolved against the daemon's &lt;code&gt;PATH&lt;/code&gt;, not a shell string — no &lt;code&gt;sh -c&lt;/code&gt; injection surface). A missing executable doesn't crash anything; it shows as &lt;strong&gt;unavailable&lt;/strong&gt; with a reason, and &lt;code&gt;cordless profiles&lt;/code&gt; lists every launcher with its source and availability.&lt;/p&gt;

&lt;p&gt;And because the agent world moved, v0.8 ships a built-in &lt;strong&gt;&lt;code&gt;copilot&lt;/code&gt;&lt;/strong&gt; profile — the standalone &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt; — right next to &lt;code&gt;claude&lt;/code&gt; and &lt;code&gt;codex&lt;/code&gt;. The nice part: attention detection is now &lt;strong&gt;preset-driven&lt;/strong&gt;, so &lt;code&gt;copilot&lt;/code&gt; (and any custom profile you tag &lt;code&gt;attentionPreset: "agent"&lt;/code&gt;) gets the same &lt;em&gt;waiting / finished&lt;/em&gt; heuristics the other agents get, for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rename tabs
&lt;/h2&gt;

&lt;p&gt;Small feature, big daily quality-of-life. &lt;code&gt;cordless rename &amp;lt;id&amp;gt; "API migration"&lt;/code&gt;, or the dashboard &lt;code&gt;e&lt;/code&gt; key, or a long-press on the phone. Titles are Unicode-normalized, control-stripped and length-capped, an empty title restores the generated default, and the change &lt;strong&gt;broadcasts live&lt;/strong&gt; to every connected client (with a monotonic revision so a stale update can't clobber a newer one).&lt;/p&gt;

&lt;h2&gt;
  
  
  Persisted history — survive the reboot
&lt;/h2&gt;

&lt;p&gt;This is the one that turned into a proper engineering story. A cordless session already survives a &lt;em&gt;daemon restart&lt;/em&gt; (it reopens the same shell in the same directory). But it came back &lt;strong&gt;blank&lt;/strong&gt; — you lost the scrollback. v0.8 fixes that: the daemon persists a &lt;strong&gt;capped, plain-text&lt;/strong&gt; copy of each session's scrollback (2000 lines / 512 KB, gzipped, user-only) and, on restore, shows it as &lt;strong&gt;frozen context above&lt;/strong&gt; the reopened session with a &lt;code&gt;── session reopened after system restart ──&lt;/code&gt; banner.&lt;/p&gt;

&lt;p&gt;A couple of decisions worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalized text, not raw bytes.&lt;/strong&gt; Sol talked me out of persisting a truncated ANSI stream (it can start mid-escape-sequence and depends on terminal state you've discarded). We store logical lines from the headless terminal buffer instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't write it into the fresh terminal.&lt;/strong&gt; My first version injected the old scrollback &lt;em&gt;into&lt;/em&gt; the reopened shell — which promptly wiped it, because a shell like PowerShell clears its own screen on startup. So restored history is kept &lt;em&gt;beside&lt;/em&gt; the live session and rendered above it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save periodically, not just on shutdown.&lt;/strong&gt; A reboot doesn't send you a polite shutdown signal, so history is flushed every few seconds while the session runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bug that only failed on Linux
&lt;/h2&gt;

&lt;p&gt;Here's the part I'm glad CI caught. My persisted-history tests were &lt;strong&gt;green on Windows&lt;/strong&gt; and I shipped v0.8.0. The &lt;strong&gt;Linux and macOS&lt;/strong&gt; CLI builds went red on the exact same test.&lt;/p&gt;

&lt;p&gt;The cause is a lovely cross-platform trap. On a graceful stop, the daemon catches &lt;code&gt;SIGTERM&lt;/code&gt; and runs &lt;code&gt;shutdown()&lt;/code&gt;: it saves each session's history, then kills the PTYs. Killing a PTY fires its exit handler — and &lt;em&gt;that&lt;/em&gt; handler deleted the just-saved history &lt;strong&gt;and&lt;/strong&gt; rewrote the "reopen these on restart" manifest without the now-exited session. So on Linux the session came back with no history; on macOS it didn't come back at all.&lt;/p&gt;

&lt;p&gt;Why did Windows pass? Because on Windows &lt;code&gt;SIGTERM&lt;/code&gt; from another process is an &lt;strong&gt;uncatchable hard-kill&lt;/strong&gt; — &lt;code&gt;shutdown()&lt;/code&gt; never runs, the exit handler never fires, and the periodically-flushed history file just… survives. The platform that "worked" only worked by accident.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;code&gt;_shuttingDown&lt;/code&gt; flag so the exit handler preserves history and the manifest during a graceful stop. What I like about it: I reproduced the Linux-only failure &lt;strong&gt;on Windows&lt;/strong&gt; by driving &lt;code&gt;shutdown()&lt;/code&gt; directly in a unit test (the harness's &lt;code&gt;SIGTERM&lt;/code&gt; can't), so the regression is now locked shut on every platform. Re-tagged, rebuilt, and v0.8.0 went out clean with the full asset set.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Same loop, tightened further: &lt;strong&gt;me on GitHub Copilot CLI, GPT-5.6 Sol as the design partner&lt;/strong&gt; — and this time with a rule I'm keeping for good: &lt;strong&gt;every feature on its own branch, merged &lt;code&gt;--no-ff&lt;/code&gt;, with clean well-documented code&lt;/strong&gt;. The v0.8 program was six branches (&lt;code&gt;feature/persisted-history&lt;/code&gt;, &lt;code&gt;feature/custom-profiles&lt;/code&gt;, &lt;code&gt;feature/copilot-profile&lt;/code&gt;, &lt;code&gt;feature/session-rename&lt;/code&gt;, &lt;code&gt;feature/session-groups&lt;/code&gt;, &lt;code&gt;feature/group-ui-phone&lt;/code&gt;) plus a &lt;code&gt;fix/history-shutdown&lt;/code&gt;. The daemon's test harness grew to &lt;strong&gt;24 suites&lt;/strong&gt; — the pure profile/attention/render checks, live rename + groups over the WebSocket, the dashboard-driven resume regression, and persisted-history across a real restart — and CI runs all of it on Windows, Linux and macOS before building and smoke-testing the self-contained binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;The feeling I was chasing: open cordless with a dozen agents running and not feel underwater. The waiting ones surface (v0.7); the rest are &lt;strong&gt;filed into groups&lt;/strong&gt; you named, launched from &lt;strong&gt;profiles&lt;/strong&gt; you defined, titled how &lt;em&gt;you&lt;/em&gt; think about them — and if the box reboots overnight, each one comes back &lt;strong&gt;with its history intact&lt;/strong&gt;. It's the browser-tabs promise, finally delivered for a swarm of terminals.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live / docs:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;cordless CLI (Windows / Linux, no Node needed):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt; — then &lt;code&gt;cordless setup&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧑‍💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part of the &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/10/ai4good-an-app-a-day/" rel="noopener noreferrer"&gt;#AI4Good&lt;/a&gt; series. Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>cli</category>
      <category>terminal</category>
      <category>productivity</category>
    </item>
    <item>
      <title>cordless v0.7: Attention State — Know Which of Your 8 Agent Sessions Needs You</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Sat, 11 Jul 2026 18:26:03 +0000</pubDate>
      <link>https://dev.to/naveenneog/cordless-v07-attention-state-know-which-of-your-8-agent-sessions-needs-you-3bi</link>
      <guid>https://dev.to/naveenneog/cordless-v07-attention-state-know-which-of-your-8-agent-sessions-needs-you-3bi</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;cordless&lt;/a&gt; is a CLI-first tool that manages your remote terminal / coding-agent sessions and puts them on your phone. &lt;strong&gt;v0.7&lt;/strong&gt; adds the thing that actually matters when you're juggling many agents: &lt;strong&gt;per-session attention state&lt;/strong&gt;. The daemon watches each session's output and infers whether it's &lt;em&gt;working&lt;/em&gt;, &lt;em&gt;idle&lt;/em&gt;, or &lt;em&gt;waiting for you&lt;/em&gt; — then badges it, sorts attention-first, and can push a notification (ntfy / webhook) so you get pinged instead of babysitting. Also new: &lt;strong&gt;scrollback search&lt;/strong&gt;, &lt;strong&gt;copy last output&lt;/strong&gt;, and &lt;strong&gt;named workspaces&lt;/strong&gt;. Designed with &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt;, built with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fccwkhdayuslouw1it0so.png" alt="cordless v0.7 — attention state" width="799" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the real problem with running eight coding agents at once: they keep &lt;strong&gt;stopping to ask you things&lt;/strong&gt;. "Apply these edits? (y/n)". "Run the tests?". One finishes; another hits an error; a third is still churning. You can't watch eight terminals. So you tab through them, and half the time you're staring at a session that's happily working while another has been blocked on a yes/no for four minutes.&lt;/p&gt;

&lt;p&gt;v0.7 is my answer to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attention state
&lt;/h2&gt;

&lt;p&gt;cordless now tracks, for every session, two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;activity&lt;/strong&gt; — &lt;code&gt;working&lt;/code&gt;, &lt;code&gt;idle&lt;/code&gt;, or &lt;code&gt;exited&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;attention&lt;/strong&gt; — &lt;code&gt;waiting&lt;/code&gt; (a prompt), &lt;code&gt;bell&lt;/code&gt;, &lt;code&gt;finished&lt;/code&gt;, or nothing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The daemon infers this &lt;strong&gt;purely from the session's PTY output&lt;/strong&gt; — no shell integration, no hooks required. In the dashboard, sessions are &lt;strong&gt;badged and sorted attention-first&lt;/strong&gt;, so the one that needs you floats to the top with a &lt;code&gt;!&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;── Sessions (3) ──  1 need attention
▸ ! claude  review PR #1284            waiting
      Apply these edits to src/api? (y/n):
  ○ codex   codex ~/src/api            idle
  ○ shell   tests ~/src/app            idle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;c&lt;/code&gt; to mark it handled; sending input clears it automatically (answering &lt;em&gt;is&lt;/em&gt; handling it). On the phone you get the same badges. &lt;code&gt;cordless sessions --attention&lt;/code&gt; lists just the ones asking for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part: what counts as "waiting"?
&lt;/h2&gt;

&lt;p&gt;This is where I leaned on Sol the most, because naive detection is a &lt;strong&gt;false-positive machine&lt;/strong&gt;. A silent build looks "stuck". A shell sitting at its prompt looks "ready" but isn't asking anything. &lt;code&gt;vim&lt;/code&gt; and &lt;code&gt;htop&lt;/code&gt; repaint constantly. The rules we landed on are deliberately conservative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A trailing &lt;strong&gt;shell prompt&lt;/strong&gt; (&lt;code&gt;$&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;❯&lt;/code&gt;) is &lt;em&gt;readiness&lt;/em&gt;, not attention. Never notify on it.&lt;/li&gt;
&lt;li&gt;A silent session is &lt;strong&gt;idle&lt;/strong&gt;, not waiting — a paused agent, a long build, and a sleeping process all look the same, so we don't guess.&lt;/li&gt;
&lt;li&gt;We only raise &lt;strong&gt;waiting&lt;/strong&gt; on &lt;strong&gt;high-confidence confirmation/agent prompts&lt;/strong&gt; — &lt;code&gt;(y/n)&lt;/code&gt;, "Continue?", "Do you want me to…?", "Enter your password:" — matched against the &lt;em&gt;last non-empty line&lt;/em&gt; after a short quiet window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alternate-screen&lt;/strong&gt; apps (vim/htop/less) suppress prompt heuristics entirely — better to miss a full-screen prompt than to cry wolf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BEL&lt;/strong&gt; (&lt;code&gt;^G&lt;/code&gt;) is an explicit signal, so it's trusted — but ignored during a session's first few seconds and right after a keypress (startup and invalid-key beeps).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;finished&lt;/strong&gt; is heuristic and scoped to &lt;code&gt;claude&lt;/code&gt;/&lt;code&gt;codex&lt;/code&gt;: back at a shell prompt after real activity. It's labelled low-confidence and can be turned off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole classifier is a pure, unit-tested module — 41 fixture checks across shells, pagers, and Claude/Codex-style prompts. And QA earned its keep: I caught a bug where a prompt appearing &lt;strong&gt;within two seconds of a keypress&lt;/strong&gt; was silently dropped (the input-grace guard also skipped the &lt;em&gt;whole&lt;/em&gt; quiet cycle). A cross-platform live test now locks that path shut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get pinged — without any cloud
&lt;/h2&gt;

&lt;p&gt;Badges are great when you're looking. When you're not, v0.7 can &lt;strong&gt;notify&lt;/strong&gt; you. Point it at an &lt;strong&gt;ntfy&lt;/strong&gt; topic (the ntfy phone app pushes it) or a &lt;strong&gt;generic webhook&lt;/strong&gt; (Slack/Discord/automation) in &lt;code&gt;config.json&lt;/code&gt;, and cordless POSTs when a session transitions to waiting / bell / finished. &lt;code&gt;cordless notify test&lt;/code&gt; validates it.&lt;/p&gt;

&lt;p&gt;cordless owns &lt;strong&gt;no cloud&lt;/strong&gt; for this, and the anti-spam is strict (Sol was insistent): one notification per state revision, a 60-second per-session cooldown, a 5-per-minute global cap, optional quiet hours, and &lt;strong&gt;no terminal output in the payload&lt;/strong&gt; by default (it can contain code or secrets). Topic and webhook URLs are treated as secrets and never logged. Delivery is async with a short timeout — a broken webhook can never stall your terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three more coder niceties
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copy last output:&lt;/strong&gt; &lt;code&gt;cordless output &amp;lt;id&amp;gt; --lines 50 --copy&lt;/code&gt; grabs a session's recent output to your clipboard (cross-platform).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrollback search:&lt;/strong&gt; &lt;code&gt;cordless search &amp;lt;id&amp;gt; "TypeError"&lt;/code&gt; scans a session's retained buffer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workspaces:&lt;/strong&gt; &lt;code&gt;cordless workspace save api-feature&lt;/code&gt; snapshots your running sessions (profile + cwd + title); &lt;code&gt;cordless workspace open api-feature&lt;/code&gt; relaunches the whole layout — "Claude on api, Codex on web, a tests shell" — in one command.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Same loop, sharpened: &lt;strong&gt;me on GitHub Copilot CLI, Sol as the design partner&lt;/strong&gt; — and this sprint I kept it honest about &lt;em&gt;version control&lt;/em&gt;. Each feature was built on its &lt;strong&gt;own branch&lt;/strong&gt; (&lt;code&gt;feature/attention-state&lt;/code&gt;, &lt;code&gt;feature/notifications&lt;/code&gt;, &lt;code&gt;feature/output-search&lt;/code&gt;, &lt;code&gt;feature/workspaces&lt;/code&gt;), tested, and merged as a unit. The daemon's test harness grew to &lt;strong&gt;16 suites&lt;/strong&gt; — pure attention heuristics, live prompt/idle detection, notifier anti-spam + delivery (injected fetch, no network), scrollback, workspaces, plus the existing protocol/security/restore coverage — and CI runs all of it on Windows and Linux, then builds and smoke-tests the self-contained binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;The feeling I was after: glance at cordless (in the terminal or on my phone) and instantly know &lt;strong&gt;which agent needs me&lt;/strong&gt; — not by tabbing through eight sessions, but because the one waiting is at the top with a &lt;code&gt;!&lt;/code&gt; and, if I've wandered off, my phone buzzed. Everything else — search, copy, workspaces — is in service of the same thing: running a lot of agents without drowning in them.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live / docs:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;cordless CLI (Windows / Linux, no Node needed):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧑‍💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part of the #AI4Good series. Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>cli</category>
      <category>terminal</category>
      <category>productivity</category>
    </item>
    <item>
      <title>cordless v0.6: Going CLI-First — Run It, Scan the QR, You're Paired</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Sat, 11 Jul 2026 17:17:06 +0000</pubDate>
      <link>https://dev.to/naveenneog/cordless-v06-going-cli-first-run-it-scan-the-qr-youre-paired-4lmo</link>
      <guid>https://dev.to/naveenneog/cordless-v06-going-cli-first-run-it-scan-the-qr-youre-paired-4lmo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;cordless&lt;/a&gt; manages your remote terminal / coding-agent (Claude Code, Codex) sessions and puts them on your phone. &lt;strong&gt;v0.6&lt;/strong&gt; makes it &lt;strong&gt;CLI-first&lt;/strong&gt;: run &lt;code&gt;cordless&lt;/code&gt; and it opens a full-screen terminal &lt;strong&gt;dashboard&lt;/strong&gt; whose starting screen shows a &lt;strong&gt;live pairing QR&lt;/strong&gt; — scan it, done. No separate &lt;code&gt;cordless pair&lt;/code&gt;, no GUI to babysit. And it now ships as &lt;strong&gt;one self-contained binary&lt;/strong&gt; (its own Node runtime &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;node-pty&lt;/code&gt; baked in), so there's nothing to install first. Designed in a running debate with &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt;, built with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe19vugzqrrghoxoxkwz0.png" alt="cordless v0.6 — the CLI-first dashboard" width="799" height="735"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every version so far treated cordless as "a daemon plus a phone app." Then the owner said something that reframed the whole thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"cordless should be a **CLI first&lt;/em&gt;&lt;em&gt;. Look at the design of a terminal app and get the features from that. I want an installer that starts cordless as a proper terminal with a QR to pair on its starting screen — right now I run &lt;code&gt;cordless pair&lt;/code&gt; separately and pair the desktop and mobile apps independently, which defeats the purpose."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a redesign, not a tweak. So I opened a long debate with Sol and rebuilt the front door.&lt;/p&gt;

&lt;h2&gt;
  
  
  cordless &lt;em&gt;is&lt;/em&gt; the terminal now
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;cordless&lt;/code&gt; with no arguments and you get the screen on that card: a brand banner, daemon + Tailscale status, your session list, and — front and center — a &lt;strong&gt;single-use pairing QR with a countdown&lt;/strong&gt;. Scan it with the phone app and you're paired. Press &lt;code&gt;p&lt;/code&gt; for a fresh code. No second command, no separate terminal.&lt;/p&gt;

&lt;p&gt;Borrowing from modern terminal apps (Warp, Windows Terminal, iTerm2) — but deliberately &lt;em&gt;not&lt;/em&gt; rebuilding one — the dashboard is a focused TUI: &lt;code&gt;↑/↓&lt;/code&gt; selects a session, &lt;code&gt;Enter&lt;/code&gt; attaches, &lt;code&gt;n&lt;/code&gt; starts a shell / Claude / Codex, &lt;code&gt;x&lt;/code&gt; kills, &lt;code&gt;d&lt;/code&gt; manages paired devices, &lt;code&gt;q&lt;/code&gt; leaves. It's a &lt;strong&gt;thin client of a persistent daemon&lt;/strong&gt;: quitting the dashboard (or closing the terminal) never stops your sessions or the phone connection — the daemon keeps owning the PTYs. Sol was firm on that boundary, and it's the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attach right there in your terminal
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Enter&lt;/code&gt; (or &lt;code&gt;cordless attach &amp;lt;id&amp;gt;&lt;/code&gt;) streams a session &lt;strong&gt;straight into your host terminal&lt;/strong&gt; — no xterm.js, no second renderer. Raw keystrokes go to the PTY, resize events forward, and the detach chord is &lt;code&gt;Ctrl-] d&lt;/code&gt;. It's a tiny tmux-like attach that reuses the exact replay/snapshot protocol the phone uses. The host terminal &lt;em&gt;is&lt;/em&gt; the renderer; cordless just pipes bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pairing is daemon-owned now
&lt;/h2&gt;

&lt;p&gt;The old &lt;code&gt;cordless pair&lt;/code&gt; minted a secret by writing a file directly. Sol flagged that: two processes minting into the same store invites races and inconsistent limits. So in v0.6 there's &lt;strong&gt;one&lt;/strong&gt; authenticated &lt;code&gt;pairing.create&lt;/code&gt; over the WebSocket, and it can only be called by a &lt;strong&gt;loopback-scoped credential from a real loopback socket&lt;/strong&gt; — the local machine's owner, never a remote phone. It's single-use, 256-bit, five-minute TTL, rate-limited, and capped. Both the dashboard and &lt;code&gt;cordless pair&lt;/code&gt; call the same path. A stolen phone token can't enroll new devices; only &lt;em&gt;you&lt;/em&gt;, at the machine, can.&lt;/p&gt;

&lt;h2&gt;
  
  
  One binary, no Node required
&lt;/h2&gt;

&lt;p&gt;The biggest ask hiding in "an installer that just works" is: &lt;strong&gt;don't make me install Node first.&lt;/strong&gt; So v0.6 ships a self-contained executable built with &lt;strong&gt;Node's Single Executable Application&lt;/strong&gt; support:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;esbuild&lt;/code&gt; bundles the whole CLI into one file (with &lt;code&gt;node-pty&lt;/code&gt; marked external).&lt;/li&gt;
&lt;li&gt;Node generates a SEA blob from that bundle.&lt;/li&gt;
&lt;li&gt;The blob is injected into a copy of the Node runtime → &lt;code&gt;cordless.exe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node-pty&lt;/code&gt; and the built web client ship &lt;strong&gt;beside&lt;/strong&gt; the exe under &lt;code&gt;resources/&lt;/code&gt;, and a small loader resolves the native module from there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trick that made it painless: &lt;code&gt;node-pty&lt;/code&gt; ships &lt;strong&gt;node-api prebuilds&lt;/strong&gt;, which are ABI-stable across Node versions — so there's no fragile per-version native rebuild. The result is a ~45 MB download that runs the dashboard, spawns real PTYs, and serves the web client for your phone, all with &lt;strong&gt;zero prerequisites&lt;/strong&gt;. CI builds and &lt;strong&gt;smoke-tests&lt;/strong&gt; it (start → spawn a PTY → stop) on Windows and Linux; macOS is built the same way but still has a runner-specific &lt;code&gt;node-pty&lt;/code&gt; spawn quirk I'm chasing, so it's flagged as pending for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The same loop as the whole project — me on &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;, &lt;strong&gt;Sol&lt;/strong&gt; as the design partner — but this time the pivot leaned hard on Sol's judgment: run the daemon in-process or as a persistent service? (service). How interactive should the TUI be for v0.6? (dashboard + minimal attach, not a pane framework). &lt;code&gt;pkg&lt;/code&gt; or Node SEA? (SEA, with node-pty external). Keeping the conversation &lt;strong&gt;stateful&lt;/strong&gt; meant Sol weighed each answer against every prior decision instead of re-litigating them. Ten test suites — protocol E2E, security headers, loopback-scope enforcement, daemon-owned pairing, the CLI client, dashboard rendering, and restore-across-restart — stay green on every push.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;The onboarding is finally one motion: &lt;strong&gt;install cordless, run it, scan the QR.&lt;/strong&gt; No Node, no &lt;code&gt;npm install -g&lt;/code&gt;, no separate pairing step, no GUI to keep open. It reads like a real terminal tool because it &lt;em&gt;is&lt;/em&gt; one — and the phone still gets the same live sessions. Turning a "daemon + app" into "a command you run" is exactly the kind of course-correction that only lands when you actually live with the thing.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live / docs:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;cordless CLI (Windows / Linux, no Node needed):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧑‍💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part of the #AI4Good series. Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>cli</category>
      <category>terminal</category>
      <category>node</category>
    </item>
    <item>
      <title>cordless v0.5: A Desktop App, a &gt;_&lt; Logo, and QR-First Security</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Sat, 11 Jul 2026 09:14:05 +0000</pubDate>
      <link>https://dev.to/naveenneog/cordless-v05-a-desktop-app-a-logo-and-qr-first-security-617</link>
      <guid>https://dev.to/naveenneog/cordless-v05-a-desktop-app-a-logo-and-qr-first-security-617</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;cordless&lt;/a&gt; manages many remote terminal / coding-agent (Claude Code, Codex) sessions &lt;strong&gt;like browser tabs&lt;/strong&gt;, with sessions that survive disconnects. &lt;strong&gt;v0.5&lt;/strong&gt; adds a &lt;strong&gt;hardened Electron desktop app&lt;/strong&gt; (Windows &lt;code&gt;.exe&lt;/code&gt;, macOS &lt;code&gt;.dmg&lt;/code&gt;, Linux &lt;code&gt;.AppImage&lt;/code&gt;/&lt;code&gt;.deb&lt;/code&gt;), a proper &lt;strong&gt;&lt;code&gt;&amp;gt;_&amp;lt;&lt;/code&gt; brand logo&lt;/strong&gt; made with &lt;strong&gt;gpt-image-2&lt;/strong&gt;, and a &lt;strong&gt;security course-correction&lt;/strong&gt;: QR pairing stays the &lt;em&gt;only&lt;/em&gt; default way in, and the desktop's one-click "Connect to this computer" uses a &lt;strong&gt;loopback-only&lt;/strong&gt; credential that a Tailscale or LAN address is &lt;em&gt;rejected&lt;/em&gt; for. As always — designed with &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt;, driven with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bxs4uylr2sczc802gk4.png" alt="cordless — now with a desktop app and a &gt;_&lt; logo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/10/cordless-remote-terminals-in-your-pocket/" rel="noopener noreferrer"&gt;shipped cordless&lt;/a&gt;, then made it &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/11/cordless-v0.4-install-guide/" rel="noopener noreferrer"&gt;livable in v0.4&lt;/a&gt; — seamless resume, an in-app QR scanner, no more truncation. v0.5 is about the two things a "real" tool needs that a phone app alone can't give you: &lt;strong&gt;a proper desktop experience&lt;/strong&gt;, and &lt;strong&gt;an identity&lt;/strong&gt;. And it's where a security instinct paid off.&lt;/p&gt;

&lt;h2&gt;
  
  
  A face for the project: the &lt;code&gt;&amp;gt;_&amp;lt;&lt;/code&gt; logo
&lt;/h2&gt;

&lt;p&gt;cordless needed a mark. I generated one with &lt;strong&gt;gpt-image-2&lt;/strong&gt; on my Azure AI Foundry deployment — a small Python script (&lt;code&gt;tooling/gen_logo.py&lt;/code&gt;) that POSTs a prompt and writes back the base64 PNG. After iterating on a few concepts, the winner was almost too obvious: &lt;strong&gt;&lt;code&gt;&amp;gt;_&amp;lt;&lt;/code&gt;&lt;/strong&gt; — a shell prompt &lt;code&gt;&amp;gt;_&lt;/code&gt; that doubles as a happy little face — set in a blue→violet gradient inside a terminal-window frame.&lt;/p&gt;

&lt;p&gt;One gotcha worth remembering: &lt;strong&gt;gpt-image-2 won't render a transparent background&lt;/strong&gt; (you get an HTTP 400 if you ask). So I generated the mark on a dark background and &lt;strong&gt;luminance-keyed it transparent&lt;/strong&gt; with Pillow (&lt;code&gt;tooling/apply_logo.py&lt;/code&gt;) — the same script then resizes it into &lt;em&gt;every&lt;/em&gt; size the project needs: PWA icons (&lt;code&gt;icon-192/512&lt;/code&gt;, a maskable variant, apple-touch, favicons), the landing-page hero, and the full set of Android launcher / adaptive-icon / splash assets.&lt;/p&gt;

&lt;p&gt;A subtle bug fell out of this: the repo's &lt;code&gt;.gitignore&lt;/code&gt; had a blanket &lt;code&gt;*.png&lt;/code&gt; rule (to keep Playwright screenshots out), and it was &lt;strong&gt;silently swallowing the new app icons&lt;/strong&gt;. The APK and PWA would have shipped with missing icons. Two lines fixed it — &lt;code&gt;!client/public/*.png&lt;/code&gt;, &lt;code&gt;!desktop/build/*.png&lt;/code&gt; — a good reminder to always check &lt;code&gt;git status&lt;/code&gt; after adding binary assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The desktop app — a hardened Electron shell
&lt;/h2&gt;

&lt;p&gt;The headline feature. When you're &lt;em&gt;at&lt;/em&gt; your dev box (or remoted into it), a phone is the wrong tool — you want a real window with a real keyboard. So v0.5 ships a &lt;strong&gt;desktop app&lt;/strong&gt; for Windows, macOS, and Linux.&lt;/p&gt;

&lt;p&gt;I designed the architecture in my running conversation with &lt;strong&gt;Sol&lt;/strong&gt;, and we landed on the pragmatic choice: the Electron window &lt;strong&gt;loads the daemon's own served page&lt;/strong&gt; at &lt;code&gt;http://127.0.0.1:&amp;lt;port&amp;gt;&lt;/code&gt;. Because it's same-origin to the daemon, there are &lt;strong&gt;zero&lt;/strong&gt; CORS/CSP changes — it's the exact same UI, sessions, tabs, and replay you already have, just in a window.&lt;/p&gt;

&lt;p&gt;The interesting part is the &lt;strong&gt;security hardening&lt;/strong&gt;, straight from Sol's review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;contextIsolation: true&lt;/code&gt;, &lt;code&gt;nodeIntegration: false&lt;/code&gt;, &lt;code&gt;sandbox: true&lt;/code&gt;, &lt;code&gt;webSecurity: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Navigation is &lt;strong&gt;pinned&lt;/strong&gt; to the trusted loopback origin — &lt;code&gt;will-navigate&lt;/code&gt; and &lt;code&gt;setWindowOpenHandler&lt;/code&gt; reject anything else; all new windows, &lt;code&gt;&amp;lt;webview&amp;gt;&lt;/code&gt;s, and permission requests (camera, mic, geolocation…) are denied.&lt;/li&gt;
&lt;li&gt;The renderer gets a &lt;strong&gt;tiny&lt;/strong&gt; preload bridge and nothing else: &lt;code&gt;{ platform, getLocalCredential, startDaemon, retry }&lt;/code&gt;. No &lt;code&gt;fs&lt;/code&gt;, no shell, no &lt;code&gt;openExternal&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every IPC call validates that the &lt;strong&gt;sender is one of our own trusted pages&lt;/strong&gt;. &lt;code&gt;startDaemon&lt;/code&gt; accepts &lt;strong&gt;no&lt;/strong&gt; path, command, or argument from the page — it resolves the installed CLI itself.&lt;/li&gt;
&lt;li&gt;If the daemon isn't running, the app shows a &lt;strong&gt;Start daemon / Retry&lt;/strong&gt; screen instead of a blank window.&lt;/li&gt;
&lt;li&gt;The port is resolved with a strict precedence — the credential's &lt;code&gt;server&lt;/code&gt;, then the daemon config, then &lt;code&gt;7443&lt;/code&gt; — and &lt;strong&gt;every&lt;/strong&gt; candidate is validated to be loopback &lt;code&gt;http&lt;/code&gt; (no &lt;code&gt;https&lt;/code&gt;, no userinfo, no path, no &lt;code&gt;localhost&lt;/code&gt; name ambiguity).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last bit of parsing is security-critical, so I pulled it into a pure module (&lt;code&gt;desktop/lib/resolve.js&lt;/code&gt;) and wrote &lt;strong&gt;24 unit tests&lt;/strong&gt; for it. It runs in CI on every push.&lt;/p&gt;

&lt;h2&gt;
  
  
  QR-first security — the course-correction that mattered
&lt;/h2&gt;

&lt;p&gt;Here's the moment I'm most glad about. While wiring the desktop app, I built a convenient "auto-connect on localhost" path. It worked. It was also &lt;strong&gt;wrong&lt;/strong&gt; — it quietly bypassed pairing. The rule for cordless is: &lt;strong&gt;QR / pairing-code is the one true way in.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I tore the auto-connect out and rebuilt it as an &lt;strong&gt;explicit, opt-in button&lt;/strong&gt;. The daemon now writes a &lt;code&gt;desktop-credential.json&lt;/code&gt; (mode &lt;code&gt;0600&lt;/code&gt;; it stores only the token's hash) tied to a device with &lt;strong&gt;&lt;code&gt;scope: "loopback"&lt;/code&gt;&lt;/strong&gt;. The &lt;code&gt;authenticate()&lt;/code&gt; path accepts that token &lt;strong&gt;only when the socket peer is &lt;code&gt;127.0.0.1&lt;/code&gt; / &lt;code&gt;::1&lt;/code&gt;&lt;/strong&gt; — checked against the real socket address, never a spoofable header. A Tailscale &lt;code&gt;100.x&lt;/code&gt; or LAN address is &lt;strong&gt;rejected outright&lt;/strong&gt;. In the UI, the &lt;strong&gt;🖥️ Connect to this computer&lt;/strong&gt; button only appears when the Electron bridge actually hands over a credential; open the same page in a plain browser and you see QR/code only.&lt;/p&gt;

&lt;p&gt;I verified it with a test that asserts exactly this: loopback IPs authenticate, and &lt;code&gt;100.64.x.x&lt;/code&gt; (Tailscale's range) does not. It's a &lt;em&gt;local convenience&lt;/em&gt; that is structurally incapable of being a &lt;em&gt;remote bypass&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ok    ip=127.0.0.1        authed=true   (expected true)
ok    ip=::1              authed=true   (expected true)
ok    ip=203.0.113.5      authed=false  (expected false)
ok    ip=100.64.1.2       authed=false  (expected false)   &amp;lt;- Tailscale, still rejected
=== LOOPBACK-SCOPE ENFORCEMENT PASS ===
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prerequisites &amp;amp; Tailscale, documented properly
&lt;/h2&gt;

&lt;p&gt;A fair complaint about the earlier docs: the &lt;strong&gt;prerequisites and Tailscale setup weren't written down clearly&lt;/strong&gt;. Fixed. The README and landing page now spell out, with Sol's help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prerequisites&lt;/strong&gt; — Node.js 22, the platform build tools &lt;code&gt;node-pty&lt;/code&gt; needs, Tailscale, and (on Windows) PowerShell 7, with download links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale setup&lt;/strong&gt; — &lt;code&gt;tailscale up&lt;/code&gt; / &lt;code&gt;status&lt;/code&gt; / &lt;code&gt;ip -4&lt;/code&gt;, an optional device &lt;strong&gt;tag&lt;/strong&gt;, a copy-pasteable &lt;strong&gt;tailnet ACL&lt;/strong&gt; that locks TCP &lt;code&gt;7443&lt;/code&gt; to your own identity, and a &lt;strong&gt;Windows Firewall&lt;/strong&gt; rule that only opens &lt;code&gt;7443&lt;/code&gt; on the Tailscale adapter (&lt;code&gt;100.64.0.0/10&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume your sessions&lt;/strong&gt; — &lt;code&gt;cordless install&lt;/code&gt; once, then open the desktop app or &lt;code&gt;http://localhost:7443&lt;/code&gt;; your tabs reattach, and after a reboot the daemon reopens them from its restore manifest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Same loop as the whole project: &lt;strong&gt;me on GitHub Copilot CLI, Sol as the design partner and reviewer&lt;/strong&gt; — and this time I kept the conversation &lt;em&gt;stateful&lt;/em&gt;, so Sol remembered every prior decision instead of re-litigating them. The desktop architecture, the Electron hardening checklist, and the loopback-credential rules all came out of that back-and-forth. CI does the heavy lifting: pushing a &lt;code&gt;v*&lt;/code&gt; tag builds the Android APK &lt;strong&gt;and&lt;/strong&gt; the three desktop installers on native GitHub runners and attaches them to the release. And &lt;code&gt;npm test&lt;/code&gt; is now a &lt;strong&gt;self-contained harness&lt;/strong&gt; that boots an isolated daemon and runs every suite — protocol E2E, security headers, the new desktop-credential + loopback-scope checks, and session-restore across a daemon restart. Seven suites, green.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;v0.5 is the version where cordless stopped feeling like a phone experiment and started feeling like a product: it has a &lt;strong&gt;name and a face&lt;/strong&gt;, it runs as a &lt;strong&gt;real desktop app&lt;/strong&gt;, and — the part I care about most — the convenience features were built &lt;strong&gt;without&lt;/strong&gt; weakening the security model. The one-click local connect is genuinely handy, and it &lt;em&gt;cannot&lt;/em&gt; be turned into a remote hole. That's the bar.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live / install the PWA:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🖥️ &lt;strong&gt;Desktop app (Win/macOS/Linux):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part of the #AI4Good series. Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>electron</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>cordless v0.4: Seamless Resume, an In-App QR Scanner, No More Truncation — and a Proper Install Guide</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:01:52 +0000</pubDate>
      <link>https://dev.to/naveenneog/cordless-v04-seamless-resume-an-in-app-qr-scanner-no-more-truncation-and-a-proper-install-3hg3</link>
      <guid>https://dev.to/naveenneog/cordless-v04-seamless-resume-an-in-app-qr-scanner-no-more-truncation-and-a-proper-install-3hg3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;cordless&lt;/a&gt; is a phone app that manages many remote terminal / coding-agent (Claude Code, Codex) sessions &lt;strong&gt;like browser tabs&lt;/strong&gt;. &lt;strong&gt;v0.4&lt;/strong&gt; adds the three things that were stopping it from being a daily driver: &lt;strong&gt;seamless resume&lt;/strong&gt; (sessions reopen when you turn your dev box on), a built-in &lt;strong&gt;QR scanner&lt;/strong&gt; in the Android app, and a &lt;strong&gt;mobile fix&lt;/strong&gt; so long output wraps instead of truncating — plus font zoom and a copyable session-details sheet. This post is also a &lt;strong&gt;proper install guide&lt;/strong&gt;: dependencies and all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F14kqlphe04k4ftur9gql.png" alt="cordless — remote terminals in your pocket" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A couple of days ago I shipped cordless — a Node daemon on my dev box that owns real PTY sessions, plus a phone app that attaches to them like tabs, with sessions that survive disconnects and replay on reconnect. Then I actually &lt;em&gt;used&lt;/em&gt; it for two days and hit three rough edges. v0.4 fixes all three. As with the whole build, I designed each fix in a running conversation with &lt;strong&gt;GPT‑5.6 Sol&lt;/strong&gt; and drove the work with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in v0.4
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Seamless resume — your tabs come back
&lt;/h3&gt;

&lt;p&gt;The daemon's PTYs die when the machine reboots (they're real OS processes). So "open my laptop and my agents are still there" needs two things: the daemon must &lt;strong&gt;auto-start&lt;/strong&gt;, and it must &lt;strong&gt;reopen what was running&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cordless install&lt;/code&gt; registers the daemon to start &lt;strong&gt;hidden at login&lt;/strong&gt; — Windows Task Scheduler, &lt;code&gt;systemd --user&lt;/code&gt;, or a macOS LaunchAgent, whichever you're on. There's a single-instance PID lock, and &lt;code&gt;cordless stop&lt;/code&gt; / &lt;code&gt;status&lt;/code&gt; to manage it.&lt;/li&gt;
&lt;li&gt;On start, the daemon reads a small &lt;strong&gt;manifest&lt;/strong&gt; of the sessions that were running and &lt;strong&gt;relaunches them&lt;/strong&gt; — fresh shells in the same directories, keeping the same session ids so your phone's tabs re-match. Each restored session gets a new &lt;em&gt;generation&lt;/em&gt; id, which the client uses to cleanly reset its replay state (a subtle but important detail Sol flagged: without it, an old client sequence number can be larger than the restored session's, and replay breaks).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's "reopen your tabs," not "resume the process" — shell state and running child processes are gone — but combined with the app's auto-reconnect, turning your machine on brings your workspace back.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. An in-app QR scanner (Android)
&lt;/h3&gt;

&lt;p&gt;Pairing prints a QR. In the browser PWA you scan it with the phone camera and it just opens. But the &lt;strong&gt;packaged Android app&lt;/strong&gt; is a different origin, so a system-camera scan wouldn't open it. The fix is the obvious one: &lt;strong&gt;scan from inside the app&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fah901nqf06l8rczcp4fr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fah901nqf06l8rczcp4fr.png" alt="pairing screen with Scan QR" width="800" height="1733"&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fch6eqt29dv6t4vmyylnl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fch6eqt29dv6t4vmyylnl.png" alt="connected terminal" width="800" height="1733"&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6auoysr1o5kksipxd6l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6auoysr1o5kksipxd6l.png" alt="touch key bar" width="800" height="1733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tap &lt;strong&gt;Scan QR&lt;/strong&gt;, point at the code, confirm the host, done. It's built on &lt;code&gt;@capacitor/barcode-scanner&lt;/code&gt; (MLKit, bundled — no Play Services model download), with a strict URL parser that rejects anything that isn't a cordless pairing link. There's also a &lt;code&gt;cordless://&lt;/code&gt; deep link registered, so a &lt;code&gt;cordless://pair?...&lt;/code&gt; link opens the app directly. (One gotcha: the scanner library needs &lt;code&gt;minSdkVersion 26&lt;/code&gt;, so the app now targets Android 8.0+.)&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Long text no longer gets truncated
&lt;/h3&gt;

&lt;p&gt;This was the most annoying one: on a narrow phone, long lines ran off the right edge with no way to read them. The root cause was a classic &lt;strong&gt;flexbox trap&lt;/strong&gt; — a flex/grid child defaults to &lt;code&gt;min-width: auto&lt;/code&gt;, so it refuses to shrink below its content's intrinsic width, and the terminal got clipped. The fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;min-width: 0&lt;/code&gt; on the terminal's flex ancestors (the fix for the clip), plus a per-active-pane &lt;strong&gt;&lt;code&gt;ResizeObserver&lt;/code&gt;&lt;/strong&gt; that re-fits xterm whenever the pane changes size (rotation, keyboard show/hide). Now the terminal always matches the visible width and &lt;strong&gt;soft-wraps&lt;/strong&gt; — no horizontal scrolling, which (per Sol) is the right call for a column-based terminal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font zoom&lt;/strong&gt; — &lt;code&gt;A−&lt;/code&gt; / &lt;code&gt;A+&lt;/code&gt; in the top bar, remembered per device.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;session details&lt;/strong&gt; sheet (tap ⓘ) showing the full title, working dir, host, state, and session id, each tap-to-copy — because tab titles have to be short.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Dependencies
&lt;/h2&gt;

&lt;p&gt;Nothing exotic. On the &lt;strong&gt;dev box&lt;/strong&gt; (the machine your sessions run on):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js ≥ 20&lt;/strong&gt; (LTS) and &lt;strong&gt;git&lt;/strong&gt;. Windows, macOS, or Linux.&lt;/li&gt;
&lt;li&gt;The daemon builds one native module, &lt;strong&gt;&lt;code&gt;node-pty&lt;/code&gt;&lt;/strong&gt; — it ships prebuilds, but if yours has to compile you'll want the platform build tools (VS Build Tools on Windows; &lt;code&gt;build-essential&lt;/code&gt; + Python on Linux; Xcode Command Line Tools on macOS).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On your &lt;strong&gt;phone&lt;/strong&gt;: just a browser (for the PWA) or Android 8.0+ (for the APK).&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;remote access&lt;/strong&gt; (optional): &lt;a href="https://tailscale.com" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt; on both devices.&lt;/p&gt;

&lt;p&gt;Only if you want to &lt;strong&gt;build the APK yourself&lt;/strong&gt;: Node ≥ 22, JDK 21, and the Android SDK (platform 34+, build-tools 34+). Most people just download the prebuilt APK from Releases.&lt;/p&gt;

&lt;p&gt;The npm dependencies &lt;code&gt;npm run setup&lt;/code&gt; pulls: daemon — &lt;code&gt;node-pty&lt;/code&gt;, &lt;code&gt;ws&lt;/code&gt;, &lt;code&gt;zod&lt;/code&gt;, &lt;code&gt;@xterm/headless&lt;/code&gt;, &lt;code&gt;@xterm/addon-serialize&lt;/code&gt;, &lt;code&gt;qrcode-terminal&lt;/code&gt;; app — &lt;code&gt;@xterm/xterm&lt;/code&gt; (+ fit), &lt;code&gt;react&lt;/code&gt;/&lt;code&gt;vite&lt;/code&gt;, and &lt;code&gt;@capacitor/*&lt;/code&gt; (&lt;code&gt;core&lt;/code&gt;, &lt;code&gt;app&lt;/code&gt;, &lt;code&gt;barcode-scanner&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;
  
  
  Install, properly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1 · On your dev box / laptop&lt;/strong&gt;&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/naveenneog/cordless
&lt;span class="nb"&gt;cd &lt;/span&gt;cordless
npm run setup      &lt;span class="c"&gt;# installs the agent + client dependencies&lt;/span&gt;
npm run build      &lt;span class="c"&gt;# builds the web app the daemon serves&lt;/span&gt;
npm start          &lt;span class="c"&gt;# starts the daemon on :7443&lt;/span&gt;

&lt;span class="c"&gt;# optional — run it automatically at every login (seamless resume):&lt;/span&gt;
node agent/src/index.js &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;2 · Pair your phone&lt;/strong&gt;&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="c"&gt;# in another terminal:&lt;/span&gt;
npm run pair       &lt;span class="c"&gt;# prints a QR + a pairing code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PWA:&lt;/strong&gt; scan the QR in your phone's browser (or open the printed URL) — it pairs automatically; then &lt;em&gt;Add to Home Screen&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android APK:&lt;/strong&gt; download &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;cordless-vX.Y.Z.apk&lt;/a&gt;, install it, open the app, tap &lt;strong&gt;Scan QR&lt;/strong&gt; (or type the server URL + code).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3 · From anywhere&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install &lt;a href="https://tailscale.com" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt; on both devices and lock port 7443 to your own devices with a tailnet ACL. Then &lt;code&gt;cordless pair&lt;/code&gt; prints a stable &lt;code&gt;*.ts.net&lt;/code&gt; URL. Never expose 7443 to the public internet.&lt;/p&gt;

&lt;p&gt;Manage it any time: &lt;code&gt;cordless status&lt;/code&gt; · &lt;code&gt;cordless stop&lt;/code&gt; · &lt;code&gt;cordless devices&lt;/code&gt; · &lt;code&gt;cordless uninstall&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;Two days of real use turned "neat demo" into "thing I actually reach for." Seamless resume means my machine rebooting overnight is a non-event — the tabs are back in the morning. The scanner removed the one annoying step in onboarding a new phone. And the wrap fix means &lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;ls -la&lt;/code&gt;, and a chatty &lt;code&gt;claude&lt;/code&gt; session are finally readable on a 6-inch screen. Small fixes, big difference — and every one of them came out of the builder-plus-reviewer loop (me on Copilot CLI, Sol reviewing) that's been the theme of this whole project.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live / install the PWA:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;
        naveenneog
      &lt;/a&gt; / &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;
        cordless
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Manage many remote terminal &amp;amp; coding-agent (Claude Code, Codex) sessions from your phone — like browser tabs. Node agent + PWA/Android client.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;cordless&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Manage many remote terminal / coding-agent sessions from your phone — like browser tabs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;cordless runs a small daemon on your dev box or laptop that owns real PTY sessions (a shell, or
&lt;code&gt;claude&lt;/code&gt; / &lt;code&gt;codex&lt;/code&gt;), and a mobile web app that attaches to them like browser tabs. Sessions keep
running when you disconnect; reconnecting replays exactly where you left off.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="nofollow noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt; · &lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;latest APK&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/naveenneog/cordless/docs/screenshot.png"&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%2Fnaveenneog%2Fcordless%2FHEAD%2Fdocs%2Fscreenshot.png" alt="cordless" width="820"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persistent sessions&lt;/strong&gt; — PTYs survive phone disconnects, network switches, and app backgrounding
Reconnect replays from your last-seen byte (or a full-screen snapshot if you were away too long).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabs for terminals&lt;/strong&gt; — run several Claude Code / Codex / shell sessions at once, switch instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch-first&lt;/strong&gt; — an on-screen key bar (Esc, Tab, Ctrl/Alt, arrows, Ctrl-C/D, pipes, paste), pinch-free
font zoom (A−/A+), soft-wrapping so long lines never truncate, and a details sheet for the full cwd /
session id. The Android…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Part of the #AI4Good series. Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>terminal</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>cordless: Managing Remote Terminals &amp; Coding Agents From My Phone, Like Browser Tabs</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Fri, 10 Jul 2026 18:45:34 +0000</pubDate>
      <link>https://dev.to/naveenneog/cordless-managing-remote-terminals-coding-agents-from-my-phone-like-browser-tabs-24m6</link>
      <guid>https://dev.to/naveenneog/cordless-managing-remote-terminals-coding-agents-from-my-phone-like-browser-tabs-24m6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;code&gt;cordless&lt;/code&gt; is a tiny &lt;strong&gt;Node daemon&lt;/strong&gt; on your dev box that owns real terminal sessions (a shell, or &lt;code&gt;claude&lt;/code&gt; / &lt;code&gt;codex&lt;/code&gt;), plus a &lt;strong&gt;phone app&lt;/strong&gt; that attaches to them &lt;strong&gt;like browser tabs&lt;/strong&gt;. Close the app, switch networks, come back later — your sessions are still running and &lt;strong&gt;replay exactly where you left off&lt;/strong&gt;. I designed it in a running conversation with &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt; on Azure, built it with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;, and verified it in a &lt;strong&gt;real browser &lt;em&gt;and&lt;/em&gt; a real Android emulator&lt;/strong&gt;. Live: &lt;strong&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futf7tx2101eidx75jyo7.png" alt="cordless — remote terminals in your pocket" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I keep leaving long-running coding-agent sessions on my dev box — a &lt;code&gt;claude&lt;/code&gt; chewing through a refactor, a build, a shell mid-task — and then walking away from the keyboard. I wanted to &lt;strong&gt;check on them and steer them from my phone&lt;/strong&gt;, the way I flip between browser tabs. Not SSH-in-a-box; something that treats each agent session as a first-class &lt;strong&gt;tab&lt;/strong&gt; that keeps living when my phone sleeps. So I built &lt;strong&gt;cordless&lt;/strong&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persistent sessions.&lt;/strong&gt; The PTYs live in the daemon. Your phone disconnecting, backgrounding, or hopping from Wi‑Fi to cellular doesn't kill anything. Reconnecting &lt;strong&gt;replays from your last-seen byte&lt;/strong&gt; — or a full-screen snapshot if you were away a while.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabs for terminals.&lt;/strong&gt; Run several Claude Code / Codex / shell sessions at once and switch instantly, with an unread dot when a background session produces output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch-first.&lt;/strong&gt; A real terminal is unusable with thumbs without help, so there's an on-screen &lt;strong&gt;key bar&lt;/strong&gt;: Esc, Tab, Ctrl/Alt (one-shot latches), arrows, Ctrl‑C/D, pipes, and paste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach it from anywhere.&lt;/strong&gt; Tailscale is the recommended path; same‑Wi‑Fi LAN also works. No ports exposed to the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web &lt;em&gt;or&lt;/em&gt; native.&lt;/strong&gt; Install the PWA straight from your phone browser, or grab the Android APK.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F16gsb7lz3itmfihj7ft2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F16gsb7lz3itmfihj7ft2.png" alt="pairing screen" width="800" height="1733"&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fukl90xp9fvpq3f76tk4s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fukl90xp9fvpq3f76tk4s.png" alt="connected terminal with the new-session sheet" width="800" height="1733"&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjsb2hj0hm42std0zglu4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjsb2hj0hm42std0zglu4.png" alt="touch key bar" width="800" height="1733"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Designed in tandem with GPT‑5.6 Sol
&lt;/h3&gt;

&lt;p&gt;The interesting part of this build is that I didn't architect it alone. I kept a &lt;strong&gt;stateful conversation&lt;/strong&gt; with &lt;strong&gt;GPT‑5.6 Sol&lt;/strong&gt; (deployed on my Azure AI Foundry) open the entire time — not one-off prompts, but a running transcript so it stayed consistent with every prior decision. Sol produced the initial architecture, and then, crucially, &lt;strong&gt;reviewed my actual code&lt;/strong&gt;. That review earned its keep.&lt;/p&gt;
&lt;h3&gt;
  
  
  The agent: node-pty, not tmux
&lt;/h3&gt;

&lt;p&gt;Each session is a real pseudo-terminal spawned with &lt;strong&gt;&lt;code&gt;node-pty&lt;/code&gt;&lt;/strong&gt; (ConPTY on Windows — which, pleasingly, built on Node 26 first try; a Unix PTY on macOS/Linux). I deliberately &lt;strong&gt;did not&lt;/strong&gt; use tmux: the daemon is &lt;em&gt;already&lt;/em&gt; the multiplexer, and tmux adds nested-terminal state, sizing quirks, and a Windows install problem. Every session is a PTY &lt;strong&gt;plus&lt;/strong&gt; a headless &lt;code&gt;@xterm/headless&lt;/code&gt; mirror (for snapshots) &lt;strong&gt;plus&lt;/strong&gt; an 8 MiB &lt;strong&gt;replay ring&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The subtle bug Sol caught here: &lt;code&gt;@xterm/headless&lt;/code&gt;'s &lt;code&gt;write()&lt;/code&gt; is asynchronous, and &lt;code&gt;reset()&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; ordered behind queued writes. So I route every terminal write and every snapshot through &lt;strong&gt;one per-session op queue&lt;/strong&gt;, and assign a batch's sequence number &lt;strong&gt;inside the write callback&lt;/strong&gt; — which keeps the replay ring, the sequence counter, and &lt;code&gt;serialize()&lt;/code&gt; perfectly consistent. Output is coalesced into ~16 ms / 32 KiB batches, one sequence number per batch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// one op queue per session; seq assigned only after the bytes are parsed&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_queueOp&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_nextSeq&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_pushRing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ring, counter and headless mirror advance together&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That consistency is what makes &lt;strong&gt;reconnect-with-replay&lt;/strong&gt; honest: attach with your last &lt;code&gt;seq&lt;/code&gt;, and the server either replays the ring from there or, if it's rolled past, serializes the headless terminal into a single reset snapshot. tmux-style survival, without tmux.&lt;/p&gt;
&lt;h3&gt;
  
  
  The client, and the 12 bugs the review caught
&lt;/h3&gt;

&lt;p&gt;The app is Vite + React + &lt;strong&gt;&lt;a href="https://xtermjs.org/" rel="noopener noreferrer"&gt;xterm.js&lt;/a&gt;&lt;/strong&gt;, served by the daemon itself so the PWA is same-origin. I wrote the connection layer, thought it was solid… and handed the whole file to Sol for review. It came back with &lt;strong&gt;twelve real issues&lt;/strong&gt; — not style nits, actual races:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stale-epoch writes &lt;strong&gt;duplicating output&lt;/strong&gt; after a reconnect,&lt;/li&gt;
&lt;li&gt;the duplicate-frame check gating on the wrong counter (applied vs received seq),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;duplicate-attach&lt;/strong&gt; and detach-during-attach races,&lt;/li&gt;
&lt;li&gt;an ack timer that &lt;strong&gt;leaked across reconnects&lt;/strong&gt; and cleared a fresh ack,&lt;/li&gt;
&lt;li&gt;Ctrl/Alt latches that could get &lt;strong&gt;stuck&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;"close tab" silently &lt;strong&gt;undone&lt;/strong&gt; by the next session-list poll.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix pattern throughout: give every socket a &lt;strong&gt;connection epoch&lt;/strong&gt; that every handler, timer, and promise verifies; serialize each tab's writes through an &lt;strong&gt;apply-chain&lt;/strong&gt;; and use &lt;strong&gt;generation counters&lt;/strong&gt; for attach/detach and resize. Boring, correct, and exactly the kind of thing a fresh reviewer spots that the author's brain has already glossed over.&lt;/p&gt;
&lt;h3&gt;
  
  
  Security, baked in from the start
&lt;/h3&gt;

&lt;p&gt;Because a paired device gets &lt;strong&gt;shell access to my machine&lt;/strong&gt;, security couldn't be an afterthought:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-device tokens&lt;/strong&gt;, only their SHA‑256 hashes stored on the box; revoke any device by id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-use, rate-limited QR pairing&lt;/strong&gt; — and the pairing secret rides in the URL &lt;strong&gt;fragment&lt;/strong&gt;, so it's never sent to (or logged by) the server.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Origin allowlist&lt;/strong&gt; on the WebSocket and pairing endpoints (blocks malicious web pages / DNS-rebinding), a strict &lt;strong&gt;CSP&lt;/strong&gt; (&lt;code&gt;script-src 'self'&lt;/code&gt;, no inline JS), and &lt;code&gt;no-store&lt;/code&gt; on every credential-bearing response.&lt;/li&gt;
&lt;li&gt;The daemon &lt;strong&gt;warns if you run it as root/Administrator&lt;/strong&gt; and binds with a clear least-privilege note.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a small automated suite that asserts all of this (cross-origin pairing → 403, cross-origin WebSocket → rejected, headers present) so it can't quietly regress.&lt;/p&gt;
&lt;h3&gt;
  
  
  The bug only a real emulator could find
&lt;/h3&gt;

&lt;p&gt;Here's my favorite part. Everything worked perfectly as a &lt;strong&gt;PWA&lt;/strong&gt; — because the browser served the app and the agent from the &lt;strong&gt;same origin&lt;/strong&gt;. Then I packaged the Android APK with &lt;strong&gt;Capacitor&lt;/strong&gt; and installed it on an emulator. Pairing failed instantly: &lt;strong&gt;"Failed to fetch."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The cause was genuinely educational. Inside a Capacitor WebView the app's origin is &lt;code&gt;http://localhost&lt;/code&gt;, so talking to the agent is &lt;strong&gt;cross-origin&lt;/strong&gt; — which triggers a &lt;strong&gt;CORS preflight&lt;/strong&gt; my server had never needed to handle, because the same-origin PWA never sent one. The fix was to add CORS &lt;strong&gt;scoped to the existing Origin allowlist&lt;/strong&gt; (echo &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; for allowed origins, answer the &lt;code&gt;OPTIONS&lt;/code&gt; preflight, and — Sol's catch — answer the &lt;strong&gt;Private Network Access&lt;/strong&gt; preflight that Chromium sends when reaching a LAN/Tailscale address). A bug that would have broken &lt;strong&gt;every&lt;/strong&gt; native-app user, invisible until I drove the real APK.&lt;/p&gt;

&lt;p&gt;After the fix, the emulator paired, connected over WebSocket, attached a session, rendered PowerShell output, and &lt;strong&gt;replayed full session history on reconnect&lt;/strong&gt; — the whole loop, natively.&lt;/p&gt;
&lt;h3&gt;
  
  
  Shipping it
&lt;/h3&gt;

&lt;p&gt;The Android APK is built by &lt;strong&gt;GitHub Actions on every &lt;code&gt;v*&lt;/code&gt; tag&lt;/strong&gt; (debug-signed, &lt;code&gt;androidScheme: http&lt;/code&gt; + cleartext so &lt;code&gt;ws://&lt;/code&gt; to a Tailscale IP works) and attached straight to the release. The landing page is GitHub Pages. Networking is &lt;strong&gt;Tailscale-first&lt;/strong&gt;: WireGuard encrypts the hop, you get a stable &lt;code&gt;*.ts.net&lt;/code&gt; name, and port 7443 never touches the public internet.&lt;/p&gt;
&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;Sessions that &lt;strong&gt;outlive the client&lt;/strong&gt; turn a phone from a read-only status screen into a real remote control for long-running agents. Check a &lt;code&gt;claude&lt;/code&gt; refactor from the sofa, unstick a build on the train, tap Ctrl‑C on a runaway process — then walk back to your desk and the exact same sessions are there in the browser. And because the whole thing is token-gated, Tailscale-scoped, and never exposed publicly, it stays &lt;em&gt;yours&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The meta-lesson: &lt;strong&gt;pairing a builder (me, via Copilot CLI) with a dedicated reviewer (GPT‑5.6 Sol) in a persistent conversation&lt;/strong&gt; produced noticeably better engineering than either alone — the 12-bug review and the CORS catch are things I'd have shipped without. And testing on a &lt;strong&gt;real emulator, not just a browser&lt;/strong&gt; is what turned "looks done" into "actually works."&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live / install the PWA:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;
        naveenneog
      &lt;/a&gt; / &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;
        cordless
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Manage many remote terminal &amp;amp; coding-agent (Claude Code, Codex) sessions from your phone — like browser tabs. Node agent + PWA/Android client.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;cordless&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Manage many remote terminal / coding-agent sessions from your phone — like browser tabs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;cordless runs a small daemon on your dev box or laptop that owns real PTY sessions (a shell, or
&lt;code&gt;claude&lt;/code&gt; / &lt;code&gt;codex&lt;/code&gt;), and a mobile web app that attaches to them like browser tabs. Sessions keep
running when you disconnect; reconnecting replays exactly where you left off.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="nofollow noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt; · &lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;latest APK&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/naveenneog/cordless/docs/screenshot.png"&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%2Fnaveenneog%2Fcordless%2FHEAD%2Fdocs%2Fscreenshot.png" alt="cordless" width="820"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persistent sessions&lt;/strong&gt; — PTYs survive phone disconnects, network switches, and app backgrounding
Reconnect replays from your last-seen byte (or a full-screen snapshot if you were away too long).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabs for terminals&lt;/strong&gt; — run several Claude Code / Codex / shell sessions at once, switch instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch-first&lt;/strong&gt; — an on-screen key bar (Esc, Tab, Ctrl/Alt, arrows, Ctrl-C/D, pipes, paste) makes a
real terminal usable with thumbs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach it from anywhere&lt;/strong&gt; — Tailscale is the recommended path; same-Wi-Fi LAN also works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure by&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Part of the #AI4Good series. Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>terminal</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>AI4Good: I Shipped an AI-for-Good App Every Few Days — Here's the Build Story</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Fri, 10 Jul 2026 08:14:57 +0000</pubDate>
      <link>https://dev.to/naveenneog/ai4good-i-shipped-an-ai-for-good-app-every-few-days-heres-the-build-story-15f6</link>
      <guid>https://dev.to/naveenneog/ai4good-i-shipped-an-ai-for-good-app-every-few-days-heres-the-build-story-15f6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ls9q6ir49rr5eioo88u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ls9q6ir49rr5eioo88u.png" alt="AI4Good — eight apps, eighteen days, AI for good" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;#AI4Good&lt;/strong&gt; started as a dare to myself: &lt;em&gt;can I use AI to build and ship something genuinely useful — for kids, for health, for our culture — one build at a time?&lt;/em&gt; Eighteen days later there were &lt;strong&gt;eight live apps&lt;/strong&gt;, each free, each on GitHub Pages. This is the story of how they came to be, in the order they were born — and &lt;strong&gt;each has its own deep-dive post&lt;/strong&gt; on the &lt;a href="https://naveenneog.github.io/AI4Good/" rel="noopener noreferrer"&gt;AI4Good blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm &lt;strong&gt;Naveen Gopalakrishna&lt;/strong&gt; — an AI Global Black Belt at Microsoft by day, a compulsive builder by night. The tooling that made this pace possible was &lt;strong&gt;GitHub Copilot CLI in autopilot mode&lt;/strong&gt; (the agent researches, writes code, runs builds, and renders assets autonomously) on top of &lt;strong&gt;Azure AI&lt;/strong&gt; — &lt;code&gt;gpt-image-2&lt;/code&gt; for art, Azure Speech for narration, and open models from Hugging Face for the heavy lifting. But tools are just tools. The thread that ties these eight together is a question I kept asking: &lt;strong&gt;who does this help?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's walk the timeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jun 22 — PrimeBeats: your music, beautifully local
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmuzpwx8j81axnhuf7ply.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmuzpwx8j81axnhuf7ply.png" alt="PrimeBeats landing page — your music, beautifully local" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where it started. &lt;a href="https://naveenneog.github.io/PrimeBeats/" rel="noopener noreferrer"&gt;&lt;strong&gt;PrimeBeats&lt;/strong&gt;&lt;/a&gt; is an Amazon-Prime-Music-style player for the songs already on your phone — &lt;strong&gt;background playback&lt;/strong&gt;, &lt;strong&gt;lock-screen controls&lt;/strong&gt;, playlists, an on-device &lt;strong&gt;Smart Radio&lt;/strong&gt;, and a real &lt;strong&gt;equalizer&lt;/strong&gt;. No account, no ads, &lt;strong&gt;100% offline&lt;/strong&gt;. Built with React Native + Expo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good:&lt;/strong&gt; ownership and privacy — your music, zero data, zero tracking, zero cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jun 23 — ActionCut: put a pro video studio in everyone's pocket
&lt;/h2&gt;

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

&lt;p&gt;The next build set the tone: take something normally locked behind subscriptions and make it &lt;strong&gt;free and open&lt;/strong&gt;. &lt;a href="https://naveenneog.github.io/ActionCut/" rel="noopener noreferrer"&gt;&lt;strong&gt;ActionCut&lt;/strong&gt;&lt;/a&gt; is a CapCut-class Android video editor — a real &lt;strong&gt;multi-track timeline&lt;/strong&gt;, &lt;strong&gt;LUT color filters&lt;/strong&gt;, GPU effects, audio mixing, and one-tap &lt;strong&gt;export presets&lt;/strong&gt; for every platform. Built in &lt;strong&gt;Kotlin + Jetpack Compose + Media3&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good:&lt;/strong&gt; creators on entry-level Android phones get studio-grade editing without a paywall or a watermark. Storytelling shouldn't require a subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jun 26 — KidKat: only the good stuff, for curious kids
&lt;/h2&gt;

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

&lt;p&gt;This is the app that named the whole campaign. &lt;a href="https://naveenneog.github.io/KidKat/" rel="noopener noreferrer"&gt;&lt;strong&gt;KidKat&lt;/strong&gt;&lt;/a&gt; plays a &lt;strong&gt;finite, parent-approved stream&lt;/strong&gt; of short educational videos — inside the official YouTube player, using the official Data API, fully &lt;strong&gt;ToS-compliant&lt;/strong&gt;. No infinite feed. No algorithmic rabbit holes. No doomscroll. A parent allowlist decides what's on the menu. Built with &lt;strong&gt;Flutter&lt;/strong&gt; for Android and iOS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good:&lt;/strong&gt; it turns the most anxiety-inducing part of modern parenting — handing a child a screen — into something calm and intentional. &lt;strong&gt;AI for good starts with protecting the youngest users.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Jun 27 — NeoFit: health that speaks your language
&lt;/h2&gt;

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

&lt;p&gt;&lt;em&gt;"Sehat, simple banayi."&lt;/em&gt; &lt;a href="https://naveenneog.github.io/NeoFit/" rel="noopener noreferrer"&gt;&lt;strong&gt;NeoFit&lt;/strong&gt;&lt;/a&gt; is a science-informed &lt;strong&gt;Indian&lt;/strong&gt; health and fitness app in &lt;strong&gt;22 Indian languages&lt;/strong&gt; — calorie tracking that actually knows &lt;em&gt;dosa, biryani, and gulab jamun&lt;/em&gt;, &lt;strong&gt;on-device food recognition&lt;/strong&gt;, AI-generated food photos, AI exercise videos, and Health Connect sync. Offline-first, &lt;strong&gt;powered by Azure AI&lt;/strong&gt;, Kotlin/Compose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good:&lt;/strong&gt; most fitness apps assume you eat like the West and read English fluently. NeoFit meets 1.4 billion people where they are — in their language, with their food. &lt;strong&gt;Inclusion is a health outcome.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Jul 04 — GpsCamera: proof you can trust
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0eanuvciigri3kvfjm7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0eanuvciigri3kvfjm7.png" alt="GpsCamera landing page — geotag every photo, exactly" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/GpsCamera/" rel="noopener noreferrer"&gt;&lt;strong&gt;GpsCamera&lt;/strong&gt;&lt;/a&gt; is a fast, native Android camera that &lt;strong&gt;burns your exact location and a live mini-map onto every photo&lt;/strong&gt;, writes standards-compliant &lt;strong&gt;GPS EXIF&lt;/strong&gt;, and files each shot into its own album. Kotlin, Compose, CameraX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good:&lt;/strong&gt; field engineers, insurance surveyors, community reporters, and site inspectors need &lt;strong&gt;verifiable, tamper-evident documentation&lt;/strong&gt;. A trustworthy timestamped-and-geotagged photo is a small tool that quietly protects people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jul 05 — The Lamp &amp;amp; the Machine: shadow-puppet theatre as an AI film studio
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6onl0dr1qqwvxke1szl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6onl0dr1qqwvxke1szl.png" alt="The Lamp &amp;amp; the Machine — Togalu Gombeyaata meets generative AI" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/Sora-Azure-MultiPart-Video-Editing/" rel="noopener noreferrer"&gt;&lt;strong&gt;The Lamp &amp;amp; the Machine&lt;/strong&gt;&lt;/a&gt; turns Karnataka's 500-year-old leather shadow-puppet theatre, &lt;strong&gt;Togalu Gombeyaata&lt;/strong&gt;, into an AI film studio — cinematic scenes from &lt;strong&gt;Sora 2&lt;/strong&gt;, narration from &lt;strong&gt;Azure AI&lt;/strong&gt;, finished with Demucs + FFmpeg, retelling the story of &lt;strong&gt;Kempegowda&lt;/strong&gt;, the founder of Bengaluru.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good:&lt;/strong&gt; generative AI as a &lt;strong&gt;preservation tool&lt;/strong&gt; — putting a fading folk art in front of a new generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jul 06 — Sopāna: a Snakes &amp;amp; Ladders that teaches
&lt;/h2&gt;

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

&lt;p&gt;&lt;a href="https://naveenneog.github.io/Sopana/" rel="noopener noreferrer"&gt;&lt;strong&gt;Sopāna&lt;/strong&gt;&lt;/a&gt; reclaims the game the West calls &lt;em&gt;Snakes &amp;amp; Ladders&lt;/em&gt; and restores its origin — the ancient Indian &lt;strong&gt;Moksha Patam&lt;/strong&gt;, where &lt;strong&gt;every snake is a vice and every ladder a virtue&lt;/strong&gt;. Land on one and the game &lt;strong&gt;animates and reads its meaning aloud&lt;/strong&gt;. Play three ways (a 2D board, a 2.5D cinematic mode, and a 3D camera), across four worlds, with up to four players on one screen. A web PWA plus an Android APK.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good:&lt;/strong&gt; it turns a children's game back into what it was designed to be — a gentle lesson in ethics and consequence, wrapped in cultural heritage. &lt;strong&gt;AI narration and art make an 800-year-old teaching tool feel alive again.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Jul 09 — Chaturanga: ancient chess, reborn in glowing 3D
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhtdbwuxjwxw0ecnzyvd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhtdbwuxjwxw0ecnzyvd.png" alt="Chaturanga landing page — the four-army game of dharma" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/Chaturanga/" rel="noopener noreferrer"&gt;&lt;strong&gt;Chaturanga&lt;/strong&gt;&lt;/a&gt; is the ancient Indian "game of the four divisions," played with modern chess rules but &lt;strong&gt;authentic piece identities&lt;/strong&gt; — Raja, Mantri, Gaja, Ashva, Ratha, Padati — carved in &lt;strong&gt;real-time glowing 3D&lt;/strong&gt;. It ships with a &lt;strong&gt;teaching AI&lt;/strong&gt; (five levels), a &lt;strong&gt;coach&lt;/strong&gt; that reviews your blunders, an &lt;strong&gt;openings trainer&lt;/strong&gt;, a rotating piece inspector, and a &lt;em&gt;Warrior's Eye&lt;/em&gt; camera. Four worlds, each teaching a moral lesson.&lt;/p&gt;

&lt;p&gt;The pieces themselves are an AI-for-good story: I generated each one from a single concept image using a &lt;strong&gt;free Hugging Face Space&lt;/strong&gt; and headless Blender — the full image-to-3D pipeline is &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/10/image-to-3d-huggingface-blender-copilot/" rel="noopener noreferrer"&gt;its own post here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good:&lt;/strong&gt; a free, offline, no-sign-in chess tutor that also carries culture — learning and heritage in one board.&lt;/p&gt;

&lt;h2&gt;
  
  
  What #AI4Good actually means
&lt;/h2&gt;

&lt;p&gt;Look at all eight together and a pattern appears — not "AI apps," but &lt;strong&gt;apps that use AI to do something good&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Who it serves&lt;/th&gt;
&lt;th&gt;The good&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jun 22&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/PrimeBeats/" rel="noopener noreferrer"&gt;PrimeBeats&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Listeners&lt;/td&gt;
&lt;td&gt;Own your music — offline, ad-free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jun 23&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/ActionCut/" rel="noopener noreferrer"&gt;ActionCut&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Creators&lt;/td&gt;
&lt;td&gt;Studio-grade editing, free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jun 26&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/KidKat/" rel="noopener noreferrer"&gt;KidKat&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Kids &amp;amp; parents&lt;/td&gt;
&lt;td&gt;Safe, finite, educational screen time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jun 27&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/NeoFit/" rel="noopener noreferrer"&gt;NeoFit&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.4B Indians&lt;/td&gt;
&lt;td&gt;Health in 22 languages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 04&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/GpsCamera/" rel="noopener noreferrer"&gt;GpsCamera&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Field workers&lt;/td&gt;
&lt;td&gt;Verifiable documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 05&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/Sora-Azure-MultiPart-Video-Editing/" rel="noopener noreferrer"&gt;The Lamp &amp;amp; the Machine&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Culture&lt;/td&gt;
&lt;td&gt;Preserving a folk art with AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 06&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/Sopana/" rel="noopener noreferrer"&gt;Sopāna&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Families&lt;/td&gt;
&lt;td&gt;Values + heritage, playfully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jul 09&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/Chaturanga/" rel="noopener noreferrer"&gt;Chaturanga&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Learners&lt;/td&gt;
&lt;td&gt;Free chess tutor + culture&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The method:&lt;/strong&gt; one build at a time, from a single laptop, with &lt;strong&gt;Copilot CLI autopilot&lt;/strong&gt; doing the heavy lifting and &lt;strong&gt;Azure AI&lt;/strong&gt; + &lt;strong&gt;Hugging Face&lt;/strong&gt; models supplying the intelligence. &lt;strong&gt;The mission:&lt;/strong&gt; put that capability toward education, health, culture, and trust — and give it away.&lt;/p&gt;

&lt;p&gt;This is just the beginning of the campaign. Next up the runway: &lt;strong&gt;Indian Tales&lt;/strong&gt;, a multilingual Togalu Gombe (shadow-puppet) storytelling app to keep a fading folk art alive.&lt;/p&gt;

&lt;p&gt;If you build things, I'd gently challenge you to the same dare: &lt;strong&gt;pick the "who does this help?" first, then let AI help you ship it.&lt;/strong&gt; That's &lt;strong&gt;#AI4Good&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the journey — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/naveen-gopalakrishna-99863040/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. Built one day at a time.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>ai</category>
      <category>opensource</category>
      <category>showcase</category>
    </item>
    <item>
      <title>Carving a Glowing 3D Army from Single Images: Hugging Face + Blender, driven by Copilot CLI Autopilot</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Fri, 10 Jul 2026 07:54:18 +0000</pubDate>
      <link>https://dev.to/naveenneog/carving-a-glowing-3d-army-from-single-images-hugging-face-blender-driven-by-copilot-cli-1700</link>
      <guid>https://dev.to/naveenneog/carving-a-glowing-3d-army-from-single-images-hugging-face-blender-driven-by-copilot-cli-1700</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — One &lt;code&gt;gpt-image-2&lt;/code&gt; concept image per piece → a &lt;strong&gt;free Hugging Face Space&lt;/strong&gt; (&lt;code&gt;tencent/Hunyuan3D-2&lt;/code&gt;) turns it into a raw 3D mesh → &lt;strong&gt;headless Blender&lt;/strong&gt; projects the concept back on as texture → a web-ready &lt;strong&gt;GLB&lt;/strong&gt; that glows in Three.js. No local GPU. All of it — the research, the scripts, the Hugging Face calls, the Blender runs, and &lt;em&gt;this very post and its auto-publish to DEV&lt;/em&gt; — was orchestrated by &lt;strong&gt;GitHub Copilot CLI in autopilot mode&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;I've been building &lt;strong&gt;&lt;a href="https://naveenneog.github.io/Chaturanga" rel="noopener noreferrer"&gt;Chaturanga&lt;/a&gt;&lt;/strong&gt; — the ancient Indian "game of the four divisions," played with modern chess moves but authentic piece identities (&lt;em&gt;Raja, Mantri, Gaja, Ashva, Ratha, Padati&lt;/em&gt;). The board above is the payoff. The hard part was the &lt;strong&gt;pieces&lt;/strong&gt;: carved-ivory war figures with real silhouettes, in four themed armies, that spin in a WebGL inspector — built for &lt;strong&gt;free, on a laptop with no GPU&lt;/strong&gt;, in a &lt;strong&gt;fully scriptable&lt;/strong&gt; way.&lt;/p&gt;

&lt;p&gt;Here's how that pipeline came together, including the parts that didn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline at a glance
&lt;/h2&gt;

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

&lt;p&gt;Four stages, each free and headless:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Concept&lt;/strong&gt; — Azure &lt;code&gt;gpt-image-2&lt;/code&gt; renders a carved-ivory concept per piece (front 3/4, plain background).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image → 3D&lt;/strong&gt; — the concept goes to a &lt;strong&gt;Hugging Face Space&lt;/strong&gt; that runs image-to-3D on donated GPU and returns a mesh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Texture&lt;/strong&gt; — &lt;strong&gt;Blender 5.1&lt;/strong&gt;, headless on CPU, projects the concept image onto the mesh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship&lt;/strong&gt; — export a decimated, textured &lt;strong&gt;GLB&lt;/strong&gt; and load it in Three.js with a little emissive glow.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2 is the heart: image → 3D on a free Hugging Face Space
&lt;/h2&gt;

&lt;p&gt;Instead of buying GPU time, I call &lt;strong&gt;&lt;a href="https://huggingface.co/spaces/tencent/Hunyuan3D-2" rel="noopener noreferrer"&gt;&lt;code&gt;tencent/Hunyuan3D-2&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; through its Gradio API. The Space runs the heavy diffusion-to-mesh step on &lt;strong&gt;free ZeroGPU&lt;/strong&gt;; I just pull back a &lt;code&gt;.glb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;gradio_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handle_file&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tencent/Hunyuan3D-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hf_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HF_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# token optional; helps rate limits
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;handle_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refs/raja.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guidance_scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;octree_resolution&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_box_rembg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_chunks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;randomize_seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/shape_generation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# geometry only — fast; texture done later in Blender
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# result -&amp;gt; a raw .glb mesh, generated on someone else's GPU, for free
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;No-signup fallback:&lt;/strong&gt; for a path that needs &lt;em&gt;zero&lt;/em&gt; accounts, I keep &lt;strong&gt;&lt;a href="https://huggingface.co/stabilityai/TripoSR" rel="noopener noreferrer"&gt;&lt;code&gt;stabilityai/TripoSR&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; (MIT) running &lt;strong&gt;locally on CPU&lt;/strong&gt; — the only open image-to-3D model I found that's genuinely CPU-feasible on Windows.&lt;/p&gt;
&lt;h3&gt;
  
  
  Discovering Spaces from the terminal
&lt;/h3&gt;

&lt;p&gt;Copilot CLI has the &lt;strong&gt;Hugging Face MCP server&lt;/strong&gt; wired in, so I search the Hub without leaving my shell. Asking for image-to-3D Spaces returns the exact shortlist this project lives on: &lt;code&gt;microsoft/TRELLIS.2&lt;/code&gt; (⭐1.7k), &lt;code&gt;tencent/Hunyuan3D-2&lt;/code&gt; (⭐3.3k, what I use), &lt;code&gt;TencentARC/InstantMesh&lt;/code&gt; (⭐1.6k).&lt;/p&gt;
&lt;h2&gt;
  
  
  From flat concept to spinning figure
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;gpt-image-2&lt;/code&gt; concept on the left, the Hugging Face + Blender result on the right:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fenjear7gr8t083cnp7v2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fenjear7gr8t083cnp7v2.png" alt="Raja: concept vs 3D" width="800" height="594"&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs62grccu9hbfm0mf1mjz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs62grccu9hbfm0mf1mjz.png" alt="Ashva: concept vs 3D" width="800" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And on the turntable (Raja · Ashva · Gaja):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpevz63zs6h9ew7tbn2e3.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpevz63zs6h9ew7tbn2e3.gif" alt="Raja turntable" width="170" height="460"&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2epg5e2ztpdq1pymdv1m.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2epg5e2ztpdq1pymdv1m.gif" alt="Ashva turntable" width="301" height="460"&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffuh4xkhxxxhehjjd7dwk.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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffuh4xkhxxxhehjjd7dwk.gif" alt="Gaja turntable" width="378" height="460"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3 — Where Blender earns its keep (and where it fights back)
&lt;/h2&gt;

&lt;p&gt;The Hugging Face mesh has a &lt;strong&gt;correct silhouette but a soft surface&lt;/strong&gt;. The crisp carving detail lives in the concept image. So Blender's job is &lt;strong&gt;concept-texture projection&lt;/strong&gt;: orient the mesh, frame an orthographic camera on its front, and set each vertex's UV to that camera's projection — so the concept &lt;em&gt;becomes&lt;/em&gt; the surface.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;blender &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;--python&lt;/span&gt; tooling/blender/texture_project.py &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    raw/raja_hunyuan.glb refs/raja.jpg web/raja.glb preview.png
&lt;span class="c"&gt;# headless CPU: orient -&amp;gt; project concept as texture -&amp;gt; decimate to ~28k faces -&amp;gt; export GLB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The honest limitations I hit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Back faces get a mirrored projection.&lt;/strong&gt; Single-camera projection is perfect head-on; the reverse is a mirror. For board pieces seen from above, an acceptable trade — but a trade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blender 4 → 5 broke my material scripts.&lt;/strong&gt; Principled BSDF sockets were renamed: &lt;code&gt;Subsurface Weight&lt;/code&gt; not &lt;code&gt;Subsurface&lt;/code&gt;, &lt;code&gt;Specular IOR Level&lt;/code&gt; not &lt;code&gt;Specular&lt;/code&gt;. &lt;code&gt;KeyError&lt;/code&gt; until I re-bound each one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eevee Next won't render headless on Windows.&lt;/strong&gt; &lt;code&gt;blender -b&lt;/code&gt; with new Eevee crashes with an access violation — it wants a GPU context a background process lacks. &lt;strong&gt;Cycles on CPU&lt;/strong&gt; renders fine; every image here is Cycles-CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No native build allowed.&lt;/strong&gt; Marching cubes runs via &lt;strong&gt;PyMCubes&lt;/strong&gt;, not a compiled extension, to stay laptop-reproducible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The detour:&lt;/strong&gt; I first tried &lt;strong&gt;Blender-only&lt;/strong&gt; procedural modeling (skin modifier, geometry nodes, baked PBR). Fully scriptable, GPU-free… and it produced &lt;strong&gt;primitive blobs after hours&lt;/strong&gt;. Great for &lt;em&gt;finishing&lt;/em&gt;, wrong tool for &lt;em&gt;creating&lt;/em&gt; organic figures. That failure is why the Hugging Face step exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  The tools I tried — and where they hit a wall
&lt;/h2&gt;

&lt;p&gt;Constraints on purpose: &lt;strong&gt;free, Windows, no GPU, scriptable.&lt;/strong&gt;&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;Route&lt;/th&gt;
&lt;th&gt;Verdict under my constraints&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hunyuan3D-2 (HF Space)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;image→mesh, free ZeroGPU&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Winner&lt;/strong&gt; — GPU-quality geometry, no local GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;TripoSR&lt;/strong&gt; (local)&lt;/td&gt;
&lt;td&gt;image→mesh, CPU&lt;/td&gt;
&lt;td&gt;✅ Fallback — only CPU-feasible local model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meshy AI&lt;/td&gt;
&lt;td&gt;REST API, 200 credits/mo&lt;/td&gt;
&lt;td&gt;⚠️ Great, but ~6 textured pieces/mo, CC-BY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tripo AI&lt;/td&gt;
&lt;td&gt;REST API, 300 credits/mo&lt;/td&gt;
&lt;td&gt;⚠️ Good, but 15 downloads/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rodin (Hyper3D)&lt;/td&gt;
&lt;td&gt;REST API, pay-on-export&lt;/td&gt;
&lt;td&gt;⚠️ Best detail, but credits burn on download&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InstantMesh / TRELLIS / Wonder3D&lt;/td&gt;
&lt;td&gt;local&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;Not CPU-feasible&lt;/strong&gt; — CUDA-only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern: hosted APIs are excellent but &lt;strong&gt;metered&lt;/strong&gt;; the best local models need a &lt;strong&gt;GPU I don't have&lt;/strong&gt;. The escape hatch is a &lt;strong&gt;community-hosted Hugging Face Space&lt;/strong&gt; — the model runs on donated GPU and I pay nothing but a queue wait.&lt;/p&gt;
&lt;h2&gt;
  
  
  The meta-story: built in Copilot CLI autopilot mode
&lt;/h2&gt;

&lt;p&gt;I didn't hand-write most of this. I drove &lt;strong&gt;GitHub Copilot CLI in autopilot mode&lt;/strong&gt; — where the agent runs shell commands, scripts, and tools &lt;strong&gt;autonomously&lt;/strong&gt;, multi-step, without stopping for approval each move:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;research sub-agent&lt;/strong&gt; fanned out across 13 image-to-3D tools and returned a sourced comparison;&lt;/li&gt;
&lt;li&gt;the agent &lt;strong&gt;wrote and ran&lt;/strong&gt; the batch driver, called the Hunyuan3D-2 Space, and retried on ZeroGPU hiccups;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;headless Blender&lt;/strong&gt; runs for texture and the turntables above — it caught the Eevee-headless crash and switched to Cycles itself;&lt;/li&gt;
&lt;li&gt;and &lt;strong&gt;this post&lt;/strong&gt; — rendered GIFs, diagram, prose — plus the &lt;strong&gt;GitHub Actions workflow that auto-published it to DEV&lt;/strong&gt; on push.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Results &amp;amp; what's next
&lt;/h2&gt;

&lt;p&gt;A full &lt;strong&gt;four-world&lt;/strong&gt; army — Kurukshetra, Ramayana, Kalinga, Devasura — each piece a real 3D figure with a themed material and emissive bloom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; the game is live at &lt;strong&gt;&lt;a href="https://naveenneog.github.io/Chaturanga" rel="noopener noreferrer"&gt;naveenneog.github.io/Chaturanga&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;
        naveenneog
      &lt;/a&gt; / &lt;a href="https://github.com/naveenneog/Chaturanga" rel="noopener noreferrer"&gt;
        Chaturanga
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Chaturanga — the ancient Indian four-army game in real 3D: a teaching chess AI with levels, an openings trainer, a piece inspector, a Warrior's Eye camera, and two themed worlds. Web + Android.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Chaturanga — the four-army game of dharma&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Chaturanga&lt;/em&gt; is the ancient Indian ancestor of chess — the game of the &lt;strong&gt;four army
divisions&lt;/strong&gt;. This is a real-3D, mobile-first take on it: authentic Chaturanga piece
&lt;strong&gt;identities&lt;/strong&gt; (Raja, Mantri, Ratha, Gaja, Ashva, Padati) that move by &lt;strong&gt;modern chess
rules&lt;/strong&gt;, where every world teaches a moral through its pieces, and a &lt;strong&gt;teaching AI&lt;/strong&gt;
explains the game as you play.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;▶ Play:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/Chaturanga/" rel="nofollow noopener noreferrer"&gt;https://naveenneog.github.io/Chaturanga/&lt;/a&gt; &amp;nbsp;·&amp;nbsp; &lt;strong&gt;⬇ Android APK:&lt;/strong&gt; see &lt;a href="https://github.com/naveenneog/Chaturanga/releases/latest" rel="noopener noreferrer"&gt;Releases&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/naveenneog/Chaturanga/docs/img/ramayana.png"&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%2Fnaveenneog%2FChaturanga%2FHEAD%2Fdocs%2Fimg%2Framayana.png" alt="Lanka — the Ramayana world"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Play the Guru&lt;/strong&gt; — an alpha-beta chess AI with &lt;strong&gt;5 difficulty levels&lt;/strong&gt; (Padati → Mantri)
running in a Web Worker so it stays smooth on phones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A coach&lt;/strong&gt; — a &lt;strong&gt;Hint&lt;/strong&gt; that names the best move and &lt;em&gt;why&lt;/em&gt;, and a blunder review that
gently flags mistakes and shows the stronger move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Openings trainer&lt;/strong&gt; — six classic openings (Italian, Ruy López, Sicilian, French
Queen's Gambit, King's Indian) walked &lt;strong&gt;move-by-move&lt;/strong&gt; with a narrated…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/naveenneog/Chaturanga" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Steal the pipeline:&lt;/strong&gt; &lt;code&gt;gpt-image-2&lt;/code&gt; → &lt;code&gt;tencent/Hunyuan3D-2&lt;/code&gt; Space → Blender projection → GLB. Free, no GPU, every step a script. For game assets, product mockups, or avatars, a free Hugging Face Space is the cheat code — and Copilot CLI autopilot stitches it all together.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>huggingface</category>
      <category>blender</category>
      <category>ai</category>
      <category>3d</category>
    </item>
  </channel>
</rss>
