<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Arnav Gupta's Tech Blog]]></title><description><![CDATA[I write about my tech projects here. Many of these posts are READMEs for my future self. ]]></description><link>https://arnav.tech</link><generator>RSS for Node</generator><lastBuildDate>Sun, 12 Apr 2026 06:14:34 GMT</lastBuildDate><atom:link href="https://arnav.tech/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Under the Hood: How 2FA TOTP Authenticator Apps Work]]></title><description><![CDATA[In the previous article, I walked through how TwoFac is structured as a Kotlin Multiplatform project. Now that the architecture is in place, it's time to zoom into one of the most important pieces ins]]></description><link>https://arnav.tech/under-the-hood-how-totp-works-and-how-twofac-generates-your-2fa-codes</link><guid isPermaLink="true">https://arnav.tech/under-the-hood-how-totp-works-and-how-twofac-generates-your-2fa-codes</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Cryptography]]></category><category><![CDATA[OTP]]></category><category><![CDATA[Security]]></category><category><![CDATA[Two-factor authentication]]></category><category><![CDATA[2FA]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Sun, 08 Mar 2026 17:28:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/a892724b-811e-4c96-aafb-4d49bc1f927f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a href="https://arnav.tech/architecting-twofac-my-journey-into-kotlin-multiplatform-module-structure">previous article</a>, I walked through how TwoFac is structured as a Kotlin Multiplatform project. Now that the architecture is in place, it's time to zoom into one of the most important pieces inside that shared core: the actual OTP generation. Ever wondered what actually happens when you open your authenticator app and see a 6-digit code counting down? Most of us just copy-paste it and move on, but there's some pretty elegant cryptography happening behind the scenes. In this post, I'm going to crack open the hood on <strong>TOTP</strong> — Time-based One-Time Passwords — explain the math, walk through the RFCs, and then show you exactly how we implement it in TwoFac.</p>
<blockquote>
<p><strong>Note:</strong> This post focuses only on OTP generation — how we take a shared secret and turn it into those familiar 6-digit codes. How we <em>store</em> secrets securely (PBKDF2, AES-GCM, the <code>accounts.json</code> file) is a story for the next post.</p>
</blockquote>
<h2><strong>Why Do We Even Need a "Second Factor"?</strong></h2>
<p>Passwords alone are fundamentally brittle. They get reused, phished, leaked in data breaches, and brute-forced. The core idea behind two-factor authentication (2FA) is to combine <strong>something you know</strong> (your password) with <strong>something you have</strong> (your phone/authenticator app).</p>
<p>Even if an attacker steals your password, they'd also need physical access to your device to get the current OTP. And because the OTP changes every 30 seconds, a stolen code becomes useless almost immediately.</p>
<p>The beauty of TOTP-based 2FA is that it works <strong>offline</strong> — your authenticator app doesn't need to call any server. Both your app and the server independently compute the same code from a shared secret and the current time. No SMS, no push notification, no network dependency. Just math.</p>
<h2><strong>Starting from the Foundation: HOTP</strong></h2>
<p>Before we can understand TOTP, we need to understand <strong>HOTP</strong> — HMAC-based One-Time Password, defined in <a href="https://datatracker.ietf.org/doc/html/rfc4226">RFC 4226</a>.</p>
<p>HOTP is conceptually simple: you have a <strong>shared secret</strong> and a <strong>counter</strong>. You feed both into an HMAC function, then extract a human-friendly number from the result.</p>
<h3><strong>The HOTP Algorithm Step-by-Step</strong></h3>
<p>Here's the formula:</p>
<pre><code class="language-plaintext">HOTP(K, C) = Truncate(HMAC-SHA1(K, C)) mod 10^d
</code></pre>
<p>Where:</p>
<ul>
<li><p><strong>K</strong> = the shared secret key (a byte array)</p>
</li>
<li><p><strong>C</strong> = an 8-byte counter value (big-endian)</p>
</li>
<li><p><strong>d</strong> = the number of digits in the output (usually 6)</p>
</li>
</ul>
<p>Let's break each step down.</p>
<h4><strong>Step 1: Counter to Bytes</strong></h4>
<p>The counter is a 64-bit integer. We convert it to an 8-byte array in big-endian order. So counter <code>5</code> becomes:</p>
<pre><code class="language-plaintext">[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05]
</code></pre>
<h4><strong>Step 2: HMAC-SHA1</strong></h4>
<p>We compute <a href="https://datatracker.ietf.org/doc/html/rfc2104">HMAC</a> using the shared secret as the key and the counter bytes as the message. HMAC-SHA1 produces a <strong>20-byte</strong> (160-bit) hash.</p>
<p>HMAC itself is defined as:</p>
<pre><code class="language-plaintext">HMAC(K, text) = H((K' ⊕ opad) || H((K' ⊕ ipad) || text))
</code></pre>
<p>Where <code>ipad</code> = <code>0x36</code> repeated, <code>opad</code> = <code>0x5c</code> repeated, and <code>K'</code> is the key padded to the hash's block size (64 bytes for SHA-1). Don't worry if this looks intimidating — in practice, every crypto library handles this for you.</p>
<h4><strong>Step 3: Dynamic Truncation</strong></h4>
<p>Here's where it gets clever. We have a 20-byte HMAC result, but we need a 6-digit number. RFC 4226 defines a <strong>dynamic truncation</strong> algorithm:</p>
<ol>
<li><p>Take the <strong>last byte</strong> of the HMAC and look at its lowest 4 bits. This gives you an <strong>offset</strong> (0–15).</p>
</li>
<li><p>Starting at that offset, extract <strong>4 consecutive bytes</strong>.</p>
</li>
<li><p>Mask off the most significant bit (to avoid sign issues), giving you a <strong>31-bit unsigned integer</strong>.</p>
</li>
</ol>
<pre><code class="language-plaintext">offset = hmac[19] &amp; 0x0F

code = ((hmac[offset]     &amp; 0x7F) &lt;&lt; 24)
     | ((hmac[offset + 1] &amp; 0xFF) &lt;&lt; 16)
     | ((hmac[offset + 2] &amp; 0xFF) &lt;&lt; 8)
     |  (hmac[offset + 3] &amp; 0xFF)
</code></pre>
<h4><strong>Step 4: Modulo</strong></h4>
<p>Finally, take that 31-bit integer modulo <code>10^digits</code> and pad with leading zeros:</p>
<pre><code class="language-plaintext">otp = code % 1000000  →  "038314"
</code></pre>
<h3><strong>Visualizing the HOTP Flow</strong></h3>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/1ed8f45d-a950-44d2-9a7a-6f28c6e22415.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>HOTP's Limitation: The Counter Sync Problem</strong></h3>
<p>With HOTP, both the client and server maintain a synchronized counter. Every time you generate a code, the counter increments. But what if you generate a code and <em>don't</em> use it? Your counter advances, but the server's doesn't. Now they're out of sync.</p>
<p>Servers deal with this by checking a "look-ahead window" — they'll try counter values <code>C</code>, <code>C+1</code>, <code>C+2</code>, ... up to some limit. But it's fragile. And a stolen HOTP code remains valid indefinitely until it's used or the counter advances past it.</p>
<p>This is exactly the problem TOTP solves.</p>
<h2><strong>Enter TOTP: Time as the Counter</strong></h2>
<p><strong>TOTP</strong> (<a href="https://datatracker.ietf.org/doc/html/rfc6238">RFC 6238</a>) is HOTP, but with one elegant twist: instead of maintaining a counter, we <strong>derive the counter from the current time</strong>.</p>
<pre><code class="language-plaintext">T = floor((CurrentUnixTime - T0) / TimeStep)
</code></pre>
<p>Where:</p>
<ul>
<li><p><strong>CurrentUnixTime</strong> = seconds since the Unix epoch (1970-01-01 00:00:00 UTC)</p>
</li>
<li><p><strong>T0</strong> = the reference time (usually 0, i.e., the epoch itself)</p>
</li>
<li><p><strong>TimeStep</strong> = how often the code changes (usually <strong>30 seconds</strong>)</p>
</li>
</ul>
<p>Then we simply call HOTP with <code>T</code> as the counter:</p>
<pre><code class="language-plaintext">TOTP(K, T) = HOTP(K, T)
</code></pre>
<p>That's it. The entire TOTP algorithm is just "replace the counter with a time-derived value."</p>
<h3><strong>Why 30 Seconds?</strong></h3>
<p>The 30-second window is a balance between security and usability. Shorter windows (like 10 seconds) would be more secure but give users barely enough time to read and type the code. Longer windows (like 60 seconds) give attackers more time to use a stolen code. 30 seconds is the sweet spot recommended by <a href="https://datatracker.ietf.org/doc/html/rfc6238">RFC 6238</a>.</p>
<h3><strong>Clock Drift Tolerance</strong></h3>
<p>What if the user's phone clock is slightly off? Servers typically accept OTPs from the <strong>previous</strong>, <strong>current</strong>, and <strong>next</strong> time windows — effectively creating a 90-second acceptance window. This is enough to handle minor clock drift without meaningfully reducing security.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/48c75c63-f0c5-416f-bfb6-4e604d5f5172.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>TOTP Supports More Hash Algorithms</strong></h3>
<p>While HOTP (RFC 4226) was specified with SHA-1 only, TOTP (RFC 6238) explicitly supports:</p>
<table>
<thead>
<tr>
<th>Algorithm</th>
<th>HMAC Output</th>
<th>RFC 6238 Test Vectors</th>
</tr>
</thead>
<tbody><tr>
<td>HMAC-SHA1</td>
<td>20 bytes</td>
<td>✅ Yes</td>
</tr>
<tr>
<td>HMAC-SHA256</td>
<td>32 bytes</td>
<td>✅ Yes</td>
</tr>
<tr>
<td>HMAC-SHA512</td>
<td>64 bytes</td>
<td>✅ Yes</td>
</tr>
</tbody></table>
<p>The dynamic truncation algorithm works the same regardless of hash output size — it always uses the last byte to find the offset, then extracts 4 bytes. The only difference is the range of possible offsets (0–15 for SHA-1 with 20 bytes, 0–15 for SHA-256 with 32 bytes, 0–15 for SHA-512 with 64 bytes — the offset is always 4 bits, so always 0–15).</p>
<p>In practice, the overwhelming majority of services use SHA-1. But some security-conscious services (like some crypto exchanges) use SHA-256 or SHA-512 for a larger HMAC output.</p>
<h2><strong>How TOTP Provisioning Works: QR Codes and</strong> <code>otpauth://</code> <strong>URIs</strong></h2>
<p>When you enable 2FA on a website, this is what happens under the hood:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/0d7eff05-2fea-4682-aaaf-9d6a4e8b58e4.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Anatomy of an</strong> <code>otpauth://</code> <strong>URI</strong></h3>
<p>The QR code encodes a URI in the <code>otpauth://</code> <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">format</a>, formalized in the IETF draft <a href="https://datatracker.ietf.org/doc/draft-linuxgemini-otpauth-uri/">draft-linuxgemini-otpauth-uri</a>:</p>
<pre><code class="language-plaintext">otpauth://totp/GitHub:alice@example.com?secret=JBSWY3DPEHPK3PXP&amp;issuer=GitHub&amp;algorithm=SHA1&amp;digits=6&amp;period=30
</code></pre>
<p>Let's break it down:</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Example</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Scheme</strong></td>
<td><code>otpauth://</code></td>
<td>The URI scheme — tells the app this is an OTP credential</td>
</tr>
<tr>
<td><strong>Type</strong></td>
<td><code>totp</code></td>
<td>Either <code>totp</code> or <code>hotp</code></td>
</tr>
<tr>
<td><strong>Label</strong></td>
<td><code>GitHub:alice@example.com</code></td>
<td>The issuer and account, separated by <code>:</code></td>
</tr>
<tr>
<td><strong>secret</strong></td>
<td><code>JBSWY3DPEHPK3PXP</code></td>
<td>The shared secret, <a href="https://datatracker.ietf.org/doc/html/rfc4648">Base32</a>-encoded (no padding)</td>
</tr>
<tr>
<td><strong>issuer</strong></td>
<td><code>GitHub</code></td>
<td>The service name (displayed in the app)</td>
</tr>
<tr>
<td><strong>algorithm</strong></td>
<td><code>SHA1</code></td>
<td>Hash algorithm: <code>SHA1</code>, <code>SHA256</code>, or <code>SHA512</code> (default: SHA1)</td>
</tr>
<tr>
<td><strong>digits</strong></td>
<td><code>6</code></td>
<td>Number of digits: 6 or 8 (default: 6)</td>
</tr>
<tr>
<td><strong>period</strong></td>
<td><code>30</code></td>
<td>TOTP time step in seconds (default: 30)</td>
</tr>
<tr>
<td><strong>counter</strong></td>
<td><em>(HOTP only)</em></td>
<td>Initial counter value for HOTP</td>
</tr>
</tbody></table>
<p>The <code>secret</code> parameter is the star of the show — it's the shared secret that both the server and your authenticator app use to independently generate matching codes. It's Base32-encoded because Base32 uses only uppercase letters and digits 2–7, making it easy to display and manually type if QR scanning fails.</p>
<h2><strong>How Do Other Authenticator Apps Store Secrets?</strong></h2>
<p>I found it really interesting to research how different authenticator apps handle OTP secret storage and backup. There's a <strong>huge</strong> range — from "barely encrypted" to "enterprise-grade security":</p>
<h3><strong>Google Authenticator</strong></h3>
<p>Google Authenticator originally stored secrets in a <strong>plaintext SQLite database</strong> on Android — no encryption at all. If your device was rooted, anyone could read all your 2FA secrets directly. Their cloud sync feature (added in 2023) <a href="https://www.androidauthority.com/google-authenticator-e2e-encryption-3317498/">initially transferred secrets without end-to-end encryption</a>, meaning Google's servers could theoretically read them. They've since <a href="https://security.googleblog.com/2023/04/google-authenticator-now-supports.html">added E2EE to cloud sync</a>, but the local storage on Android still relies on the OS-level sandboxing rather than application-level encryption.</p>
<h3><strong>Microsoft Authenticator</strong></h3>
<p>Microsoft Authenticator takes security more seriously. On iOS, secrets are stored in the <a href="https://developer.apple.com/documentation/security/keychain-services">Keychain</a>, and on Android they use <a href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">encrypted shared preferences</a>. Cloud backups are encrypted — iCloud backups are protected by Keychain, and Android backups go to Microsoft's servers with account-based encryption.</p>
<h3><strong>Authy (Twilio)</strong></h3>
<p>Authy was one of the first authenticator apps to offer cloud backup. They encrypt secrets using a user-provided "backups password" (processed via <a href="https://datatracker.ietf.org/doc/html/rfc2898">PBKDF2</a> or similar KDF) before uploading. The downside? The app is proprietary and <a href="https://www.twilio.com/docs/authy">was sunset in 2024</a>, with users urged to migrate. Also, in 2024, Twilio <a href="https://techcrunch.com/2024/07/03/twilio-says-hackers-identified-cell-phone-numbers-of-two-factor-app-authy-users/">disclosed a breach</a> where phone numbers associated with Authy accounts were leaked (though not the secrets themselves).</p>
<h3><strong>1Password &amp; Bitwarden</strong></h3>
<p>Both 1Password and Bitwarden store TOTP secrets as part of their broader encrypted vault. They use <a href="https://csrc.nist.gov/publications/detail/sp/800-38d/final">AES-256-GCM</a> with keys derived from your master password via <a href="https://datatracker.ietf.org/doc/html/rfc2898">PBKDF2</a> (Bitwarden) or <a href="https://www.rfc-editor.org/rfc/rfc9106.html">Argon2</a> (1Password). The secrets are just another encrypted field in your vault item. Both have been <a href="https://bitwarden.com/help/is-bitwarden-audited/">independently audited</a> and are open-source (Bitwarden fully, 1Password's crypto components partially).</p>
<h3><strong>2FAS</strong></h3>
<p><a href="https://2fas.com/">2FAS</a> is fully open-source and stores secrets encrypted on the device. They use a user-provided password to derive an encryption key. Backups are encrypted files that you control — no mandatory cloud. Their <a href="https://github.com/twofas">source code is available on GitHub</a>, which is a huge plus for auditability.</p>
<h3><strong>Ente Auth</strong></h3>
<p><a href="https://ente.io/auth/">Ente Auth</a> is another open-source contender. They use end-to-end encryption with <a href="https://doc.libsodium.org/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction">XChaCha20-Poly1305</a> for their cloud sync, with keys derived from the user's password via Argon2. Their code is <a href="https://github.com/ente-io/ente">fully open-source</a> and has been independently audited.</p>
<h3><strong>Summary Table</strong></h3>
<table>
<thead>
<tr>
<th>App</th>
<th>Encryption at Rest</th>
<th>Cloud Backup</th>
<th>Backup Encryption</th>
<th>Open Source</th>
</tr>
</thead>
<tbody><tr>
<td>Google Authenticator</td>
<td>OS sandbox only</td>
<td>Optional (Google)</td>
<td>E2EE (added later)</td>
<td>❌</td>
</tr>
<tr>
<td>Microsoft Authenticator</td>
<td>Keychain / EncryptedPrefs</td>
<td>iCloud/Microsoft</td>
<td>Account-based</td>
<td>❌</td>
</tr>
<tr>
<td>Authy</td>
<td>Encrypted</td>
<td>Yes (Twilio)</td>
<td>Password-based</td>
<td>❌</td>
</tr>
<tr>
<td>1Password</td>
<td>AES-256-GCM</td>
<td>Yes (1Password)</td>
<td>Master password</td>
<td>Partial</td>
</tr>
<tr>
<td>Bitwarden</td>
<td>AES-256-GCM</td>
<td>Yes (Bitwarden)</td>
<td>Master password</td>
<td>✅</td>
</tr>
<tr>
<td>2FAS</td>
<td>Encrypted</td>
<td>Optional (file)</td>
<td>Password-based</td>
<td>✅</td>
</tr>
<tr>
<td>Ente Auth</td>
<td>XChaCha20-Poly1305</td>
<td>Yes (Ente)</td>
<td>E2EE (Argon2)</td>
<td>✅</td>
</tr>
<tr>
<td><strong>TwoFac</strong></td>
<td>AES-256-GCM</td>
<td>Optional (file)</td>
<td>Password-based (PBKDF2)</td>
<td>✅</td>
</tr>
</tbody></table>
<h2><strong>Diving Into Our Codebase</strong></h2>
<p>Now that we understand the theory, let's look at how TwoFac implements all of this. All the OTP logic lives in our <code>sharedLib</code> module — the pure Kotlin Multiplatform library that powers every platform (Android, iOS, Desktop, Web, CLI, Wear OS).</p>
<h3><strong>The Crypto Layer:</strong> <code>cryptography-kotlin</code></h3>
<p>We use <a href="https://github.com/whyoleg/cryptography-kotlin">cryptography-kotlin</a> by <a href="https://github.com/whyoleg">whyoleg</a> — specifically <code>dev.whyoleg.cryptography</code> v0.5.0 — for all our cryptographic primitives. The beauty of this library is that it wraps <strong>platform-native</strong> crypto providers:</p>
<table>
<thead>
<tr>
<th>Platform</th>
<th>Crypto Provider</th>
</tr>
</thead>
<tbody><tr>
<td>JVM (Android, Desktop, Wear OS)</td>
<td>JDK (<code>java.security</code> / <code>javax.crypto</code>)</td>
</tr>
<tr>
<td>Native (iOS, macOS, Linux CLI)</td>
<td>OpenSSL 3</td>
</tr>
<tr>
<td>Web (Wasm)</td>
<td><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API">Web Crypto API</a></td>
</tr>
</tbody></table>
<p>This means we're not shipping a custom crypto implementation — on each platform, we're delegating to the <strong>battle-tested, OS-level crypto</strong> that's already there. Here's how the dependencies are configured in <code>sharedLib/build.gradle.kts</code>:</p>
<pre><code class="language-kotlin">// commonMain
implementation(libs.crypto.kt.core)       // Core API

// jvmMain
implementation(libs.crypto.kt.jdk)        // → java.security / javax.crypto

// nativeMain (iOS, macOS, Linux)
implementation(libs.crypto.kt.openssl)    // → OpenSSL 3

// wasmJsMain (Web, Browser Extension)
implementation(libs.crypto.kt.web)        // → Web Crypto API
</code></pre>
<h3><strong>The CryptoTools Interface</strong></h3>
<p>All cryptographic operations are abstracted behind a <code>CryptoTools</code> interface:</p>
<pre><code class="language-kotlin">interface CryptoTools {
    enum class Algo { SHA1, SHA256, SHA512 }

    suspend fun hmacSha(algorithm: Algo, key: ByteString, data: ByteString): ByteString
    suspend fun createSigningKey(passKey: String, salt: ByteString? = null): SigningKey
    suspend fun encrypt(key: ByteString, secret: ByteString): ByteString
    suspend fun decrypt(encryptedData: ByteString, key: ByteString): ByteString
}
</code></pre>
<p>Notice everything is a <code>suspend fun</code> — this isn't just a Kotlin convention. On some platforms (especially WebCrypto), cryptographic operations are <strong>inherently asynchronous</strong>. By making the interface suspend-based, we can use the native async crypto APIs without blocking.</p>
<p>The <code>DefaultCryptoTools</code> implementation wires everything to <code>cryptography-kotlin</code>:</p>
<pre><code class="language-kotlin">class DefaultCryptoTools(val cryptoProvider: CryptographyProvider) : CryptoTools {

    val hmac = cryptoProvider.get(HMAC)

    override suspend fun hmacSha(
        algorithm: CryptoTools.Algo,
        key: ByteString,
        data: ByteString
    ): ByteString {
        val keyDecoder = when (algorithm) {
            CryptoTools.Algo.SHA1 -&gt; hmac.keyDecoder(SHA1)
            CryptoTools.Algo.SHA256 -&gt; hmac.keyDecoder(SHA256)
            CryptoTools.Algo.SHA512 -&gt; hmac.keyDecoder(SHA512)
        }
        val hmacKey = keyDecoder.decodeFromByteString(HMAC.Key.Format.RAW, key)
        val signature = hmacKey.signatureGenerator().generateSignature(data.toByteArray())
        return ByteString(signature)
    }
}
</code></pre>
<h3><strong>The OTP Interface</strong></h3>
<p>Both HOTP and TOTP implement a common <code>OTP</code> interface:</p>
<pre><code class="language-kotlin">interface OTP {
    val digits: Int                      // 6 or 8
    val algorithm: CryptoTools.Algo      // SHA1, SHA256, SHA512
    val secret: String                   // Base32-encoded
    val accountName: String              // e.g. "alice@example.com"
    val issuer: String?                  // e.g. "GitHub"

    suspend fun generateOTP(counter: Long): String
    suspend fun validateOTP(otp: String, counter: Long): Boolean
}
</code></pre>
<h3><strong>Our HOTP Implementation</strong></h3>
<p>The <code>HOTP</code> class is a direct implementation of RFC 4226:</p>
<pre><code class="language-kotlin">class HOTP(
    override val digits: Int = 6,
    override val algorithm: CryptoTools.Algo = CryptoTools.Algo.SHA1,
    override val secret: String,
    override val accountName: String,
    override val issuer: String?,
) : OTP {

    private val cryptoTools = DefaultCryptoTools(CryptographyProvider.Default)

    override suspend fun generateOTP(counter: Long): String {
        // 1. Counter → 8 bytes (big-endian)
        val counterBytes = ByteArray(8) { i -&gt;
            ((counter shr ((7 - i) * 8)) and 0xFF).toByte()
        }

        // 2. Decode the Base32 secret
        val secretBytes = Encoding.decodeBase32(secret)

        // 3. HMAC
        val hmac = cryptoTools.hmacSha(
            algorithm,
            ByteString(secretBytes),
            ByteString(counterBytes)
        )

        // 4. Dynamic Truncation
        val fourBytes = dynamicTruncate(hmac)

        // 5. Modulo → pad with zeros
        val otp = fourBytes % 10.0.pow(digits.toDouble()).toInt()
        return otp.toString().padStart(digits, '0')
    }
}
</code></pre>
<p>The dynamic truncation method follows the RFC exactly:</p>
<pre><code class="language-kotlin">internal fun dynamicTruncate(hmac: ByteString): Int {
    // Last byte's lowest 4 bits = offset (0-15)
    val offset = (hmac.get(hmac.size - 1) and 0x0F).toInt()

    // Extract 4 bytes, mask the MSB for a 31-bit unsigned int
    return ((hmac.get(offset).toInt() and 0x7F) shl 24) or
           ((hmac.get(offset + 1).toInt() and 0xFF) shl 16) or
           ((hmac.get(offset + 2).toInt() and 0xFF) shl 8) or
            (hmac.get(offset + 3).toInt() and 0xFF)
}
</code></pre>
<p>One thing I want to highlight: our <code>dynamicTruncate</code> uses <code>hmac.size - 1</code> instead of hardcoding <code>19</code> (like you'd see in SHA-1-only implementations). This makes it work correctly with SHA-256 (32 bytes) and SHA-512 (64 bytes) too.</p>
<h3><strong>Our TOTP Implementation</strong></h3>
<p>The <code>TOTP</code> class is remarkably simple because it just wraps HOTP:</p>
<pre><code class="language-kotlin">class TOTP(
    override val digits: Int = 6,
    override val algorithm: CryptoTools.Algo = CryptoTools.Algo.SHA1,
    override val secret: String,
    override val accountName: String,
    override val issuer: String?,
    private val baseTime: Long = 0,    // Unix epoch
    val timeInterval: Long = 30        // 30 seconds
) : OTP {

    // Reuse HOTP internally!
    private val hotp = HOTP(digits, algorithm, secret, accountName, issuer)

    private fun timeToCounter(currentTime: Long): Long {
        return (currentTime - baseTime) / timeInterval
    }

    override suspend fun generateOTP(currentTime: Long): String {
        val counter = timeToCounter(currentTime)
        return hotp.generateOTP(counter)
    }

    override suspend fun validateOTP(otp: String, currentTime: Long): Boolean {
        val counter = timeToCounter(currentTime)
        // Check previous, current, and next windows (±1)
        return hotp.validateOTP(otp, counter - 1) ||
               hotp.validateOTP(otp, counter) ||
               hotp.validateOTP(otp, counter + 1)
    }
}
</code></pre>
<p>This is one of my favorite parts of the codebase. The <code>TOTP</code> class doesn't duplicate <em>any</em> crypto logic — it just converts time into a counter and delegates to <code>HOTP</code>. The <code>validateOTP</code> method checks ±1 time windows (the standard tolerance for clock drift).</p>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/19d51e2e-7627-4f4b-8419-2699ebd738c1.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Parsing</strong> <code>otpauth://</code> <strong>URIs</strong></h3>
<p>When you scan a QR code, we need to parse the <code>otpauth://</code> URI and create the right OTP object. This is handled by <code>OtpAuthURI</code>:</p>
<pre><code class="language-kotlin">object OtpAuthURI {
    fun parse(uri: String): OTP {
        require(uri.startsWith("otpauth://"))

        // Extract type: "totp" or "hotp"
        val typeStr = uri.substring(10, uri.indexOf("/", 10))

        // Extract label: "GitHub:alice@example.com"
        val label = uri.substring(uri.indexOf("/", 10) + 1, uri.indexOf("?"))
        val labelIssuer = label.substringBefore(":", "")
        val accountName = label.substringAfter(":", label).trim()

        // Parse query parameters
        val params = paramsStr.split("&amp;").associate { /* ... */ }

        val secret = params["secret"]!!
        val algorithm = when (params["algorithm"]?.uppercase()) {
            "SHA256" -&gt; CryptoTools.Algo.SHA256
            "SHA512" -&gt; CryptoTools.Algo.SHA512
            else -&gt; CryptoTools.Algo.SHA1  // Default
        }

        return when (type) {
            Type.TOTP -&gt; TOTP(
                digits = params["digits"]?.toIntOrNull() ?: 6,
                algorithm = algorithm,
                secret = secret,
                timeInterval = params["period"]?.toLongOrNull() ?: 30L,
                accountName = accountName,
                issuer = issuer
            )
            Type.HOTP -&gt; HOTP(/* ... */)
        }
    }
}
</code></pre>
<p>The parser also has a <code>Builder</code> that works in reverse — given an <code>OTP</code> object, it constructs the <code>otpauth://</code> URI. This is used when exporting or backing up accounts.</p>
<h3><strong>Base32 Encoding</strong></h3>
<p>OTP secrets in the <code>otpauth://</code> URI are Base32-encoded per <a href="https://datatracker.ietf.org/doc/html/rfc4648">RFC 4648</a>. We implement our own Base32 encoder/decoder in <code>Encoding.kt</code>:</p>
<pre><code class="language-kotlin">object Encoding {
    const val ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"

    fun decodeBase32(base32: String): ByteArray {
        val cleanInput = base32.replace("=", "").uppercase()
        // Each Base32 character represents 5 bits
        // We accumulate bits in a buffer and emit bytes when we have 8+
        var buffer = 0
        var bitsLeft = 0
        /* ... */
    }
}
</code></pre>
<p>Why Base32 instead of Base64? Base32 uses only uppercase letters and digits 2–7, so there's no ambiguity between <code>0</code>/<code>O</code>, <code>1</code>/<code>l</code>/<code>I</code>, or <code>+</code>/<code>/</code>. This matters because users might need to manually type the secret if QR scanning fails.</p>
<h3><strong>How We Store OTP Accounts</strong></h3>
<p>Each OTP account is stored as a <code>StoredAccount</code>:</p>
<pre><code class="language-kotlin">@Serializable
data class StoredAccount(
    val accountID: Uuid,          // Unique identifier
    val accountLabel: String,     // Display name (e.g. "GitHub - alice")
    val salt: String,             // Salt for key derivation
    val encryptedURI: String,     // The otpauth:// URI, encrypted with AES-GCM
)
</code></pre>
<p>Notice that we store the <strong>encrypted</strong> <code>otpauth://</code> URI, not the raw secret. The entire URI (which contains the secret, algorithm, issuer, etc.) is encrypted with AES-256-GCM using a key derived from the user's password via PBKDF2. Each account gets its own salt.</p>
<p>This means even if someone gets access to the storage file, they can't read any OTP secrets without the user's password. But that's a topic for the next post — where we'll dive deep into PBKDF2, AES-GCM, and the <code>accounts.json</code> storage format.</p>
<h2><strong>Verifying Against the RFCs</strong></h2>
<p>One thing I'm particularly proud of is that our test suite validates against the <strong>official RFC test vectors</strong>. Here's a snippet from our <a href="https://github.com/championswimmer/TwoFac/tree/main/sharedLib/src/commonTest/kotlin/tech/arnav/twofac/lib/otp/HOTPTest.kt">HOTP tests</a>:</p>
<pre><code class="language-kotlin">// Secret: "12345678901234567890" (ASCII)
// Base32: "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
val expectedOTPs = listOf(
    "755224", // Counter = 0
    "287082", // Counter = 1
    "359152", // Counter = 2
    "969429", // Counter = 3
    "338314", // Counter = 4
    "254676", // Counter = 5
    "287922", // Counter = 6
    "162583", // Counter = 7
    "399871", // Counter = 8
    "520489"  // Counter = 9
)
</code></pre>
<p>These are the exact test vectors from <a href="https://datatracker.ietf.org/doc/html/rfc4226#appendix-D">RFC 4226 Appendix D</a>. Our <a href="https://github.com/championswimmer/TwoFac/tree/main/sharedLib/src/commonTest/kotlin/tech/arnav/twofac/lib/otp/TOTPTest.kt">TOTP tests</a> go even further, validating against <a href="https://datatracker.ietf.org/doc/html/rfc6238#appendix-B">RFC 6238's test vectors</a> with all three hash algorithms:</p>
<pre><code class="language-kotlin">// RFC 6238 test vectors with 8-digit codes
RFC6238TestVector(59, "1970-01-01 00:00:59", key_sha1, SHA1, "94287082"),
RFC6238TestVector(59, "1970-01-01 00:00:59", key_sha256, SHA256, "46119246"),
RFC6238TestVector(59, "1970-01-01 00:00:59", key_sha512, SHA512, "90693936"),
// ... and more timestamps across decades
</code></pre>
<p>If our implementation matches the RFC test vectors on every platform (JVM, Native, Wasm), we can be confident we're computing OTPs correctly.</p>
<h2><strong>The Full Picture</strong></h2>
<p>Here's how everything fits together in TwoFac — from scanning a QR code to displaying the 6-digit code on your screen:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/b8821533-7a62-46ba-add8-7d63c8dd08e8.png" alt="" style="display:block;margin:0 auto" />

<h2><strong>What's Next?</strong></h2>
<p>In this post we covered the <em>generation</em> side of things — how a shared secret and the current time produce those familiar 6-digit codes. But generation is only half the story.</p>
<p>In the next post, we'll explore the <em>storage</em> side: how TwoFac protects your secrets at rest using <strong>PBKDF2</strong> for key derivation and <strong>AES-256-GCM</strong> for authenticated encryption. We'll walk through the <code>accounts.json</code> format, why we chose the iteration counts we did, and how the encryption pipeline works across all platforms.</p>
<hr />
<h3><strong>Links and References</strong></h3>
<ul>
<li><p><a href="https://github.com/championswimmer/TwoFac">TwoFac GitHub Repository</a></p>
</li>
<li><p><a href="https://datatracker.ietf.org/doc/html/rfc4226">RFC 4226 — HOTP: An HMAC-Based One-Time Password Algorithm</a></p>
</li>
<li><p><a href="https://datatracker.ietf.org/doc/html/rfc6238">RFC 6238 — TOTP: Time-Based One-Time Password Algorithm</a></p>
</li>
<li><p><a href="https://datatracker.ietf.org/doc/html/rfc2104">RFC 2104 — HMAC: Keyed-Hashing for Message Authentication</a></p>
</li>
<li><p><a href="https://datatracker.ietf.org/doc/html/rfc4648">RFC 4648 — Base Encodings (Base32)</a></p>
</li>
<li><p><a href="https://datatracker.ietf.org/doc/draft-linuxgemini-otpauth-uri/">draft-linuxgemini-otpauth-uri — The otpauth URI Format</a></p>
</li>
<li><p><a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Google Authenticator Key URI Format</a></p>
</li>
<li><p><a href="https://github.com/whyoleg/cryptography-kotlin">cryptography-kotlin by whyoleg</a></p>
</li>
<li><p><a href="https://csrc.nist.gov/publications/detail/sp/800-38d/final">NIST SP 800-38D — AES-GCM</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Architecting TwoFac: My Journey into Kotlin Multiplatform Module Structure]]></title><description><![CDATA[In my previous post, I talked about why I'm building TwoFac. The short version? I got tired of proprietary "digital cages" like Authy and wanted an authenticator that was open, secure, and—most import]]></description><link>https://arnav.tech/architecting-twofac-my-journey-into-kotlin-multiplatform-module-structure</link><guid isPermaLink="true">https://arnav.tech/architecting-twofac-my-journey-into-kotlin-multiplatform-module-structure</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Kotlin Multiplatform]]></category><category><![CDATA[compose]]></category><category><![CDATA[OTP Verification]]></category><category><![CDATA[authentication]]></category><category><![CDATA[Security]]></category><category><![CDATA[password manager]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Tue, 03 Mar 2026 01:44:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/0458cc78-ce8c-4b2b-a899-c4990cb67e64.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my <a href="https://arnav.tech/my-next-project-building-the-open-source-cross-platform-authenticator-i-always-wanted">previous post</a>, I talked about why I'm building <strong>TwoFac</strong>. The short version? I got tired of proprietary "digital cages" like Authy and wanted an authenticator that was open, secure, and—most importantly—everywhere I am.</p>
<p>When I started sketching out the project, I knew I wanted it to run on everything from my Android phone and Apple Watch to my Linux terminal and Chrome browser. But as usually happens during such "yak shaving sessions," I spent a good chunk of time just thinking about the architecture. How do you share code between a SwiftUI Watch app, a Wasm-based browser extension, and a native Linux CLI without losing your mind?</p>
<p>Today, I want to walk you through the technical decisions and the module structure that makes TwoFac possible.</p>
<h2>The Core Philosophy: Logic as a Library</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/0131d884-636b-41d4-9202-eb71db3d6455.jpg" alt="" style="display:block;margin:0 auto" />

<p>A mistake I see often in cross-platform development is trying to force a single UI framework onto every device. While Compose Multiplatform is amazing (and we use it!), sometimes you just want a native experience—like on the tiny circular screen of a watch.</p>
<p>I decided that the "brain" of TwoFac—the part that handles TOTP/HOTP generation, <code>otpauth://</code> parsing, and encryption—should be a pure, logic-only library. It shouldn't know or care about buttons, screens, or viewmodels.</p>
<p>This became our <code>sharedLib</code> module.</p>
<h3>How the Platforms Connect to the Core</h3>
<p>Because <code>sharedLib</code> is pure Kotlin Multiplatform, it gets exported in formats native to every ecosystem we target. But how do the actual applications consume it? This is where the module structure gets interesting.</p>
<p><strong>The</strong> <code>composeApp</code> <strong>Bridge:</strong> Most of our visual applications—Android, Desktop, and Web—reside in the <code>composeApp</code> module. This module depends directly on <code>sharedLib</code> as a standard Kotlin library. It acts as the shared UI layer, using Compose Multiplatform to draw the screens.</p>
<ul>
<li><p><strong>Desktop:</strong> The <code>composeApp</code> can be compiled into native desktop GUI applications, packaged as standard installers (<code>.dmg</code> for macOS, <code>.msi</code> for Windows, <code>.deb</code> for Linux).</p>
</li>
<li><p><strong>Web (WasmJS):</strong> For the web, <code>composeApp</code> uses the new <code>wasmJs</code> target. This allows us to deploy the app as a downloadable Progressive Web App (PWA) running via an <code>index.html</code>. Incredibly, we use the exact same compiled Wasm binary to power the <code>popup.html</code> and <code>sidepanel.html</code> of our Chrome and Firefox browser extensions!</p>
</li>
</ul>
<p><strong>Thin Wrappers for Mobile:</strong> If <code>composeApp</code> holds the UI, how do the mobile apps work? We have very thin <code>androidApp</code> and <code>iosApp</code> modules that essentially just "boot up" the shared UI.</p>
<ul>
<li><p>For Android, <code>androidApp</code> just contains the <code>MainActivity</code> that calls the Compose content. With the recent Android Gradle Plugin (AGP) 9.0 updates, keeping the Android application plugin (<code>com.android.application</code>) separate from the Kotlin Multiplatform plugin (<code>org.jetbrains.kotlin.multiplatform</code>) is actually mandated, making this thin wrapper pattern not just good architecture, but a requirement.</p>
</li>
<li><p>Similarly, <code>iosApp</code> is just a standard Xcode project with a Swift entry point that delegates to the shared Compose UI framework.</p>
</li>
</ul>
<p><strong>The Pure Native CLI (</strong><code>cliApp</code><strong>):</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/f892d427-aef1-47e6-8184-f519b3120321.jpg" alt="" style="display:block;margin:0 auto" />

<p>Our CLI tool is completely separate from <code>composeApp</code>. It depends directly on <code>sharedLib</code>. But here is the cool part: <code>cliApp</code> uses Kotlin/Native to compile into a pure native binary for Mac, Windows, and Linux. There is zero JVM involved when you run <code>2fac</code> in your terminal; it links against <code>sharedLib</code> as a native <code>klib</code>, making it incredibly fast.</p>
<p><strong>Why the Watches Stand Alone:</strong> You might notice we have separate <code>watchApp</code> (Android Wear) and iOS watch modules. Why not use <code>composeApp</code>? A full-blown Compose Multiplatform UI is heavy, and on a tiny circular screen, the UI needs are fundamentally different. I didn't want the watch apps to inherit the bloat of the main application. Instead, they are designed to be thin clients that simply display synced accounts. Therefore, they bypass <code>composeApp</code> entirely and depend directly on <code>sharedLib</code> to handle the decryption and TOTP generation locally, using native UI frameworks (like SwiftUI for the Apple Watch) for the best possible performance and look.</p>
<h3>Desktop and CLI Applications</h3>
<p>Our desktop and command-line interfaces are tailored for each major operating system. The CLI links directly to the <code>sharedLib</code> for instant native execution, while the graphical desktop apps use <code>composeApp</code> to render the UI.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/3d16ea49-b84c-4e8d-84ef-f5d284107c62.png" alt="" style="display:block;margin:0 auto" />

<h3>Mobile and Watch Applications</h3>
<p>For mobile devices, <code>composeApp</code> acts as the shared UI layer wrapped by very thin native application projects. However, the watch apps bypass the shared UI layer entirely, directly consuming <code>sharedLib</code> and using platform-native UIs (Compose for WearOS and SwiftUI for Apple Watch).</p>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/d6914e9e-8b0e-4345-9064-edd18674d435.png" alt="" style="display:block;margin:0 auto" />

<h3>Web App and Browser Extensions</h3>
<p>Leveraging the power of Wasm, the exact same <code>composeApp</code> output is used to generate our Progressive Web App as well as our browser extensions, ensuring complete feature parity across the web ecosystem.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/c37826b7-00bf-4bb1-a008-15a806defefc.png" alt="" style="display:block;margin:0 auto" />

<h2>The "Terminal First" Experience</h2>
<p>I use CLI tools extensively (lately, I've been living in tools like Claude Code), and I wanted TwoFac to feel like a native citizen of the terminal.</p>
<p>Since <code>sharedLib</code> can compile to native binaries, our <code>cliApp</code> is a lightning-fast tool with zero JVM overhead. I'm using <a href="https://ajalt.github.io/clikt/">Clikt</a> for command parsing and <a href="https://github.com/ajalt/mordant">Mordant</a> for those pretty terminal colors we all love.</p>
<p>When I run <code>2fac generate github</code>, I don't want to wait for a runtime to boot up. I want my code, and I want it now.</p>
<h2>Managing the Chaos with Version Catalogs</h2>
<p>If you've followed my blog for a while, you know <a href="https://arnav.tech/managing-libraries-and-dependencies-in-android-projects-with-gradle-version-catalog">I'm a big fan of Gradle Version Catalogs</a>. With 10+ targets and multiple modules, trying to keep versions in sync manually would be a nightmare.</p>
<p>Every single dependency in TwoFac is managed in <code>gradle/libs.versions.toml</code>. Whether it's the Kotlin version or a specific crypto provider, it's defined once.</p>
<pre><code class="language-toml">[versions]
kotlin = "2.3.10"
kstore = "2.0.4"
crypto-kt = "0.5.0"

[libraries]
kstore-core = { module = "tech.arnav:kstore", version.ref = "kstore" }
crypto-core = { module = "dev.whyoleg.cryptography:cryptography-core", version.ref = "crypto-kt" }
</code></pre>
<h2>Data Persistence: Enter KStore</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6261c5c8d82f4558aff863be/9c493acb-c735-4ef0-b609-974ad674bbe2.jpg" alt="" style="display:block;margin:0 auto" />

<p>One problem I faced early on was how to handle data storage. A browser extension uses <code>localStorage</code>, a mobile app uses files (or DataStore), and a CLI tool might use a hidden folder in <code>$HOME</code>.</p>
<p>I absolutely love <a href="https://github.com/championswimmer/KStore">KStore</a> for this. It provides a unified, coroutine-based API for storage across all platforms. In TwoFac, we use it to save our <code>accounts.json</code>—the encrypted store for all your 2FA secrets—regardless of whether we're on a watch, a phone, or a server.</p>
<h3>Finding the "Right" Directory with AppDirs</h3>
<p>But where exactly should <code>accounts.json</code> live? On Windows, it should be in <code>AppData/Roaming</code>. On macOS, it's <code>Library/Application Support</code>. On Linux, it's usually <code>$HOME/.local/share</code>.</p>
<p>To handle this, I used the <a href="https://github.com/Syer123/kotlin-multiplatform-appdirs">Kotlin Multiplatform AppDirs</a> library. It's a KMP rewrite of the classic Java <code>AppDirs</code> library that helps you find these platform-specific directories without writing a single line of <code>expect/actual</code> code.</p>
<h3>A Fork for the Modern Age</h3>
<p>You might notice in our <code>libs.versions.toml</code> that we're using my own fork of KStore (<code>tech.arnav:kstore</code>).</p>
<p>While the original <a href="https://github.com/xxfast/KStore">xxfast/KStore</a> is excellent, it hadn't been updated to use the latest <strong>Kotlin Multiplatform Hierarchy Template</strong>. This new internal structure in Kotlin 1.9.20+ makes it much easier to share code between intermediate targets (like <code>appleMain</code> or <code>nativeMain</code>). I forked it to ensure TwoFac could benefit from the most modern Gradle setups and to add support for the <code>wasmJs</code> targets we need for our browser extensions.</p>
<h2>What's Next?</h2>
<p>By keeping the core logic decoupled from the UI, we've built an architecture that is both flexible and robust. The same security audits that apply to the CLI tool automatically apply to the Apple Watch app, because they are running the exact same code.</p>
<p>In the next post, I'll dive into the <strong>Cryptography</strong> side of things—how we use <code>cryptography-kotlin</code> to keep your secrets safe while making them accessible across all your devices.</p>
<hr />
<h3>Links and References</h3>
<ul>
<li><p><a href="https://github.com/championswimmer/TwoFac">TwoFac GitHub Repository</a></p>
</li>
<li><p><a href="https://www.jetbrains.com/lp/compose-multiplatform/">Compose Multiplatform Documentation</a></p>
</li>
<li><p><a href="https://kotlinlang.org/docs/multiplatform.html">Kotlin Multiplatform Official Guide</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How Personal AI Agents and Agent Orchestrators like OpenClaw or GasTown are Made]]></title><description><![CDATA[Over the last few months, projects like Gas Town by Steve Yegge and OpenClaw by Peter Steinberger have made “AI agent orchestrators” feel suddenly mainstream. It is tempting to treat them as a new kind of intelligence, but under the hood they are sti...]]></description><link>https://arnav.tech/how-personal-ai-agents-and-agent-orchestrators-like-openclaw-or-gastown-are-made</link><guid isPermaLink="true">https://arnav.tech/how-personal-ai-agents-and-agent-orchestrators-like-openclaw-or-gastown-are-made</guid><category><![CDATA[openclaw]]></category><category><![CDATA[Agent-Orchestration]]></category><category><![CDATA[ai-agent]]></category><category><![CDATA[#llmagents]]></category><category><![CDATA[skills]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Wed, 18 Feb 2026 02:12:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771380657943/79e3e11b-f27c-4c84-9f8c-c128bac53344.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over the last few months, projects like <a target="_blank" href="https://github.com/steveyegge/gastown"><strong>Gas Town</strong></a> by Steve Yegge and <a target="_blank" href="https://docs.openclaw.ai/concepts/architecture"><strong>OpenClaw</strong></a> by Peter Steinberger have made “AI agent orchestrators” feel suddenly mainstream. It is tempting to treat them as a new kind of intelligence, but under the hood they are still a small set of primitives wired together with discipline: an LLM API call, a state loop, tools, memory, and orchestration.</p>
<p>This raises a practical question: what is actually inside an “agent,” and how is it different from <a target="_blank" href="https://chatgpt.com/"><strong>ChatGPT</strong></a> (a chat UI over a model) or coding tools like <a target="_blank" href="https://code.claude.com/docs/en/overview"><strong>Claude Code</strong></a> (an agentic coding surface)? <a target="_blank" href="https://github.com/steveyegge/gastown">Gas Town’s README</a> frames it as a “multi‑agent orchestration system for Claude Code with persistent work tracking” — i.e., an orchestration layer on top of existing coding agents, not a new model class. Likewise, <a target="_blank" href="https://code.claude.com/docs/en/overview">Claude Code’s docs</a> describe it as an “agentic coding tool that reads your codebase, edits files, runs commands, and integrates with your development tools” — a powerful agent shell, not the core model itself.</p>
<p>In this article I’ll walk through each layer, show TypeScript code snippets, and then assemble a minimal reference implementation, using public docs as anchors. The goal is to make “agent” a concrete stack you can build and reason about, rather than a black box.</p>
<h2 id="heading-1-the-bare-metal-llm-call">1) The bare-metal LLM call</h2>
<p>Everything starts with a plain text-in, text-out call. OpenAI’s Responses API is the recommended entry point. The SDK example below is the minimum viable “agent brain” — it just generates text.</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> OpenAI <span class="hljs-keyword">from</span> <span class="hljs-string">"openai"</span>;

