<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Woojin Ahn</title>
    <description>The latest articles on Forem by Woojin Ahn (@woojinahn).</description>
    <link>https://forem.com/woojinahn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3783663%2F044c1ae6-731a-43c4-9675-cb353c9e6f42.jpeg</url>
      <title>Forem: Woojin Ahn</title>
      <link>https://forem.com/woojinahn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/woojinahn"/>
    <language>en</language>
    <item>
      <title>A 13MB Cursor Monitor: AppKit, Undocumented APIs, Zero Dependencies</title>
      <dc:creator>Woojin Ahn</dc:creator>
      <pubDate>Fri, 13 Mar 2026 16:54:49 +0000</pubDate>
      <link>https://forem.com/woojinahn/a-13mb-cursor-monitor-appkit-undocumented-apis-zero-dependencies-1k72</link>
      <guid>https://forem.com/woojinahn/a-13mb-cursor-monitor-appkit-undocumented-apis-zero-dependencies-1k72</guid>
      <description>&lt;p&gt;My company recently started supporting Cursor, so I jumped in. But checking usage meant opening the dashboard every time — and if Max mode was on without me noticing, requests burned through fast.&lt;/p&gt;

&lt;p&gt;I wanted a &lt;strong&gt;glanceable counter, always visible&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I built one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4l8oz8s9hm9yw02tfud9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4l8oz8s9hm9yw02tfud9.png" alt="Menu bar showing CursorMeter with usage fraction" width="282" height="62"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  V1: SwiftUI — Done in a Weekend, Regretted in a Week
&lt;/h2&gt;

&lt;p&gt;The first version took two days. SwiftUI's &lt;code&gt;MenuBarExtra&lt;/code&gt; made it trivial to get something on screen. I was proud of it.&lt;/p&gt;

&lt;p&gt;Then I checked Activity Monitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;56 MB of memory.&lt;/strong&gt; For a menu bar app that displays a single number.&lt;/p&gt;

&lt;p&gt;I tried to optimize — removed animations, simplified views, cached aggressively. Nothing moved the needle. The SwiftUI runtime itself was the cost. &lt;code&gt;MenuBarExtra&lt;/code&gt; allocates a full SwiftUI rendering pipeline whether you need it or not.&lt;/p&gt;

&lt;p&gt;For a "set and forget" menu bar app that runs 24/7, 56MB felt like a tax I shouldn't be paying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rewrite: Burning It All Down
&lt;/h2&gt;

&lt;p&gt;I threw out the entire UI layer and rebuilt everything in pure AppKit. &lt;code&gt;NSStatusItem&lt;/code&gt;, &lt;code&gt;NSPopover&lt;/code&gt;, &lt;code&gt;NSViewController&lt;/code&gt; — the APIs your framework tries to hide from you.&lt;/p&gt;

&lt;p&gt;It was painful. SwiftUI's declarative bindings became manual &lt;code&gt;NSTextField&lt;/code&gt; updates. Layout constraints replaced &lt;code&gt;VStack&lt;/code&gt;. Every state change needed explicit UI synchronization.&lt;/p&gt;

&lt;p&gt;But the result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;SwiftUI&lt;/th&gt;
&lt;th&gt;AppKit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory (RSS)&lt;/td&gt;
&lt;td&gt;~56 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~13 MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of UI code&lt;/td&gt;
&lt;td&gt;~200&lt;/td&gt;
&lt;td&gt;~660&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;3x more code, 4x less memory. For a menu bar app, I'd make that trade every time.&lt;/p&gt;

&lt;p&gt;Other SwiftUI-based menu bar apps I've seen typically sit around 30–50MB. The framework overhead is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API That Doesn't Exist
&lt;/h2&gt;

&lt;p&gt;Cursor has no public API for usage data. But their dashboard has to get the numbers from somewhere.&lt;/p&gt;

&lt;p&gt;Browser DevTools → Network tab → three requests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/api/usage&lt;/code&gt; — per-model request counts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/usage-summary&lt;/code&gt; — billing cycle, plan usage in USD cents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/auth/me&lt;/code&gt; — user info&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No documentation. Cookie-based auth. The response shape has changed at least once since I started.&lt;/p&gt;