<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> OpenAI({ apiKey: process.env.OPENAI_API_KEY });

<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.responses.create({
  model: <span class="hljs-string">"gpt-5.2"</span>,
  input: <span class="hljs-string">"Write a one-sentence bedtime story about a unicorn."</span>,
});

<span class="hljs-built_in">console</span>.log(response.output_text);
</code></pre>
<pre><code class="lang-mermaid">sequenceDiagram
  participant App
  participant OpenAI
  App-&gt;&gt;OpenAI: POST /responses (model, input)
  OpenAI--&gt;&gt;App: response.items
  App-&gt;&gt;App: extract output_text
</code></pre>
<p>References:</p>
<ul>
<li>OpenAI Text generation guide: https://platform.openai.com/docs/guides/text</li>
<li>OpenAI TypeScript SDK: https://github.com/openai/openai-node</li>
</ul>
<h2 id="heading-2-turning-completion-into-chat-the-for-loop">2) Turning completion into chat (the for-loop)</h2>
<p>Chat is not a separate API concept. It is just a loop that keeps appending history. The API is stateless unless you pass context. This for-loop is the whole trick.</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> history = [
  { role: <span class="hljs-string">"developer"</span>, content: <span class="hljs-string">"You are a concise coding assistant."</span> },
];

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> userInput <span class="hljs-keyword">of</span> turns) {
  history.push({ role: <span class="hljs-string">"user"</span>, content: userInput });
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.responses.create({
    model: <span class="hljs-string">"gpt-5.2"</span>,
    input: history,
  });
  history.push({ role: <span class="hljs-string">"assistant"</span>, content: response.output_text ?? <span class="hljs-string">""</span> });
}
</code></pre>
<pre><code class="lang-mermaid">flowchart TB
  U[User turn] --&gt; H[Append to history]
  H --&gt; R["responses.create(input=history)"]
  R --&gt; A[Assistant turn]
  A --&gt; H
  H -. alternative .-&gt; P[previous_response_id chain]
  P -.-&gt; R
</code></pre>
<p>At this stage the model is only using its internal knowledge plus what you send. It does not search. It does not execute. It is still a “completion engine”. For example, at this stage if you ask the model about recent events (after its training cutoff date), it will not be able to tell you. The initial release of ChatGPT, before any search capabilities, was just this. </p>
<p>References:</p>
<ul>
<li>OpenAI Conversation state: https://platform.openai.com/docs/guides/conversation-state</li>
</ul>
<h2 id="heading-3-tool-calling-the-model-asks-your-code-executes">3) Tool calling: the model asks, your code executes</h2>
<p>Tool calling is where agents become agents. The model can request a tool; <strong>your application executes it</strong> and sends the output back. OpenAI’s docs describe the flow explicitly: “Execute code on the application side with input from the tool call,” and “When the model calls a function, you must execute it and return the result.”</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> tools = [
  {
    <span class="hljs-keyword">type</span>: <span class="hljs-string">"function"</span>,
    name: <span class="hljs-string">"get_weather"</span>,
    description: <span class="hljs-string">"Get the current weather for a location."</span>,
    parameters: {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">"object"</span>,
      properties: {
        location: { <span class="hljs-keyword">type</span>: <span class="hljs-string">"string"</span> },
        units: { <span class="hljs-keyword">type</span>: <span class="hljs-string">"string"</span>, <span class="hljs-built_in">enum</span>: [<span class="hljs-string">"celsius"</span>, <span class="hljs-string">"fahrenheit"</span>] },
      },
      required: [<span class="hljs-string">"location"</span>, <span class="hljs-string">"units"</span>],
      additionalProperties: <span class="hljs-literal">false</span>,
    },
  },
];

<span class="hljs-keyword">const</span> input = [{ role: <span class="hljs-string">"user"</span>, content: <span class="hljs-string">"What's the weather in Paris?"</span> }];

<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.responses.create({
  model: <span class="hljs-string">"gpt-5.2"</span>,
  tools,
  input,
});

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> item <span class="hljs-keyword">of</span> response.output) {
  <span class="hljs-keyword">if</span> (item.type !== <span class="hljs-string">"function_call"</span> || item.name !== <span class="hljs-string">"get_weather"</span>) <span class="hljs-keyword">continue</span>;
  <span class="hljs-keyword">const</span> result = getWeather(<span class="hljs-built_in">JSON</span>.parse(item.arguments));
  input.push({
    <span class="hljs-keyword">type</span>: <span class="hljs-string">"function_call_output"</span>,
    call_id: item.call_id,
    output: <span class="hljs-built_in">JSON</span>.stringify(result),
  });
}
</code></pre>
<pre><code class="lang-mermaid">sequenceDiagram
  participant App
  participant Model
  participant Tool
  App-&gt;&gt;Model: prompt + tools
  loop while tool calls
    Model--&gt;&gt;App: function_call(name, args, call_id)
    App-&gt;&gt;Tool: execute(args)
    Tool--&gt;&gt;App: result
    App-&gt;&gt;Model: function_call_output(call_id, result)
  end
  Model--&gt;&gt;App: final response
</code></pre>
<p>The Responses API is described as “agentic by default,” because it supports multiple tool calls inside a single request.</p>
<p>References:</p>
<ul>
<li>OpenAI Function calling guide: https://platform.openai.com/docs/guides/function-calling</li>
<li>OpenAI Responses API benefits: https://platform.openai.com/docs/guides/migrate-to-responses</li>
</ul>
<h2 id="heading-4-writing-agents-and-coding-agents">4) Writing agents and coding agents</h2>
<p>Once tool calling works, specialization is just tool design. A writing agent needs search and citations. A coding agent needs shell access, tests, and documentation. Both are the same loop, different tools.</p>
<p><strong>Writing agent (search + citations)</strong></p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> tools = [
  {
    <span class="hljs-keyword">type</span>: <span class="hljs-string">"function"</span>,
    name: <span class="hljs-string">"web_search"</span>,
    description: <span class="hljs-string">"Search the web for sources to cite."</span>,
    parameters: {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">"object"</span>,
      properties: { query: { <span class="hljs-keyword">type</span>: <span class="hljs-string">"string"</span> } },
      required: [<span class="hljs-string">"query"</span>],
      additionalProperties: <span class="hljs-literal">false</span>,
    },
  },
];

<span class="hljs-keyword">const</span> input = [
  { role: <span class="hljs-string">"developer"</span>, content: <span class="hljs-string">"You are a writing agent. Cite sources with inline links."</span> },
  { role: <span class="hljs-string">"user"</span>, content: <span class="hljs-string">"Write two paragraphs about the history of Markdown."</span> },
];
</code></pre>
<p>Example trace (writing agent, simplified):</p>
<pre><code class="lang-json">[
  {<span class="hljs-attr">"role"</span>:<span class="hljs-string">"user"</span>,<span class="hljs-attr">"content"</span>:<span class="hljs-string">"Write two paragraphs about Markdown with citations."</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"function_call"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"web_search"</span>,<span class="hljs-attr">"arguments"</span>:<span class="hljs-string">"{\"query\":\"Markdown history original announcement\"}"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"function_call_output"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"web_search"</span>,<span class="hljs-attr">"output"</span>:<span class="hljs-string">"[{\"title\":\"Daring Fireball: Markdown\",\"url\":\"https://daringfireball.net/projects/markdown/\",\"snippet\":\"Markdown is a text-to-HTML conversion tool...\"}]"</span>},
  {<span class="hljs-attr">"role"</span>:<span class="hljs-string">"assistant"</span>,<span class="hljs-attr">"content"</span>:<span class="hljs-string">"Markdown was introduced by John Gruber as a text-to-HTML conversion tool... ([source](https://daringfireball.net/projects/markdown/))."</span>}
]
</code></pre>
<p><strong>Coding agent (shell + tests)</strong></p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> tools = [
  {
    <span class="hljs-keyword">type</span>: <span class="hljs-string">"function"</span>,
    name: <span class="hljs-string">"shell"</span>,
    description: <span class="hljs-string">"Run a shell command in the repo."</span>,
    parameters: {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">"object"</span>,
      properties: { command: { <span class="hljs-keyword">type</span>: <span class="hljs-string">"string"</span> } },
      required: [<span class="hljs-string">"command"</span>],
      additionalProperties: <span class="hljs-literal">false</span>,
    },
  },
];

<span class="hljs-keyword">const</span> input = [
  { role: <span class="hljs-string">"developer"</span>, content: <span class="hljs-string">"You are a coding agent. Use shell to run tests and inspect files."</span> },
  { role: <span class="hljs-string">"user"</span>, content: <span class="hljs-string">"Tests are failing. Find the failure and suggest a patch."</span> },
];
</code></pre>
<p>Example trace (coding agent, simplified):</p>
<pre><code class="lang-json">[
  {<span class="hljs-attr">"role"</span>:<span class="hljs-string">"user"</span>,<span class="hljs-attr">"content"</span>:<span class="hljs-string">"Tests are failing. Find the failure and suggest a patch."</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"function_call"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"shell"</span>,<span class="hljs-attr">"arguments"</span>:<span class="hljs-string">"{\"command\":\"npm test -- --runInBand\"}"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"function_call_output"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"shell"</span>,<span class="hljs-attr">"output"</span>:<span class="hljs-string">"{\"exitCode\":1,\"stdout\":\"FAIL src/math.test.ts\",\"stderr\":\"Expected 3, received 4\"}"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"function_call"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"shell"</span>,<span class="hljs-attr">"arguments"</span>:<span class="hljs-string">"{\"command\":\"sed -n '1,120p' src/math.ts\"}"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"function_call_output"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"shell"</span>,<span class="hljs-attr">"output"</span>:<span class="hljs-string">"{\"exitCode\":0,\"stdout\":\"export const add = (a,b)=&gt;a+b+1;\"}"</span>},
  {<span class="hljs-attr">"role"</span>:<span class="hljs-string">"assistant"</span>,<span class="hljs-attr">"content"</span>:<span class="hljs-string">"Bug in add(): remove the +1. Suggested patch: export const add = (a,b)=&gt;a+b;"</span>}
]
</code></pre>
<pre><code class="lang-mermaid">flowchart TB
  U[User task] --&gt; P[Agent prompt + policy]
  P --&gt; M[Model turn]
  M --&gt; D{Tool needed?}
  D --&gt;|No| OUT[Final draft / patch]
  D --&gt;|Yes| R[Orchestrator / tool router]
  R --&gt; W[Writer stack&lt;br/&gt;web_search + cite formatter]
  R --&gt; C[Coder stack&lt;br/&gt;shell + tests + file inspector]
  W --&gt; O1[Tool outputs]
  C --&gt; O2[Tool outputs]
  O1 --&gt; F[Normalize + append to context]
  O2 --&gt; F
  F --&gt; M
</code></pre>
<p>References:</p>
<ul>
<li>OpenAI tool calling flow: https://platform.openai.com/docs/guides/function-calling</li>
<li>OpenAI practical guide to building agents: https://openai.com/business/guides-and-resources/a-practical-guide-to-building-ai-agents/</li>
</ul>
<h2 id="heading-5-context-limits-compaction-and-memory">5) Context limits, compaction, and memory</h2>
<p>Every agent hits the context window. This creates three issues: rising cost, forgotten instructions, and session loss. The fixes are standard:</p>
<ul>
<li><strong>Compaction</strong>: OpenAI provides server-side compaction and a standalone <code>/responses/compact</code> endpoint that returns a compacted context window.</li>
<li><strong>Memory</strong>: store long-term context in a database (vector store or logs) and retrieve it when needed. Lilian Weng’s survey describes this as short-term memory (context window) vs. long-term memory (external retrieval).</li>
<li><strong>Session boundaries</strong>: a new session has no idea about prior runs unless you explicitly pass memory or chain responses.</li>
</ul>
<pre><code class="lang-mermaid">flowchart TB
  subgraph ContextWindow[Context window]
    Prompt[Current turn + recent history] --&gt; Model[LLM]
    Model --&gt; Output[Response]
  end
  Output --&gt;|store| Memory[Long-term memory]
  Memory --&gt;|retrieve relevant| Prompt
  Output --&gt;|compact| Compact[Compacted context item]
  Compact --&gt; Prompt
</code></pre>
<p>References:</p>
<ul>
<li>OpenAI Compaction guide: https://platform.openai.com/docs/guides/compaction</li>
<li>OpenAI Conversation state: https://platform.openai.com/docs/guides/conversation-state</li>
<li>Lilian Weng, LLM Powered Autonomous Agents: https://lilianweng.github.io/posts/2023-06-23-agent/</li>
</ul>
<h2 id="heading-6-orchestrators-and-multi-agent-fan-out">6) Orchestrators and multi-agent fan-out</h2>
<p>Orchestrators are what make OpenClaw-style systems feel “real.” One agent delegates to many specialists, then merges the result. OpenAI’s agent guide describes two patterns: a <strong>manager</strong> agent (agents-as-tools) and a <strong>decentralized</strong> handoff model. Both are graph execution with tool calls at the edges.</p>
<p>A key implementation detail: if the model emits multiple tool calls, the orchestration layer decides whether to execute them sequentially or in parallel. OpenAI’s Agents SDK docs explicitly expose this switch (<code>parallel_tool_calls</code>) and also describe code-first orchestration with parallel execution primitives (for example, <code>asyncio.gather</code>). In a shell-centric harness, the same rule applies: independent tool calls can be dispatched in separate subprocesses (for example, separate <code>bash</code>/command workers) and then fan-in their outputs; dependent calls should stay serialized.</p>
<pre><code class="lang-mermaid">flowchart LR
  Manager[Manager agent] --&gt;|fan-out| R1[Research agent]
  Manager --&gt;|fan-out| R2[Writing agent]
  Manager --&gt;|fan-out| R3[Coding agent]
  R1 --&gt; Merge[Fan-in merge]
  R2 --&gt; Merge
  R3 --&gt; Merge
  Merge --&gt; Manager
  Manager --&gt; Final[Final response]
</code></pre>
<p>Example fan-out trace (simplified JSON):</p>
<pre><code class="lang-json">[
  {<span class="hljs-attr">"role"</span>:<span class="hljs-string">"user"</span>,<span class="hljs-attr">"content"</span>:<span class="hljs-string">"Draft a 1-page brief on vector databases with citations and a code sample."</span>},
  {<span class="hljs-attr">"role"</span>:<span class="hljs-string">"assistant"</span>,<span class="hljs-attr">"content"</span>:<span class="hljs-string">"I will delegate research, citations, and code sample."</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"tool_call"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"agent_research"</span>,<span class="hljs-attr">"arguments"</span>:<span class="hljs-string">"{\"task\":\"Background + key concepts\"}"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"tool_call"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"agent_citations"</span>,<span class="hljs-attr">"arguments"</span>:<span class="hljs-string">"{\"task\":\"Find authoritative sources\"}"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"tool_call"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"agent_code"</span>,<span class="hljs-attr">"arguments"</span>:<span class="hljs-string">"{\"task\":\"Minimal TS example with a vector store API\"}"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"tool_result"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"agent_research"</span>,<span class="hljs-attr">"output"</span>:<span class="hljs-string">"Key concepts: embeddings, ANN search, HNSW..."</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"tool_result"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"agent_citations"</span>,<span class="hljs-attr">"output"</span>:<span class="hljs-string">"Sources: openai docs, Weng survey, FAISS paper..."</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"tool_result"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"agent_code"</span>,<span class="hljs-attr">"output"</span>:<span class="hljs-string">"Code sample: embed + upsert + query"</span>},
  {<span class="hljs-attr">"role"</span>:<span class="hljs-string">"assistant"</span>,<span class="hljs-attr">"content"</span>:<span class="hljs-string">"Merged brief with citations and code sample."</span>}
]
</code></pre>
<p>References:</p>
<ul>
<li>OpenAI practical guide to building agents: https://openai.com/business/guides-and-resources/a-practical-guide-to-building-ai-agents/</li>
<li>OpenAI Agents guide: https://platform.openai.com/docs/guides/agents</li>
<li>OpenAI Agents SDK orchestration patterns: https://openai.github.io/openai-agents-python/multi_agent/</li>
<li>OpenAI Agents SDK model settings (<code>parallel_tool_calls</code>): https://openai.github.io/openai-agents-python/ref/model_settings/</li>
<li>OpenAI cookbook parallel agents example: https://developers.openai.com/cookbook/examples/agents_sdk/parallel_agents</li>
<li>Node.js child processes (<code>spawn</code>/<code>exec</code> for separate subprocesses): https://nodejs.org/api/child_process.html</li>
</ul>
<h2 id="heading-7-skills-mcp-and-capability-expansion">7) Skills, MCP, and capability expansion</h2>
<p>Tools are the base layer, but two concepts make agent systems feel <em>modular</em>: <strong>Skills</strong> and <strong>MCP</strong>. OpenAI defines Skills as “versioned bundles of files” with a <code>SKILL.md</code> manifest, designed to codify reusable processes. You can mount them in hosted or local shell environments so the model can invoke them as needed. Skills are powerful precisely because they can package both instructions and code.</p>
<p>Example: mounting a skill in a hosted shell (TypeScript):</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.responses.create({
  model: <span class="hljs-string">"gpt-5.2"</span>,
  tools: [
    {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">"shell"</span>,
      environment: {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">"container_auto"</span>,
        skills: [{ <span class="hljs-keyword">type</span>: <span class="hljs-string">"skill_reference"</span>, skill_id: <span class="hljs-string">"skill_basic_math"</span> }],
      },
    },
  ],
  input: <span class="hljs-string">"Use the basic-math skill to add 144 and 377."</span>,
});
</code></pre>
<p>MCP (Model Context Protocol) is the other half of the expansion story. It’s an open standard for connecting models to external systems. OpenAI’s connector/MCP tooling exposes this directly in the Responses API: “You can give models new capabilities using <strong>connectors</strong> and <strong>remote MCP servers</strong>.” These tools can be gated by explicit approval, and the docs emphasize the risk surface: “A malicious server can exfiltrate sensitive data from anything that enters the model’s context.”</p>
<p>Example: MCP tool setup (remote server) with approval gating:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.responses.create({
  model: <span class="hljs-string">"gpt-5.2"</span>,
  tools: [
    {
      <span class="hljs-keyword">type</span>: <span class="hljs-string">"mcp"</span>,
      server_label: <span class="hljs-string">"github"</span>,
      server_url: <span class="hljs-string">"https://mcp.github.example/sse"</span>,
      require_approval: <span class="hljs-string">"always"</span>,
      allowed_tools: [<span class="hljs-string">"list_prs"</span>, <span class="hljs-string">"get_commit"</span>, <span class="hljs-string">"merge_pr"</span>],
    },
  ],
  input: <span class="hljs-string">"Summarize open PRs for repo foo/bar."</span>,
});
</code></pre>
<p>Typical layout (Skills, MCP servers, and integrations):</p>
<pre><code>agent-system/                     &lt;--- repo root for the agent harness
  skills/                         &lt;--- reusable skill bundles
    basic-math/ 
      SKILL.md   &lt;--- skill manifest + instructions
      scripts/         
        add.py   &lt;--- executable to do deterministic logic 
  mcp-servers/                    &lt;--- MCP server implementations
    github/                       
      package.json           &lt;--- server deps + metadata
      src/ 
        server.ts         &lt;--- MCP server entrypoint (can be expressjs)
        tools/ 
          list_prs.ts     &lt;--- uses github GraphQL api to return PRs
          get_commit.ts   &lt;--- find commit details
          merge_pr.ts     &lt;--- makes POST request to merge a PR
  integrations/                    &lt;--- local connector implementations
    connectors/  
      dropbox.ts       &lt;--- Dropbox connector (handles auth etc)
      gmail.ts         &lt;--- Gmail connector
</code></pre><p>MCP servers usually expose a list of tools (their input schemas and names). The model sees those tools and can call them like any other tool call; your harness still owns approvals and can restrict which tools are exposed.</p>
<p>Example: a GitHub MCP server in action (hypothetical trace):</p>
<pre><code class="lang-json">[
  {<span class="hljs-attr">"role"</span>:<span class="hljs-string">"user"</span>,<span class="hljs-attr">"content"</span>:<span class="hljs-string">"Show me open PRs in foo/bar and merge the top one if checks are green."</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"mcp_list_tools"</span>,<span class="hljs-attr">"server_label"</span>:<span class="hljs-string">"github"</span>,<span class="hljs-attr">"tools"</span>:[<span class="hljs-string">"list_prs"</span>,<span class="hljs-string">"get_pr"</span>,<span class="hljs-string">"merge_pr"</span>]},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"tool_call"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"list_prs"</span>,<span class="hljs-attr">"arguments"</span>:<span class="hljs-string">"{\"repo\":\"foo/bar\",\"state\":\"open\"}"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"tool_result"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"list_prs"</span>,<span class="hljs-attr">"output"</span>:<span class="hljs-string">"[{\"number\":42,\"title\":\"Fix build\",\"checks\":\"green\"}]"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"mcp_approval_request"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"merge_pr"</span>,<span class="hljs-attr">"arguments"</span>:<span class="hljs-string">"{\"repo\":\"foo/bar\",\"number\":42}"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"mcp_approval_response"</span>,<span class="hljs-attr">"approve"</span>:<span class="hljs-literal">true</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"tool_call"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"merge_pr"</span>,<span class="hljs-attr">"arguments"</span>:<span class="hljs-string">"{\"repo\":\"foo/bar\",\"number\":42}"</span>},
  {<span class="hljs-attr">"type"</span>:<span class="hljs-string">"tool_result"</span>,<span class="hljs-attr">"name"</span>:<span class="hljs-string">"merge_pr"</span>,<span class="hljs-attr">"output"</span>:<span class="hljs-string">"merged"</span>},
  {<span class="hljs-attr">"role"</span>:<span class="hljs-string">"assistant"</span>,<span class="hljs-attr">"content"</span>:<span class="hljs-string">"Merged PR #42 after checks passed."</span>}
]
</code></pre>
<p>Permissions and approvals are the key guardrail. The MCP guide notes: “By default, OpenAI will request your approval before any data is shared with a connector or remote MCP server.” You can relax these approvals once trust is established, but the default is permissioned by design.</p>
<p>References:</p>
<ul>
<li>OpenAI Skills guide: https://platform.openai.com/docs/guides/tools-skills</li>
<li>OpenAI Connectors and MCP servers: https://platform.openai.com/docs/guides/tools-connectors-mcp</li>
<li>MCP overview: https://modelcontextprotocol.io/docs/getting-started/intro</li>
</ul>
<h2 id="heading-8-harnessed-tools-permissions-and-shell-centric-agents">8) Harnessed tools, permissions, and shell-centric agents</h2>
<p>Tool calls are <strong>requests</strong>, not execution. The harness (your app) is responsible for enforcing permissions, sandboxing, and actually running commands. The OpenAI shell guide is blunt: “Running arbitrary shell commands can be dangerous. Always sandbox execution, apply allowlists or denylists where possible, and log tool activity for auditing.” It also clarifies how the control loop works: the model emits <code>shell_call</code> items, <strong>your runtime executes them</strong>, then you send back <code>shell_call_output</code>.</p>
<p>This is why shell‑centric agents are so powerful. Tools like Codex CLI, or lightweight local harnesses, treat the model as a planner and the shell as the executor. Minimal harnesses (think “single file shell agent”) can already support build/test/fix loops. Stack a scheduler, memory, and orchestration on top, and you are now halfway to systems like OpenClaw.</p>
<p>References:</p>
<ul>
<li>OpenAI Shell tool: https://platform.openai.com/docs/guides/tools-shell</li>
<li>OpenAI Local shell tool: https://platform.openai.com/docs/guides/tools-local-shell</li>
<li>OpenAI Connectors/MCP approvals: https://platform.openai.com/docs/guides/tools-connectors-mcp</li>
</ul>
<h2 id="heading-9-a-minimal-openclaw-style-reference-implementation">9) A minimal OpenClaw-style reference implementation</h2>
<p>Here’s the smallest possible control plane, expanded with a more OpenClaw‑like shape. OpenClaw’s docs describe a <strong>Gateway</strong>: a single long‑lived daemon that owns messaging surfaces, exposes a WebSocket API, and emits system events like <code>cron</code> and <code>heartbeat</code>. The Gateway runs on a host continuously, and its periodic cron ticks are the trigger to poll queues, check health, and schedule work. When new tasks arrive from chat or clients, the Gateway creates jobs in a queue, spawns agent runs (often in separate worker processes or runtimes), streams progress back to clients, and finally persists results into memory.</p>
<p>In other words: the “cron tick” is not just a timer, it is the heartbeat of the control plane. It’s where the Gateway decides which jobs to claim, which agents to spawn, and how to advance long‑running work. It also decides when a job is complete (all sub‑agents have returned, or exit criteria met) and sends a final status back to the original chat thread.</p>
<pre><code class="lang-mermaid">flowchart LR
  Chat[Chat providers] --&gt;|ingest| Gateway((Gateway daemon))
  Clients[CLI/Web/Admin] &lt;--&gt; |WebSocket| Gateway
  Nodes[Devices/Tools] &lt;--&gt; |WebSocket| Gateway
  Gateway --&gt; Cron[cron tick]
  Cron --&gt; Queue[Job queue]
  Queue --&gt; Workers[Agent workers]
  Workers --&gt; Results[Results + logs]
  Results --&gt; Gateway
  Gateway --&gt; Memory[Global memory/store]
  Gateway --&gt; Updates[Thread updates]
</code></pre>
<pre><code class="lang-ts"><span class="hljs-keyword">type</span> Job = {
  id: <span class="hljs-built_in">string</span>;
  source: <span class="hljs-string">"chat"</span> | <span class="hljs-string">"client"</span>;
  threadId: <span class="hljs-built_in">string</span>;
  prompt: <span class="hljs-built_in">string</span>;
  status: <span class="hljs-string">"queued"</span> | <span class="hljs-string">"running"</span> | <span class="hljs-string">"done"</span> | <span class="hljs-string">"failed"</span>;
};

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onIncomingChat</span>(<span class="hljs-params">threadId: <span class="hljs-built_in">string</span>, prompt: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-comment">// Chat/client message becomes a queued job.</span>
  <span class="hljs-keyword">await</span> enqueueJob({
    id: <span class="hljs-string">`job_<span class="hljs-subst">${<span class="hljs-built_in">Date</span>.now()}</span>`</span>,
    source: <span class="hljs-string">"chat"</span>,
    threadId,
    prompt,
    status: <span class="hljs-string">"queued"</span>,
  });
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">cronTick</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// Claim a small batch so multiple workers can share the queue.</span>
  <span class="hljs-keyword">const</span> jobs = <span class="hljs-keyword">await</span> claimQueuedJobs({ limit: <span class="hljs-number">5</span> });

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> job <span class="hljs-keyword">of</span> jobs) {
    <span class="hljs-comment">// Update status and notify the originating thread.</span>
    <span class="hljs-keyword">await</span> updateJob(job.id, <span class="hljs-string">"running"</span>);
    <span class="hljs-keyword">await</span> notifyThread(job.threadId, <span class="hljs-string">"started"</span>, { jobId: job.id });

    <span class="hljs-comment">// Pull relevant memory before delegating work.</span>
    <span class="hljs-keyword">const</span> memory = <span class="hljs-keyword">await</span> searchMemory(job.prompt);

    <span class="hljs-comment">// Fan out to specialist agents in parallel.</span>
    <span class="hljs-keyword">const</span> [research, citations] = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
      runAgent(<span class="hljs-string">"research"</span>, <span class="hljs-string">`Research background: <span class="hljs-subst">${job.prompt}</span>`</span>),
      runAgent(<span class="hljs-string">"citations"</span>, <span class="hljs-string">`Find sources for: <span class="hljs-subst">${job.prompt}</span>`</span>),
    ]);

    <span class="hljs-comment">// Fan-in: assemble a final draft with memory + sources.</span>
    <span class="hljs-keyword">const</span> draft = <span class="hljs-keyword">await</span> runAgent(
      <span class="hljs-string">"writer"</span>,
      <span class="hljs-string">`Draft answer for: <span class="hljs-subst">${job.prompt}</span>\n\nContext:\n<span class="hljs-subst">${memory}</span>\n\nSources:\n<span class="hljs-subst">${citations}</span>`</span>
    );

    <span class="hljs-comment">// Persist result and close out the job.</span>
    <span class="hljs-keyword">await</span> writeMemory(<span class="hljs-string">`Job <span class="hljs-subst">${job.id}</span> summary:\n<span class="hljs-subst">${draft}</span>`</span>);
    <span class="hljs-keyword">await</span> updateJob(job.id, <span class="hljs-string">"done"</span>);
    <span class="hljs-keyword">await</span> notifyThread(job.threadId, <span class="hljs-string">"completed"</span>, { jobId: job.id, draft });
  }
}
</code></pre>
<p>That is the full stack: LLM call, loop, tools, memory, orchestration. Everything else is engineering around reliability, safety, cost, and UX.</p>
<p>References:</p>
<ul>
<li>OpenHands repo: https://github.com/OpenHands/OpenHands</li>
<li>OpenClaw Gateway architecture: https://docs.openclaw.ai/concepts/architecture</li>
<li>ReAct paper (Reasoning + Acting): https://arxiv.org/abs/2210.03629</li>
</ul>
<h2 id="heading-closing">Closing</h2>
<p>If you strip away the hype, modern agent frameworks are just well‑engineered loops: deterministic code around probabilistic models. The wins come from tool design, memory strategy, and orchestration discipline, not from “prompt magic.”</p>
<p>If you want to build this out further, a few practical next steps make the biggest difference:</p>
<p>1) <strong>Add guardrails</strong>: tool approvals, allowlists/denylists, and sandboxing. The OpenAI shell tool docs stress that executing arbitrary commands is dangerous and should be sandboxed and logged. https://platform.openai.com/docs/guides/tools-shell
2) <strong>Make tools modular</strong>: Skills and MCP servers let you package capabilities and expose them safely. See OpenAI’s Skills guide and Connectors/MCP docs. https://platform.openai.com/docs/guides/tools-skills https://platform.openai.com/docs/guides/tools-connectors-mcp
3) <strong>Scale orchestration</strong>: if you want many agents, study systems like Gas Town (multi‑agent orchestration with persistent work tracking) and OpenClaw’s Gateway architecture for long‑lived control planes. https://github.com/steveyegge/gastown https://docs.openclaw.ai/concepts/architecture
4) <strong>Harden memory</strong>: learn how compaction, conversation state, and retrieval are handled in production. https://platform.openai.com/docs/guides/compaction https://platform.openai.com/docs/guides/conversation-state
5) <strong>Learn from coding agents</strong>: Claude Code is a concrete example of an “agentic coding tool” that reads codebases, edits files, and runs commands. https://code.claude.com/docs/en/overview</p>
<p>Start tiny: one LLM call, one loop, one tool. Then scale the loop into a harness, and scale the harness into a system.</p>
<h2 id="heading-further-reading">Further reading</h2>
<ul>
<li>OpenAI Agents guide: https://platform.openai.com/docs/guides/agents</li>
<li>OpenAI Function calling guide: https://platform.openai.com/docs/guides/function-calling</li>
<li>OpenAI Compaction guide: https://platform.openai.com/docs/guides/compaction</li>
<li>OpenAI Skills guide (building and using skills): https://platform.openai.com/docs/guides/tools-skills</li>
<li>OpenAI Connectors and MCP servers: https://platform.openai.com/docs/guides/tools-connectors-mcp</li>
<li>OpenAI guide to adding remote MCP servers as tools: https://platform.openai.com/docs/guides/tools-connectors-mcp</li>
<li>MCP overview: https://modelcontextprotocol.io/docs/getting-started/intro</li>
<li>Gas Town (Steve Yegge): https://github.com/steveyegge/gastown</li>
<li>OpenClaw Gateway architecture: https://docs.openclaw.ai/concepts/architecture</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[env.sync.local - Syncing API keys and secrets between devices in my home LAN]]></title><description><![CDATA[My desk is a heterogeneous graveyard of differing architectures. I have a MacBook Pro for daily work, a Razer laptop that mostly serves as an expensive space heater for Windows testing, and a Beelink MiniPC that oscillates between Linux and Windows d...]]></description><link>https://arnav.tech/envsynclocal-syncing-api-keys-and-secrets-between-devices-in-my-home-lan</link><guid isPermaLink="true">https://arnav.tech/envsynclocal-syncing-api-keys-and-secrets-between-devices-in-my-home-lan</guid><category><![CDATA[ssh]]></category><category><![CDATA[scp]]></category><category><![CDATA[encryption]]></category><category><![CDATA[secrets]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Tue, 10 Feb 2026 01:02:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770685146265/133b6616-42dc-418e-ad70-c587ff2b40ff.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My desk is a heterogeneous graveyard of differing architectures. I have a MacBook Pro for daily work, a Razer laptop that mostly serves as an expensive space heater for Windows testing, and a Beelink MiniPC that oscillates between Linux and Windows depending on how much I want to fight with local LLMs that day. It is a great playground for testing but an absolute nightmare for managing secrets.</p>
<p>The moment you generate a Google Maps API key on one machine, you have started a countdown. Eventually, you will need that key on the other two machines. Usually, this involves the shameful act of Slacking a secret to yourself or the tedious ritual of digging through a password manager. I just wanted the keys to exist everywhere, as if by magic.</p>
<p>So I build <strong><a target="_blank" href="https://github.com/championswimmer/env.sync.local">env.sync.local</a></strong> - go check it out and install from my Github. </p>
<h3 id="heading-the-problem-the-copy-paste-tax">The Problem: The Copy-Paste Tax</h3>
<p>We have been conditioned to accept the "Manual Copy-Paste" tax as a cost of doing business. When you work across three different operating systems, syncing a <code>.env</code> file usually involves a game of telephone with insecure intermediaries. </p>
<p>I needed to scratch a few very specific itches. First, no master server. I didn't want to maintain a "secrets vault" or a central authority. If any two of my machines are online, they should be able to talk without a middleman. Second, zero configuration. If I buy a new machine, I should be able to run an install script and have it automatically discover the rest of the mesh. Finally, security at rest. Secrets should not sit in plaintext on my disk, even if the disk is encrypted.</p>
<h3 id="heading-architecture-discovery-and-transport">Architecture: Discovery and Transport</h3>
<p>The first hurdle was discovery. Static IP addresses are a relic of the past in a home network context. I leaned on mDNS (Bonjour/Avahi). It is a beautiful protocol that allows machines to shout "I am here" into the local void. By registering an <code>_envsync._tcp</code> service, any node can find its peers without a config file. No more hardcoding IP addresses like it is 1998.</p>
<p>For the transport layer, I started with a simple HTTP server fallback, but SCP (over SSH) is the real winner here. It provides authenticated, encrypted pipes out of the box. If I can already SSH into my MiniPC, <code>env-sync</code> can just piggyback on that existing trust to move files. It is pragmatic, it uses existing infrastructure, and it works.</p>
<h3 id="heading-the-crypto-age-and-multi-recipient-encryption">The Crypto: AGE and Multi-Recipient Encryption</h3>
<p>Initially, the project was just moving plaintext files. That felt wrong. If someone stole my Beelink, they would have the keys to my entire kingdom. I moved to AGE (Actually Good Encryption) for at-rest security. </p>
<p>The challenge with a decentralized system is key management. If there is no central server to manage a shared secret, how does a new machine join the party? I implemented a multi-recipient model. Every machine generates its own X25519 key pair. When you add a new machine, it broadcasts its public key. The existing machines discover this new key and re-encrypt their secrets to include the new recipient.</p>
<p>I added a "remote trigger" feature to solve the bootstrap problem. A new machine can SSH into an existing peer and say: "Here is my public key, re-encrypt everything for me and let me sync." This makes it zero-config for the existing nodes and provides instant gratification for the new one.</p>
<h3 id="heading-evolution-from-bash-to-go">Evolution: From Bash to Go</h3>
<p>The project started as a collection of Bash scripts. Bash is the ultimate glue. It allowed me to build the MVP in a single weekend. But Bash has very low ceilings. Once you start managing background daemons, parsing JSON from mDNS tools, and fighting with cross-platform pathing on WSL2, you are in for a world of pain. Shell quoting is a dark art that I would rather not practice daily.</p>
<p>I migrated the entire core to Go for a few specific reasons. First, the single binary. I wanted a statically linked artifact that I could drop onto any machine without worrying about dependencies. Second, native AGE. Using the <code>filippo.io/age</code> library meant I didn't need the <code>age</code> binary installed on the host. Finally, concurrency. Go's select loops and goroutines made managing the mDNS listener and the sync cron significantly more robust.</p>
<p>The result is a system that feels invisible. I add a key on one machine, and by the time I have walked over to my other desk and opened a terminal, the key has already propagated. It is eventually consistent, decentralized, and stays out of my way.</p>
<h3 id="heading-the-nerd-stats">The Nerd Stats</h3>
<p>For those who want the technical specs:</p>
<ul>
<li>Discovery: mDNS via <code>_envsync._tcp</code> on port 5739.</li>
<li>Transport: SCP/SSH (Default) or HTTP (Fallback).</li>
<li>Encryption: AGE (X25519).</li>
<li>Conflict Resolution: Per-key timestamps (latest write wins).</li>
<li>Distribution: Peer-to-peer gossip-adjacent sync.</li>
</ul>
<p>We spend so much time automating our CI/CD pipelines for production, yet we often leave our local development environments in the stone age. <code>env-sync</code> is my attempt to bring a little bit of that automation home. If it saves me from one more "where is that API key?" hunt, it has already paid for itself.</p>
]]></content:encoded></item><item><title><![CDATA[Running llama.cpp (compiled from source) on AMD Strix Halo 395]]></title><description><![CDATA[Just a quick doc/note/tutorial for referencing myself later. 
Here's how to get llama.cpp running with Vulkan support on AMD AI Max 395 (Strix Halo) based devices. I tried it on a Beelink GTR 9 Pro, but should work for Framework Ddesktop too. 
Instal...]]></description><link>https://arnav.tech/running-llamacpp-compiled-from-source-on-amd-strix-halo-395</link><guid isPermaLink="true">https://arnav.tech/running-llamacpp-compiled-from-source-on-amd-strix-halo-395</guid><category><![CDATA[Vulkan]]></category><category><![CDATA[LLaMa]]></category><category><![CDATA[graphics]]></category><category><![CDATA[drivers]]></category><category><![CDATA[inference]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Sat, 31 Jan 2026 02:14:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769825572256/0080b981-1b21-4dda-891f-60b5acb65135.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Just a quick doc/note/tutorial for referencing myself later. </p>
<p>Here's how to get llama.cpp running with Vulkan support on AMD AI Max 395 (Strix Halo) based devices. I tried it on a Beelink GTR 9 Pro, but should work for Framework Ddesktop too. </p>
<h3 id="heading-installing-a-few-prerequisites">Installing a few Prerequisites</h3>
<pre><code class="lang-shell">sudo apt install amd-smi rocminfo glslc  libvulkan-dev vulkan-tools   mesa-vulkan-drivers   clinfo
</code></pre>
<h3 id="heading-download-and-build-llamacpp">Download and build llama.cpp</h3>
<p>Get it from github </p>
<pre><code class="lang-shell">git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
</code></pre>
<p>Build it for Vulkan support </p>
<pre><code class="lang-shell">cmake -B build \
  -DGGML_VULKAN=ON \
  -DCMAKE_BUILD_TYPE=Release

cmake --build build -j$(nproc)
</code></pre>
<h3 id="heading-run-a-local-llm-model">Run a local LLM model</h3>
<p>Once it is built, we can run a model </p>
<pre><code class="lang-shell">./build/bin/llama-cli \
  -m ~/.lmstudio/models/lmstudio-community/GLM-4.7-Flash-GGUF/GLM-4.7-Flash-Q4_K_M.gguf \
  -ngl 999 \
  -c 4096 \
  -t $(nproc) \
  --color on \
  -p "Explain how Vulkan helps LLM inference on AMD GPUs."