&lt;p&gt;This meant two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. No hardcoding.&lt;/strong&gt; The &lt;code&gt;/api/usage&lt;/code&gt; response contains model names as dynamic keys. If Cursor adds &lt;code&gt;claude-4-opus&lt;/code&gt; tomorrow, the parser picks it up automatically — no app update needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Expect failure.&lt;/strong&gt; Both API calls run in parallel. If &lt;code&gt;/api/usage-summary&lt;/code&gt; fails but &lt;code&gt;/api/usage&lt;/code&gt; succeeds, the app shows what it can. If both fail, it shows the last known data. The app never crashes on an API change — it just degrades gracefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero Dependencies, on Purpose
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Package.swift&lt;/code&gt; imports nothing. No Alamofire, no KeychainAccess, no SwiftyJSON. Just macOS SDK frameworks.&lt;/p&gt;

&lt;p&gt;This wasn't ideology. It was pragmatism:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app does three HTTP requests. I don't need a networking library for that.&lt;/li&gt;
&lt;li&gt;Keychain access is ~80 lines of &lt;code&gt;Security&lt;/code&gt; framework calls. A dependency would be longer.&lt;/li&gt;
&lt;li&gt;JSON decoding is &lt;code&gt;Codable&lt;/code&gt;. It's built in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The side benefit: no supply chain to audit, no version conflicts, no "this dependency requires macOS 15 but I target 14" surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AppKit isn't dead — it's just unfashionable.&lt;/strong&gt; For long-running, low-footprint apps, the memory savings are real and measurable. SwiftUI is better for most things. But not this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swift 6 strict concurrency is worth the pain.&lt;/strong&gt; The API client is an &lt;code&gt;actor&lt;/code&gt;. The UI is &lt;code&gt;@MainActor&lt;/code&gt;. All models are &lt;code&gt;Sendable&lt;/code&gt;. It took effort to get it compiling, but an entire category of bugs simply can't happen now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build for the API to break.&lt;/strong&gt; When you depend on undocumented endpoints, "it works today" is the only guarantee. Dynamic parsing + graceful degradation isn't a nice-to-have — it's the whole architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The App
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ze2ct06ai0ledbblnph.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ze2ct06ai0ledbblnph.png" alt="Popover showing usage details" width="522" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/WoojinAhn/CursorMeter" rel="noopener noreferrer"&gt;CursorMeter&lt;/a&gt; is open source (MIT). macOS 14+, zero dependencies, ~13MB memory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://raw.githubusercontent.com/WoojinAhn/CursorMeter/main/Scripts/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use Cursor daily and want to keep an eye on your usage without opening a browser, give it a try. &lt;a href="https://github.com/WoojinAhn/CursorMeter/issues" rel="noopener noreferrer"&gt;Issues and feedback&lt;/a&gt; are always welcome.&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>swift</category>
      <category>macos</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a GitHub star monitor in a single YAML file — zero dependencies, zero config</title>
      <dc:creator>Woojin Ahn</dc:creator>
      <pubDate>Sat, 21 Feb 2026 09:21:27 +0000</pubDate>
      <link>https://forem.com/woojinahn/i-built-a-github-star-monitor-in-a-single-yaml-file-zero-dependencies-zero-config-2ccb</link>
      <guid>https://forem.com/woojinahn/i-built-a-github-star-monitor-in-a-single-yaml-file-zero-dependencies-zero-config-2ccb</guid>
      <description>&lt;p&gt;GitHub doesn't notify you when someone stars your repo. Or unstars it. You either refresh your profile page like a maniac, or you just... never find out.&lt;/p&gt;

&lt;p&gt;Tools like &lt;a href="https://star-history.com" rel="noopener noreferrer"&gt;star-history.com&lt;/a&gt; let you look up trends, but you have to visit the site every time. GitHub Apps like &lt;a href="https://github.com/marketplace/star-notifier" rel="noopener noreferrer"&gt;Star Notifier&lt;/a&gt; automate it, but now a third-party service is handling your tokens. Self-hosted solutions like &lt;a href="https://github.com/omkarcloud/github-star-notifier" rel="noopener noreferrer"&gt;github-star-notifier&lt;/a&gt; need a server running somewhere.&lt;/p&gt;

&lt;p&gt;I wanted something &lt;strong&gt;automatic&lt;/strong&gt; that requires &lt;strong&gt;zero infrastructure&lt;/strong&gt;. So I built it using nothing but GitHub Actions.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;GitHub Star Checker&lt;/strong&gt; monitors star counts across all your public repositories and notifies you when stars change — gains or losses.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs every hour by default (configurable from the GitHub UI)&lt;/li&gt;
&lt;li&gt;Sends alerts via &lt;strong&gt;GitHub Issues&lt;/strong&gt; or &lt;strong&gt;Gmail&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Generates &lt;strong&gt;weekly and monthly reports&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;All logic lives in a &lt;strong&gt;single workflow file&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No server, no database, no dependencies — just one YAML file and GitHub's free compute.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0m6cbfbg7vvyjg9y00c0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0m6cbfbg7vvyjg9y00c0.png" alt="GitHub Actions workflow dispatch UI showing schedule, notification channel, and report options" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A cron job triggers every hour on GitHub Actions&lt;/li&gt;
&lt;li&gt;Fetches star counts for all your public, non-fork repos via the GitHub API&lt;/li&gt;
&lt;li&gt;Compares with the previous snapshot stored in &lt;code&gt;stars.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If anything changed → sends a notification&lt;/li&gt;
&lt;li&gt;Commits the updated data back to the repo&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The workflow updates itself — including the schedule and notification settings — so you configure everything from the GitHub UI. No code editing needed.&lt;/p&gt;