</code></pre>
]]></content:encoded></item><item><title><![CDATA[sideproject diaries: sharetime.zone - A Simple Timezone Sharing Tool]]></title><description><![CDATA[If you have friends or clients scattered across the globe, you know the drill. You agree to meet at "5 PM," but then follows the inevitable dance of clarification: "My 5 PM or your 5 PM?" "Wait, is that daylight savings?" "Let me just send you a cale...]]></description><link>https://arnav.tech/sideproject-diaries-sharetimezone-a-simple-timezone-sharing-tool</link><guid isPermaLink="true">https://arnav.tech/sideproject-diaries-sharetimezone-a-simple-timezone-sharing-tool</guid><category><![CDATA[timezone]]></category><category><![CDATA[Vue.js]]></category><category><![CDATA[Netlify]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Wed, 28 Jan 2026 02:58:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769569015718/4ecabc92-93ed-44b8-b1a4-0680ced9d70b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you have friends or clients scattered across the globe, you know the drill. You agree to meet at "5 PM," but then follows the inevitable dance of clarification: "My 5 PM or your 5 PM?" "Wait, is that daylight savings?" "Let me just send you a calendar invite so we don't mess this up."</p>
<p>I built <a target="_blank" href="https://sharetime.zone">sharetime.zone</a> because I found myself in this loop way too often. Whether it was catching up with friends or consulting with clients in different locations, I needed a way to just say, "Here is the time," without the mental math.</p>
<h2 id="heading-the-idea-simplicity-first">The Idea: Simplicity First</h2>
<p>The core idea is dead simple. I wanted to be able to share a link that instantly communicates a specific time in a specific timezone, converted to the viewer's local time.</p>
<p>Instead of opening a timezone converter app, selecting cities, and copying a long generated link, I wanted something I could type out manually if I had to.</p>
<p>So, <code>sharetime.zone</code> works like this:</p>
<p><strong><a target="_blank" href="https://sharetime.zone/IST/1700">sharetime.zone/IST/1700</a></strong></p>
<p>That's it. You click that link, and it tells you exactly what 5:00 PM Indian Standard Time is in <em>your</em> local timezone. No friction, no confusion.</p>
<h2 id="heading-for-the-developers-it-works-in-curl">For the Developers: It Works in cURL</h2>
<p>Since I spend most of my time in the terminal, I didn't want to leave the command line just to check a time. I figured, why not make it work with <code>curl</code>?</p>
<p>I added support for cURL user agents, so you can literally fetch the time from your terminal:</p>
<pre><code class="lang-bash">curl sharetime.zone/IST/1700
</code></pre>
<p>And it returns a plain text response with the converted time.</p>
<h3 id="heading-why-edge-functions">Why Edge Functions?</h3>
<p>You might wonder, "Why not just a standard serverless function?" or "Why does this need special handling?"</p>
<p>In a browser-based app (like the Vue frontend of this site), determining your local timezone is trivial. The JavaScript running in your browser can simply ask the browser: "Hey, what timezone are we in?" (usually via <code>Intl.DateTimeFormat().resolvedOptions().timeZone</code>).</p>
<p><strong>But <code>curl</code> is different.</strong> When you make a request from your terminal, you aren't sending your timezone information. You're just sending a raw HTTP request. Without that context, the server has no idea if you are in Tokyo or Toronto.</p>
<p>This is where <strong>Netlify Edge Functions</strong> shine. Because they run at the "edge"—on servers distributed globally and closest to the user—they have immediate access to the request context, including the user's IP-based geolocation.</p>
<p>I can access the user's timezone directly from the request context:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// netlify/edge-functions/curl-response.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> (request: Request, context: Context) =&gt; {
  <span class="hljs-keyword">const</span> clientTZ = context.geo.timezone;
  <span class="hljs-comment">// ... logic to convert time to clientTZ</span>
}
</code></pre>
<p>This allows the tool to provide a personalized answer ("5 PM IST is 7:30 AM in <em>your</em> timezone") without you ever having to tell it where you are.</p>
<h3 id="heading-deployed-on-netlify">Deployed on Netlify</h3>
<p>Deploying this was surprisingly straightforward. I defined the edge function in my <code>netlify.toml</code>:</p>
<pre><code class="lang-toml"><span class="hljs-section">[[edge_functions]]</span>
    <span class="hljs-attr">function</span> = <span class="hljs-string">"curl-response"</span>
    <span class="hljs-attr">path</span> = <span class="hljs-string">"/*"</span>
</code></pre>
<p>This tells Netlify to intercept <em>every</em> request to the site with this function. Inside the function, I check if the User-Agent contains <code>curl</code>. If it does, I hijack the response and send back the text output. If not, I let the request pass through to the standard Vue app.</p>
<h3 id="heading-performance-at-the-edge">Performance at the Edge</h3>
<p>Another huge benefit is speed. "What time is it?" is a query that doesn't need a centralized database or a server sitting in a data center in Virginia. It's a calculation that can happen anywhere.</p>
<p>By using Edge Functions, the logic runs on a server physically close to you.</p>
<ol>
<li><strong>Low Latency:</strong> The request travels a shorter distance.</li>
<li><strong>No Central Bottleneck:</strong> There's no single server coordinating everyone's time queries.</li>
<li><strong>Deno Runtime:</strong> Netlify Edge Functions use Deno, which starts up incredibly fast compared to cold-booting a Node.js container.</li>
</ol>
<h2 id="heading-what-i-learnt-building-this">What I Learnt Building This</h2>
<p>While the app looks simple on the surface, timezones are notoriously tricky. Here are a few things I picked up along the way:</p>
<h3 id="heading-1-deno-is-different">1. Deno is Different</h3>
<p>Since Netlify Edge Functions run on Deno, I couldn't just drop in my usual Node.js code. I had to adapt to the Deno runtime environment, using URL imports for dependencies (like <code>https://deno.land/x/ptera</code>) instead of <code>npm install</code>. It was a fun challenge to step outside the <code>node_modules</code> comfort zone.</p>
<h3 id="heading-2-timezones-with-luxon">2. Timezones with Luxon</h3>
<p>For the frontend, I leaned heavily on the <strong>Luxon</strong> library. Handling timezones "correctly" is a rabbit hole of edge cases—Daylight Saving Time (DST) shifts, ambiguous abbreviations (is "CST" Central Standard Time or China Standard Time?), and historical changes.</p>
<p>Luxon made this manageable, but it also taught me that you can't just assume a timezone offset is constant. You have to calculate it for the specific instant in time you're talking about.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>This was a fun weekend project that solved a real itch for me. It’s open source, so feel free to poke around the code or fork it if you want to add support for your favorite obscure timezone.</p>
<p>Give it a try next time you're setting up a meeting: <strong>sharetime.zone</strong></p>
]]></content:encoded></item><item><title><![CDATA[Beyond Copilot, Cursor and Claude Code: The Unbundled Coding AI Tools Stack]]></title><description><![CDATA[The AI coding landscape has shifted. We went from "just pay $20 for GitHub Copilot" to a completely unbundled stack where you mix and match your Provider, Model, and Tool.
It’s messy, but if you get the setup right, you get better performance for a f...]]></description><link>https://arnav.tech/beyond-copilot-cursor-and-claude-code-the-unbundled-coding-ai-tools-stack</link><guid isPermaLink="true">https://arnav.tech/beyond-copilot-cursor-and-claude-code-the-unbundled-coding-ai-tools-stack</guid><category><![CDATA[agentic AI]]></category><category><![CDATA[cursor]]></category><category><![CDATA[copilot]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Thu, 18 Dec 2025 02:03:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766016429361/722c2a48-afed-4f54-a26c-adb7b4709435.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The AI coding landscape has shifted. We went from "just pay $20 for GitHub Copilot" to a completely unbundled stack where you mix and match your <strong>Provider</strong>, <strong>Model</strong>, and <strong>Tool</strong>.</p>
<p>It’s messy, but if you get the setup right, you get better performance for a fraction of the cost.</p>
<p>Here is how I break down the current state of AI coding going into 2026, and how to navigate the choices between mainstream giants (Cursor, Claude) and the new wave of open models (Minimax M2, Qwen3, Kimi K2).</p>
<blockquote>
<p>NOTE: Parts of this article are written using Gemini 3 based on some of my rough notes on this topic.</p>
</blockquote>
<h2 id="heading-the-breakdown-the-3-layers">The Breakdown: The 3 Layers</h2>
<p>Just like we separate our frontend from our backend, the AI stack is now decoupled:</p>
<ol>
<li><p><strong>The Provider:</strong> Who runs the GPU? (AWS, Cerebras, your own Mac Studio).</p>
</li>
<li><p><strong>The Model:</strong> The brain. (GPT-5.1, Claude Opus, Qwen3).</p>
</li>
<li><p><strong>The Tool:</strong> Where you type. (IDE, CLI, or a background agent).</p>
</li>
</ol>
<p>Not only are there multiple mix-and-match combinations possible, I have even been using different setups during different sessions/stages working on the same project too.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766019208528/b3760018-8000-4c9b-a523-a807428ca937.jpeg" alt class="image--center mx-auto" /></p>
<p>Let’s dive into each.</p>
<h2 id="heading-layer-1-the-provider">Layer 1: The Provider</h2>
<p>This is purely about <em>who</em> runs the model. You have six options, ranging from "easy but pricey" to "DIY hardware."</p>
<ol>
<li><p><strong>Proprietary Labs:</strong> You go straight to the source (OpenAI, Anthropic, Google). Tokens are expensive, which is why we now see "Max" plans with flat fees to cap the bleeding.</p>
</li>
<li><p><strong>The Big Clouds:</strong> AWS Bedrock, GCP Vertex, Azure. This is mostly for enterprises that want the billing on their existing cloud contract. It’s safe, but rarely the fastest or cheapest. But that said, apart from Google’s models only available on their TPUs, other SOTA models are available via the big 3 clouds mostly.</p>
</li>
<li><p><strong>The Chinese Labs:</strong> Alibaba (Qwen), DeepSeek, Moonshot serve their models directly too. They are fairly cheap and performant, but you have to deal with the censorship. Asking about Tiananmen Square might flag you, but for React components, they are beasts. Creating new account on Alibaba Cloud is not exactly pleasant UX either.</p>
</li>
<li><p><strong>Infra-as-a-Service (Resellers):</strong> Providers like <strong>DeepInfra</strong>, <strong>Parasail</strong>, and <strong>Novitas</strong> rent massive GPU clusters and resell access by the token. They are significantly cheaper than the labs, but there is a catch: they often aggressively quantize models (lowering precision) to save VRAM, so you might lose some "smartness" on edge cases. There are now some <code>exacto</code> models to counter this problem.</p>
</li>
<li><p><strong>Dedicated LLM Chips:</strong> This is where the speed is. <strong>Cerebras</strong> and <strong>Groq</strong> don't use standard GPUs; they use wafer-scale engines or LPUs.</p>
<ul>
<li><p><em>Real numbers:</em> Cerebras is clocking <strong>~3,000 tokens per second</strong> on Llama 3.1 70B.1 That is not reading speed; that is "blink and it's done" speed.</p>
</li>
<li><p>I’d highly recommend trying out gpt-oss-120b on groq/cerebras and seeing the speed. Once code starts appearing immediately after you hit enter, it can change your mind about the importance of tok/s numbers.</p>
</li>
</ul>
</li>
<li><p><strong>Self-Hosting (Local):</strong> Hardware has finally caught up.</p>
<ul>
<li><p><strong>Apple:</strong> A Mac Studio with an M2/M4 Ultra chip and 128GB+ RAM.</p>
</li>
<li><p><strong>AMD:</strong> The new <strong>Strix Halo (Ryzen AI Max+ 395)</strong> APUs support up to 128GB of LPDDR5X RAM.</p>
</li>
<li><p><em>The Result:</em> You can run <strong>gpt-oss-120b</strong> or <strong>GLM-4.5-Air</strong> locally with zero API costs and total privacy.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-layer-2-the-model">Layer 2: The Model</h2>
<p>In the unbundled stack, the "Model" is the fungible component. Developers no longer ask "Is Copilot good?" but rather "Which model is best for refactoring legacy C++ vs. writing new React components?" The market has segmented into three distinct categories: The Classic Trio (High Intelligence), Chinese Open Models (High Value), and Fast/Vibe Models (High Velocity).</p>
<h3 id="heading-the-classic-trio-the-gold-medals">The Classic Trio: The Gold Medals</h3>
<p>These models represent the current state-of-the-art (SOTA) for high-complexity tasks. They are generally too expensive for autocomplete but essential for architectural planning, complex debugging, and tasks requiring deep "System 2" reasoning.</p>
<ol>
<li>GPT-5.1 Codex Max (OpenAI):</li>
</ol>
<p>This is the gold standard for agentic coding. It is built for long-running, detailed work.</p>
<ul>
<li><p><strong>Capability:</strong> It shows a remarkable aptitude for sustaining long-horizon coding tasks. Benchmarks place it at the top of SWE-Bench Verified with a score of <strong>77.9%</strong> (for "xhigh" thinking level)</p>
</li>
<li><p><strong>Mechanism:</strong> Its key innovation is <strong>compaction</strong>, allowing it to work over millions of tokens by summarizing past interactions. This unlocks "project-scale refactors" where the model must understand the implications of a change across hundreds of files</p>
</li>
<li><p><strong>Use Case:</strong> Deep debugging sessions, multi-hour agent loops, and massive refactors.</p>
</li>
</ul>
<ol start="2">
<li>Claude Opus 4.5 (Anthropic):</li>
</ol>
<p>Anthropic's flagship model excels in nuance and "human-like" reasoning.</p>
<ul>
<li><p><strong>Capability:</strong> It is described as "dynamic rather than overthinking," capable of interpreting ambiguous requirements and reasoning over architectural tradeoffs.8 It scores <strong>80.9%</strong> on SWE-Bench, slightly edging out competitors in certain evaluations</p>
</li>
<li><p><strong>Mechanism:</strong> Opus 4.5 preserves "thinking blocks" from previous turns in the model context, allowing it to maintain a coherent chain of thought throughout a conversation</p>
</li>
<li><p><strong>Use Case:</strong> Architectural planning, complex system design, and tasks where "understanding the intent" is more important than raw speed.</p>
</li>
</ul>
<ol start="3">
<li>Gemini 3 Pro (Google):</li>
</ol>
<p>The multimodal beast.</p>
<ul>
<li><p><strong>Capability:</strong> With a <strong>1 Million token context window</strong> as standard, Gemini 3 Pro is the premier model for "whole repo" understanding. It outperforms competitors in multimodal tasks—reading screenshots of UIs alongside code—and boasts a GPQA Diamond score of <strong>91.9%</strong>, indicating PhD-level scientific reasoning</p>
</li>
<li><p><strong>Mechanism:</strong> It uses a "thinking process" that allows it to reason deeply before generating a response, similar to Chain-of-Thought prompting but baked into the model architecture</p>
</li>
<li><p><strong>Use Case:</strong> Large-scale codebase understanding, multimodal UI debugging, and scientific/mathematical coding tasks.</p>
</li>
</ul>
<h3 id="heading-the-chinese-open-models-the-commoditizers">The Chinese Open Models: The Commoditizers</h3>
<p>The gap between proprietary SOTA and open weights has collapsed. Models from Chinese labs offer 90-95% of the performance of the "Classic Trio" at nearly zero marginal cost for local users or significantly reduced API costs.</p>
<p>Qwen3 Coder (Alibaba):</p>
<p>Qwen3 Coder is an open-source marvel supporting 119 languages.</p>
<ul>
<li><p><strong>Innovation:</strong> It features a "Hybrid Thinking Mode" that intelligently switches between deep reasoning (slow) and fast response based on the complexity of the query.34 This adaptability makes it an incredibly versatile tool for polyglot developers.</p>
</li>
<li><p><strong>Performance:</strong> It rivals Claude Sonnet 4 in coding benchmarks, making it a viable free alternative for local hosting</p>
</li>
</ul>
<p>DeepSeek V3 / R1:</p>
<p>Known for its "Deep Thinking" capabilities, DeepSeek R1 forces the model to verify its own logic steps before outputting code.</p>
<ul>
<li><p><strong>Innovation:</strong> The R1 model utilizes reinforcement learning strategies to "self-correct" during the generation process. This significantly reduces logic errors in complex algorithms, making it widely considered the best price-to-performance model on the market</p>
</li>
<li><p><strong>Economics:</strong> Its ultra-low cost (due to efficient MoE architecture) allows developers to use it for "brute force" tasks—generating 100 variations of a function to find the best one—without breaking the bank.</p>
</li>
</ul>
<p>Moonshot Kimi K2:</p>
<p>Kimi K2 is a "Thinking Agent" model.</p>
<ul>
<li><p><strong>Innovation:</strong> It is meticulously optimized for agentic capabilities, specifically tool use. Unlike models that struggle with multi-step instructions, Kimi K2 is trained to execute 200-300 sequential tool calls (search, file read, file write) without losing the thread of the task</p>
</li>
<li><p><strong>Performance:</strong> It scores 71.3% on SWE-Bench Verified, placing it in the upper echelon of coding models</p>
</li>
</ul>
<h3 id="heading-the-velocity-models-fast-amp-vibe">The Velocity Models: "Fast" &amp; "Vibe"</h3>
<p>A new category of model has emerged focused purely on latency. These are used for "Vibe Coding"—where the developer iterates rapidly in natural language, expecting near-instant visual feedback.</p>
<p>Grok Code Fast (xAI):</p>
<p>Grok Code Fast is optimized for sheer speed in agentic workflows.</p>
<ul>
<li><p><strong>Philosophy:</strong> xAI recognized that agentic loops (Reasoning -&gt; Tool Call -&gt; Observation -&gt; Reasoning) are often bottlenecked by the model's latency. Grok Code Fast is designed to be "blazing fast," allowing it to call dozens of tools before the user has even finished reading the first paragraph of the thinking trace</p>
</li>
<li><p><strong>Specs:</strong> It supports a 256k context window and is particularly adept at TypeScript, Python, and Rust</p>
</li>
</ul>
<p>GLM-4.5 Air (Zhipu AI):</p>
<p>A lightweight powerhouse.</p>
<ul>
<li><p><strong>Efficiency:</strong> GLM-4.5 Air requires only 32-64GB of VRAM to run locally. This makes it the most powerful model that can comfortably fit on a high-end consumer workstation (like a dual 3090 setup or a Mac Studio) while maintaining strong instruction following capabilities.39</p>
</li>
<li><p><strong>Adoption:</strong> It has become a favorite for local "vibe coding" setups where users want privacy without sacrificing too much intelligence. Even Windsurf and Cursor have built their “own” models by basically fine-tuning this.</p>
</li>
</ul>
<p>gpt-oss-120b:</p>
<p>The gpt-oss-120b represents the community's effort to run massive models on dedicated silicon like the Strix Halo Running a 120B model locally at acceptable speeds transforms the development experience, offering near-SOTA capabilities without the latency or cost of the cloud. But if run on a cloud like Cerebras or Groq, it can also give almost 1000+ tokens/second of speedy outputs.</p>
<h2 id="heading-layer-3-the-coding-interface">Layer 3: The Coding Interface</h2>
<p>The tool layer is where the developer lives. In the bundled era, this was VS Code with a proprietary plugin. In the unbundled era, the tool is a flexible chassis that accepts any provider and any model. The market has segmented into Agentic IDEs, Open Source tools, CLIs, and Vibe Coding Platforms.</p>
<h3 id="heading-the-agentic-ides-cursor-windsurf-copilot">The Agentic IDEs: Cursor, Windsurf, Copilot</h3>
<p>These are forks or heavily modified versions of VS Code designed to integrate AI natively into the editor's core loop, rather than bolting it on as a sidebar.</p>
<p>Cursor:</p>
<p>Cursor is the pioneer of the unbundled IDE space.</p>
<ul>
<li><p><strong>Features:</strong> It allows users to "tab" through code generation and features "Composer," a multi-file editing agent. Crucially, Cursor allows users to input their own API keys (BYOK) for models like Claude Opus or GPT-4o, giving them control over costs and capabilities</p>
</li>
<li><p><strong>Philosophy:</strong> Cursor treats the AI as a pair programmer that "lives" in the editor, capable of seeing the cursor position, diffs, and terminal output.</p>
</li>
</ul>
<p>Windsurf:</p>
<p>Windsurf focuses on "Flow" states.</p>
<ul>
<li><strong>Features:</strong> It utilizes deep context awareness of the codebase ("Cascade") to predict the developer's next move. By indexing the codebase locally, it allows for "context-aware" completions that understand the broader architecture of the project.</li>
</ul>
<p>GitHub Copilot (Evolution):</p>
<p>Even the progenitor of the bundled model has had to adapt. Copilot has evolved to support multiple models (Anthropic, Gemini, OpenAI) within its interface, acknowledging the unbundling trend. However, it still largely operates as a "subscription" service rather than a pure BYOK platform.</p>
<h3 id="heading-the-open-source-amp-bring-your-own-key-byok-tools">The Open Source &amp; "Bring Your Own Key" (BYOK) Tools</h3>
<p>For developers who reject vendor lock-in or telemetry, a new class of open-source editors and extensions has risen.</p>
<p>Roo Code (formerly Cline):</p>
<p>Roo Code is a VS Code extension that transforms the editor into an autonomous agent.</p>
<ul>
<li><p><strong>Features:</strong> It explicitly supports OpenRouter, Anthropic, DeepSeek, and Gemini APIs. It features "Modes" (Code, Architect, Ask, Debug) to tailor the AI's behavior and system prompts to the specific task</p>
</li>
<li><p><strong>Control:</strong> Roo Code gives the developer absolute control. You can approve every tool call, set budget limits, and switch providers on the fly. It is the tool of choice for the "sovereign developer."</p>
</li>
</ul>
<p>Kilo Code:</p>
<p>Kilo Code offers a transparent "pay for what you use" model.</p>
<ul>
<li><strong>Features:</strong> It integrates with local models (via Ollama) or cloud APIs. It focuses on "Agentic Engineering," providing a team of AI agents that work together to build software</li>
</ul>
<h3 id="heading-the-cli-renaissance">The CLI Renaissance</h3>
<p>A surprising trend in 2024-25 is the return to the Command Line Interface (CLI). As agents become more capable, the graphical overhead of an IDE becomes unnecessary for many tasks.</p>
<p>Claude Code:</p>
<p>Anthropic's CLI tool allows developers to manage complex refactors directly from the terminal.</p>
<ul>
<li><strong>Workflow:</strong> Users can issue natural language commands like "refactor the auth module to use JWTs," and the CLI manages the file reads, edits, and git commits autonomously.46 It creates a "headless" coding experience where the human acts as the manager.</li>
</ul>
<p>Gemini CLI &amp; OpenCode:</p>
<p>Google's Gemini CLI brings the power of Gemini 3 Pro/Flash to the terminal OpenCode provides an open-source, vendor-neutral alternative, popular among Vim/Neovim users who prefer lightweight, keyboard-driven workflows. These tools appeal to "vim lovers" and power users who want to script their AI interactions.</p>
<h3 id="heading-vibe-coding-platforms">Vibe Coding Platforms</h3>
<p>"Vibe Coding" refers to a development style where the user provides natural language intent ("Make it pop," "Add a dark mode toggle"), and the AI handles the implementation details entirely. The user validates the "vibe" (the outcome) rather than the code.</p>
<p><a target="_blank" href="http://Lovable.dev">Lovable.dev</a>:</p>
<p>Lovable is a platform that generates full-stack web apps from text.</p>
<ul>
<li><strong>Capabilities:</strong> It integrates with backends like Supabase and handles deployment. It effectively replaces the junior dev + DevOps loop for prototypes.48 Users can describe a "CRM for a dog walking business," and Lovable spins up the database, authentication, and frontend UI in minutes.</li>
</ul>
<p><a target="_blank" href="http://Bolt.new">Bolt.new</a>:</p>
<p>Bolt is a browser-based full-stack builder.</p>
<ul>
<li><strong>Capabilities:</strong> It creates a containerized environment where the AI can write and execute code, allowing for instant preview and iteration.49 It is the ultimate "low-floor" tool for Vibe Coding, allowing anyone with an idea to build a functional app without setting up a local dev environment.</li>
</ul>
<p>Jules (Google), Copilot, Cursor Agents:</p>
<p>Jules and the Background Agents by existing IDE heavyweights represents the "asynchronous" future of vibe coding.</p>
<ul>
<li><strong>Workflow:</strong> Unlike an IDE assistant, these works on GitHub PRs. You assign it a task ("Upgrade all dependencies and fix breaking changes"), and it creates a Pull Request hours later.51 It is "fire and forget" software development, where the AI acts as a remote contractor.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766023322509/d0057610-c494-4dad-a5c3-ecb1134f9690.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-fungible-future">The Fungible Future</h2>
<p>The "Copilot Era" of 2023-2024 was defined by convenience and bundling. The "Unbundled Era" of 2026 is defined by control, specialization, and arbitrage.</p>
<p>The three layers of the stack—<strong>Provider</strong>, <strong>Model</strong>, and <strong>Tool</strong>—have become fully fungible. A developer today might write code in <strong>Cursor</strong> (Tool), routed via <strong>DeepInfra</strong> (Provider) to <strong>DeepSeek V3</strong> (Model) for drafting, while using <strong>Claude Code</strong> (Tool) with <strong>Opus 4.5</strong> (Model) for architectural review.</p>
<p>This unbundling has three profound implications:</p>
<ol>
<li><p><strong>Price Deflation:</strong> The competition at the Provider and Model layers (driven by Chinese labs and open weights) ensures that the cost of intelligence trends toward zero for standard tasks.</p>
</li>
<li><p><strong>Tool Innovation:</strong> Freed from the burden of training models, tool builders (Roo, Cursor, Bolt) are innovating rapidly on UX, creating new paradigms like "Vibe Coding" where the interface is the product, not the model.</p>
</li>
<li><p><strong>Sovereignty:</strong> The ability to self-host or choose vendor-neutral protocols (MCP) protects developers from platform capture. The stack is no longer a monolith; it is a set of Lego blocks.</p>
</li>
</ol>
<p>As we look toward late 2026, the distinction between "writing code" and "managing agents" will continue to blur. The unbundled stack provides the necessary infrastructure for this transition, allowing developers to assemble the exact cognitive supply chain required to build the future.</p>
]]></content:encoded></item><item><title><![CDATA[The AI Revolution: Following the Path of Microchips and Cloud Computing]]></title><description><![CDATA[The artificial intelligence revolution unfolding today mirrors two previous technological waves that fundamentally transformed how we work and create. Like the microchip revolution of the 1960s-80s and the cloud computing wave of the 1990s-2000s, AI ...]]></description><link>https://arnav.tech/the-ai-revolution-following-the-path-of-microchips-and-cloud-computing</link><guid isPermaLink="true">https://arnav.tech/the-ai-revolution-following-the-path-of-microchips-and-cloud-computing</guid><category><![CDATA[ai wave]]></category><category><![CDATA[Microchips]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Sun, 17 Aug 2025 12:40:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755434376920/b356cec4-c952-4c38-b044-5cbb9bd45628.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The artificial intelligence revolution unfolding today mirrors two previous technological waves that fundamentally transformed how we work and create. Like the microchip revolution of the 1960s-80s and the cloud computing wave of the 1990s-2000s, AI is democratizing access to powerful capabilities while simultaneously displacing some jobs and creating exponentially more opportunities. Understanding these historical patterns reveals why companies investing early in enterprise AI access—much like those that pioneered laptop distribution and cloud adoption—will capture the greatest gains from this transformation.</p>
<h2 id="heading-the-microchip-revolution-from-labs-to-laptops">The Microchip Revolution: From Labs to Laptops</h2>
<h3 id="heading-the-transformation-pattern">The Transformation Pattern</h3>
<p>The microchip revolution followed a clear democratization arc. <strong>In the 1960s and early 1970s, computing existed primarily in corporate mainframes and university research centers</strong>. These room-sized machines required specialized knowledge and institutional access—individual programmers couldn't become computer experts without affiliation to top laboratories or universities.<a target="_blank" href="https://www.imec-int.com/en/what-we-offer/semiconductor-education-and-workforce-development/microchips/history">^1</a></p>
<p>The introduction of microprocessors changed everything. <strong>Intel's 4004 in 1971 and the more powerful 8008 in 1972 made it possible to build computers that could sit on office desks</strong>. By the late 1970s and early 1980s, <strong>personal computers became mass-market products, with the IBM PC in 1981 establishing the dominant standard</strong>. This shift enabled <strong>anyone with a personal computer to start writing software and experimenting with code</strong>.<a target="_blank" href="https://www.talan.com/global/en/microchip-macro-revolution">^3</a><a target="_blank" href="https://stacklegend.com/en/blog/exciting-stories-of-the-it-industry-1970s">^5</a></p>
<h3 id="heading-job-displacement-and-creation">Job Displacement and Creation</h3>
<p><strong>The computer revolution eliminated traditional roles like secretaries who took dictation and typists</strong>, but created vastly more opportunities. According to McKinsey Global Institute research, <strong>while approximately 3.5 million jobs were destroyed in the US between 1980 and the present due to personal computing and the Internet, over 19 million new jobs were created—a net gain of 15.8 million positions</strong>. This represents <strong>about 10% of today's civilian labor force working in occupations that exist directly because of computer technology</strong>.<a target="_blank" href="https://www.mckinsey.com/featured-insights/future-of-work/what-can-history-teach-us-about-technology-and-jobs">^6</a></p>
<p>The new jobs spanned multiple categories: hardware manufacturing, software development, system administration, and entirely new industries like call centers that <strong>couldn't exist without computer terminals to access customer information</strong>.<a target="_blank" href="https://www.mckinsey.com/featured-insights/future-of-work/what-can-history-teach-us-about-technology-and-jobs">^6</a></p>
<h2 id="heading-the-cloud-computing-wave-from-server-rooms-to-services">The Cloud Computing Wave: From Server Rooms to Services</h2>
<h3 id="heading-the-transformation-pattern-1">The Transformation Pattern</h3>
<p>The cloud revolution followed a similar democratization model. <strong>In the 1990s and early 2000s, every tech company providing internet services needed to run their own physical servers</strong>. Becoming a system administrator, database administrator, or Linux engineer required working at companies with substantial infrastructure investments.<a target="_blank" href="https://www.4cit.group/traditional-dba-jobs-shifting-as-a-result-of-the-cloud/">^7</a></p>
<p><strong>The introduction of Infrastructure-as-a-Service platforms in 2006, led by Amazon Web Services, radically changed how businesses manage computing resources</strong>. Instead of massive upfront capital expenditures for servers, <strong>companies could provision computing power with just a few clicks, transforming capital expenses into operational expenses</strong>. This shift <strong>significantly lowered the barrier to entry for starting software companies and created an economic boom that continues today</strong>.<a target="_blank" href="https://blog.servermania.com/the-history-of-cloud-computing">^9</a></p>
<h3 id="heading-job-evolution-in-the-cloud-era">Job Evolution in the Cloud Era</h3>
<p>Cloud computing didn't simply eliminate traditional IT roles—it transformed them. <strong>Traditional database administrators saw their responsibilities shift from hardware installation and maintenance to database customization, query optimization, and security management</strong>. <strong>System administrators evolved into DevOps engineers and cloud architects, focusing on orchestrating distributed systems rather than managing physical hardware</strong>.<a target="_blank" href="https://careersdonewrite.com/blog/will-cloud-computing-eliminate-system-administrator-jobs">^8</a></p>
<p>The cloud era <strong>created infinitely more opportunities than it displaced, with backend and web developer jobs exploding as the barrier to building consumer products dropped dramatically</strong>. Companies could now <strong>focus on application logic rather than infrastructure management, enabling rapid innovation in web and mobile applications</strong>.<a target="_blank" href="https://iariw.org/wp-content/uploads/2024/08/Niebel-and-Schomburg-2024-Cloud-computing-ICT-investments-and-productivity-Session-7D-.pdf">^11</a></p>
<h2 id="heading-the-ai-wave-from-gpu-clusters-to-universal-access">The AI Wave: From GPU Clusters to Universal Access</h2>
<h3 id="heading-current-access-barriers">Current Access Barriers</h3>
<p>Today's AI landscape mirrors the early stages of both previous revolutions. <strong>Access to powerful AI capabilities has been limited to organizations with massive GPU clusters</strong>. <strong>Training sophisticated AI models requires enormous computational power—GPT-4 training alone required around 30 megawatts of power</strong>, while <strong>high-end GPUs can consume 400-700 watts per chip, similar to a microwave</strong>.<a target="_blank" href="https://about.bnef.com/insights/commodities/power-for-ai-easier-said-than-built/">^13</a><a target="_blank" href="https://www.robeco.com/en-uk/insights/2023/11/the-energy-challenge-of-powering-ai-chips">^15</a></p>
<p><strong>Normal software engineers struggle to become ML experts or create AI applications without exposure to these expensive, specialized computing resources</strong>. The barriers are substantial: <strong>AI data centers require exponentially more electricity than traditional facilities</strong>, and <strong>GPU clusters can cost millions of dollars to establish and operate</strong>.<a target="_blank" href="https://cloud.google.com/blog/products/data-analytics/democratization-of-ml-and-ai-with-google-cloud">^16</a><a target="_blank" href="https://www.rcrwireless.com/20250318/featured/ai-data-centers-power">^18</a></p>
<h3 id="heading-democratization-through-multiple-vectors">Democratization Through Multiple Vectors</h3>
<p>The democratization of AI is happening through several converging trends:</p>
<p><strong>Enterprise Token Access</strong>: Companies are increasingly <strong>providing employees with unlimited or generous AI token allowances, with enterprise AI spending growing 75% year-over-year</strong>. <strong>Organizations investing in AI access for employees are seeing an average 41% return on investment</strong>, with <strong>75% of knowledge workers now using AI at work</strong>.<a target="_blank" href="https://www.microsoft.com/en-us/worklab/work-trend-index/ai-at-work-is-here-now-comes-the-hard-part">^19</a><a target="_blank" href="https://www.snowflake.com/en/news/press-releases/snowflake-research-reveals-that-92-percent-of-early-adopters-see-roi-from-ai-investments/">^21</a></p>
<p><strong>Open Source Models</strong>: The proliferation of open-source AI models is reducing dependency on proprietary systems. <strong>Powerful models can now run locally on consumer hardware, with Apple's M-series chips particularly well-suited for AI workloads</strong>. <strong>Local AI implementations on Mac Silicon can run sophisticated models like Llama and Qwen without cloud dependencies</strong>.<a target="_blank" href="https://venturebeat.com/ai/you-can-now-run-the-most-powerful-open-source-ai-models-locally-on-mac-m4-computers-thanks-to-exo-labs/">^22</a></p>
<p><strong>Improved Local Compute</strong>: <strong>Apple's M4 chips enable users to run some of the most advanced open-source large language models locally</strong>, while <strong>techniques like "Mixture of Experts" architectures are improving training efficiency</strong>. This allows <strong>individual developers and small organizations to experiment with AI without massive infrastructure investments</strong>.<a target="_blank" href="https://about.bnef.com/insights/commodities/power-for-ai-easier-said-than-built/">^23</a><a target="_blank" href="https://forum.level1techs.com/t/local-ai-on-m-chip-macbooks/220407">^24</a></p>
<h3 id="heading-productivity-gains-and-job-creation-patterns">Productivity Gains and Job Creation Patterns</h3>
<p>Early evidence suggests AI follows the same job displacement/creation pattern as previous waves. <strong>Research shows AI can improve highly skilled worker performance by nearly 40% when used within its capabilities</strong>, while <strong>enterprise studies indicate AI helps workers save time (90%), focus on important work (85%), and be more creative (84%)</strong>.<a target="_blank" href="https://www.microsoft.com/en-us/worklab/work-trend-index/ai-at-work-is-here-now-comes-the-hard-part">^25</a></p>
<p><strong>Like previous technological revolutions, AI is eliminating some routine tasks while creating demand for new skills</strong>. The <strong>economic potential of generative AI could enable labor productivity growth of 0.1 to 0.6 percent annually through 2040</strong>, with <strong>work automation potentially adding 0.5 to 3.4 percentage points annually to productivity growth</strong>.<a target="_blank" href="https://www.mckinsey.com/capabilities/mckinsey-digital/our-insights/the-economic-potential-of-generative-ai-the-next-productivity-frontier">^26</a></p>
<h2 id="heading-the-investment-opportunity-learning-from-history">The Investment Opportunity: Learning from History</h2>
<h3 id="heading-early-adopter-advantages">Early Adopter Advantages</h3>
<p>Companies that invested early in previous technological waves captured disproportionate returns. <strong>Organizations that provided employees with laptops during the PC revolution gained competitive advantages through improved mobility and productivity</strong>. Similarly, <strong>early cloud adopters reduced infrastructure costs while enabling faster innovation cycles</strong>.<a target="_blank" href="https://iariw.org/wp-content/uploads/2024/08/Niebel-and-Schomburg-2024-Cloud-computing-ICT-investments-and-productivity-Session-7D-.pdf">^12</a></p>
<p>The same pattern is emerging with AI. <strong>Companies like Spotify are investing heavily in AI-powered features, using artificial intelligence to drive deeper personalization and user engagement</strong>. <strong>Early enterprise AI adopters report seeing ROI within months of implementation, with 92% of organizations already seeing returns on their AI investments</strong>.<a target="_blank" href="https://www.snowflake.com/en/news/press-releases/snowflake-research-reveals-that-92-percent-of-early-adopters-see-roi-from-ai-investments/">^27</a></p>
<h3 id="heading-strategic-implications">Strategic Implications</h3>
<p>The historical pattern suggests that <strong>companies providing generous enterprise AI access and unlimited tokens will see the most significant gains from this technological wave</strong>. Just as <strong>cloud adoption enabled the creation of new business models and reduced barriers to innovation</strong>, <strong>universal AI access within organizations is likely to unlock new forms of productivity and creativity</strong>.<a target="_blank" href="https://iariw.org/wp-content/uploads/2024/08/Niebel-and-Schomburg-2024-Cloud-computing-ICT-investments-and-productivity-Session-7D-.pdf">^12</a></p>
<p><strong>The key insight from both the microchip and cloud revolutions is that democratization—making powerful capabilities accessible to more people—creates exponentially more value than it destroys</strong>. The companies that recognize this pattern and invest accordingly will be positioned to capture the greatest benefits as AI transforms every aspect of business operations.</p>
<p>The AI wave represents not just another technological shift, but the continuation of a decades-long trend toward democratizing computational power. Organizations that embrace this democratization—by providing their teams with unlimited AI access and encouraging experimentation—will write the next chapter in the ongoing story of technology-driven economic growth.</p>
<div>⁂</div>

]]></content:encoded></item><item><title><![CDATA[My Next Project: Building the Open-Source, Cross-Platform Authenticator I Always Wanted]]></title><description><![CDATA[Like many of you, I have a workflow. It’s a finely tuned set of tools and habits that lets me move between my different machines—a Mac for primary development, a Windows machine for gaming and testing—without missing a beat. For years, a key piece of...]]></description><link>https://arnav.tech/my-next-project-building-the-open-source-cross-platform-authenticator-i-always-wanted</link><guid isPermaLink="true">https://arnav.tech/my-next-project-building-the-open-source-cross-platform-authenticator-i-always-wanted</guid><category><![CDATA[Open Source]]></category><category><![CDATA[authentication]]></category><category><![CDATA[authorization]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Sat, 21 Jun 2025 23:19:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750547881820/742b92c6-38ae-4dcd-bad4-b5f156b2edd5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Like many of you, I have a workflow. It’s a finely tuned set of tools and habits that lets me move between my different machines—a Mac for primary development, a Windows machine for gaming and testing—without missing a beat. For years, a key piece of that workflow was the Authy desktop app. It was simple, it was reliable, and it was <em>there</em>. Need a 2FA code on my desktop? Click, copy, paste. Done. It was a perfect little piece of utility software.</p>
<p>And then, the email landed. Twilio announced they were sunsetting the Authy desktop app.</p>
<p>Initially, the end-of-life was set for August 2024, which was disappointing but manageable. But then, in a move that felt like a slap in the face to its most loyal users, the date was abruptly pulled forward to March 19, 2024.1 The developer community's reaction was, to put it mildly, not positive. One user summed it up perfectly: "This is an excellent way to piss off thousands of developers and make sure we never touch any of your products again".1</p>
<p>The real salt in the wound, however, wasn't just the loss of the app. It was the realization of the digital cage we'd been living in. Authy provided no way to export your 2FA secrets.1 The only way to migrate to a new authenticator was to painstakingly go through every single one of your accounts, disable 2FA, and then re-enable it with a new app. For anyone with more than a handful of accounts, this was a nightmare.</p>
<p>This experience was a catalyst. It crystallized a problem that had been nagging at me for a while: our reliance on proprietary, centrally-controlled services for our most critical security functions. The moment a company decides to change its strategy, pivot, or simply cut costs, we, the users, are left stranded. This project, therefore, is more than just an attempt to build a replacement for a defunct app. It’s a philosophical stance. It's about reclaiming our digital sovereignty and building a tool based on principles of openness, user control, and data ownership. It’s about building the authenticator I, and many others, always wanted.</p>
<blockquote>
<p>NOTE: Many parts of this article is paraphrased using Google Gemini Deep Research based on my rough notes for this project.</p>
</blockquote>
<h2 id="heading-surveying-the-landscape-a-crowded-field-with-room-for-one-more">Surveying the Landscape: A Crowded Field with Room for One More</h2>
<p>Before diving into a new project, it’s always wise to survey the existing landscape. I’m not the first person to have this idea, and there are some truly excellent open-source authenticator apps out there. The goal isn't to reinvent the wheel, but to see if there's a specific niche—my niche—that isn't being perfectly served.</p>
<h3 id="heading-the-contenders-2fas-and-ente">The Contenders: 2FAS and Ente</h3>
<p>Two projects immediately stand out: 2FAS and Ente Auth.</p>
<p><strong>2FAS</strong> is fantastic and has a massive following for good reason. It's fully open-source, doesn't require an account to use, has a beautiful and simple user interface, and even provides browser extensions for one-tap authentication.4 It hits many of the right notes on privacy and usability. However, it falls short of my personal "be everywhere" requirement. It lacks official desktop clients, and while there are browser extensions, it's not the same as a native application. Furthermore, there's no support for wearables like the Apple Watch, a feature some users have explicitly wished for.5</p>
<p><strong>Ente Auth</strong> gets even closer to my ideal. It is also open-source and has been praised by the likes of Linus Tech Tips. Crucially, it offers a full suite of native clients for Android, iOS, Windows, macOS, and Linux, plus a web app.9 This is a huge win for cross-platform users. It provides end-to-end encrypted cloud backup and sync, which is a great feature for most people.</p>
<p>But here’s the rub. Ente's sync is tied to <em>their</em> cloud service.8 While it is end-to-end encrypted and the company has a strong privacy focus, it doesn't satisfy the power-user desire for complete and total control over the backup location. What if I want to store my encrypted secrets in my own S3 bucket? Or on my personal Google Drive? Or sync them to a local server via an<code>rsync</code> endpoint? This is where I see the gap.</p>
<p>My project aims to fill the niche for the user who wants the comprehensive, multi-platform presence of Ente but with the absolute data sovereignty of being able to choose their own backup provider. It's for the developer who demands a first-class, scriptable Command-Line Interface (CLI) not just as an afterthought, but as a core part of the experience.</p>
<h2 id="heading-the-vision-a-manifesto-for-the-perfect-authenticator">The Vision: A Manifesto for the Perfect Authenticator</h2>
<p>This project is guided by a set of core principles that address the shortcomings of existing solutions and embody the spirit of user empowerment.</p>
<ol>
<li><p><strong>Platform Ubiquity:</strong> The app must be available and feel native on every platform I use: iOS, Android, watchOS, Wear OS, Windows, macOS, Linux, the Web (as a PWA and browser extension), and the command line.</p>
</li>
<li><p><strong>User Sovereignty:</strong> Your secrets are yours. Period. The application will <strong>never</strong> require you to use a hosted cloud service. Backups will be fully under your control.</p>
</li>
<li><p><strong>Interoperability:</strong> The core logic should be a self-contained, distributable library that other developers can use in their own projects.</p>
</li>
<li><p><strong>Openness:</strong> Everything, from the cryptographic core to the UI, will be open-source, auditable, and community-driven.</p>
</li>
<li><p><strong>Intelligence:</strong> The app should do more than just display six-digit codes. It should provide context and act as a security advisor to help users make smarter decisions.</p>
</li>
</ol>
<h3 id="heading-feature-breakdown">Feature Breakdown</h3>
<p>With those principles in mind, here is the planned feature set:</p>
<ul>
<li><p><strong>Baseline Requirements (v1.0):</strong></p>
<ul>
<li><p>Support for TOTP (Time-based) and HOTP (HMAC-based) one-time passwords.</p>
</li>
<li><p>Ability to add accounts by scanning a QR code or manually entering the secret string.</p>
</li>
<li><p>GUI apps for iOS, Android, watchOS, and Wear OS.</p>
</li>
<li><p>GUI apps for desktop: Windows, macOS, and Linux.</p>
</li>
<li><p>A fully-featured web app (PWA) and browser extensions for Chrome and Firefox.</p>
</li>
<li><p>A first-class, scriptable CLI for power users and automation.</p>
</li>
<li><p>Ability to create encrypted backups locally, without involving any cloud service.</p>
</li>
</ul>
</li>
<li><p><strong>Future Features (v2.0 and beyond):</strong></p>
<ul>
<li><p>A simple, secure password manager for storing private passwords alongside 2FA secrets.</p>
</li>
<li><p>Multiple backup "providers," allowing you to "bring your own cloud." This includes options like a personal S3-compatible bucket, a Google Drive or Apple iCloud folder, or even an <code>rsync</code>-compatible endpoint.</p>
</li>
</ul>
</li>
</ul>
<p>To make the vision clearer, here’s how my proposed app (let's tentatively call it <strong>AuthZen</strong>) stacks up against the competition:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Feature</td><td>AuthZen (Proposed)</td><td>Authy (Legacy)</td><td>2FAS</td><td>Ente</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Platforms</strong></td><td></td><td></td><td></td><td></td></tr>
<tr>
<td>Desktop (Win/Mac/Linux)</td><td>✅</td><td>❌ (EOL)</td><td>❌</td><td>✅</td></tr>
<tr>
<td>Browser Extension</td><td>✅</td><td>❌ (EOL)</td><td>✅</td><td>✅</td></tr>
<tr>
<td>CLI</td><td>✅ (First-Class)</td><td>❌</td><td>❌</td><td>✅</td></tr>
<tr>
<td>Wearable (watchOS/WearOS)</td><td>✅</td><td>❌</td><td>❌</td><td>❌</td></tr>
<tr>
<td><strong>Backup &amp; Sync</strong></td><td></td><td></td><td></td><td></td></tr>
<tr>
<td>Local/Offline Export</td><td>✅</td><td>❌</td><td>✅</td><td>✅</td></tr>
<tr>
<td>Bring-Your-Own Cloud (S3, etc.)</td><td>✅ (v2)</td><td>❌</td><td>❌</td><td>❌</td></tr>
<tr>
<td>Vendor-Managed Cloud Sync</td><td>❌ (by design)</td><td>✅</td><td>✅</td><td>✅</td></tr>
</tbody>
</table>
</div><p>This table shows the clear value proposition. AuthZen is designed for the user who wants it all: every platform, complete control over their data, and a powerful CLI, all wrapped in an open-source package.</p>
<h2 id="heading-the-architectural-blueprint-kotlin-one-codebase-to-rule-them-all">The Architectural Blueprint: Kotlin, One Codebase to Rule Them All</h2>
<p>So, how is it possible for a solo developer to build and maintain an application across such a vast array of platforms? The answer lies in a carefully chosen, modern tech stack that maximizes code reuse without sacrificing performance or the native feel of each platform. The cornerstone of this entire project is <strong>Kotlin</strong>.</p>
<h3 id="heading-why-kotlin-multiplatform-is-the-only-sane-choice">Why Kotlin Multiplatform is the Only Sane Choice</h3>
<p>Building native apps for iOS (Swift/SwiftUI), Android (Kotlin/Compose), and Desktop (C++/Qt, C#/WPF, Electron/JS, etc.) would require three or more separate codebases. This is simply not feasible. This is where <strong>Kotlin Multiplatform (KMP)</strong> comes in. KMP is a technology from JetBrains that allows you to share code between different platforms while retaining full access to native APIs.12</p>
<p>This isn't like older cross-platform frameworks that give you a one-size-fits-none solution. With KMP, you decide what to share. For this project, the plan is to share almost everything: the business logic, the data models, the network code, and even the UI. The fact that Google now officially supports KMP for its Jetpack libraries gives this approach immense credibility and ensures its longevity.14</p>
<h3 id="heading-the-core-engine-lib2fa">The Core Engine: <code>lib2fa</code></h3>
<p>The heart of the application will be a shared Kotlin module, tentatively named <code>lib2fa</code>. This library will contain all the critical, non-UI logic:</p>
<ul>
<li><p>The implementation of the TOTP and HOTP algorithms.</p>
</li>
<li><p>Logic for parsing <code>otpauth://</code> URIs.</p>
</li>
<li><p>Encryption and decryption routines for backups.</p>
</li>
<li><p>State management for the list of accounts.</p>
</li>
<li><p>(In v2.0) The integration logic for the various backup providers (S3, Google Drive, etc.).</p>
</li>
</ul>
<p>The beauty of KMP is how this single library can be compiled for different targets:</p>
<ul>
<li><p><strong>For JVM Targets (Android, Desktop):</strong> The <code>lib2fa</code> code will be compiled into a standard Java <code>.jar</code> file, which can be easily consumed by the Android and Desktop JVM applications.12</p>
</li>
<li><p><strong>For Native Targets (iOS, CLI):</strong> Using <strong>Kotlin/Native</strong>, the <em>exact same</em> Kotlin code is compiled down to native machine code.15 For iOS and watchOS, this produces a</p>
<p>  <code>.xcframework</code> that can be imported directly into an Xcode project. For the desktop and CLI apps, it produces standalone executables or shared libraries (<code>.so</code> on Linux, <code>.dll</code> on Windows).15 This directly fulfills the goal of having a core engine that is truly interoperable.</p>
</li>
</ul>
<h3 id="heading-painting-the-pixels-with-compose-multiplatform">Painting the Pixels with Compose Multiplatform</h3>
<p>For all the graphical user interfaces, I'll be using <strong>Compose Multiplatform</strong>. This is a declarative UI framework from JetBrains, based on Android's popular Jetpack Compose, that allows you to share your UI code across Android, iOS, desktop, and even the web.13</p>
<p>This means I can write the UI for a feature <em>once</em> in Kotlin, and it will run everywhere.</p>
<ul>
<li><p>On <strong>Desktop</strong>, Compose uses the powerful Skia 2D graphics library for rendering, giving it native performance.17</p>
</li>
<li><p>On <strong>Android</strong>, it's the native Jetpack Compose framework.</p>
</li>
<li><p>On <strong>iOS</strong>, it renders its UI into a native <code>UIViewController</code>, allowing for seamless integration with the rest of the iOS ecosystem.18</p>
</li>
<li><p>On the <strong>Web</strong>, it compiles to WebAssembly (WASM), offering near-native performance in the browser.20</p>
</li>
</ul>
<p>This unification of logic and UI is the project's strategic enabler. When I add the v2.0 password manager feature, I'll add the logic to <code>lib2fa</code> and the UI screens to the shared Compose module. With one push, that feature will be available consistently and simultaneously across every single platform. This is a massive advantage in development speed and ensures a cohesive user experience that many cross-platform projects lack.</p>
<h3 id="heading-the-cli-a-purely-native-experience">The CLI: A Purely Native Experience</h3>
<p>The Command-Line Interface won't be a second-class citizen. It will be a dedicated Kotlin/Native application that depends directly on the native-compiled <code>lib2fa</code>. I'll use a robust library like <code>kotlinx-cli</code> to build a powerful and user-friendly interface with commands, subcommands, and flags, making it perfect for scripting and automation.22</p>
<p>Here’s a clear breakdown of the architecture:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Target Platform</td><td>Core Logic (lib2fa)</td><td>UI Framework</td><td>Final Artifact(s)</td></tr>
</thead>
<tbody>
<tr>
<td>Android / Wear OS</td><td>Kotlin/JVM</td><td>Jetpack Compose</td><td><code>.jar -&gt; .apk</code></td></tr>
<tr>
<td>iOS / watchOS</td><td>Kotlin/Native</td><td>Compose Multiplatform</td><td><code>.xcframework -&gt; .ipa</code></td></tr>
<tr>
<td>Desktop (Win, Mac, Linux)</td><td>Kotlin/JVM</td><td>Compose for Desktop (Skia)</td><td><code>.exe, .dmg, .deb</code></td></tr>
<tr>
<td>CLI (Win, Mac, Linux)</td><td>Kotlin/Native</td><td>None (e.g., kotlinx-cli)</td><td><code>.so/.dll -&gt; binary</code></td></tr>
<tr>
<td>Web (PWA / Extension)</td><td>Kotlin/WASM</td><td>Compose for Web (Alpha)</td><td><code>wasm + js</code></td></tr>
</tbody>
</table>
</div><h2 id="heading-the-road-ahead-open-source-open-for-contribution">The Road Ahead: Open Source, Open for Contribution</h2>
<p>This is an ambitious project, but it's one I'm incredibly passionate about. The entire codebase, from <code>lib2fa</code> to every UI component, will be open-source under a permissive license. Trust is paramount for a security tool, and the only way to earn it is through transparency.</p>
<p>This is just the beginning of the journey. I'm starting this project not just for myself, but for everyone who felt the sting of the Authy shutdown and wished for a better, more open alternative. I'll be setting up a GitHub repository soon where you can follow the progress, star the project, contribute ideas, and eventually, submit code.</p>
<p>Let's build the tool we all deserve.</p>
<h4 id="heading-works-referenced"><strong>Works Referenced</strong></h4>
<ol>
<li><p>Twilio reminds users that Authy Desktop apps die in March – not in August - The Register, accessed on June 21, 2025, <a target="_blank" href="https://www.theregister.com/2024/02/15/twilio_authy_eol/">https://www.theregister.com/2024/02/15/twilio_authy_eol/</a></p>
</li>
<li><p>Topic: Authy for Desktop End of Life (EOL) @ AskWoody, accessed on June 21, 2025, <a target="_blank" href="https://www.askwoody.com/forums/topic/authy-for-desktop-end-of-life-eol/">https://www.askwoody.com/forums/topic/authy-for-desktop-end-of-life-eol/</a></p>
</li>
<li><p>help.twilio.com, accessed on June 21, 2025, <a target="_blank" href="https://help.twilio.com/articles/22771146070299-User-guide-End-of-Life-EOL-for-Twilio-Authy-Desktop-app">https://help.twilio.com/articles/22771146070299-User-guide-End-of-Life-EOL-for-Twilio-Authy-Desktop-app</a></p>
</li>
<li><p>2FA Authenticator (2FAS) on the App Store, accessed on June 21, 2025, <a target="_blank" href="https://apps.apple.com/us/app/2fa-authenticator-2fas/id1217793794">https://apps.apple.com/us/app/2fa-authenticator-2fas/id1217793794</a></p>
</li>
<li><p>2FAS Review - PCMag, accessed on June 21, 2025, <a target="_blank" href="https://www.pcmag.com/reviews/2fas">https://www.pcmag.com/reviews/2fas</a></p>
</li>
<li><p>2FAS - 2FA Authentication App Reviews (2025) - Product Hunt, accessed on June 21, 2025, <a target="_blank" href="https://www.producthunt.com/products/2fas-2fa-authentication-app/reviews">https://www.producthunt.com/products/2fas-2fa-authentication-app/reviews</a></p>
</li>
<li><p>Ente Auth - 2FA Authenticator – Apps on Google Play, accessed on June 21, 2025, <a target="_blank" href="https://play.google.com/store/apps/details/ente_Authenticator?id=io.ente.auth&amp;hl=en_AU">https://play.google.com/store/apps/details/ente_Authenticator?id=io.ente.auth&amp;hl=en_AU</a></p>
</li>
<li><p>What is Ente Auth? - WorkOS, accessed on June 21, 2025, <a target="_blank" href="https://workos.com/blog/what-is-ente-auth">https://workos.com/blog/what-is-ente-auth</a></p>
</li>
<li><p>Ente Auth - 2FA Authenticator - Apps on Google Play, accessed on June 21, 2025, <a target="_blank" href="https://play.google.com/store/apps/details?id=io.ente.auth">https://play.google.com/store/apps/details?id=io.ente.auth</a></p>
</li>
<li><p>Ente Auth - Open source 2FA authenticator, with E2EE backups, accessed on June 21, 2025, <a target="_blank" href="https://ente.io/auth/">https://ente.io/auth/</a></p>
</li>
<li><p>Ente Auth - 2FA Authenticator on the App Store, accessed on June 21, 2025, <a target="_blank" href="https://apps.apple.com/us/app/ente-auth-2fa-authenticator/id6444121398">https://apps.apple.com/us/app/ente-auth-2fa-authenticator/id6444121398</a></p>
</li>
<li><p>Kotlin Multiplatform Development: A Comprehensive Guide - Riseup Labs, accessed on June 21, 2025, <a target="_blank" href="https://riseuplabs.com/kotlin-multiplatform-development-comprehensive-guide/">https://riseuplabs.com/kotlin-multiplatform-development-comprehensive-guide/</a></p>
</li>
<li><p>Kotlin Multiplatform – Build Cross-Platform Apps - JetBrains, accessed on June 21, 2025, <a target="_blank" href="https://www.jetbrains.com/kotlin-multiplatform/">https://www.jetbrains.com/kotlin-multiplatform/</a></p>
</li>
<li><p>Kotlin Multiplatform Overview - Android Developers, accessed on June 21, 2025, <a target="_blank" href="https://developer.android.com/kotlin/multiplatform">https://developer.android.com/kotlin/multiplatform</a></p>
</li>
<li><p>Kotlin/Native | Kotlin Documentation, accessed on June 21, 2025, <a target="_blank" href="https://kotlinlang.org/docs/native-overview.html">https://kotlinlang.org/docs/native-overview.html</a></p>
</li>
<li><p>Kotlin Multiplatform | Kotlin Documentation, accessed on June 21, 2025, <a target="_blank" href="https://kotlinlang.org/docs/multiplatform.html">https://kotlinlang.org/docs/multiplatform.html</a></p>
</li>
<li><p>Compose Multiplatform – Beautiful UIs Everywhere - JetBrains, accessed on June 21, 2025, <a target="_blank" href="https://www.jetbrains.com/compose-multiplatform/">https://www.jetbrains.com/compose-multiplatform/</a></p>
</li>
<li><p>Has anyone used Compose Multiplatform? : r/FlutterDev - Reddit, accessed on June 21, 2025, <a target="_blank" href="https://www.reddit.com/r/FlutterDev/comments/1afeo2r/has_anyone_used_compose_multiplatform/">https://www.reddit.com/r/FlutterDev/comments/1afeo2r/has_anyone_used_compose_multiplatform/</a></p>
</li>
<li><p>Building a subscription tracker Desktop and iOS app with compose multiplatform - Setup, accessed on June 21, 2025, <a target="_blank" href="https://dev.to/kuroski/building-a-subscription-tracker-desktop-and-ios-app-with-compose-multiplatform-5feg">https://dev.to/kuroski/building-a-subscription-tracker-desktop-and-ios-app-with-compose-multiplatform-5feg</a></p>
</li>
<li><p>Create your Compose Multiplatform app - JetBrains, accessed on June 21, 2025, <a target="_blank" href="https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-create-first-app.html">https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-create-first-app.html</a></p>
</li>
<li><p>Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable. - GitHub, accessed on June 21, 2025, <a target="_blank" href="https://github.com/JetBrains/compose-multiplatform">https://github.com/JetBrains/compose-multiplatform</a></p>
</li>
<li><p>Kotlin/kotlinx-cli: Pure Kotlin implementation of a generic CLI parser. - GitHub, accessed on June 21, 2025, <a target="_blank" href="https://github.com/Kotlin/kotlinx-cli">https://github.com/Kotlin/kotlinx-cli</a></p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Evaluating SotA LLM Models trying to solve a net-new LeetCode style puzzle]]></title><description><![CDATA[I am sure a lot of you would have seen this particular meme template. It has given rise to entire genre of tiktoks where girls are amazed at how much calculation guys do to pick which stall to go to in a crowded row of urinals.

I actually even made ...]]></description><link>https://arnav.tech/evaluating-sota-llm-models-trying-to-solve-a-net-new-leetcode-style-puzzle</link><guid isPermaLink="true">https://arnav.tech/evaluating-sota-llm-models-trying-to-solve-a-net-new-leetcode-style-puzzle</guid><category><![CDATA[llm]]></category><category><![CDATA[leetcode]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Model Evaluation]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Thu, 23 Jan 2025 01:56:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ln2J1t5zGYo/upload/13849f00e0ef2c4fc8e0df08b69a7f9e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I am sure a lot of you would have seen this particular meme template. It has given rise to entire genre of tiktoks where girls are amazed at how much calculation guys do to pick which stall to go to in a crowded row of urinals.</p>
<p><img src="https://i.imgflip.com/12msgn.jpg" alt="High Quality Urinal Guy (More text room) Blank Meme Template" class="image--center mx-auto" /></p>
<p>I actually even made a whole game to place people on urinal stalls using various code-generating tools like <a target="_blank" href="https://bolt.new">bolt.new</a> to have fun with this</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/championswimmer/status/1881512904161415625">https://twitter.com/championswimmer/status/1881512904161415625</a></div>
<p> </p>
<h2 id="heading-the-prompt">The Prompt</h2>
<p>Anyway, that led me to wonder, how would today’s (that is Jan 2025) state of the art LLM models fare on solving a LeetCode-style puzzle of optimally placing people in a row of urinal stalls which are already partially occupied.</p>
<p>So I tried my best to come up with a prompt that’s worded like typical LeetCode questions</p>
<blockquote>
<p>There are 8 urinal stalls and some people are already using some of them. We have to place one new person in that stall. The current state of the urinal stalls is represented by a 8 digit number like 01100110 which means there are people occupying stalls 2, 3,6 and 7 After the new person is placed at a position, the final "comfort" score is calculated. We go from one end to another, and for every person (i.e. a '1') we do the following</p>
<ul>
<li><p>add +1 for empty slot on either left or right (+2 if both empty)</p>
</li>
<li><p>add another +1 for 2 empty slots on either left or right (+2 if on both sides)</p>
</li>
</ul>
<p>For the following existing urinal stall configurations, give the answer that in which position should the new person be added, and what will be the total comfort score of that configurations. If there are more than one positions where the highest comfort score can be achieved, any of those positions is a correct answer. (stalls are counted 1-indexed, starting from left).</p>
<p>Question Inputs</p>
<ol>
<li><p>01101010</p>
</li>
<li><p>11010101</p>
</li>
<li><p>11001011</p>
</li>
<li><p>00111100</p>
</li>
<li><p>01001010</p>
</li>
<li><p>11001110</p>
</li>
<li><p>10000110</p>
</li>
<li><p>11000110</p>
</li>
</ol>
</blockquote>
<p>This particular question is something I just conjured up 30 min ago, so obviously this exact question would not be in the training data. But the way this question is framed with the particular format of input and output, is fairly common from LeetCode to events like Advent of Code, so the LLMs are definitely trained on tons of data of this particular format.</p>
<h2 id="heading-the-answers">The Answers</h2>
<p>Let’s see how different LLM models fare with this prompt. These are all 1st-attempt answers, on a fresh chat without any previous context, conversation history or “memory”.</p>
<h3 id="heading-claude-35-sonnet">Claude 3.5 Sonnet</h3>
<pre><code class="lang-markdown">I'll help solve this urinal placement optimization problem. Let me solve it step by step for each configuration.

<span class="hljs-bullet">1.</span> For 01101010:
Let me check each possible position and calculate scores.
The best position is stall 8 (rightmost), giving a final configuration of 01101011.
Final score calculation:
<span class="hljs-bullet">-</span> Position 2: +1 (right empty)
<span class="hljs-bullet">-</span> Position 3: +1 (right empty)
<span class="hljs-bullet">-</span> Position 6: +2 (both sides empty)
<span class="hljs-bullet">-</span> Position 7: +1 (right empty)
<span class="hljs-bullet">-</span> Position 8: +1 (left empty)
Total comfort score: 6

<span class="hljs-bullet">2.</span> For 11010101:
Only positions 4 and 6 are available.
Best position is 4, giving 11011101.
Final score calculation:
<span class="hljs-bullet">-</span> Position 1: +1 (right empty)
<span class="hljs-bullet">-</span> Position 2: +1 (right empty)
<span class="hljs-bullet">-</span> Position 4: +2 (initially both empty)
<span class="hljs-bullet">-</span> Position 6: +1 (right empty)
<span class="hljs-bullet">-</span> Position 8: +1 (left empty)
Total comfort score: 6

<span class="hljs-bullet">3.</span> For 11001011:
Position 5 is the best choice, giving 11001111.
Final score calculation:
<span class="hljs-bullet">-</span> Position 1: +1 (right empty)
<span class="hljs-bullet">-</span> Position 2: +1 (right empty)
<span class="hljs-bullet">-</span> Position 5: +0 (no empty adjacent)
<span class="hljs-bullet">-</span> Position 6: +0 (no empty adjacent)
<span class="hljs-bullet">-</span> Position 7: +0 (no empty adjacent)
<span class="hljs-bullet">-</span> Position 8: +1 (left empty)
Total comfort score: 3

<span class="hljs-bullet">4.</span> For 00111100:
Position 1 is optimal, giving 10111100.
Final score calculation:
<span class="hljs-bullet">-</span> Position 1: +1 (right empty)
<span class="hljs-bullet">-</span> Position 3: +0 (no empty adjacent)
<span class="hljs-bullet">-</span> Position 4: +0 (no empty adjacent)
<span class="hljs-bullet">-</span> Position 5: +0 (no empty adjacent)
<span class="hljs-bullet">-</span> Position 6: +1 (right empty)
Total comfort score: 2

<span class="hljs-bullet">5.</span> For 01001010:
Position 8 is optimal, giving 01001011.
Final score calculation:
<span class="hljs-bullet">-</span> Position 2: +2 (both sides empty)
<span class="hljs-bullet">-</span> Position 5: +2 (both sides empty)
<span class="hljs-bullet">-</span> Position 7: +1 (right empty)
<span class="hljs-bullet">-</span> Position 8: +1 (left empty)
Total comfort score: 6

<span class="hljs-bullet">6.</span> For 11001110:
Position 5 is optimal, giving 11001110.
Final score calculation:
<span class="hljs-bullet">-</span> Position 1: +1 (right empty)
<span class="hljs-bullet">-</span> Position 2: +1 (right empty)
<span class="hljs-bullet">-</span> Position 5: +0 (no empty adjacent)
<span class="hljs-bullet">-</span> Position 6: +1 (right empty)
<span class="hljs-bullet">-</span> Position 7: +1 (right empty)
Total comfort score: 4

<span class="hljs-bullet">7.</span> For 10000110:
Position 4 is optimal, giving 10001110.
Final score calculation:
<span class="hljs-bullet">-</span> Position 1: +2 (right empty + double right empty)
<span class="hljs-bullet">-</span> Position 4: +2 (both sides empty)
<span class="hljs-bullet">-</span> Position 6: +1 (right empty)
<span class="hljs-bullet">-</span> Position 7: +1 (right empty)
Total comfort score: 6

<span class="hljs-bullet">8.</span> For 11000110:
Position 4 is optimal, giving 11001110.
Final score calculation:
<span class="hljs-bullet">-</span> Position 1: +1 (right empty)
<span class="hljs-bullet">-</span> Position 2: +1 (right empty)
<span class="hljs-bullet">-</span> Position 4: +1 (right empty)
<span class="hljs-bullet">-</span> Position 6: +1 (right empty)
<span class="hljs-bullet">-</span> Position 7: +1 (right empty)
Total comfort score: 5

Summary of answers:
<span class="hljs-bullet">1.</span> Position 8, Score 6
<span class="hljs-bullet">2.</span> Position 4, Score 6
<span class="hljs-bullet">3.</span> Position 5, Score 3
<span class="hljs-bullet">4.</span> Position 1, Score 2
<span class="hljs-bullet">5.</span> Position 8, Score 6
<span class="hljs-bullet">6.</span> Position 5, Score 4
<span class="hljs-bullet">7.</span> Position 4, Score 6
<span class="hljs-bullet">8.</span> Position 4, Score 5
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">The answers are mostly wrong. Sometimes they calculations seem to track well, until it doesn’t. For many cases it says “Position X, +1 (right empty)” when clearly there is a 1 to the right of that slot.</div>
</div>

<h3 id="heading-gemini-15-pro">Gemini 1.5 Pro</h3>
<pre><code class="lang-markdown"><span class="hljs-strong">**Understanding the Scoring**</span>

<span class="hljs-bullet">*</span> <span class="hljs-strong">**Proximity:**</span> Each occupied stall (1) gets points for adjacent empty stalls (0).  One point for each side with an empty stall, up to two points per side.
<span class="hljs-bullet">*</span> <span class="hljs-strong">**Total Score:**</span> The sum of the proximity points for all occupied stalls.

<span class="hljs-strong">**Analyzing the Inputs**</span>

To find the best position for the new person, we'll consider each possible placement and calculate the resulting comfort score.

<span class="hljs-strong">**1. 01101010**</span>

<span class="hljs-bullet">*</span> <span class="hljs-strong">**Best Position:**</span> 5 (0110<span class="hljs-strong">**1**</span>1010)
<span class="hljs-bullet">*</span> <span class="hljs-strong">**Score:**</span> 10 
<span class="hljs-bullet">    *</span> (Stall 2: 2 points) + (Stall 3: 2 points) + (Stall 5: 2 points) + (Stall 7: 2 points) + (Stall 8: 2 points) = 10

<span class="hljs-strong">**2. 11010101**</span>

<span class="hljs-bullet">*</span> <span class="hljs-strong">**Best Position:**</span> 3 (11<span class="hljs-strong">**1**</span>10101) 
<span class="hljs-bullet">*</span> <span class="hljs-strong">**Score:**</span> 6
<span class="hljs-bullet">    *</span> (Stall 1: 1 point) + (Stall 2: 1 point) + (Stall 3: 2 points) + (Stall 4: 1 point) + (Stall 6: 1 point) + (Stall 8: 0 points) = 6

<span class="hljs-strong">**3. 11001011**</span>

<span class="hljs-bullet">*</span> <span class="hljs-strong">**Best Position:**</span>  3 (11<span class="hljs-strong">**1**</span>01011)
<span class="hljs-bullet">*</span> <span class="hljs-strong">**Score:**</span> 8
<span class="hljs-bullet">    *</span> (Stall 1: 1 point) + (Stall 2: 1 point) + (Stall 3: 2 points) + (Stall 5: 2 points) + (Stall 7: 1 point) + (Stall 8: 1 point) = 8

<span class="hljs-strong">**4. 00111100**</span>

<span class="hljs-bullet">*</span> <span class="hljs-strong">**Best Positions:**</span> 1 or 8 (<span class="hljs-strong">**1**</span>0111100 or 0011110<span class="hljs-strong">**1**</span>)
<span class="hljs-bullet">*</span> <span class="hljs-strong">**Score:**</span> 8
<span class="hljs-bullet">    *</span> (Stall 1/8: 2 points) + (Stall 3: 2 points) + (Stall 4: 2 points) + (Stall 5: 2 points) = 8

<span class="hljs-strong">**5. 01001010**</span>

<span class="hljs-bullet">*</span> <span class="hljs-strong">**Best Position:**</span> 3 (01<span class="hljs-strong">**1**</span>01010)
<span class="hljs-bullet">*</span> <span class="hljs-strong">**Score:**</span> 10
<span class="hljs-bullet">    *</span> (Stall 2: 2 points) + (Stall 3: 2 points) + (Stall 5: 2 points) + (Stall 7: 2 points) + (Stall 8: 2 points) = 10

<span class="hljs-strong">**6. 11001110**</span>

<span class="hljs-bullet">*</span> <span class="hljs-strong">**Best Position:**</span> 3 (11<span class="hljs-strong">**1**</span>01110)
<span class="hljs-bullet">*</span> <span class="hljs-strong">**Score:**</span> 6
<span class="hljs-bullet">    *</span> (Stall 1: 1 point) + (Stall 2: 1 point) + (Stall 3: 2 points) + (Stall 5: 1 point) + (Stall 6: 1 point) + (Stall 7: 0 points) = 6

<span class="hljs-strong">**7. 10000110**</span>

<span class="hljs-bullet">*</span> <span class="hljs-strong">**Best Position:**</span> 4 (100<span class="hljs-strong">**1**</span>0110)
<span class="hljs-bullet">*</span> <span class="hljs-strong">**Score:**</span> 10
<span class="hljs-bullet">    *</span> (Stall 1: 2 points) + (Stall 4: 4 points) + (Stall 6: 2 points) + (Stall 7: 2 points) = 10

<span class="hljs-strong">**8. 11000110**</span>

<span class="hljs-bullet">*</span> <span class="hljs-strong">**Best Position:**</span>  4 (110<span class="hljs-strong">**1**</span>0110)
<span class="hljs-bullet">*</span> <span class="hljs-strong">**Score:**</span> 8
<span class="hljs-bullet">    *</span> (Stall 1: 1 point) + (Stall 2: 1 point) + (Stall 4: 4 points) + (Stall 6: 1 point) + (Stall 7: 1 point) = 8
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🚨</div>
<div data-node-type="callout-text">The calculations seem mostly wrong. It does make sure to calculate only for occupied slots, and not for the empty slots, so the calculations aren’t “random”, just wrong.</div>
</div>

<h3 id="heading-gemini-20-flash-experimental">Gemini 2.0 Flash Experimental</h3>
<pre><code class="lang-markdown">Here's the breakdown of optimal placement and comfort scores for each urinal configuration:

<span class="hljs-strong">**Understanding the Scoring**</span>

It's crucial to understand the scoring system. For each occupied stall (1), we check the adjacent empty stalls (0s).

<span class="hljs-bullet">*</span>   <span class="hljs-strong">**+1 for each adjacent empty stall:**</span> Up to +2 per occupied stall.
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**+1 for each pair of adjacent empty stalls:**</span> Up to +2 per occupied stall.

<span class="hljs-strong">**Calculations**</span>