&lt;p&gt;Since it runs in its own forked repo, your existing repositories stay completely untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the notifications look like
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GitHub Issue alert
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73rrcsffsysr03isi1p5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F73rrcsffsysr03isi1p5.png" alt="GitHub Issue notification showing a new star gain on a repository" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Email alert
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmav26fk4rsrikh92aws1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmav26fk4rsrikh92aws1.png" alt="Gmail inbox showing a GitHub star change alert email" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Weekly report
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg1zqtwxc1p89k4ht8u7h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg1zqtwxc1p89k4ht8u7h.png" alt="GitHub Issue showing a weekly star summary report with total counts" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup in 60 seconds
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/WoojinAhn/github-star-checker/fork" rel="noopener noreferrer"&gt;&lt;strong&gt;Fork the repo&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Enable workflows in the Actions tab&lt;/li&gt;
&lt;li&gt;Add your &lt;a href="https://github.com/settings/tokens/new" rel="noopener noreferrer"&gt;Personal Access Token&lt;/a&gt; (&lt;code&gt;repo&lt;/code&gt; + &lt;code&gt;workflow&lt;/code&gt; scopes) as &lt;code&gt;STAR_MONITOR_TOKEN&lt;/code&gt; in Settings &amp;gt; Secrets&lt;/li&gt;
&lt;li&gt;Run the workflow — done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first run records your current star counts. From the second run onward, you'll get notified about any changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's it.&lt;/strong&gt; No YAML to write, no config files to edit, no CLI to install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use a GitHub App?
&lt;/h2&gt;

&lt;p&gt;Fair question. Apps like Star Notifier are convenient — install and go. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They're &lt;strong&gt;third-party services&lt;/strong&gt; processing your GitHub data&lt;/li&gt;
&lt;li&gt;You can't customize the logic or notification format&lt;/li&gt;
&lt;li&gt;If the service goes down or gets discontinued, you lose it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project is &lt;strong&gt;entirely yours&lt;/strong&gt;. It's a fork in your account, running on GitHub's own infrastructure. You can read every line of code, modify anything, and it'll keep running as long as GitHub Actions exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interesting technical bits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-modifying YAML&lt;/strong&gt;: When you change settings via &lt;code&gt;workflow_dispatch&lt;/code&gt;, the workflow uses &lt;code&gt;sed&lt;/code&gt; to update its own cron schedule and env variables, then commits the change. Future runs pick up the new settings automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-conflict git strategy&lt;/strong&gt;: The workflow stashes local changes, rebases on remote, then pops — handling concurrent scheduled runs gracefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;32-day history retention&lt;/strong&gt;: Daily snapshots are stored in &lt;code&gt;stars-history.json&lt;/code&gt; for weekly/monthly reports, with automatic pruning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Being honest about the trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not real-time&lt;/strong&gt;: GitHub Actions cron can delay 5–30 minutes. This is periodic monitoring, not instant webhook-based alerts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification channels&lt;/strong&gt;: Currently supports GitHub Issues and Gmail. No Slack/Discord/Telegram yet (PRs welcome!).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit history&lt;/strong&gt;: The workflow commits &lt;code&gt;stars.json&lt;/code&gt; updates to the forked repo. This is by design (persistence without external storage), but your fork's commit log will grow over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API usage&lt;/strong&gt;: Each run makes one paginated API call. Even with 100+ repos, it stays well within GitHub's rate limits.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;👉 &lt;a href="https://github.com/WoojinAhn/github-star-checker" rel="noopener noreferrer"&gt;&lt;strong&gt;github.com/WoojinAhn/github-star-checker&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fork it, set one secret, and forget about it. You'll know when your stars move.&lt;/p&gt;

&lt;p&gt;If you find it useful, a ⭐ would be appreciated — and yes, I'll get notified about it 😄&lt;/p&gt;

</description>
      <category>github</category>
      <category>opensource</category>
      <category>actions</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