Here's the analysis for each configuration:

<span class="hljs-strong">**1. 01101010**</span>

<span class="hljs-bullet">*</span>   Placing at index 0: 11101010. Score: (1+1)+(1+1)+(1+1)+(1+1) = 8
<span class="hljs-bullet">*</span>   Placing at index 3: 01111010. Score: (1+1)+(1+1)+(1+1)+(1+1) = 8
<span class="hljs-bullet">*</span>   Placing at index 5: 01101110. Score: (1+1)+(1+1)+(1+1)+(1+1) = 8
<span class="hljs-bullet">*</span>   Placing at index 7: 01101011. Score: (1+1)+(1+1)+(1+1)+(1+1) = 8
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Optimal Placement:**</span> 0, 3, 5, or 7.
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort Score:**</span> 8

<span class="hljs-strong">**2. 11010101**</span>

<span class="hljs-bullet">*</span>   Placing at index 2: 11110101. Score: (0+1)+(1+1)+(1+1)+(1+1) = 6
<span class="hljs-bullet">*</span>   Placing at index 4: 11011101. Score: (1+1)+(1+1)+(1+1)+(0+1) = 6
<span class="hljs-bullet">*</span>   Placing at index 6: 11010111. Score: (1+1)+(1+1)+(0+1)+(1+1) = 6
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Optimal Placement:**</span> 2, 4, or 6.
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort Score:**</span> 6

<span class="hljs-strong">**3. 11001011**</span>

<span class="hljs-bullet">*</span>   Placing at index 2: 11101011. Score: (0+1)+(1+2)+(1+1)+(1+1) = 7
<span class="hljs-bullet">*</span>   Placing at index 3: 11011011. Score: (1+1)+(1+1)+(1+1)+(1+1) = 8
<span class="hljs-bullet">*</span>   Placing at index 5: 11001111. Score: (1+1)+(1+1)+(0+1)+(1+1) = 6
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Optimal Placement:**</span> 3
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort Score:**</span> 8

<span class="hljs-strong">**4. 00111100**</span>

<span class="hljs-bullet">*</span>   Placing at index 0: 10111100. Score: (1+1)+(1+1)+(1+1)+(1+1) = 8
<span class="hljs-bullet">*</span>   Placing at index 1: 01111100. Score: (1+0)+(0+1)+(1+1)+(1+1) = 6
<span class="hljs-bullet">*</span>   Placing at index 6: 00111110. Score: (1+1)+(1+1)+(1+0)+(0+1) = 6
<span class="hljs-bullet">*</span>   Placing at index 7: 00111101. Score: (1+1)+(1+1)+(1+1)+(1+1) = 8
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Optimal Placement:**</span> 0 or 7
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort Score:**</span> 8

<span class="hljs-strong">**5. 01001010**</span>

<span class="hljs-bullet">*</span>   Placing at index 0: 11001010. Score: (0+2)+(1+1)+(1+1)+(1+1) = 7
<span class="hljs-bullet">*</span>   Placing at index 2: 01101010. Score: (1+1)+(1+1)+(1+1)+(1+1) = 8
<span class="hljs-bullet">*</span>   Placing at index 3: 01011010. Score: (1+1)+(0+2)+(1+1)+(1+1) = 7
<span class="hljs-bullet">*</span>   Placing at index 5: 01001110. Score: (1+1)+(1+1)+(1+1)+(1+1) = 8
<span class="hljs-bullet">*</span>   Placing at index 7: 01001011. Score: (1+1)+(1+1)+(1+1)+(1+1) = 8
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Optimal Placement:**</span> 2, 5, or 7
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort Score:**</span> 8

<span class="hljs-strong">**6. 11001110**</span>

<span class="hljs-bullet">*</span>   Placing at index 2: 11101110. Score: (0+1)+(1+2)+(0+1)+(0+1) = 5
<span class="hljs-bullet">*</span>   Placing at index 3: 11011110. Score: (1+1)+(1+1)+(0+1)+(0+1) = 6
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Optimal Placement:**</span> 3
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort Score:**</span> 6

<span class="hljs-strong">**7. 10000110**</span>

<span class="hljs-bullet">*</span>   Placing at index 1: 11000110. Score: (0+1)+(2+1)+(1+1) = 6
<span class="hljs-bullet">*</span>   Placing at index 2: 10100110. Score: (1+1)+(1+1)+(1+1) = 6
<span class="hljs-bullet">*</span>   Placing at index 3: 10010110. Score: (1+1)+(1+1)+(1+1) = 6
<span class="hljs-bullet">*</span>   Placing at index 4: 10001110. Score: (1+1)+(0+1)+(0+1) = 4
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Optimal Placement:**</span> 1, 2, or 3
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort Score:**</span> 6

<span class="hljs-strong">**8. 11000110**</span>

<span class="hljs-bullet">*</span>   Placing at index 2: 11100110. Score: (0+1)+(2+1)+(1+1) = 6
<span class="hljs-bullet">*</span>   Placing at index 3: 11010110. Score: (1+1)+(1+1)+(1+1) = 6
<span class="hljs-bullet">*</span>   Placing at index 4: 11001110. Score: (1+1)+(0+1)+(0+1) = 4
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Optimal Placement:**</span> 2 or 3
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort Score:**</span> 6
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🚫</div>
<div data-node-type="callout-text">This one tries to give multiple valid answers for each one. But the actual answers are again fairly wrong.</div>
</div>

<h3 id="heading-gemini-20-experimental-advanced">Gemini 2.0 Experimental Advanced</h3>
<pre><code class="lang-markdown">Okay, I understand the problem. Let's analyze each configuration and determine the best position for the new person and the resulting comfort score.

<span class="hljs-strong">**How the Comfort Score is Calculated**</span>

For each person (1) in the urinal row:

<span class="hljs-bullet">*</span>   <span class="hljs-strong">**+1 point:**</span>  If there's an empty slot (0) to their immediate left or right.
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**+1 point (additional):**</span> If there are two empty slots (00) to their immediate left or right.

<span class="hljs-strong">**Analysis of Each Configuration**</span>

Here's the breakdown for each input:

<span class="hljs-strong">**1. 01101010**</span>

<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Possible new person positions:**</span> 1, 4, 6, 8
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Analysis:**</span>
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 1:**</span> <span class="hljs-code">`11101010`</span> -&gt; Comfort: 0 + 1 + 2 + 0 + 1 + 0 + 1 + 0 = 5
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 4:**</span> <span class="hljs-code">`01111010`</span> -&gt; Comfort: 0 + 1 + 2 + 2 + 2 + 0 + 1 + 0 = 8
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 6:**</span> <span class="hljs-code">`01101110`</span> -&gt; Comfort: 0 + 1 + 1 + 0 + 1 + 2 + 2 + 0 = 7
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 8:**</span> <span class="hljs-code">`01101011`</span> -&gt; Comfort: 0 + 1 + 1 + 0 + 1 + 0 + 1 + 1 = 5
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Best position:**</span> 4
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort score:**</span> 8

<span class="hljs-strong">**2. 11010101**</span>

<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Possible new person positions:**</span> 3, 5, 7
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Analysis:**</span>
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 3:**</span> <span class="hljs-code">`11110101`</span> -&gt; Comfort: 1 + 2 + 2 + 2 + 0 + 1 + 0 + 1 = 9
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 5:**</span> <span class="hljs-code">`11011101`</span> -&gt; Comfort: 1 + 1 + 0 + 1 + 2 + 2 + 0 + 1 = 8
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 7:**</span> <span class="hljs-code">`11010111`</span> -&gt; Comfort: 1 + 1 + 0 + 1 + 0 + 1 + 2 + 2 = 8
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Best position:**</span> 3
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort score:**</span> 9

<span class="hljs-strong">**3. 11001011**</span>

<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Possible new person positions:**</span> 3, 4, 6
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Analysis:**</span>
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 3:**</span> <span class="hljs-code">`11101011`</span> -&gt; Comfort: 1 + 2 + 1 + 0 + 1 + 0 + 2 + 1 = 8
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 4:**</span> <span class="hljs-code">`11011011`</span> -&gt; Comfort: 1 + 1 + 0 + 2 + 2 + 0 + 2 + 1 = 9
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 6:**</span> <span class="hljs-code">`11001111`</span> -&gt; Comfort: 1 + 1 + 0 + 0 + 1 + 2 + 2 + 2 = 9
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Best positions:**</span> 4, 6
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort score:**</span> 9

<span class="hljs-strong">**4. 00111100**</span>

<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Possible new person positions:**</span> 1, 2, 7, 8
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Analysis:**</span>
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 1:**</span> <span class="hljs-code">`10111100`</span> -&gt; Comfort: 1 + 0 + 2 + 2 + 2 + 1 + 0 + 0 = 8
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 2:**</span> <span class="hljs-code">`01111100`</span> -&gt; Comfort: 0 + 1 + 2 + 2 + 2 + 1 + 0 + 0 = 8
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 7:**</span> <span class="hljs-code">`00111110`</span> -&gt; Comfort: 0 + 0 + 1 + 2 + 2 + 2 + 1 + 0 = 8
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 8:**</span> <span class="hljs-code">`00111101`</span> -&gt; Comfort: 0 + 0 + 1 + 2 + 2 + 1 + 0 + 1 = 7
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Best positions:**</span> 1, 2, 7
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort score:**</span> 8

<span class="hljs-strong">**5. 01001010**</span>

<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Possible new person positions:**</span> 1, 3, 4, 6, 8
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Analysis:**</span>
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 1:**</span> <span class="hljs-code">`11001010`</span> -&gt; Comfort: 1 + 2 + 0 + 0 + 1 + 0 + 1 + 0 = 5
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 3:**</span> <span class="hljs-code">`01101010`</span> -&gt; Comfort: 0 + 1 + 2 + 0 + 1 + 0 + 1 + 0 = 5
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 4:**</span> <span class="hljs-code">`01011010`</span> -&gt; Comfort: 0 + 1 + 0 + 2 + 2 + 0 + 1 + 0 = 6
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 6:**</span> <span class="hljs-code">`01001110`</span> -&gt; Comfort: 0 + 1 + 0 + 0 + 1 + 2 + 2 + 0 = 6
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 8:**</span> <span class="hljs-code">`01001011`</span> -&gt; Comfort: 0 + 1 + 0 + 0 + 1 + 0 + 1 + 1 = 4
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Best positions:**</span> 4, 6
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort score:**</span> 6

<span class="hljs-strong">**6. 11001110**</span>

<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Possible new person positions:**</span> 3, 4, 8
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Analysis:**</span>
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 3:**</span> <span class="hljs-code">`11101110`</span> -&gt; Comfort: 1 + 2 + 1 + 0 + 2 + 2 + 2 + 0 = 10
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 4:**</span> <span class="hljs-code">`11011110`</span> -&gt; Comfort: 1 + 1 + 0 + 2 + 2 + 2 + 2 + 0 = 10
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 8:**</span> <span class="hljs-code">`11001111`</span> -&gt; Comfort: 1 + 1 + 0 + 0 + 1 + 2 + 2 + 2 = 9
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Best positions:**</span> 3, 4
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort score:**</span> 10

<span class="hljs-strong">**7. 10000110**</span>

<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Possible new person positions:**</span> 2, 3, 4, 5, 8
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Analysis:**</span>
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 2:**</span> <span class="hljs-code">`11000110`</span> -&gt; Comfort: 1 + 2 + 0 + 0 + 0 + 2 + 1 + 0 = 6
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 3:**</span> <span class="hljs-code">`10100110`</span> -&gt; Comfort: 1 + 0 + 2 + 0 + 0 + 2 + 1 + 0 = 6
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 4:**</span> <span class="hljs-code">`10010110`</span> -&gt; Comfort: 1 + 0 + 0 + 2 + 0 + 2 + 1 + 0 = 6
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 5:**</span> <span class="hljs-code">`10001110`</span> -&gt; Comfort: 1 + 0 + 0 + 0 + 2 + 2 + 2 + 0 = 7
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 8:**</span> <span class="hljs-code">`10000111`</span> -&gt; Comfort: 1 + 0 + 0 + 0 + 0 + 1 + 2 + 2 = 6
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Best position:**</span> 5
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort score:**</span> 7

<span class="hljs-strong">**8. 11000110**</span>

<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Possible new person positions:**</span> 3, 4, 5, 8
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Analysis:**</span>
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 3:**</span> <span class="hljs-code">`11100110`</span> -&gt; Comfort: 1 + 2 + 1 + 0 + 0 + 2 + 1 + 0 = 7
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 4:**</span> <span class="hljs-code">`11010110`</span> -&gt; Comfort: 1 + 1 + 0 + 2 + 0 + 2 + 1 + 0 = 7
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 5:**</span> <span class="hljs-code">`11001110`</span> -&gt; Comfort: 1 + 1 + 0 + 0 + 2 + 2 + 2 + 0 = 8
<span class="hljs-bullet">    *</span>   <span class="hljs-strong">**Position 8:**</span> <span class="hljs-code">`11000111`</span> -&gt; Comfort: 1 + 1 + 0 + 0 + 0 + 1 + 2 + 2 = 7
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Best position:**</span> 5
<span class="hljs-bullet">*</span>   <span class="hljs-strong">**Comfort score:**</span> 8

<span class="hljs-strong">**Summary Table**</span>

| Input      | Best Position(s) | Comfort Score |
| :--------- | :--------------- | :------------ |
| 01101010   | 4                | 8             |
| 11010101   | 3                | 9             |
| 11001011   | 4, 6             | 9             |
| 00111100   | 1, 2, 7          | 8             |
| 01001010   | 4, 6             | 6             |
| 11001110   | 3, 4             | 10            |
| 10000110   | 5                | 7             |
| 11000110   | 5                | 8             |
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🗣</div>
<div data-node-type="callout-text">This is the first model which seems to at least narrate back the scoring logic coherently (and addmittedly more clearly than it is described in my question itself). But despite that, the actual scoring for the data is still completely wrong.</div>
</div>

<h3 id="heading-chat-gpt-4o">Chat GPT 4o</h3>
<pre><code class="lang-markdown">To solve this problem, we'll compute the total "comfort" score for each valid position where the new person can be placed and find the position that yields the highest comfort score. Here's the solution for each input configuration:

---

<span class="hljs-section">### <span class="hljs-strong">**1. Input: `01101010`**</span></span>
<span class="hljs-bullet">-</span> Valid positions: 1, 5, 8.
<span class="hljs-bullet">-</span> Comfort scores:
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**1**</span>: Final state <span class="hljs-code">`11101010`</span>. Score = 8.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**5**</span>: Final state <span class="hljs-code">`01101110`</span>. Score = 8.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**8**</span>: Final state <span class="hljs-code">`01101011`</span>. Score = 8.
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Answer**</span>: Position 1 (or 5 or 8), <span class="hljs-strong">**Comfort score: 8**</span>.

---

<span class="hljs-section">### <span class="hljs-strong">**2. Input: `11010101`**</span></span>
<span class="hljs-bullet">-</span> Valid positions: 3, 6.
<span class="hljs-bullet">-</span> Comfort scores:
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**3**</span>: Final state <span class="hljs-code">`11110101`</span>. Score = 7.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**6**</span>: Final state <span class="hljs-code">`11011101`</span>. Score = 7.
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Answer**</span>: Position 3 (or 6), <span class="hljs-strong">**Comfort score: 7**</span>.

---

<span class="hljs-section">### <span class="hljs-strong">**3. Input: `11001011`**</span></span>
<span class="hljs-bullet">-</span> Valid positions: 4, 5.
<span class="hljs-bullet">-</span> Comfort scores:
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**4**</span>: Final state <span class="hljs-code">`11011011`</span>. Score = 8.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**5**</span>: Final state <span class="hljs-code">`11001111`</span>. Score = 8.
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Answer**</span>: Position 4 (or 5), <span class="hljs-strong">**Comfort score: 8**</span>.

---

<span class="hljs-section">### <span class="hljs-strong">**4. Input: `00111100`**</span></span>
<span class="hljs-bullet">-</span> Valid positions: 1, 2, 7, 8.
<span class="hljs-bullet">-</span> Comfort scores:
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**1**</span>: Final state <span class="hljs-code">`10111100`</span>. Score = 4.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**2**</span>: Final state <span class="hljs-code">`01111100`</span>. Score = 4.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**7**</span>: Final state <span class="hljs-code">`00111110`</span>. Score = 4.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**8**</span>: Final state <span class="hljs-code">`00111101`</span>. Score = 4.
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Answer**</span>: Position 1 (or 2, 7, 8), <span class="hljs-strong">**Comfort score: 4**</span>.

---

<span class="hljs-section">### <span class="hljs-strong">**5. Input: `01001010`**</span></span>
<span class="hljs-bullet">-</span> Valid positions: 1, 3, 5, 7, 8.
<span class="hljs-bullet">-</span> Comfort scores:
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**1**</span>: Final state <span class="hljs-code">`11001010`</span>. Score = 10.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**3**</span>: Final state <span class="hljs-code">`01101010`</span>. Score = 9.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**5**</span>: Final state <span class="hljs-code">`01011010`</span>. Score = 9.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**7**</span>: Final state <span class="hljs-code">`01001110`</span>. Score = 9.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**8**</span>: Final state <span class="hljs-code">`01001011`</span>. Score = 9.
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Answer**</span>: Position 1, <span class="hljs-strong">**Comfort score: 10**</span>.

---

<span class="hljs-section">### <span class="hljs-strong">**6. Input: `11001110`**</span></span>
<span class="hljs-bullet">-</span> Valid positions: 5.
<span class="hljs-bullet">-</span> Comfort scores:
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**5**</span>: Final state <span class="hljs-code">`11011110`</span>. Score = 7.
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Answer**</span>: Position 5, <span class="hljs-strong">**Comfort score: 7**</span>.

---

<span class="hljs-section">### <span class="hljs-strong">**7. Input: `10000110`**</span></span>
<span class="hljs-bullet">-</span> Valid positions: 2, 3, 4, 5.
<span class="hljs-bullet">-</span> Comfort scores:
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**2**</span>: Final state <span class="hljs-code">`11000110`</span>. Score = 10.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**3**</span>: Final state <span class="hljs-code">`10100110`</span>. Score = 9.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**4**</span>: Final state <span class="hljs-code">`10010110`</span>. Score = 9.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**5**</span>: Final state <span class="hljs-code">`10001110`</span>. Score = 9.
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Answer**</span>: Position 2, <span class="hljs-strong">**Comfort score: 10**</span>.

---

<span class="hljs-section">### <span class="hljs-strong">**8. Input: `11000110`**</span></span>
<span class="hljs-bullet">-</span> Valid positions: 4, 5.
<span class="hljs-bullet">-</span> Comfort scores:
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**4**</span>: Final state <span class="hljs-code">`11010110`</span>. Score = 8.
<span class="hljs-bullet">  -</span> Place at position <span class="hljs-strong">**5**</span>: Final state <span class="hljs-code">`11001110`</span>. Score = 7.
<span class="hljs-bullet">-</span> <span class="hljs-strong">**Answer**</span>: Position 4, <span class="hljs-strong">**Comfort score: 8**</span>.

---

<span class="hljs-section">### <span class="hljs-strong">**Summary of Results**</span></span>
<span class="hljs-bullet">1.</span> <span class="hljs-strong">**01101010**</span> → Position 1 (or 5, 8), Score = 8.  
<span class="hljs-bullet">2.</span> <span class="hljs-strong">**11010101**</span> → Position 3 (or 6), Score = 7.  
<span class="hljs-bullet">3.</span> <span class="hljs-strong">**11001011**</span> → Position 4 (or 5), Score = 8.  
<span class="hljs-bullet">4.</span> <span class="hljs-strong">**00111100**</span> → Position 1 (or 2, 7, 8), Score = 4.  
<span class="hljs-bullet">5.</span> <span class="hljs-strong">**01001010**</span> → Position 1, Score = 10.  
<span class="hljs-bullet">6.</span> <span class="hljs-strong">**11001110**</span> → Position 5, Score = 7.  
<span class="hljs-bullet">7.</span> <span class="hljs-strong">**10000110**</span> → Position 2, Score = 10.  
<span class="hljs-bullet">8.</span> <span class="hljs-strong">**11000110**</span> → Position 4, Score = 8.
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">❗</div>
<div data-node-type="callout-text">Once again, this is also entirely wrong. It even calculated wrong “valid positions”. For example in the first case, 5 is not a valid position to insert the new person.</div>
</div>

<h3 id="heading-gpt-o1-preview">GPT o1 Preview</h3>
<p>Finally it is time for our “thinking” model to have a go at it. It thought for close to 3 minutes for this.</p>
<pre><code class="lang-markdown">To solve this problem, for each urinal stall configuration we considered all vacant positions (positions where the value is '0') and calculated the comfort score after placing a new person in that position. The comfort score is determined by the sum of comfort levels of all occupied stalls after the new person is placed, where the comfort for each person is calculated based on adjacent empty stalls as per the given rules.

For each configuration, we compared the total comfort scores for all possible placements of the new person and selected the position that resulted in the highest comfort score. If multiple positions yield the same highest score, any one of those positions can be the answer.

Here are the answers for the given configurations:

<span class="hljs-bullet">1.</span> <span class="hljs-strong">**Position:**</span> 7, <span class="hljs-strong">**Comfort Score:**</span> 6
<span class="hljs-bullet">2.</span> <span class="hljs-strong">**Position:**</span> 3, <span class="hljs-strong">**Comfort Score:**</span> 4
<span class="hljs-bullet">3.</span> <span class="hljs-strong">**Position:**</span> 6, <span class="hljs-strong">**Comfort Score:**</span> 4
<span class="hljs-bullet">4.</span> <span class="hljs-strong">**Position:**</span> 1, <span class="hljs-strong">**Comfort Score:**</span> 2
<span class="hljs-bullet">5.</span> <span class="hljs-strong">**Position:**</span> 1, <span class="hljs-strong">**Comfort Score:**</span> 7
<span class="hljs-bullet">6.</span> <span class="hljs-strong">**Position:**</span> 8, <span class="hljs-strong">**Comfort Score:**</span> 4
<span class="hljs-bullet">7.</span> <span class="hljs-strong">**Position:**</span> 3, <span class="hljs-strong">**Comfort Score:**</span> 7
<span class="hljs-bullet">8.</span> <span class="hljs-strong">**Position:**</span> 3, <span class="hljs-strong">**Comfort Score:**</span> 5

istration of the new person should be in the following stalls with the corresponding comfort scores.

<span class="hljs-strong">**Answers:**</span>

<span class="hljs-bullet">1.</span> 7 6
<span class="hljs-bullet">2.</span> 3 4
<span class="hljs-bullet">3.</span> 6 4
<span class="hljs-bullet">4.</span> 1 2
<span class="hljs-bullet">5.</span> 1 7
<span class="hljs-bullet">6.</span> 8 4
<span class="hljs-bullet">7.</span> 3 7
<span class="hljs-bullet">8.</span> 3 5
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🧠</div>
<div data-node-type="callout-text">Finally, at least some correct answers! Except it did get the first input wrong. Position 7 is already occupied. But it got rest of the inputs correct and calculated the score correctly too! There’s a type/incomplete word “istration”, not sure how that happened in the output.</div>
</div>

<h3 id="heading-deepseek-r1-671b">DeepSeek R1 671B</h3>
<p>The entire chain of thought is pasted here - <a target="_blank" href="https://gist.github.com/championswimmer/361e91a8ead97c2afa85502d07bbf8fa">https://gist.github.com/championswimmer/361e91a8ead97c2afa85502d07bbf8fa</a>. It “thought” for 4+ minutes, and generated 700+ lines of thoughts before giving the answer.</p>
<pre><code class="lang-markdown"><span class="hljs-strong">**Answers**</span>

<span class="hljs-bullet">1.</span> <span class="hljs-strong">**Input: 01101010**</span>  
   <span class="hljs-strong">**Position:**</span> 1  
   <span class="hljs-strong">**Comfort Score:**</span> 6  

<span class="hljs-bullet">2.</span> <span class="hljs-strong">**Input: 11010101**</span>  
   <span class="hljs-strong">**Position:**</span> 3 (or 5 or 7)  
   <span class="hljs-strong">**Comfort Score:**</span> 5  

<span class="hljs-bullet">3.</span> <span class="hljs-strong">**Input: 11001011**</span>  
   <span class="hljs-strong">**Position:**</span> 5  
   <span class="hljs-strong">**Comfort Score:**</span> 7  

<span class="hljs-bullet">4.</span> <span class="hljs-strong">**Input: 00111100**</span>  
   <span class="hljs-strong">**Position:**</span> 1 or 8  
   <span class="hljs-strong">**Comfort Score:**</span> 14  

<span class="hljs-bullet">5.</span> <span class="hljs-strong">**Input: 01001010**</span>  
   <span class="hljs-strong">**Position:**</span> 7  
   <span class="hljs-strong">**Comfort Score:**</span> 7  

<span class="hljs-bullet">6.</span> <span class="hljs-strong">**Input: 11001110**</span>  
   <span class="hljs-strong">**Position:**</span> 8  
   <span class="hljs-strong">**Comfort Score:**</span> 8  

<span class="hljs-bullet">7.</span> <span class="hljs-strong">**Input: 10000110**</span>  
   <span class="hljs-strong">**Position:**</span> 3  
   <span class="hljs-strong">**Comfort Score:**</span> 9  

<span class="hljs-bullet">8.</span> <span class="hljs-strong">**Input: 11000110**</span>  
   <span class="hljs-strong">**Position:**</span> 7  
   <span class="hljs-strong">**Comfort Score:**</span> 7  

Each answer selects an optimal position (1-indexed) and the resulting maximum comfort score. If multiple positions yield the same score, any can be chosen.
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💭</div>
<div data-node-type="callout-text">Unfortunately, despite claims that R1 beats O1, at least for this question it got all the answers wrong. The chain of thought itself is an interesting piece of text. It keeps doing a bunch of <em>“oh wait, no…”</em> and keeps correcting itself, but it doesn’t really <strong><em>reason</em></strong> well at all and keeps faltering in its own chain of logic.</div>
</div>

<h2 id="heading-result-summary">Result Summary</h2>
<p>I don’t know about the “AGI” hype of o1, but it definitely marks itself a rank above the others, when faced with a net-new question, which it never faced in training data. If the chain-of-thought was published, it would definitely be intriguing to look at, but at least on the board it scores a 7:1 win, when the closest anyone else could do (Claude 3.5 Sonnet) was a 1:7 loss.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Model</strong></td><td><strong>Correct Solution</strong></td><td><strong>Remarks</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Claude 3.5 Sonnet</td><td>1 ✅ 7 ❌</td><td>Wrong calculations and answers, but one input solved (with correct explanation and breakdown)</td></tr>
<tr>
<td>Gemini 1.5 Pro</td><td>❌</td><td>Explained back the logic, but all calculations and answers wrong</td></tr>
<tr>
<td>Gemini 2.0 Flash Exp</td><td>❌</td><td>Explained back the logic, but all calculations and answers wrong</td></tr>
<tr>
<td>Gemini 2.0 Exp Adv</td><td>❌</td><td>Explained back the logic, but all calculations and answers wrong</td></tr>
<tr>
<td>GPT 4o</td><td>❌</td><td>All calculations and answers wrong</td></tr>
<tr>
<td>GPT o1 Preview</td><td>1 ❌ 7 ✅</td><td>&lt;think&gt; for 3 min. Explanations not given in final answers, but gave correct position and score for all inputs except the first one.</td></tr>
<tr>
<td>DeepSeek R1 671B</td><td>❌</td><td>&lt;think&gt; for 4min, 700 lines. Final answers are all wrong.</td></tr>
</tbody>
</table>
</div><p>If you try this prompt out with any open models like <strong>DeepSeek R1</strong>, <strong>LLaMa, Mistral</strong> etc, let me know in comments how did it go and did any of them solve it.</p>
<h2 id="heading-bonus-8-minutes-of-thinking">Bonus - <em>8 minutes of “thinking”</em></h2>
<p>Just for run, here is <code>DeepSeek R1 Distilled Llama 7b</code> thinking for a whole 8 minutes</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=HioHAXgjSYk">https://www.youtube.com/watch?v=HioHAXgjSYk</a></div>
<p> </p>
<p>As is evident, just “CoT”ing your way out of incapable models does not really work. I don’t think a model can start reasoning just by forcing it to generate <code>&lt;think&gt;</code> blocks in its intermediate output and aligning it with reinforcement learning. Sure, it improves certain things, but it also opens you up to the jailbreak scenario where the <em>thoughts</em> spiral out of control, growing larger than the context, and thus pushing the original prompt out of the window, and the final answer having no relation to the actual prompt.</p>
]]></content:encoded></item><item><title><![CDATA[Making one Jest test file depend on outputs from another]]></title><description><![CDATA[I'll start off by saying this is an ugly hack, and I hope if you have reached this article, you really know what you're doing.
Tests should be independent, and ideally not block each other. Jest is designed to not make test file execution order deter...]]></description><link>https://arnav.tech/making-one-jest-test-file-depend-on-outputs-from-another</link><guid isPermaLink="true">https://arnav.tech/making-one-jest-test-file-depend-on-outputs-from-another</guid><category><![CDATA[Jest]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Fri, 09 Aug 2024 08:02:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1723190454944/f91c46ed-9f8c-4d70-9603-e4e15a965325.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'll start off by saying this is an ugly hack, and I hope if you have reached this article, you really know what you're doing.</p>
<p>Tests should be independent, and ideally not block each other. Jest is designed to not make test file execution order deterministic, nor sequential, and there's a reason for that. <code>one.spec.ts</code> and <code>two.spec.ts</code> should be completely independent set of tests that can run from start to end without interacting with each other. There's a reason unit tests are called <em>unit tests.</em></p>
<p>That said, let me describe where I wanted to do something like this. Below is a simplified diagram of some systems that we use, and the kind of tests we want to do.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723187129332/773a2a8f-5608-417d-a2e9-ea8af1fc36ff.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-the-flow-of-the-system">The flow of the system</h3>
<ul>
<li><p>home page - a list of categories (from content service)</p>
</li>
<li><p>categories - a list of contents (from content service)</p>
</li>
<li><p>contents - contain a content id with which further data is to be fetched from playback service</p>
</li>
<li><p>player - gets playback URLs and runs the correct rendition* of content based on network conditions / device capability etc.</p>
</li>
</ul>
<blockquote>
<p>A 'rendition' means different formats like 4K, 1080p, with or without Dolby audio etc.</p>
</blockquote>
<h3 id="heading-the-test-files-and-what-they-do">The test files and what they do</h3>
<h4 id="heading-contentteststs">content.tests.ts</h4>
<p>Tests that home page data can be fetched, and further data of all the contents inside it can be fetched. This is not a typical unit test, this is a periodic test that runs every hour, and we want it to fail if the API is not sending the correct data. (Think of this as more of an API monitoring test)</p>
<h4 id="heading-playbackteststs">playback.tests.ts</h4>
<p>This gets a content id, and based on that fetches content metadata and playback URLs from the playback service. It tests that playback URLs for all content ids are actually available, and the playback URLs contain valid playable video content / streams.</p>
<h3 id="heading-why-we-want-tests-to-be-dependent-on-each-other">Why we want tests to be dependent on each other?</h3>
<p>When <code>content.tests.ts</code> runs, it already fetches all the content ids (N categories, avg M contents in each - so <code>M x N</code> content ids)</p>
<p>We wish to run <code>playback.tests.ts</code> on all of those <code>M x N</code> content ids. Instead of repeating the work of fetching them inside <code>playback.tests.ts</code> we want to simply use the contents fetched inside <code>content.tests.ts</code></p>
<h3 id="heading-hacking-up-a-basic-global-observable-inside-jest-tests">Hacking up a basic global 'observable' inside Jest tests</h3>
<h4 id="heading-contentteststs-1">content.tests.ts</h4>
<p>So here's what we do. We make a global object <code>contentList</code> in <code>content.tests.ts</code> that will be imported in <code>playback.tests.ts</code> and we make a promise <code>contentListReady</code> that resolves when the content is downloaded (or times out at 6 seconds)</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> contentList: Content[] = []
<span class="hljs-comment">// internal variable to be set true when all assets are downloaded</span>
<span class="hljs-keyword">let</span> _contentsReady = <span class="hljs-literal">false</span>

<span class="hljs-comment">// hack to wait for all assets to be downloaded so that they can be used in `playback.tests.ts`</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> contentListReady: <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">Boolean</span>&gt; = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
     <span class="hljs-comment">// Poll every second to check if asset list is ready</span>
     <span class="hljs-comment">// If asset list is ready, resolve the promise</span>
     <span class="hljs-comment">// If asset list is not ready after 6 seconds, reject the promise</span>
     <span class="hljs-comment">// Either way, clear the interval and the timeout</span>
    <span class="hljs-keyword">const</span> looper = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (_assetListReady &amp;&amp; assetList.length &gt; <span class="hljs-number">0</span>) {
            <span class="hljs-built_in">clearTimeout</span>(waiter)
            <span class="hljs-built_in">clearInterval</span>(looper)
            resolve(<span class="hljs-literal">true</span>)
        }
    }, <span class="hljs-number">1000</span>)
    <span class="hljs-keyword">const</span> waiter = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">clearInterval</span>(looper)
        <span class="hljs-built_in">clearTimeout</span>(waiter)
        reject(<span class="hljs-string">"Asset List not ready"</span>)
    }, <span class="hljs-number">6000</span>)
})

describe(<span class="hljs-string">'content'</span>, <span class="hljs-function">() =&gt;</span> {
    test(<span class="hljs-string">'get home'</span>, <span class="hljs-keyword">async</span> () =&gt; { <span class="hljs-comment">/* ... */</span> })
    test(<span class="hljs-string">'get categories'</span>, <span class="hljs-keyword">async</span> () =&gt; { <span class="hljs-comment">/* ... */</span> })
    test(<span class="hljs-string">'get contents'</span>, <span class="hljs-keyword">async</span> () =&gt; { 
        <span class="hljs-comment">// contents are available here </span>
        contents.forEach( <span class="hljs-function"><span class="hljs-params">content</span> =&gt;</span> contentList.push(content) )
        _contentsReady = <span class="hljs-literal">true</span>
    })
})
</code></pre>
<p>In the due course of running all iterations of content tests, the `contentList` will get populated. When all the contents are fetched, we will set <code>_contentsReady</code> to true.</p>
<h4 id="heading-playbackteststs-1">playback.tests.ts</h4>
<p>In the <code>beforeAll</code> hook we wait for content list to be ready</p>
<pre><code class="lang-typescript">describe(<span class="hljs-string">'playback'</span>, <span class="hljs-function">() =&gt;</span> {
    beforeAll(<span class="hljs-keyword">async</span> () =&gt; {
        info(<span class="hljs-string">"Waiting for asset list to be ready..."</span>)
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">await</span> assetListReady) info(<span class="hljs-string">"Asset list is ready"</span>)
        <span class="hljs-keyword">else</span> error(<span class="hljs-string">"Asset timed out"</span>)

    })

    test(<span class="hljs-string">'playback sample'</span>, <span class="hljs-function">() =&gt;</span> {
        expect(<span class="hljs-literal">true</span>).toBeTruthy()
    })
})
</code></pre>
<p>Thus even when jest actually starts all your tests in parallel, <code>playback.tests.ts</code> won't actually start executing its tests, till <code>content.tests.ts</code> either resolves or rejects the <code>contentListReady</code> promise.</p>
<p>Do remember to clear the timeout as well as the interval in that <code>contentListReady</code> promise, or else Jest will warn you about open handles at the end of the tests.</p>
]]></content:encoded></item><item><title><![CDATA[Using Clickhouse as an events store on Railway.app]]></title><description><![CDATA[NOTE: I encountered this while working on the 1px.li project. You can find the source code of that here. It is hosted on Railway. You can find the hosting configuration and what all services it uses on Railway here.

For most things on http://1px.li ...]]></description><link>https://arnav.tech/using-clickhouse-as-an-events-store-on-railwayapp</link><guid isPermaLink="true">https://arnav.tech/using-clickhouse-as-an-events-store-on-railwayapp</guid><category><![CDATA[ClickHouse]]></category><category><![CDATA[Databases]]></category><category><![CDATA[railway-app]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Sun, 28 Jan 2024 11:42:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706441477799/78527bfa-cda9-4bfb-8796-683c98bc1ff1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>NOTE: I encountered this while working on the 1px.li project. You can find the source code of that <a target="_blank" href="https://github.com/championswimmer/onepixel_backend">here</a>. It is hosted on Railway. You can find the hosting configuration and what all services it uses on <a target="_blank" href="https://railway.app/project/d26a3016-de8c-4f9e-a873-aa810176f825">Railway here.</a></p>
</blockquote>
<p>For most things on <a target="_blank" href="https://t.co/MzBScT2FOM">http://1px.li</a> so far I want to have a clean Dockerfile and docker-compose - and have a setup that replicates well on localhost as well as on Railway. So far Postgres was a charm. Clickhouse is not an officially supported "database" on Railway though.</p>
<p><img src="https://pbs.twimg.com/media/GE7IvITboAAVASC?format=jpg&amp;name=medium" alt="Image" /></p>
<p>But searching for Clickhouse does turn up a community-maintained image from marketplace, made by <a target="_blank" href="https://twitter.com/GregorySchier">Greg Scheir</a>, a Railway employee. It is basically straight up <code>clickhouse/clickhouse-server:23.3.8.21</code> docker image only, which is great for my "whatever works on localhost, works same on Railway" principle.</p>
<p><img src="https://pbs.twimg.com/media/GE7I8anakAI_9pR?format=jpg&amp;name=large" alt="Image" /></p>
<p>Since the docker image takes variables <code>CLICKHOUSE_USER</code>, <code>CLICKHOUSE_PASSWORD</code> and <code>CLICKHOUSE_DB</code> by default, it is really easy to configure those things when deploying to Railway - just set those variables.</p>
<p><img src="https://pbs.twimg.com/media/GE7KggTbkAAkDHc?format=jpg&amp;name=4096x4096" alt="Image" /></p>
<p><img src="https://pbs.twimg.com/media/GE7Kk2mbUAApinr?format=jpg&amp;name=large" alt="Image" /></p>
<p>The challenge really comes in connecting to this database from other Railway services. So what happens for a databases like Postgres is that Railway sets up a TCP proxy for you - so that you can connect with the database's native driver that talks over raw TCP. Some databases (including Clickhouse) do allow connecting over HTTP(S) as well - but typically that is inefficient and unnecessary if your servers are inside a VPC already. While the docs at <a target="_blank" href="https://railway.app">Railway.app</a> do seem to say that TCP proxying is available for any service, at least when I launched the Clickhouse template I found there is no way to set up TCP proxy, but only use the HTTP connection.</p>
<p><img src="https://pbs.twimg.com/media/GE7MRWfboAEexz9?format=jpg&amp;name=large" alt="Image" /></p>
<p><img src="https://pbs.twimg.com/media/GE7MTWgacAE64_O?format=jpg&amp;name=large" alt="Image" /></p>
<p>Using Clickhouse over HTTP is something that'll take slight bit of digging to figure out all the pieces of the puzzle.</p>
<p><img src="https://pbs.twimg.com/media/GE7NfTXbQAAsBF1?format=jpg&amp;name=large" alt="Image" /></p>
<p>The official drivers seems to be made only for native connections, but actually it does support HTTP mode as well. It is said to be "<strong><em>experimental</em></strong>" though - I believe some types of data might fail while escaping it to be able to pass through HTTP bodies. (I haven't faced any problem so far)</p>
<p><strong>But</strong>, I was not interested in using the raw driver. I am already using Gorm, and wanted to use Gorm for Clickhouse as well. But the gorm driver doesn't quite clearly mention that you <em>can</em> connect over HTTP too just by passing <code>http://</code> as the schema instead of <code>clickhouse://</code></p>
<p><img src="https://pbs.twimg.com/media/GE7N7PTaEAAK8I-?format=jpg&amp;name=large" alt="Image" /></p>
<p>The gorm clickhouse driver has moved to v2 of the <a target="_blank" href="https://github.com/ClickHouse/clickhouse-go">clickhouse golang library</a>, and thus, it can automatically use HTTP mode when the URL is HTTP protocol. Had to take to look through the source code to figure out that they handle it, before I could use it. Now to actually connect to this service on Railway, the best is to formulate 2 URLs, one for private and one for public access.</p>
<p><strong>Private Access:</strong></p>
<ul>
<li><p>you can use http, not https (within your cloud)</p>
</li>
<li><p>server is exposed on the port of that service (default: 8123)</p>
</li>
</ul>
<p><strong>Public Access</strong></p>
<ul>
<li><p>use https (Railway will handle TLS for you)</p>
</li>
<li><p>server is exposed on 443, not the internal port, so don't specify port in this URL</p>
</li>
</ul>
<p><img src="https://pbs.twimg.com/media/GE7PbYNbQAAhO_j?format=jpg&amp;name=medium" alt="Image" /></p>
<p>The public URL is what you can connect to from your laptop. The private URL is what you should use from your server hosted inside Railway to access this.</p>
]]></content:encoded></item><item><title><![CDATA[MX Master on Mac OS - Jittery Cursor Position and Jumping Pointer]]></title><description><![CDATA[I have been a long-time MX Master user. 5+ years with the MX Master 2S and now 1+ with MX Master 3. (I do not use the 3S because it has a different kind of receiver that doesn't let me use the same receiver for MX Keys)
One issue that has been plagui...]]></description><link>https://arnav.tech/mx-master-on-mac-os-jittery-cursor-position-and-jumping-pointer</link><guid isPermaLink="true">https://arnav.tech/mx-master-on-mac-os-jittery-cursor-position-and-jumping-pointer</guid><category><![CDATA[mx master]]></category><category><![CDATA[mouse]]></category><category><![CDATA[macOS]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Tue, 02 Jan 2024 20:23:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/UihqZIiVcxY/upload/37e91e1f33228701264bac33aa7e4aa7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have been a long-time MX Master user. 5+ years with the MX Master 2S and now 1+ with MX Master 3. (I do not use the 3S because it has a different kind of receiver that doesn't let me use the same receiver for MX Keys)</p>
<p>One issue that has been plaguing me recently was that the cursor position was jittery, and when moving the mouse, the cursor was jumping around a bit.</p>
<p>I noticed that it happened a lot more if I was having an additional external monitor attached.</p>
<p>Just to note it down properly, here are the things I am using</p>
<ul>
<li><p>Macbook Pro 16" - 2021 Model (M1 Max)</p>
</li>
<li><p>MacOS 12.1</p>
</li>
<li><p>MX Master 3 Mouse</p>
</li>
<li><p>Logi Options+ 1.60.xxx</p>
</li>
</ul>
<p>So what solved the problem was a weird set of steps (I do not know why this solved the problem).</p>
<ol>
<li><p>I turned down the Mouse tracking speed in system settings to really low</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704226746151/bed023c9-25c4-4fa2-8a68-c49955161944.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Inside Logi Options+ I increased the pointer speed to much higher to compensate for that.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704226833338/dcb24476-e7b4-4a75-b72a-5c95dcd09fbf.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p>If I have to hazard a guess, this way the actual hardware interface tracks the mouse much slower (hence less jumps) and the Logi Options+ driver translates that to bigger movements at the software layer. I do not know this for sure, and thus I do not know exactly why this set of steps fixes this problem, but that it does.</p>
<p>Anyway, documenting this mainly for myself to find this solution back again few years later when I update my Macbook to M4 Max or something and face this issue again, I guess. If you faced the same and this solved for you, do leave a comment. Thanks :)</p>
]]></content:encoded></item><item><title><![CDATA[Nesting test files under the main file in Project View of Visual Studio Code and Jetbrains IDEs]]></title><description><![CDATA[Background
So let me start by saying that I come from the world of Java (originally an Android developer, before branching out to do all the other stuff). Which means there's a very rigid idea about how test files are structured back in my world.

Re...]]></description><link>https://arnav.tech/nesting-test-files-under-the-main-file-in-project-view-of-visual-studio-code-and-jetbrains-ides</link><guid isPermaLink="true">https://arnav.tech/nesting-test-files-under-the-main-file-in-project-view-of-visual-studio-code-and-jetbrains-ides</guid><category><![CDATA[Go Language]]></category><category><![CDATA[Testing]]></category><category><![CDATA[IDEs]]></category><category><![CDATA[file structure]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Fri, 10 Nov 2023 08:49:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699604673053/181829ef-496c-49a0-b4bd-15b4c3a4bbbe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-background">Background</h3>
<p>So let me start by saying that I come from the world of Java (originally an Android developer, before branching out to do all the other stuff). Which means there's a very rigid idea about how test files are structured back in my world.</p>
<p><img src="https://imgr.whimsical.com/thumbnails/EcsR4kFNXo5zNLZKAFDxKV/WpqnMT8QLFG9b6mfvCk7yE" alt="Software Project Structure with Unit Tests" /></p>
<p>Recently I started working on a Golang sample project <sup>1</sup> and I <a target="_blank" href="https://github.com/championswimmer/onepixel_backend/issues/22">was told</a> that in Go, typically you keep test files beside your source files. Makes sense, Go's understanding of modules is different from Java's understanding of packages. In Java, when building the <code>test</code> package, the code from main and test are merged semantically keeping the test and the source <em>beside</em> each other in runtime.</p>
<p>Fair enough. Some Javascript projects do the same too - and I am ok doing this.</p>
<h3 id="heading-my-problem-with-project-structure">My Problem with Project Structure</h3>
<p>What I do not like though is this type of project structure (file explorer view) by default</p>
<pre><code class="lang-plaintext">- resources
- docs
- src 
    - users 
        - user_controller.go
        - user_controller_test.go
    - articles
        - article_data.go
        - article_data_tests.go
        - article_handler.go
        - article_handler_tests.go
</code></pre>
<p>What I would <strong><em>much rather</em></strong> prefer is - at first glance just see all my code.</p>
<pre><code class="lang-plaintext">- resources
- docs
- src 
    - users 
        - user_controller.go
    - articles
        - article_data.go
        - article_handler.go
</code></pre>
<p>and then, if I <strong><em>want to</em></strong> look at tests as well, I can expand them and see that as well</p>
<h3 id="heading-collapsing-test-files">Collapsing Test Files</h3>
<p>I'll keep it short - you can do this on Jetbrains (Intellij IDEA, Goland etc) as well as Visual Studio Code. I am sure other IDEs have it too - I don't use them - so not my concern for now.</p>
<h4 id="heading-jetbrains">Jetbrains</h4>
<p>On JetBrains IDEs the concerned feature we are looking for is called <code>File Nesting</code></p>
<p>Search for that in the actions bar,</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699605616741/06457bf7-b776-40d3-8fd7-36cdfb6c3007.png" alt class="image--center mx-auto" /></p>
<p>You'll get into this little modal</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699605638041/ba6ba875-7939-43de-9f8b-03ce9d666ce0.png" alt class="image--center mx-auto" /></p>
<p>There is a simple pattern to map there.</p>
<p>If you want all files of type <code>xyz_test.go</code> to be nested under <code>xyz.go</code> then just add this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699605686209/6084a45c-dc73-4717-bede-fcb29a2e5d52.png" alt class="image--center mx-auto" /></p>
<p>As you can see, some other patterns, like <code>_string.go</code> and <code>_easyjson.go</code> are already there. These are common Go libraries for JSON parsing and String i18n/l10n which generate such files. I added the <code>_test.go</code> into it.</p>
<p>Now, this is how my src folder looks like by default when I open it</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699605770327/05e688ef-cd41-4c88-9a9f-5d7496d60fc1.png" alt class="image--center mx-auto" /></p>
<p>And the tests can be expanded if I want to see them</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699605794346/5ab62058-24fd-4db2-a9ed-9ad7fe7680ec.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-visual-studio-code">Visual Studio Code</h4>
<p>The same thing can be done for Visual Studio Code. The feature over there is also called <code>File Nesting</code> - we need to go to Settings and enable it first (it is disabled by default).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699605877148/102d3cc9-31b2-4034-a984-ce879c79cc08.png" alt class="image--center mx-auto" /></p>
<p>The file pattern is slightly different here. I have added 2 rules there - one for nesting <code>go.sum</code> file under the <code>go.mod</code> file (I like that Jetbrains Goland does this out of the box). For the other case I write</p>
<p><code>*.go : ${basename}_test.go</code></p>
<p>The description in VS Code for that setting (in my above screenshot as well) should be self-explanatory.</p>
<p>And voila, the same thing works on VS Code for now.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699606051264/979b5c14-c321-48c6-9dd7-263d3457c550.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699606066912/959ef1f4-ce63-43b4-b79a-ad2985ed0b2c.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-references">References</h4>
<ul>
<li>[1] <a target="_blank" href="https://github.com/championswimmer/onepixel_backend">https://github.com/championswimmer/onepixel_backend</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Understanding the shift of Frontend Development towards Declarative UI and redux-like state management]]></title><description><![CDATA[Declarative UI and one-way flow of state is something that invariably developers seem to prefer and all frontend engineering platforms have been progressively moving towards it.
The problem though lies that most developers starting out are not coache...]]></description><link>https://arnav.tech/understanding-the-shift-of-frontend-development-towards-declarative-ui</link><guid isPermaLink="true">https://arnav.tech/understanding-the-shift-of-frontend-development-towards-declarative-ui</guid><category><![CDATA[React]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Frontend Development]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Wed, 26 Jul 2023 12:12:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/qWwpHwip31M/upload/4ee4fbedb3b7399090e6c582c2595e39.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Declarative UI and one-way flow of state is something that invariably developers seem to prefer and all frontend engineering platforms have been progressively moving towards it.</p>
<p>The problem though lies that most developers starting out are not coached properly for it.</p>
<h2 id="heading-how-ui-is-represented">How UI is represented ?</h2>
<p>Let's try to understand the problem here.</p>
<p>Classically how UI is represented and how it triggers logic is a bit like this</p>
<p>UI interacts with some "controller" like layer which in turn interacts with local DBs or APIs.</p>
<p>That's pretty much the basic 'structure' of this model.</p>
<p><img src="https://pbs.twimg.com/media/F01230TX0AkPjro?format=jpg&amp;name=medium" alt="Image" /></p>
<p>As UIs keep updating, more and more complexities take place.</p>
<p>More UI elements are 'interactive' - clicking, toggling, typing, and scrolling them trigger different functions on the controllers, they fetch/save more stuff to the DB/API, and more things in UI updates.</p>
<p>Over time as the complexity grows and grows, problems arise. What if when the user is not logged in, someone scrolls the feed? We want only 2 folds of info for guest users or else trigger login.</p>
<p>Ok after login when they come back to this screen how to handle that?</p>
<p>That's just one example, but what keeps happening is that these different UI elements which trigger logic in different 'controller' like places need to have co-dependency on each other. Sometimes cyclic dependencies hit your performance with a ton of bricks.</p>
<p>Eventually, some of your senior developers - the ones that keep a copy of SICP under their pillow, and keep uttering "monad" and "closure" with an expression like food bloggers do when reviewing European dishes - want 'state management' to become 'declarative'.</p>
<p>Time to unpack.</p>
<h2 id="heading-understanding-state">Understanding State</h2>
<p>So to understand what 'state' is, and get the essence of the rest of what I'll be saying, it might serve well to go for a quick trip to a state visualiser, like say - XState.</p>
<p>Here go play around with the visualiser here (you can add more states)</p>
<p><a target="_blank" href="https://stately.ai/viz">https://stately.ai/viz</a></p>
<p><img src="https://pbs.twimg.com/media/F02AhzmWwAEqK62?format=jpg&amp;name=medium" alt="Image" /></p>
<p>Attaching here a few framework diagrams that you might have encountered in your life which try to sell the idea of 'one way flow of state' or some similar idea (in different words).</p>
<p>Recognise any? (read the alts, if you can't)</p>
<p><img src="https://pbs.twimg.com/media/F02At14WIAIMX9r?format=png&amp;name=small" alt="Flux design pattern by Facebook" /></p>
<p><img src="https://pbs.twimg.com/media/F02A5cKWIAQ1lAY?format=png&amp;name=900x900" alt="VueJS official docs of Vuex state management" /></p>
<p><img src="https://pbs.twimg.com/media/F02A255WYAgQdEV?format=png&amp;name=small" alt="Flutter Bloc design pattern" /></p>
<p><img src="https://pbs.twimg.com/media/F02A8kMWwAESwKx?format=png&amp;name=medium" alt="Apple official docs for state management with SwiftUI" /></p>
<p>This is typically held in contrast to 'bad', 'older' state management philosophies like "2-way data binding" or "MVC"</p>
<p>The OG guys who promulgated that was Microsoft &amp; Angular</p>
<p>I think the simple theory - "these have more arrows, i.e. must be bad" holds good collective wisdom 😂</p>
<p><img src="https://pbs.twimg.com/media/F02BN6zXoAEMZpi?format=png&amp;name=small" alt="Angular docs describing 2-way bindings
" /></p>
<p><img src="https://pbs.twimg.com/media/F02CSLAXoAMwaT3?format=png&amp;name=small" alt="Flux vs MVC from Facebook's doc on Flux" /></p>
<p><img src="https://pbs.twimg.com/media/F02CVeCagAE-D6T?format=png&amp;name=small" alt="MVC model described on MDN official docs" /></p>
<h2 id="heading-a-better-way-to-work-with-state">A better way to work with state ?</h2>
<p>What are the wise, senior, developers who have spent a lot of years wrangling with the basic problem of "users do something on UI, some logic runs, the UI changes to something else" saying though?</p>
<p>And why is the utopia they are showing still not lead to performant UIs?</p>
<p>What they (or 'we', I am part of this group) are saying is</p>
<p>"whatever you see on the UI is a representation of state"</p>
<p>Everything. Buttons being disabled or enabled, to the data the user fills in the input boxes, to what shows up on the Twitter app's feed - it is all state.</p>
<p>Now if we can make this little magic box, which, regardless of whatever weirdass complexities we bring into the 'business logic' of your program, can just chuck a "state" into, and it chucks out the "UI" that should represent that state, we are golden!</p>
<p><img src="https://pbs.twimg.com/media/F02Ck2qaIAE7ckj?format=jpg&amp;name=medium" alt="Image" /></p>
<p>Never will the UI chuck something back into the magic box though. And never will the state change because the magic box said so. The magic box has but one function.</p>
<p>Take the 'state' in a machine-readable format (JSON, XML, objects, whatever) and turn it into user interface.</p>
<p>But there is a problem with this approach</p>
<p>See the thing is user interfaces are not like frames of a movie. We do not (and should not) generate the whole damn frame every time something changes</p>
<p>You clicked like on this tweet? Cool just update the ♡ to ❤️, not the whole page!</p>
<p>The utopia of declarative UI lies in aligning "view hierarchy" with "information hierarchy".</p>
<p>This is something that is visualised by nothing better than 2 images from VueJS's official docs.</p>
<p>If you look at the UI (grey, left) do you immediately see the code I have written?</p>
<p><img src="https://pbs.twimg.com/media/F02EBzUXgAAeflj?format=png&amp;name=large" alt="Image" /></p>
<p><img src="https://pbs.twimg.com/media/F02EDGGXgAMOjmU?format=png&amp;name=900x900" alt="Image" /></p>
<p><img src="https://pbs.twimg.com/media/F02FA1CacAUpqwm?format=png&amp;name=small" alt="Image" /></p>
<p>The key to understanding why the 2 hierarchies must align (in the minds of those who are creating this) is because of the aforemention problem - clicking the like button shouldn't 'redraw' my whole screen.</p>
<p>For example, refreshing the articles should do this</p>
<p><img src="https://pbs.twimg.com/media/F02GaarakAQB45Q?format=jpg&amp;name=medium" alt="Image" /></p>
<p>Or more precisely, if I clicked like on a particular post, even more specific set of things to update.</p>
<p><img src="https://pbs.twimg.com/media/F02G278akAMWnZ-?format=jpg&amp;name=medium" alt="Image" /></p>
<h2 id="heading-caveats-and-dragons-beware">Caveats and Dragons Beware</h2>
<p>Apart from the 'creators' of this product - which include the designers of the UI to a degree, and not only the engineers of the UI but those 'generating' the objects that constitute state (which could be happening partly on the backend) - aligning on this hierarchy which itself is not the easiest thing to happen (when a group of 20 people are building technically complex things, everyone visualising the exact same hierarchy is not that easy), there also are certain things we need from that 'magical box' we talked about.</p>
<p>The magic box must be smart enough to actually see which parts of the information tree actually changed, and accordingly, on the UI actually change only those parts of the view tree (for eg. highlighted in pink)</p>
<p>Some frameworks put the onus of the 'smartness' on devs, some don't</p>
<p><img src="https://pbs.twimg.com/media/F02I1eGaUAAyHqB?format=jpg&amp;name=medium" alt="Image" /></p>
<p>Putting the onus of the smartness on the devs has it's downsides of coaching the devs about this whole thing (so far I have been talking in this thread) - they need to be made cognizant of the 'cost of drawing' elements, and 'lifting up the state' term starts to appear soon.</p>
<p>Being able to untangle a thread of state from a mess of highly co-dependent variables in the memory, 6 screens deep in the flow of an app with 50 screens, is not exactly easy to wrangle for new developers, especially after parachuting into an already complex project.</p>
<p>Frameworks like Flutter put the onus on the developers, and all online articles which cover "improving performance on Flutter" will always start with the question "are you updating more stateful widgets than you need?"</p>
<p>Some others do some smartness on your behalf, like React+Redux where there are magical "dom diffing" operations under the hood which will diff the new state with old state to figure out which elements need drawing again, or Jetpack Compose where Compose internally does this.</p>
<p>The framework doing the smartness for you has 2 downsides.</p>
<ol>
<li><p>you still need to architect the information hierarchy properly. if the object tree itself gets updated right from the root (or is undiffable), framework cannot do any magic</p>
</li>
<li><p>more magic == less debugability. simple</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Asking ChatGPT to build a YouTube Download App for me.]]></title><description><![CDATA[So I have started seeing lots of people starting to publish videos where they ask generative AI to generate the code for an entire app and fairly complex apps are getting generated.
Thought I will try it out myself as well.

The cover image is genera...]]></description><link>https://arnav.tech/asking-chatgpt-to-build-a-youtube-download-app-for-me</link><guid isPermaLink="true">https://arnav.tech/asking-chatgpt-to-build-a-youtube-download-app-for-me</guid><category><![CDATA[chatgpt]]></category><category><![CDATA[openai]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[Artificial Intelligence]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Mon, 03 Jul 2023 17:58:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1688404628591/e513beb8-7f8c-49b5-8e31-414d196bf58b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So I have started seeing lots of people starting to publish videos where they ask generative AI to generate the code for an entire app and fairly complex apps are getting generated.</p>
<p>Thought I will try it out myself as well.</p>
<blockquote>
<p>The cover image is generated using AlphaCTR.com</p>
</blockquote>
<h2 id="heading-the-prompt">The Prompt</h2>
<p>So the idea is simple. I want to make a GUI wrapper for <a target="_blank" href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> - a tool I frequently use to download videos (or parts thereof) from YouTube.</p>
<p>I asked <a target="_blank" href="https://chat.openai.com/?model=gpt-4">ChatGPT (GPT4)</a> to build this app for me.</p>
<p>So here was my prompt. (It is fairly detailed).</p>
<pre><code class="lang-plaintext">I want to make a Flutter app that downloads videos using the 'yt-dlp' tool. 

Assume the following 
1. I do not need to setup yt-dlp, it exists on my PC already. 
2. Do not help me with Flutter installation and plugin adding steps. 
3. Only show the code for the lib/main.dart file 

The app's UI will have the following 

1. Title bar: "YouTube Downloader GUI" 
2. Input Box [id: ytUrl, placeholder: "youtube video url"]
3. A row of 3 checkboxes 
  a. "Video" [id: dlVid, default: checked]
  b. "Audio" [id: dlAud, default: unchecked] 
  c. "Thumbnail" [id: dlThumb, default: unchecked]
4. Two input boxes for start and end time 
  a. "Start time" [id: timeStart] 
  b. "End Time" [id: timeEnd] 
5. Two dropdown boxes to select quality of audio and video 
  a. id: qualVid, choices: [480p, 720p, 1080p, 2K, 4K], default: 1080p 
  b.  id: qualAud, choices: [64k, 128k, 192k, 256k, 320k] default: 192k 


The logic for downloading should be this. 

1. If dlAud = true, download audio separately 
2. If dlThumb = true, download thumbnail too 
3. If start and end time are given, download only that portion of the video 
4. If ytUrl is not provided or doesn't contain 'youtube.com' in it show a Dialog "Enter valid YouTube URL"
5. Factor in the qualVid and qualAud when downloading audio and video. 
6. Download videos in mp4 form and audio in mp3 form
</code></pre>
<p>Highlighting a few things here</p>
<ol>
<li><p>I explicitly asked only to generate the <code>lib/main.dart</code> code for me because otherwise ChatGPT by default provides all the info required to setup a Flutter project and install dependencies. Since I am well versed with Flutter at a basic level to do these things myself, I asked it to avoid it for faster output</p>
</li>
<li><p>I am building a Flutter app (and not say, React Native Desktop) especially because this is a platform I am slightly familiar with and I can debug the final output</p>
</li>
</ol>
<h2 id="heading-the-initial-output">The Initial Output</h2>
<p>So here is the code that ChatGPT generated for me</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:process_run/cmd_run.dart'</span>;

<span class="hljs-keyword">void</span> main() {
  runApp(MyApp());
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'YouTube Downloader GUI'</span>,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyHomePage</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  MyHomePage({Key key}) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  _MyHomePageState createState() =&gt; _MyHomePageState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_MyHomePageState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">MyHomePage</span>&gt; </span>{
  <span class="hljs-built_in">String</span> ytUrl;
  <span class="hljs-built_in">bool</span> dlVid = <span class="hljs-keyword">true</span>;
  <span class="hljs-built_in">bool</span> dlAud = <span class="hljs-keyword">false</span>;
  <span class="hljs-built_in">bool</span> dlThumb = <span class="hljs-keyword">false</span>;
  TimeOfDay timeStart;
  TimeOfDay timeEnd;
  <span class="hljs-built_in">String</span> qualVid = <span class="hljs-string">'1080p'</span>;
  <span class="hljs-built_in">String</span> qualAud = <span class="hljs-string">'192k'</span>;

  <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">String</span>&gt; qualVidOptions = [<span class="hljs-string">'480p'</span>, <span class="hljs-string">'720p'</span>, <span class="hljs-string">'1080p'</span>, <span class="hljs-string">'2K'</span>, <span class="hljs-string">'4K'</span>];
  <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">String</span>&gt; qualAudOptions = [<span class="hljs-string">'64k'</span>, <span class="hljs-string">'128k'</span>, <span class="hljs-string">'192k'</span>, <span class="hljs-string">'256k'</span>, <span class="hljs-string">'320k'</span>];

  <span class="hljs-keyword">void</span> download() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (ytUrl == <span class="hljs-keyword">null</span> || !ytUrl.contains(<span class="hljs-string">'youtube.com'</span>)) {
      showDialog(
        context: context,
        builder: (context) =&gt; AlertDialog(
          content: Text(<span class="hljs-string">'Enter valid YouTube URL'</span>),
        ),
      );
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-built_in">String</span> command = <span class="hljs-string">'yt-dlp -f \'bestvideo[height&lt;=<span class="hljs-subst">${qualVid.replaceAll(<span class="hljs-string">'p'</span>, <span class="hljs-string">''</span>)}</span>]'</span>
        <span class="hljs-string">'+bestaudio/best[height&lt;=<span class="hljs-subst">${qualVid.replaceAll(<span class="hljs-string">'p'</span>, <span class="hljs-string">''</span>)}</span>]\' <span class="hljs-subst">$ytUrl</span>'</span>;
    <span class="hljs-keyword">if</span> (dlAud) command += <span class="hljs-string">' -x --audio-format mp3 --audio-quality <span class="hljs-subst">${qualAud}</span>'</span>;
    <span class="hljs-keyword">if</span> (dlThumb) command += <span class="hljs-string">' --write-thumbnail'</span>;
    <span class="hljs-keyword">if</span> (timeStart != <span class="hljs-keyword">null</span> &amp;&amp; timeEnd != <span class="hljs-keyword">null</span>)
      command +=
          <span class="hljs-string">' --postprocessor-args "-ss <span class="hljs-subst">${timeStart.format(context)}</span> -to <span class="hljs-subst">${timeEnd.format(context)}</span>"'</span>;

    <span class="hljs-keyword">await</span> runCmd(ProcessCmd(command.split(<span class="hljs-string">' '</span>)[<span class="hljs-number">0</span>], command.split(<span class="hljs-string">' '</span>).sublist(<span class="hljs-number">1</span>)));
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(
        title: Text(<span class="hljs-string">'YouTube Downloader GUI'</span>),
      ),
      body: Padding(
        padding: EdgeInsets.all(<span class="hljs-number">16.0</span>),
        child: Column(
          children: &lt;Widget&gt;[
            TextField(
              onChanged: (value) =&gt; ytUrl = value,
              decoration: InputDecoration(
                labelText: <span class="hljs-string">'YouTube Video URL'</span>,
              ),
            ),
            Row(
              children: &lt;Widget&gt;[
                Checkbox(
                  value: dlVid,
                  onChanged: (<span class="hljs-built_in">bool</span> value) {
                    setState(() {
                      dlVid = value;
                    });
                  },
                ),
                Text(<span class="hljs-string">'Video'</span>),
                Checkbox(
                  value: dlAud,
                  onChanged: (<span class="hljs-built_in">bool</span> value) {
                    setState(() {
                      dlAud = value;
                    });
                  },
                ),
                Text(<span class="hljs-string">'Audio'</span>),
                Checkbox(
                  value: dlThumb,
                  onChanged: (<span class="hljs-built_in">bool</span> value) {
                    setState(() {
                      dlThumb = value;
                    });
                  },
                ),
                Text(<span class="hljs-string">'Thumbnail'</span>),
              ],
            ),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    onTap: () <span class="hljs-keyword">async</span> {
                      TimeOfDay time = <span class="hljs-keyword">await</span> showTimePicker(
                        context: context,
                        initialTime: TimeOfDay.now(),
                      );
                      <span class="hljs-keyword">if</span> (time != <span class="hljs-keyword">null</span>) setState(() =&gt; timeStart = time);
                    },
                    readOnly: <span class="hljs-keyword">true</span>,
                    decoration: InputDecoration(
                      labelText: <span class="hljs-string">'Start Time'</span>,
                      hintText: timeStart?.format(context) ?? <span class="hljs-string">''</span>,
                    ),
                  ),
                ),
                SizedBox(width: <span class="hljs-number">16</span>),
                Expanded(
                  child: TextField(
                    onTap: () <span class="hljs-keyword">async</span> {
                      TimeOfDay time = <span class="hljs-keyword">await</span> showTimePicker(
                        context: context,
                        initialTime: TimeOfDay.now(),
                      );
                      <span class="hljs-keyword">if</span> (time != <span class="hljs-keyword">null</span>) setState(() =&gt; timeEnd = time);
                    },
                    readOnly: <span class="hljs-keyword">true</span>,
                    decoration: InputDecoration(
                      labelText: <span class="hljs-string">'End Time'</span>,
                      hintText: timeEnd?.format(context) ?? <span class="hljs-string">''</span>,
                    ),
                  ),
                ),
              ],
            ),
            DropdownButton(
              value: qualVid,
              items: qualVidOptions
                  .map((value) =&gt; DropdownMenuItem(
                        child: Text(value),
                        value: value,
                      ))
                  .toList(),
              onChanged: (<span class="hljs-built_in">String</span> value) {
                setState(() {
                  qualVid = value;
                });
              },
            ),
            DropdownButton(
              value: qualAud,
              items: qualAudOptions
                  .map((value) =&gt; DropdownMenuItem(
                        child: Text(value),
                        value: value,
                      ))
                  .toList(),
              onChanged: (<span class="hljs-built_in">String</span> value) {
                setState(() {
                  qualAud = value;
                });
              },
            ),
            ElevatedButton(
              onPressed: download,
              child: Text(<span class="hljs-string">'Download'</span>),
            ),
          ],
        ),
      ),
    );
  }
}
</code></pre>
<p>It does also mention that this might not cover all corner cases and be mindful of YouTube policies</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688403741059/97beac24-ae7c-4863-8877-7b6096814b72.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-trying-to-use-it">Trying to use it</h2>
<p>Let's see what happens when I try to use it. I am not sure if this code will work straight out of the box or not!</p>
<p>So it didn't work out of the box, and the main reason for that is that ChatGPT's knowledge cutoff date is Sept 2021, which means most things in Flutter and Dart that have evolved since then wouldn't have found its way into ChatGPT's codex. One particular thing that affects is that ChatGPT-generated Flutter code doesn't use <code>const</code> constructors. Because it would not have been trained on much code with <code>const</code></p>
<p>Here is a full diff of the changes I had to do to get it just working.</p>
<p><a target="_blank" href="https://github.com/championswimmer/yt_dlp_ui/commit/8a1fe9ba3a2bb33120779138b66222c804a25859">https://github.com/championswimmer/yt_dlp_ui/commit/8a1fe9ba3a2bb33120779138b66222c804a25859</a></p>
<p>Some highlights</p>
<ol>
<li><h4 id="heading-ofcourse-lots-of-const-additions">ofcourse, lots of <code>const</code> additions</h4>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688406393189/1befdc99-95d0-4f4b-87e3-a1685c766c8c.png" alt class="image--center mx-auto" /></p>
<ol>
<li><h4 id="heading-not-understanding-2023-era-dart-null-safety">not understanding 2023-era Dart null safety</h4>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688406472460/b6c201cc-a9bb-45c8-85e9-9ff92eb90ee1.png" alt class="image--center mx-auto" /></p>
<ol>
<li><h4 id="heading-using-some-older-apis-of-the-processrun-library">using some older APIs of the <code>process_run</code> library</h4>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688406530853/627f2d5f-7915-41fc-a3af-5ca80881c821.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-getting-it-to-work">Getting it to work</h2>
<p>Had to make one final change (which ChatGPT already had warned me about) - to disable MacOS sandbox mode, to be able to run shelle commands</p>
<p>Basically go to <code>macos/Runner/DebugProfile.entitlements</code> and</p>
<pre><code class="lang-xml">    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>com.apple.security.app-sandbox<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">false</span>/&gt;</span>
</code></pre>
<p>Mostly after these changes, it basically works!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688406806533/0529d804-16c3-4e13-aba3-07f13cde6408.gif" alt class="image--center mx-auto" /></p>
<ul>
<li><p>The start/end time input boxes are timepickers, which we need to fix!</p>
</li>
<li><p>I stopped the download midway that's why it didn't finish</p>
</li>
<li><p>Will need to fix how the video/audio quality is translated in the shell command</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I think this is pretty good. Obviously the way this will get <strong><em>weaponised</em></strong> won't be us talking to a damn chatbot and asking it to code. There will be better ways this will integrate into the coding workflow.</p>
<p>I will next try to achieve the same using <a target="_blank" href="https://github.com/PicoCreator/smol-dev-js">smol-dev-js</a> which is based on smol.ai and I'll probably write up about that when I do.</p>
<p>Till then, this is fairly promising. And I will finally turn the <code>Small App Ideas.md</code> page from by Obsidian into actual apps maybe!</p>
]]></content:encoded></item><item><title><![CDATA[Publishing a Kotlin Multiplatform Project in all platforms (Win, Mac, Linux, JVM, JS) with Github Actions]]></title><description><![CDATA[So I recently made a Parking Lot CLI program (the kind of ones you make for low-level system design interview questions). You can check the following terminal recording.

The interesting thing is, it is made using Kotlin Multiplatform, which means it...]]></description><link>https://arnav.tech/publishing-a-kotlin-multiplatform-project-in-all-platforms-win-mac-linux-jvm-js-with-github-actions</link><guid isPermaLink="true">https://arnav.tech/publishing-a-kotlin-multiplatform-project-in-all-platforms-win-mac-linux-jvm-js-with-github-actions</guid><category><![CDATA[github-actions]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[multiplatform]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Mon, 02 Jan 2023 14:12:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/12a3ca8297c32a29370036d360881f9f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So I recently made a Parking Lot CLI program (the kind of ones you make for low-level system design interview questions). You can check the following terminal recording.</p>
<p><a target="_blank" href="https://asciinema.org/a/549083"><img src="https://asciinema.org/a/549083.svg" alt="asciicast" /></a></p>
<p>The interesting thing is, it is made using Kotlin Multiplatform, which means it can be compiled to the following formats</p>
<ul>
<li><p>Native Windows .exe executable (via mingw)</p>
</li>
<li><p>Native MacOS executable</p>
</li>
<li><p>Native Linux binary</p>
</li>
<li><p>.jar file for running on JVM</p>
</li>
<li><p>.js file that can be run using NodeJS</p>
</li>
</ul>
<p>And they way I have set it up on Github, on every tag, it releases all these binaries.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672668339377/d5ebbb52-670c-4e75-9365-b644fd402e3b.png" alt class="image--center mx-auto" /></p>
<p>So how does that work, you'll ask? Here's how.</p>
<h2 id="heading-compilation-steps">Compilation Steps</h2>
<h3 id="heading-1-javascript">1. JavaScript</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build_js:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v3</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Set</span> <span class="hljs-string">up</span> <span class="hljs-string">JDK</span> <span class="hljs-number">17</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-java@v3</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">java-version:</span> <span class="hljs-string">'17'</span>
          <span class="hljs-attr">distribution:</span> <span class="hljs-string">'microsoft'</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">with</span> <span class="hljs-string">Gradle</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">gradle/gradle-build-action@v2</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">gradle-version:</span> <span class="hljs-string">current</span>
          <span class="hljs-attr">arguments:</span> <span class="hljs-string">compileProductionExecutableKotlinJs</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Tar</span> <span class="hljs-string">Artifact</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">tar</span> <span class="hljs-string">-czf</span> <span class="hljs-string">ParkingLot-LLD-Kotlin-MPP.nodejs.tgz</span> <span class="hljs-string">build/js/packages/ParkingLot-LLD-Kotlin-MPP</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Upload</span> <span class="hljs-string">JS</span> <span class="hljs-string">Build</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-artifact@v2</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">js-build</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">ParkingLot-LLD-Kotlin-MPP.nodejs.tgz</span>
</code></pre>
<h3 id="heading-2-jvm">2. JVM</h3>
<pre><code class="lang-yaml">  <span class="hljs-attr">build_jvm:</span>
    <span class="hljs-string">...</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-string">...</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">with</span> <span class="hljs-string">Gradle</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">arguments:</span> <span class="hljs-string">assembleDist</span> <span class="hljs-comment"># java distribution</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Upload</span> <span class="hljs-string">JVM</span> <span class="hljs-string">Build</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-artifact@v2</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">jvm-build</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">build/distributions/ParkingLot-LLD-Kotlin-MPP-*.zip</span> <span class="hljs-comment"># this is the path where it is created</span>
</code></pre>
<h3 id="heading-3-macos">3. MacOS</h3>
<pre><code class="lang-yaml">  <span class="hljs-attr">build_macos:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">macos-latest</span> <span class="hljs-comment"># build this on a Mac</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-string">...</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">with</span> <span class="hljs-string">Gradle</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">arguments:</span> <span class="hljs-string">macosX64Binaries</span> <span class="hljs-comment"># to build mac binary</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Upload</span> <span class="hljs-string">MacOS</span> <span class="hljs-string">Build</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-artifact@v2</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">macos-build</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">build/bin/macosX64/releaseExecutable/ParkingLot-LLD-Kotlin-MPP.kexe</span>
</code></pre>
<hr />
<p>Similarly Windows and Linux are done too. You can check the entire workflow here</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/championswimmer/ParkingLot-LLD-Kotlin-MPP/blob/main/.github/workflows/build.yml">https://github.com/championswimmer/ParkingLot-LLD-Kotlin-MPP/blob/main/.github/workflows/build.yml</a></div>
]]></content:encoded></item><item><title><![CDATA[Validating Github Actions Workflow files in Jetbrains IDEs]]></title><description><![CDATA[If you maintain projects on Github, you must have come across Github Actions - which is Github's own CI provider where you can greate workflow files in .github/workflow/<xyz>.yml and get Github to build your project, run lint, test PRs, deploy to rel...]]></description><link>https://arnav.tech/validating-github-actions-workflow-files-in-jetbrains-ides</link><guid isPermaLink="true">https://arnav.tech/validating-github-actions-workflow-files-in-jetbrains-ides</guid><category><![CDATA[github-actions]]></category><category><![CDATA[WebStorm]]></category><category><![CDATA[Jetbrains]]></category><category><![CDATA[Validation]]></category><category><![CDATA[json-schema]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Tue, 27 Dec 2022 10:07:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/0bf22059d88e90616fd3707bd88c0483.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you maintain projects on <a target="_blank" href="https://github.com">Github</a>, you must have come across <a target="_blank" href="https://github.com/features/actions">Github Actions</a> - which is Github's own CI provider where you can greate workflow files in <code>.github/workflow/&lt;xyz&gt;.yml</code> and get Github to build your project, run lint, test PRs, deploy to releases etc.</p>
<p>I use Github Actions extensively, mainly for my opensource projects as Github gives you unlimited free tier build minutes for OSS repos. And I prefer having a full end-to-end CI setup on my projects so it makes it easier for me to review and merge PRs.</p>
<p>Anyway, one problem I used to face is absence of validation of the <code>workflow.yml</code> files when working on IDEs. Visual Studio code does seem to have some plugins which can validate Github Workflow files, but on Jetbrains IDEs (IntelliJ IDEA, Webstorm, GoLand etc) there isn't something like that.</p>
<p>But <a target="_blank" href="https://github.com/SchemaStore/schemastore">SchemaStore</a> does have JSON Schema definitions for Github Actions. Which we can use to validate our workflow files. Here's how-</p>
<h2 id="heading-add-json-schema-for-github-workflow-in-ide-settings">Add JSON Schema for Github Workflow in IDE Settings</h2>
<p>Go here in Preferences</p>
<ul>
<li><p>Preferences</p>
<ul>
<li><p>Languages &amp; Frameworks</p>
<ul>
<li>JSON Schema Mappings</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672135302794/c7ef58f2-98d5-4630-95a7-1a012905d891.png" alt class="image--center mx-auto" /></p>
<p>Add a new JSON Schema Mapping.</p>
<p>Give it a name, like <code>github-workflow</code></p>
<p>Add this as the path to the JSON Schema <code>https://json.schemastore.org/github-workflow.json</code> (you can click the 🌍 globe icon and search "Github Workflow" in the SchemaStore list too).</p>
<p>Now we need to add a file pattern, to tell the IDE which files to be validated with this JSON Schema. Add this file pattern</p>
<p><code>.github/workflows/*.yml</code></p>
<p>And voila.... schema validation shoud start working</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672135503643/306f6d8c-0a1d-4c37-8eaa-335706d3301d.png" alt class="image--center mx-auto" /></p>
<p>You'll also get auto-complete and descriptions of the keys when you edit Github Workflow files now.</p>
]]></content:encoded></item><item><title><![CDATA[Creating and Publishing Visual Studio Code Color Themes]]></title><description><![CDATA[Visual Studio Code has this in-built color theme called Quiet Light which I absolutely love, as I prefer using mostly light themes when coding in the day, when my room is filled with light.
Yesterday after a long long time I was coding in darkness an...]]></description><link>https://arnav.tech/creating-and-publishing-visual-studio-code-color-themes</link><guid isPermaLink="true">https://arnav.tech/creating-and-publishing-visual-studio-code-color-themes</guid><category><![CDATA[Visual Studio Code]]></category><category><![CDATA[vscode extensions]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Wed, 21 Dec 2022 10:27:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1671616226802/dHbvd6zEym.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Visual Studio Code has this in-built color theme called <strong>Quiet Light</strong> which I absolutely love, as I prefer using mostly light themes when coding in the day, when my room is filled with light.</p>
<p>Yesterday after a long long time I was coding in darkness and wanted to switch to a dark theme. I don't remember, in the past which themes I have mostly used for Dark Mode, and I was really yearning for a theme that would be similar to <strong>Quiet Light</strong> in terms of the color palette. While I did find <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=JosphatGitogoSangara.quiet-dark">this one extension</a> claiming to implement dark version of Quiet Light, it didn't actually quite live up to the expectation.</p>
<p>As usually happens during such yak shaving sessions, I started off with trying to edit that theme's json file, and finally ended up considering publishing my own theme to the <a target="_blank" href="https://marketplace.visualstudio.com">VS Code Marketplace</a>. So here are the steps to make your own theme and publish them.</p>
<h2 id="heading-create-a-vs-code-extension-package">Create a VS Code Extension Package</h2>
<p>VS Code team have made a <code>yeoman generator</code> for creating VS Code extensions. Start from there.</p>
<pre><code class="lang-bash">❯ npm install -g yo generator-code
❯ yo code
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671616957924/yNV_-IGqv.png" alt="my answers for the yo code wizard" class="image--center mx-auto" /></p>
<p>This is what the structure of the newly created project look like</p>
<pre><code class="lang-bash">.
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── icon.png          <span class="hljs-comment"># add an icon for marketplace listing</span>
├── package.json
├── quieter-dark-color-theme-0.0.1.vsix
├── screenshot.png    <span class="hljs-comment"># add a screenshot for marketplace</span>
├── themes
│   └── Quieter Dark-color-theme.json
└── vsc-extension-quickstart.md
</code></pre>
<blockquote>
<p>Make sure the theme file ends with <code>-color-theme.json</code> and then VS Code will give you code completion help when editing it.</p>
</blockquote>
<p>There are two main things inside a theme, <code>colors</code> and <code>tokenColors</code></p>
<ul>
<li><p><strong>colors</strong> define background, foreground colors for elements of the UI of VS Code itself, including sidebars, status bars, editor panes, tooltips etc</p>
</li>
<li><p><strong>tokenColors</strong> define syntax highlighting, like classes, functions, keywords etc.</p>
</li>
</ul>
<p>Once you have created the theme as per your liking, you can move it (or symlink it) into <code>~/.vscode/extensions/</code> to have it installed to your local IDE and try it out. Once you are satisfied, you can move on to publishing it.</p>
<blockquote>
<p>You'll find more detailed documentation on creating color themes in the official docs<br /><a target="_blank" href="https://code.visualstudio.com/api/extension-guides/color-them">https://code.visualstudio.com/api/extension-guides/color-them</a></p>
</blockquote>
<h2 id="heading-publishing-a-visual-studio-code-color-theme-to-the-marketplace">Publishing a Visual Studio Code Color Theme to the Marketplace</h2>
<p>Before we go to the further steps, keep <code>vsce</code> (the VS Code Extension manager CLI) installed.</p>
<pre><code class="lang-bash">npm install -g @vscode/vsce
</code></pre>
<p>Before you can publish, you need to ensure a few steps.</p>
<ol>
<li><p>Create a Azure Devops organisation if you do not have one. <a target="_blank" href="https://learn.microsoft.com/azure/devops/organizations/accounts/create-organization">(Go here)</a></p>
</li>
<li><p>Get a Personal Access Token inside that organisation. This PAT must have <code>Marketplace &gt; Manage</code> scope in it.</p>
<p><img src="https://code.visualstudio.com/assets/api/working-with-extensions/publishing-extension/token1.png" alt="Personal settings menu" /></p>
<p><img src="https://code.visualstudio.com/assets/api/working-with-extensions/publishing-extension/token1.png" alt="Personal settings menu" /></p>
</li>
<li><p>Save this personal access token somewhere. I prefer having it saved in my <code>~/.profile</code> file as <code>VSC_MARKET_PAT=xxxxxxxxx</code> so I can use the env variable <code>$VSC_MARKET_PAT</code> wherever I need it.</p>
</li>
<li><p>Create a VS Code Marketplace <strong>publisher</strong>. <em>Note this is important to remember what publisher</em> <strong><em>name</em></strong> <em>you make here as you need to add it to your package.json too.</em> <a target="_blank" href="https://marketplace.visualstudio.com/manage">(Go here to make this)</a></p>
</li>
<li><p>Check that the publisher you made is working by doing <code>vsce login &lt;publisher name&gt;</code> in your terminal, and making sure the login works.</p>
</li>
</ol>
<p>Make sure you add a <code>name</code> and a <code>publisher</code> to your package.json before proceeding.</p>
<p>To publish your extension run</p>
<pre><code class="lang-bash">vsce publish
</code></pre>
<p>You can also specify a version of your extension, when making changes and updating it</p>
<pre><code class="lang-bash">vsce publish 0.0.1 
<span class="hljs-comment"># or </span>
vsce publish minor <span class="hljs-comment"># 0.1.0 increment </span>
<span class="hljs-comment"># or </span>
vsce publish major <span class="hljs-comment"># 1.0.0 increment</span>
</code></pre>
<blockquote>
<p>You'll get more detailed information including how to disable your extension or deprecate it, and get analytics data of it in the official docs<br /><a target="_blank" href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension">https://code.visualstudio.com/api/working-with-extensions/publishing-extension</a></p>
</blockquote>
<h2 id="heading-links-and-references">Links and References</h2>
<ul>
<li><p><a target="_blank" href="https://github.com/championswimmer/vscode-theme-quieter-dark">https://github.com/championswimmer/vscode-theme-quieter-dark</a><br />source code of the extension I made  </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/championswimmer/vscode-theme-quieter-dark">https://github.com/championswimmer/vscode-theme-quieter-dark</a></div>
<p> </p>
</li>
<li><p><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=championswimmer.quieter-dark-color-theme">https://marketplace.visualstudio.com/items?itemName=championswimmer.quieter-dark-color-theme</a><br />published link of the extension  </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://marketplace.visualstudio.com/items?itemName=championswimmer.quieter-dark-color-theme">https://marketplace.visualstudio.com/items?itemName=championswimmer.quieter-dark-color-theme</a></div>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Managing libraries and dependencies in Android projects with Gradle version catalog]]></title><description><![CDATA[In large multi-module Android (or for that matter any gradle-based) project, version management starts to become a bit of a pain. 
The most common way people have been coping with that was using buildSrc, but the problem with that is buildSrc changes...]]></description><link>https://arnav.tech/managing-libraries-and-dependencies-in-android-projects-with-gradle-version-catalog</link><guid isPermaLink="true">https://arnav.tech/managing-libraries-and-dependencies-in-android-projects-with-gradle-version-catalog</guid><category><![CDATA[Android]]></category><category><![CDATA[gradle]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Java]]></category><category><![CDATA[Android Studio]]></category><dc:creator><![CDATA[Arnav Gupta]]></dc:creator><pubDate>Tue, 26 Apr 2022 09:48:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1650966407703/pe58JFNYa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In large multi-module Android (or for that matter any gradle-based) project, version management starts to become a bit of a pain. </p>
<p>The most common way people have been coping with that <a target="_blank" href="https://proandroiddev.com/better-dependencies-management-using-buildsrc-kotlin-dsl-eda31cdb81bf">was using buildSrc</a>, but the problem with that is <code>buildSrc</code> changes cause cache invalidations across your build and increases build times. </p>
<p>There obviously now calls to action to <a target="_blank" href="https://medium.com/bumble-tech/how-to-use-composite-builds-as-a-replacement-of-buildsrc-in-gradle-64ff99344b58">move from buildSrc to includeBuild</a> </p>
<p>If you have other custom build logic, and build plugins, it ofcourse makes sense to move to the <code>includeBuild</code> mechanism, but if you needed <code>buildSrc</code> only for version management of dependencies, there is a better way - <a target="_blank" href="https://docs.gradle.org/current/userguide/platforms.html">gradle version catalogs</a></p>
<p>Elaborate explanations of how it works is available in the previous link, I will quickly cover the basic set of steps </p>
<h3 id="heading-add-versions-and-libraries-to-settingsgradlekts">Add versions and libraries to <code>settings.gradle(.kts)</code></h3>
<blockquote>
<p>NOTE: All my examples are in kts because buildscripts in kotlin have better DX (autocomplete, linting etc) </p>
</blockquote>
<p>This is for example what I added in my <code>settings.gradle.kts</code> (expanding on the lines afterwards) </p>
<pre><code>dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }

    versionCatalogs {
        create(<span class="hljs-string">"libs"</span>) {
            version(<span class="hljs-string">"androidx.navigation"</span>, <span class="hljs-string">"2.4.1"</span>)
            version(<span class="hljs-string">"android.material"</span>, <span class="hljs-string">"1.7.0-alpha01"</span>)
            version(<span class="hljs-string">"moshi"</span>, <span class="hljs-string">"1.13.0"</span>)
            version(<span class="hljs-string">"retrofit"</span>, <span class="hljs-string">"2.9.0"</span>)
            version(<span class="hljs-string">"moshi"</span>, <span class="hljs-string">"1.13.0"</span>)

            <span class="hljs-class"><span class="hljs-keyword">library</span>(<span class="hljs-params"><span class="hljs-string">"android.material"</span>, <span class="hljs-string">"com.google.android.material"</span>, <span class="hljs-string">"material"</span></span>).<span class="hljs-title">versionRef</span>(<span class="hljs-params"><span class="hljs-string">"android.material"</span></span>)
            <span class="hljs-title"><span class="hljs-keyword">library</span></span>(<span class="hljs-params"><span class="hljs-string">"androidx.navigation.fragment"</span>, <span class="hljs-string">"androidx.navigation"</span>, <span class="hljs-string">"navigation-fragment-ktx"</span></span>).<span class="hljs-title">versionRef</span>(<span class="hljs-params"><span class="hljs-string">"androidx.navigation"</span></span>)
            <span class="hljs-title"><span class="hljs-keyword">library</span></span>(<span class="hljs-params"><span class="hljs-string">"androidx.navigation.ui"</span>, <span class="hljs-string">"androidx.navigation"</span>, <span class="hljs-string">"navigation-ui-ktx"</span></span>).<span class="hljs-title">versionRef</span>(<span class="hljs-params"><span class="hljs-string">"androidx.navigation"</span></span>)
            <span class="hljs-title"><span class="hljs-keyword">library</span></span>(<span class="hljs-params"><span class="hljs-string">"androidx.core"</span>, <span class="hljs-string">"androidx.core:core-ktx:1.7.0"</span></span>)
            <span class="hljs-title"><span class="hljs-keyword">library</span></span>(<span class="hljs-params"><span class="hljs-string">"androidx.appcompat"</span>, <span class="hljs-string">"androidx.appcompat:appcompat:1.4.1"</span></span>)
            <span class="hljs-title"><span class="hljs-keyword">library</span></span>(<span class="hljs-params"><span class="hljs-string">"okhttp"</span>, <span class="hljs-string">"com.squareup.okhttp3:okhttp:5.0.0-alpha.6"</span></span>)
            <span class="hljs-title"><span class="hljs-keyword">library</span></span>(<span class="hljs-params"><span class="hljs-string">"retrofit"</span>, <span class="hljs-string">"com.squareup.retrofit2"</span>, <span class="hljs-string">"retrofit"</span></span>).<span class="hljs-title">versionRef</span>(<span class="hljs-params"><span class="hljs-string">"retrofit"</span></span>)
            <span class="hljs-title"><span class="hljs-keyword">library</span></span>(<span class="hljs-params"><span class="hljs-string">"retrofit.converter.moshi"</span>, <span class="hljs-string">"com.squareup.retrofit2"</span>, <span class="hljs-string">"converter-moshi"</span></span>).<span class="hljs-title">versionRef</span>(<span class="hljs-params"><span class="hljs-string">"retrofit"</span></span>)
            <span class="hljs-title"><span class="hljs-keyword">library</span></span>(<span class="hljs-params"><span class="hljs-string">"moshi"</span>, <span class="hljs-string">"com.squareup.moshi"</span>, <span class="hljs-string">"moshi-kotlin"</span></span>).<span class="hljs-title">versionRef</span>(<span class="hljs-params"><span class="hljs-string">"moshi"</span></span>)
            <span class="hljs-title"><span class="hljs-keyword">library</span></span>(<span class="hljs-params"><span class="hljs-string">"moshi.compiler"</span>, <span class="hljs-string">"com.squareup.moshi"</span>, <span class="hljs-string">"moshi-kotlin-codegen"</span></span>).<span class="hljs-title">versionRef</span>(<span class="hljs-params"><span class="hljs-string">"moshi"</span></span>)

            <span class="hljs-title">bundle</span>(<span class="hljs-params"><span class="hljs-string">"androidx.appcompat"</span>, listOf(<span class="hljs-params"><span class="hljs-string">"androidx.core"</span>, <span class="hljs-string">"androidx.appcompat"</span></span>)</span>)
            <span class="hljs-title">bundle</span>(<span class="hljs-params"><span class="hljs-string">"androidx.navigation"</span>, listOf(<span class="hljs-params"><span class="hljs-string">"androidx.navigation.fragment"</span>, <span class="hljs-string">"androidx.navigation.ui"</span></span>)</span>)
            <span class="hljs-title">bundle</span>(<span class="hljs-params"><span class="hljs-string">"retrofit"</span>, listOf(<span class="hljs-params"><span class="hljs-string">"retrofit"</span>, <span class="hljs-string">"retrofit.converter.moshi"</span></span>)</span>)
        }
    }
}</span>
</code></pre><h4 id="heading-1-creating-libs-block">1. Creating 'libs' block</h4>
<pre><code>dependencyResolutionManagement {
  versionCatalogs {
    create(<span class="hljs-string">"libs"</span>) {...}
  }
}
</code></pre><h4 id="heading-2-add-version-constants">2. Add version constants</h4>
<pre><code>version("android.material", "1.7.0-alpha01")
version("moshi", "1.13.0")
version("retrofit", "2.9.0")
</code></pre><h4 id="heading-3-add-each-dependency">3. Add each dependency</h4>
<p>You can refer versions constants via <code>versionRef()</code> </p>
<pre><code><span class="hljs-class"><span class="hljs-keyword">library</span>(<span class="hljs-params"><span class="hljs-string">"android.material"</span>, <span class="hljs-string">"com.google.android.material"</span>, <span class="hljs-string">"material"</span></span>).<span class="hljs-title">versionRef</span>(<span class="hljs-params"><span class="hljs-string">"android.material"</span></span>)</span>
</code></pre><h4 id="heading-4-create-bundles-if-there-are-a-set-of-libraries-that-get-used-together">4. Create 'bundles' if there are a set of libraries that get used together</h4>
<pre><code><span class="hljs-selector-tag">bundle</span>(<span class="hljs-string">"androidx.appcompat"</span>, listOf(<span class="hljs-string">"androidx.core"</span>, <span class="hljs-string">"androidx.appcompat"</span>))
</code></pre><h3 id="heading-using-version-catalog-libraries-in-buildgradlekts-files">Using version catalog libraries in <code>build.gradle(.kts)</code> files</h3>
<p>The way to use these in your module(s)' build.gradle is as follows </p>
<pre><code><span class="hljs-selector-tag">dependencies</span> {

    <span class="hljs-selector-tag">implementation</span>(libs.bundles.androidx.appcompat)
    <span class="hljs-selector-tag">implementation</span>(libs.android.material)
    <span class="hljs-selector-tag">implementation</span>(<span class="hljs-string">"androidx.constraintlayout:constraintlayout:2.1.3"</span>)
    <span class="hljs-selector-tag">implementation</span>(libs.bundles.androidx.navigation)
    <span class="hljs-selector-tag">testImplementation</span>(<span class="hljs-string">"junit:junit:4.13.2"</span>)
    <span class="hljs-selector-tag">androidTestImplementation</span>(<span class="hljs-string">"androidx.test.ext:junit:1.1.3"</span>)
    <span class="hljs-selector-tag">androidTestImplementation</span>(<span class="hljs-string">"androidx.test.espresso:espresso-core:3.4.0"</span>)
}
</code></pre><p>Normal libraries are used like this </p>
<pre><code>    <span class="hljs-selector-tag">implementation</span>(<span class="hljs-selector-tag">libs</span><span class="hljs-selector-class">.android</span><span class="hljs-selector-class">.material</span>)
</code></pre><p>While an entire bundle can be added like this </p>
<pre><code>    <span class="hljs-selector-tag">implementation</span>(<span class="hljs-selector-tag">libs</span><span class="hljs-selector-class">.android</span><span class="hljs-selector-class">.material</span>)
</code></pre><h3 id="heading-what-does-not-work">What does not work ?</h3>
<p>There are a few caveats (as of April 2022, most I expect to improve) </p>
<h4 id="heading-gradle-74">Gradle 7.4+</h4>
<p>If you are using Gradle 7.3 or below, then version catalogs are unstable/experimental API and you need to mark that as so. From 7.4 they are default enabled </p>
<h4 id="heading-android-studio-support-of-version-management">Android Studio Support of Version Management</h4>
<p>Dependencies inside version catalogs do not show up in <strong>Project Structure</strong> options of Android Studio nor does studio remind you to update the dependencies that are out-of-date. </p>
<p>As you can see below there is a warning regarding version catalogs, and the version catalog dependencies do not show up 
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650964851253/DJUqK8dvR.png" alt="image.png" /></p>
<h4 id="heading-autocomplete-works-with-kotlin-only">Autocomplete works with Kotlin only</h4>
<p>If you use <code>build.gradle</code> (groovy) instead of <code>build.gradle.kts</code> (kotlinscript), then the <code>libs.x.y</code> dependencies will not have autocomplete or type-checking</p>
<p>Below is autocomplete working only inside <code>.kts</code> files. 
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650964965128/RjHh6mos9.png" alt="image.png" /></p>
]]></content:encoded></item></channel></rss>