<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Masumi Kawasaki 💭</title>
    <description>The latest articles on DEV Community by Masumi Kawasaki 💭 (@geeknees).</description>
    <link>https://dev.to/geeknees</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%2F485186%2Fed035e5e-cdc3-44b5-912a-2fa1e8ead6e3.jpeg</url>
      <title>DEV Community: Masumi Kawasaki 💭</title>
      <link>https://dev.to/geeknees</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/geeknees"/>
    <language>en</language>
    <item>
      <title>I Built Minesweeper So an AI Can Slack Off and Play: MCP Edition</title>
      <dc:creator>Masumi Kawasaki 💭</dc:creator>
      <pubDate>Sun, 25 Jan 2026 13:45:31 +0000</pubDate>
      <link>https://dev.to/geeknees/i-built-minesweeper-so-an-ai-can-slack-off-and-play-mcp-edition-4gc4</link>
      <guid>https://dev.to/geeknees/i-built-minesweeper-so-an-ai-can-slack-off-and-play-mcp-edition-4gc4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I cover the API-side implementation in a separate post:&lt;br&gt;
&lt;a href="https://dev.to/geeknees/i-built-minesweeper-so-an-ai-can-slack-off-and-play-the-api-edition-jfn"&gt;I Built Minesweeper So an AI Can Slack Off and Play: API Edition&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;MCP is often discussed as a practical tool for getting real work done with AI. This time, I intentionally used it for &lt;strong&gt;play&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The goal is simple: &lt;strong&gt;let an AI play Minesweeper on its own&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The tone is a bit jokey—but the implementation is not. That’s the theme: &lt;em&gt;use MCP for something silly, but build it properly&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you squint, MCP is “a common protocol that lets AI call external tools.” In this post, I’ll share what it took to build an &lt;strong&gt;stdio MCP server&lt;/strong&gt; that can hit a &lt;strong&gt;Rails-based Minesweeper API&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;I built an &lt;strong&gt;MCP stdio server&lt;/strong&gt; that allows an AI to control a Minesweeper (Rails 8) Web/API from an MCP host (Claude Code / Claude Desktop / Codex, etc.).&lt;/p&gt;

&lt;p&gt;The MCP host can call it as a set of tools, enabling everything needed to play the game over HTTP: starting a game, fetching state, operating on cells, and ending the game.&lt;/p&gt;

&lt;p&gt;Concretely, the server provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user_start&lt;/code&gt; — start a new game tied to a user&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;game_state&lt;/code&gt; — fetch the public game state (no token required)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;game_open&lt;/code&gt; / &lt;code&gt;game_flag&lt;/code&gt; / &lt;code&gt;game_chord&lt;/code&gt; — operate on the board&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;game_end&lt;/code&gt; — end the game&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_games&lt;/code&gt; — fetch a list of public games (optional)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I intentionally kept the surface area minimal—only what’s needed for “AI plays Minesweeper by itself”—while keeping the tool boundaries readable as an MCP design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The architecture is intentionally simple: the MCP server called by the AI just forwards requests to the Rails API.&lt;/p&gt;

&lt;p&gt;The UI is optional and &lt;strong&gt;spectator-only&lt;/strong&gt;. All operations go through the API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI (MCP Host)
    │  stdio (JSON-RPC)
    ▼
MCP Server (minesweeper-mcp)
    │  HTTP (Bearer token)
    ▼
Rails Minesweeper API
    │
    ▼
Spectator UI (optional)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP server’s responsibilities are strictly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;validate input → call Rails API → normalize response&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All game logic lives on the Rails side. Keeping MCP “thin” makes it easier to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation highlights
&lt;/h2&gt;

&lt;p&gt;The guiding principle was: &lt;strong&gt;thin, but strict&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The MCP server receives JSON-RPC over stdio and connects to the Rails API with the shortest path possible—while explicitly handling the footguns.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Config validation
&lt;/h3&gt;

&lt;p&gt;The server reads &lt;code&gt;MINESWEEPER_BASE_URL&lt;/code&gt; and &lt;code&gt;MINESWEEPER_BEARER_TOKEN&lt;/code&gt; from environment variables and validates them at startup.&lt;/p&gt;

&lt;p&gt;The token is &lt;strong&gt;required only for “mutation tools”&lt;/strong&gt;. Public endpoints (state / games) work without a token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requireBearerToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;user_start&lt;/code&gt; and &lt;code&gt;user_games&lt;/code&gt;, if &lt;code&gt;user_slug&lt;/code&gt; is omitted, the server falls back to &lt;code&gt;MINESWEEPER_USER_SLUG&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Keep the Rails API client thin
&lt;/h3&gt;

&lt;p&gt;URL construction and error normalization are encapsulated in a &lt;code&gt;RailsClient&lt;/code&gt;. Tool handlers stay extremely simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;validate → call client → return&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openCell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the code readable and avoids spreading HTTP details across tool logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use the MCP SDK for stdio
&lt;/h3&gt;

&lt;p&gt;stdio JSON-RPC is handled by the MCP SDK via &lt;code&gt;Server&lt;/code&gt; + &lt;code&gt;StdioServerTransport&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because stdout is reserved for the MCP protocol, &lt;strong&gt;all logs go to stderr only&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Tool boundaries match API intent
&lt;/h3&gt;

&lt;p&gt;Tools are split to reflect the Rails API semantics: &lt;code&gt;user_start&lt;/code&gt;, &lt;code&gt;game_open&lt;/code&gt;, &lt;code&gt;game_flag&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;I avoided unnecessary abstractions on the MCP side. The priority is directness: &lt;em&gt;“this action is exactly this tool.”&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Startup debug output (opt-in)
&lt;/h3&gt;

&lt;p&gt;For debugging, &lt;code&gt;MINESWEEPER_DEBUG_STARTUP=1&lt;/code&gt; enables startup info printed to stderr.&lt;/p&gt;

&lt;p&gt;Token values are never printed—only whether a token is present.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing strategy
&lt;/h2&gt;

&lt;p&gt;Because the MCP server is “thin,” there aren’t many places that need unit tests.&lt;/p&gt;

&lt;p&gt;However, integrations (external API + stdio protocol) break easily, so I tested across &lt;strong&gt;three layers: unit / integration / e2e&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;env validation&lt;/li&gt;
&lt;li&gt;URL construction&lt;/li&gt;
&lt;li&gt;error normalization&lt;/li&gt;
&lt;li&gt;input validation (coordinate bounds, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Integration tests
&lt;/h3&gt;

&lt;p&gt;Spin up the Rails API and actually call &lt;code&gt;start/state/open/end&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is where boundary behavior is validated in reality, e.g.:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“mutations fail without a token”&lt;/li&gt;
&lt;li&gt;“state can be fetched publicly”&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  E2E tests
&lt;/h3&gt;

&lt;p&gt;Send MCP JSON-RPC over stdio and validate the full flow:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;initialize&lt;/code&gt; → &lt;code&gt;tools/list&lt;/code&gt; → &lt;code&gt;tools/call&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Only after this passes can you confidently say “an AI can use this.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas and lessons learned
&lt;/h2&gt;

&lt;p&gt;The implementation was straightforward, but the real blockers were the “boring-but-deadly” details.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Binding between token and user_slug
&lt;/h3&gt;

&lt;p&gt;If the token is tied to a &lt;em&gt;different user&lt;/em&gt; on the Rails side, &lt;code&gt;/users/:slug/start&lt;/code&gt; returns 401.&lt;/p&gt;

&lt;p&gt;From the MCP side, the Authorization header is correct—but the API sees it as “valid token, wrong user.”&lt;/p&gt;

&lt;p&gt;Having logs that show &lt;code&gt;token_digest&lt;/code&gt; and &lt;code&gt;slug&lt;/code&gt; mapping was a lifesaver.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Handling stdio safely
&lt;/h3&gt;

&lt;p&gt;stdout is protocol-only. If logs leak into stdout, you’re dead.&lt;/p&gt;

&lt;p&gt;During local verification it’s easy to accidentally print to stdout, so being strict about stderr-only logging mattered a lot.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Staying “thin” is harder than it sounds
&lt;/h3&gt;

&lt;p&gt;You keep wanting to add convenient abstractions—but the Rails app is the rightful owner of game logic.&lt;/p&gt;

&lt;p&gt;A thin MCP server is simply less fragile. This project reminded me that sometimes you need the courage to &lt;strong&gt;not add features&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;MCP is a practical tool—but it’s also a great toy.&lt;/p&gt;

&lt;p&gt;And when you build toys seriously, you end up learning the serious things naturally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;implement the protocol correctly&lt;/li&gt;
&lt;li&gt;tighten error boundaries&lt;/li&gt;
&lt;li&gt;test in three layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: an environment where an AI can play Minesweeper on its own.&lt;/p&gt;

&lt;p&gt;If I find another fun way to misuse MCP productively, I’ll write about that too.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/geeknees/minesweeper-mcp" rel="noopener noreferrer"&gt;https://github.com/geeknees/minesweeper-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🇯🇵 This article is also available in Japanese:&lt;br&gt;
&lt;a href="https://zenn.dev/geeknees/articles/e266f9343932e6" rel="noopener noreferrer"&gt;https://zenn.dev/geeknees/articles/e266f9343932e6&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>node</category>
      <category>minesweeper</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built Minesweeper So an AI Can Slack Off and Play: The API Edition</title>
      <dc:creator>Masumi Kawasaki 💭</dc:creator>
      <pubDate>Sun, 25 Jan 2026 13:43:48 +0000</pubDate>
      <link>https://dev.to/geeknees/i-built-minesweeper-so-an-ai-can-slack-off-and-play-the-api-edition-jfn</link>
      <guid>https://dev.to/geeknees/i-built-minesweeper-so-an-ai-can-slack-off-and-play-the-api-edition-jfn</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Instead of only delegating work to AI, I started wondering: &lt;strong&gt;how do you let an AI slack off and play?&lt;/strong&gt;&lt;br&gt;
That idea became &lt;em&gt;AI-only Minesweeper&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Humans are not allowed to operate it. &lt;strong&gt;Only the AI can interact with the game via an API.&lt;/strong&gt;&lt;br&gt;
In other words, the “UI” is the API itself—and the whole system is designed around that premise.&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%2Fyf50zc2ir7tjcvs466gc.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%2Fyf50zc2ir7tjcvs466gc.png" alt=" " width="800" height="1031"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article is the &lt;strong&gt;API edition&lt;/strong&gt;, so I’ll save the MCP side for another post. Here, I’ll focus on the API design and the rules that make it possible for an AI to actually play.&lt;/p&gt;
&lt;h2&gt;
  
  
  Goals of the API Design
&lt;/h2&gt;

&lt;p&gt;The primary goal is simple: make &lt;strong&gt;“AI-only operation”&lt;/strong&gt; work cleanly.&lt;/p&gt;

&lt;p&gt;Humans are spectators, so the API behavior &lt;em&gt;is&lt;/em&gt; the game experience. That’s why the flow must be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simple,&lt;/li&gt;
&lt;li&gt;predictable,&lt;/li&gt;
&lt;li&gt;and explicit in its state transitions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concretely, the API is designed as a single path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;start&lt;/code&gt; creates a new game every time,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;open / flag / chord / end&lt;/code&gt; progress the game.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Game state transitions are explicit:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;waiting -&amp;gt; playing -&amp;gt; won/lost -&amp;gt; ended&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This helps the AI predict what will happen next.&lt;/p&gt;

&lt;p&gt;Secret info (mine positions) is stored in a separate table. The public API only returns what’s necessary by converting internal state into a board representation.&lt;/p&gt;

&lt;p&gt;The public board rendering is centralized in &lt;code&gt;GameState#render_board&lt;/code&gt;.&lt;br&gt;
The API only returns the transformed 2D array—internal state stays hidden.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_board&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
  &lt;span class="n"&gt;mines_set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mine_set&lt;/span&gt;
  &lt;span class="n"&gt;opened&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;opened_set&lt;/span&gt;
  &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flagged_set&lt;/span&gt;

  &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Game&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BOARD_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Game&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BOARD_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;cell_display&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;mines: &lt;/span&gt;&lt;span class="n"&gt;mines_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;opened: &lt;/span&gt;&lt;span class="n"&gt;opened&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;flagged: &lt;/span&gt;&lt;span class="n"&gt;flagged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Endpoint Design
&lt;/h2&gt;

&lt;p&gt;The flow is intentionally straightforward:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;start -&amp;gt; state -&amp;gt; open/flag/chord -&amp;gt; end&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It’s a single “happy path” so the AI doesn’t get lost.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;state&lt;/code&gt; is &lt;strong&gt;public (no auth)&lt;/strong&gt;, which is quietly convenient: spectators and debugging tools can use the exact same endpoint.&lt;/p&gt;

&lt;p&gt;The star of the show is &lt;code&gt;open&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;opens an unopened cell&lt;/li&gt;
&lt;li&gt;returns 422 if the cell is flagged&lt;/li&gt;
&lt;li&gt;idempotent if the cell is already open&lt;/li&gt;
&lt;li&gt;supports zero-expansion (flood fill), so the AI can easily “probe” to gain information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The controller just catches exceptions and maps them to 422, while the game logic lives in the model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_cell!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:x&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:y&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="n"&gt;broadcast_updates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:changed_cells&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;game: &lt;/span&gt;&lt;span class="vi"&gt;@game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_state&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Game&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InvalidMove&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"invalid_move"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flag&lt;/code&gt; is a toggle with a maximum of 10 flags. That constraint forces the AI to commit: it can’t spam flags everywhere.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chord&lt;/code&gt; is only allowed when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the selected cell is a number cell, and&lt;/li&gt;
&lt;li&gt;the number of adjacent flags matches that number&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When it works, progress accelerates. When the condition isn’t met, it returns 422—so it’s a great “decision-making checkpoint” for the AI.&lt;/p&gt;

&lt;p&gt;Request bodies are kept minimal (e.g., &lt;code&gt;POST /games/:public_id/open { x, y }&lt;/code&gt;).&lt;br&gt;
More detailed specs live in &lt;code&gt;docs/api.md&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Board Representation and State Transitions
&lt;/h2&gt;

&lt;p&gt;The board is returned as a 2D array: &lt;code&gt;board[y][x]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each cell is represented as a string:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#&lt;/code&gt; = unopened&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;F&lt;/code&gt; = flag&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0-8&lt;/code&gt; = opened number&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only on defeat do special symbols appear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;X&lt;/code&gt; = the mine that was triggered&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;M&lt;/code&gt; = mines that were not triggered&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;!&lt;/code&gt; = incorrect flags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;State transitions are:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;waiting -&amp;gt; playing -&amp;gt; won/lost -&amp;gt; ended&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Win conditions are either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open all non-mine cells, or&lt;/li&gt;
&lt;li&gt;place flags on all mines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API owns this logic, so the AI only needs the board strings to decide the next move.&lt;/p&gt;
&lt;h2&gt;
  
  
  Authentication and Safety
&lt;/h2&gt;

&lt;p&gt;All action endpoints require:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The token is displayed &lt;strong&gt;only once&lt;/strong&gt; at user creation time. No re-display, no re-issue.&lt;br&gt;
Basically: &lt;em&gt;lose it and it’s over.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Authentication is centralized in &lt;code&gt;ApplicationController&lt;/code&gt; so every action can share it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticate_user!&lt;/span&gt;
  &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
  &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Bearer "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"missing_token"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unauthorized&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Bearer "&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
  &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UserToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;user_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UserToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;token_digest: &lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;user_token&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"invalid_token"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unauthorized&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="vi"&gt;@current_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For basic bot resistance, I added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a honeypot, and&lt;/li&gt;
&lt;li&gt;a simple “time trap” that rejects submissions made in under 3 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn’t meant to be hardcore security—just a lightweight guardrail against lazy mass signups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;When you push the entire game UI into the API, the AI ends up with a clean equivalence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calling the API = playing the game&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Humans can stay in spectator mode, and the AI can make decisions purely from the board it receives. That keeps both implementation and usage pleasantly simple.&lt;/p&gt;

&lt;p&gt;Next time (the MCP edition), I’ll write about how to actually “let the AI play” using this API.&lt;br&gt;
If you’re curious, check out &lt;code&gt;docs/api.md&lt;/code&gt; too.&lt;/p&gt;

&lt;p&gt;mcp: &lt;a href="https://dev.to/geeknees/i-built-minesweeper-so-an-ai-can-slack-off-and-play-mcp-edition-4gc4"&gt;https://dev.to/geeknees/i-built-minesweeper-so-an-ai-can-slack-off-and-play-mcp-edition-4gc4&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/geeknees/minesweeper-for-ai" rel="noopener noreferrer"&gt;https://github.com/geeknees/minesweeper-for-ai&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🇯🇵 This article is also available in Japanese:&lt;br&gt;
&lt;a href="https://zenn.dev/geeknees/articles/634ad69d7c347e" rel="noopener noreferrer"&gt;https://zenn.dev/geeknees/articles/634ad69d7c347e&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>rails</category>
      <category>minesweeper</category>
    </item>
    <item>
      <title>Building an MCP Server: Bringing WakaTime Data to AI Agents</title>
      <dc:creator>Masumi Kawasaki 💭</dc:creator>
      <pubDate>Wed, 21 Jan 2026 06:25:50 +0000</pubDate>
      <link>https://dev.to/geeknees/building-an-mcp-server-bringing-wakatime-data-to-ai-agents-ncc</link>
      <guid>https://dev.to/geeknees/building-an-mcp-server-bringing-wakatime-data-to-ai-agents-ncc</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;"How much did I code today?"&lt;br&gt;
"What was my project breakdown last week?"&lt;/p&gt;

&lt;p&gt;If you're a developer using WakaTime, these questions probably sound familiar. But opening a browser to check the dashboard feels like unnecessary friction.&lt;/p&gt;

&lt;p&gt;I've been automating my daily retrospectives with AI, and having access to "how I spent my time" data makes those reflections much more accurate. While GitHub commit history is helpful, it doesn't capture the full picture—actual coding time, language breakdowns, project-by-project focus levels. If AI Agents could access WakaTime's rich data, the quality of automated retrospectives would improve dramatically.&lt;/p&gt;

&lt;p&gt;So I built an MCP (Model Context Protocol) server to &lt;strong&gt;make WakaTime data directly accessible to AI Agents&lt;/strong&gt;. Now agents can answer questions like "show me this week's coding time by project" and incorporate that data into retrospective reports.&lt;/p&gt;

&lt;p&gt;This article walks through the implementation of &lt;code&gt;wakatime-mcp&lt;/code&gt; and explains the key technical points of building an MCP server.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is wakatime-mcp?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;wakatime-mcp&lt;/code&gt; is an MCP server implementation that exposes WakaTime API functionality via the MCP protocol.&lt;/p&gt;
&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;p&gt;It provides two tools:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;wakatime_summaries&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Fetches daily summaries for a specified date range, with optional project filtering and timezone support.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wakatime_summaries"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timezone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Asia/Tokyo"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;wakatime_today&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Fetches today's summary. Defaults to Asia/Tokyo timezone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wakatime_today"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timezone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Asia/Tokyo"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  No Installation Required
&lt;/h3&gt;

&lt;p&gt;Using &lt;code&gt;npx&lt;/code&gt;, you can run it directly without installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;WAKATIME_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_API_KEY"&lt;/span&gt;
npx &lt;span class="nt"&gt;-y&lt;/span&gt; github:geeknees/wakatime-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To add it to your AI Agent's MCP configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github:geeknees/wakatime-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"WAKATIME_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_API_KEY"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can get your WakaTime API Key from the &lt;a href="https://wakatime.com/settings/account" rel="noopener noreferrer"&gt;WakaTime settings page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Deep Dive
&lt;/h2&gt;

&lt;p&gt;The entire implementation is about 170 lines of TypeScript—surprisingly concise. Here are the key technical points.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. WakaTime API Authentication
&lt;/h3&gt;

&lt;p&gt;WakaTime uses a slightly unusual authentication method. Instead of Bearer tokens, it requires &lt;strong&gt;Base64-encoding the API key and passing it as a Basic auth header&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;basicAuthHeaderFromApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Basic &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;b64&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, but easy to miss if you don't read the docs carefully.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. MCP Protocol Implementation
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; makes MCP server implementation remarkably straightforward. You basically register two handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wakatime-mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Handler that returns available tools&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ListToolsRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wakatime_summaries&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Fetches WakaTime daily summaries for a date range&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* JSON Schema */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Handler that executes tool calls&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CallToolRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Tool execution logic&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stdio communication is handled entirely by &lt;code&gt;StdioServerTransport&lt;/code&gt;, so you don't need to worry about protocol details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StdioServerTransport&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Timezone-Aware Date Handling
&lt;/h3&gt;

&lt;p&gt;For the "today" summary, we need to account for timezones. Node.js's &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; makes this straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;todayYmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Asia/Tokyo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-CA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;numeric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2-digit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2-digit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;formatToParts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;year&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;month&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;day&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the &lt;code&gt;en-CA&lt;/code&gt; locale automatically gives us &lt;code&gt;YYYY-MM-DD&lt;/code&gt; formatting—a nice little trick.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Symlink Support for npx Execution
&lt;/h3&gt;

&lt;p&gt;When running via &lt;code&gt;npx&lt;/code&gt;, the entry point is often invoked through a symlink. To handle this, we use &lt;code&gt;realpath&lt;/code&gt; to resolve the actual file path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldRunMain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;importMetaUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;argv1&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;argv1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;argvUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pathToFileURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;argv1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;importMetaUrl&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;argvUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;argvRealUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pathToFileURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;realpathSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;argv1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;importMetaRealPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;realpathSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fileURLToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;importMetaUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;importMetaRealUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pathToFileURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;importMetaRealPath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;importMetaRealUrl&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;argvRealUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, &lt;code&gt;npx github:geeknees/wakatime-mcp&lt;/code&gt; wouldn't trigger the main function. Small detail, but crucial.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Type-Safe Validation with Zod
&lt;/h3&gt;

&lt;p&gt;Input validation uses Zod for runtime type checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SummariesInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SummariesInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you both runtime validation and TypeScript type inference—great developer experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Usage
&lt;/h2&gt;

&lt;p&gt;Here's how it works in practice for automated retrospectives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Daily Retrospectives
&lt;/h3&gt;

&lt;p&gt;Combining GitHub commit history with WakaTime coding time provides much more accurate daily reflections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Agent: Tell me about your work today

User: Create today's retrospective

AI Agent: (executes wakatime_today)
You coded for 5 hours 23 minutes today:
- TypeScript: 3h 45m
- Markdown: 1h 15m
- JSON: 23m

Main project was wakatime-mcp, and looking at your GitHub commits,
you were working on MCP implementation and documentation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Weekly Reports
&lt;/h3&gt;

&lt;p&gt;It's also useful for weekly work analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: Generate last week's work summary

AI Agent: (executes wakatime_summaries)
Work time for January 13-19, 2025:
Total coding: 28h 15m

By project:
- wakatime-mcp: 12h 30m (new development)
- my-app: 8h 45m (feature additions)
- docs: 7h 00m (documentation)

By language:
- TypeScript: 18h 20m
- Python: 6h 30m
- Markdown: 3h 25m

The data clearly shows you focused on new MCP server
development last week.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Integration with Custom Skills
&lt;/h3&gt;

&lt;p&gt;When combined with custom skills like Obsidian Daily Summary, you can generate comprehensive retrospectives that include WakaTime data alongside GitHub activity and meeting notes, providing deeper insights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building an MCP server is simpler than you might think. With &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;, you can create something functional in just a few hours. The key points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Understand the protocol&lt;/strong&gt;: Once you grasp MCP basics, the SDK handles the heavy lifting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leverage existing APIs&lt;/strong&gt;: Most services provide APIs—just expose them via MCP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical use cases&lt;/strong&gt;: AI Agent integration opens up new possibilities for data utilization&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;wakatime-mcp&lt;/code&gt; is publicly available as a general-purpose tool for any WakaTime user. Give it a try!&lt;/p&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/geeknees/wakatime-mcp" rel="noopener noreferrer"&gt;https://github.com/geeknees/wakatime-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;WakaTime API Documentation: &lt;a href="https://wakatime.com/developers" rel="noopener noreferrer"&gt;https://wakatime.com/developers&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Why not build an MCP server for a service you use? I hope this article helps you get started.&lt;/p&gt;

&lt;p&gt;🇯🇵 This article is also available in Japanese:&lt;br&gt;
&lt;a href="https://zenn.dev/geeknees/articles/cee94e3708f940" rel="noopener noreferrer"&gt;https://zenn.dev/geeknees/articles/cee94e3708f940&lt;/a&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>wakatime</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Building a GitHub Copilot CLI Extension - Implementing WakaTime Integration</title>
      <dc:creator>Masumi Kawasaki 💭</dc:creator>
      <pubDate>Wed, 21 Jan 2026 06:00:07 +0000</pubDate>
      <link>https://dev.to/geeknees/building-a-github-copilot-cli-extension-implementing-wakatime-integration-5c0a</link>
      <guid>https://dev.to/geeknees/building-a-github-copilot-cli-extension-implementing-wakatime-integration-5c0a</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;GitHub Copilot CLI is a powerful tool that enables AI pair programming in your terminal. Did you know that Copilot CLI recently added a custom hooks feature for extensibility?&lt;/p&gt;

&lt;p&gt;Using this feature, I created a library called &lt;a href="https://www.npmjs.com/package/@geeknees/copilot-cli-wakatime" rel="noopener noreferrer"&gt;&lt;code&gt;@geeknees/copilot-cli-wakatime&lt;/code&gt;&lt;/a&gt; that automatically tracks Copilot CLI usage time in WakaTime. In this article, I'll share the implementation details and how to build Copilot CLI extensions.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;A tool that automatically sends Copilot CLI session activity to WakaTime, visualizing your AI coding time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Hooks into Copilot CLI session lifecycle events (start, tool use, end)&lt;/li&gt;
&lt;li&gt;Sends heartbeats with 60-second rate limiting (per project)&lt;/li&gt;
&lt;li&gt;Cross-platform support (macOS, Linux, Windows)&lt;/li&gt;
&lt;li&gt;Zero-config after setup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Copilot CLI Hook System
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How Hooks Work
&lt;/h3&gt;

&lt;p&gt;Copilot CLI provides a "hook system" that can execute external commands at specific points in a session. Hook configurations are placed in &lt;code&gt;.github/hooks/&amp;lt;hook-name&amp;gt;.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Available events:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sessionStart&lt;/code&gt;: When a session begins&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;postToolUse&lt;/code&gt;: After each tool execution&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sessionEnd&lt;/code&gt;: When a session ends&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hook Configuration Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sessionStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"copilot-cli-wakatime hook sessionStart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"timeoutSec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"copilot-cli-wakatime hook postToolUse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"timeoutSec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Receiving Payloads
&lt;/h3&gt;

&lt;p&gt;Copilot CLI passes a JSON payload to hook commands via standard input (stdin).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/Users/username/project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"toolName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;The library consists of three main components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt; (&lt;code&gt;cli.ts&lt;/code&gt;): Entry point&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hook Handler&lt;/strong&gt; (&lt;code&gt;hook.ts&lt;/code&gt;): Event processing and WakaTime integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utilities&lt;/strong&gt; (&lt;code&gt;util.ts&lt;/code&gt;): Rate limiting, Git detection, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1. CLI Entry Point
&lt;/h3&gt;

&lt;p&gt;A simple command router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runHook&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./hook.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runInit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./init.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;init&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runInit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--force&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--debug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runHook&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Usage:
  copilot-cli-wakatime init [--force]
  copilot-cli-wakatime hook &amp;lt;event&amp;gt; [--debug]`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Hook Handler
&lt;/h3&gt;

&lt;p&gt;The hook handler performs the following operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Opt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Read payload from stdin&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readStdin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Prevent self-recursion (skip WakaTime tool calls)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;toolName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wakatime-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Detect Git repository root&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repoRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRepoRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Create virtual entity file&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.copilot-cli.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;ensureFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Check rate limit (60-second interval)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shouldRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 6. Send heartbeat to WakaTime CLI&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;homeCfgPath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wakatime-cli&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--entity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--entity-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--project&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;copilot-cli-wakatime/0.1.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 7. Debug output (optional)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Don't break hooks on failure&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Key Implementation Points
&lt;/h3&gt;

&lt;h4&gt;
  
  
  a. Reading stdin
&lt;/h4&gt;

&lt;p&gt;Node.js stdin is a stream, so we promisify it to read all data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readStdin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEncoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  b. Git Repository Detection
&lt;/h4&gt;

&lt;p&gt;Get the Git repository root from the working directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRepoRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;git&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rev-parse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--show-toplevel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  c. Rate Limiting Implementation
&lt;/h4&gt;

&lt;p&gt;Save the last send timestamp to a file per project and prevent duplicate sends within 60 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stateDir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;copilot-wakatime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Following the XDG Base Directory specification, state files are stored in &lt;code&gt;~/.local/state/copilot-wakatime/&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  d. Virtual Entity File
&lt;/h4&gt;

&lt;p&gt;WakaTime requires an actual file path, so we create a virtual file &lt;code&gt;.copilot-cli.ts&lt;/code&gt; at the repository root. Using the &lt;code&gt;.ts&lt;/code&gt; extension ensures the language is properly recognized in the WakaTime dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.copilot-cli.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;ensureFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  e. Error Handling
&lt;/h4&gt;

&lt;p&gt;To prevent hook execution failures from breaking the entire Copilot CLI session, errors are handled silently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't break hooks on failure&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detailed output is only shown in debug mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Init Command
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;init&lt;/code&gt; command creates &lt;code&gt;.github/hooks/wakatime.json&lt;/code&gt; in the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runInit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hooksPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hooks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wakatime.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hooksPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;force&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Already exists: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hooksPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use --force to overwrite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hooksPath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hooksPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hookConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Created: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hooksPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing Strategy
&lt;/h2&gt;

&lt;p&gt;Using Node.js 20+'s built-in test runner (&lt;code&gt;node:test&lt;/code&gt;) with TypeScript loader (&lt;code&gt;tsx&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node --test --import tsx test/**/*.test.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:watch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node --test --watch --import tsx test/**/*.test.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:assert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shouldRateLimit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/util.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shouldRateLimit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allows first call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`test-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shouldRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocks second call within time window&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`test-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;shouldRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// First call&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strictEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shouldRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Should block&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Importance of Shebang
&lt;/h3&gt;

&lt;p&gt;To work as a CLI tool, a shebang is required at the top of &lt;code&gt;cli.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the command specified in &lt;code&gt;package.json&lt;/code&gt;'s &lt;code&gt;bin&lt;/code&gt; field executable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. ES Module Configuration
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;"type": "module"&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;, and use &lt;code&gt;.js&lt;/code&gt; extensions in import paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runHook&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./hook.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// .js, not .ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript doesn't transform extensions during transpilation, so specify the output filename (&lt;code&gt;.js&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. stdin Reading Timing
&lt;/h3&gt;

&lt;p&gt;If you don't start reading stdin immediately when the hook is called, Copilot CLI might hang.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. WakaTime CLI Dependency
&lt;/h3&gt;

&lt;p&gt;It assumes users have &lt;code&gt;wakatime-cli&lt;/code&gt; installed. I documented installation instructions in the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Copilot CLI Hook System is Flexible
&lt;/h3&gt;

&lt;p&gt;You can intervene at any stage of the session lifecycle, supporting various use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logging (this implementation)&lt;/li&gt;
&lt;li&gt;Security checks&lt;/li&gt;
&lt;li&gt;Automated deployment triggers&lt;/li&gt;
&lt;li&gt;Custom notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Best Practices for CLI Development with TypeScript
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use ES modules&lt;/li&gt;
&lt;li&gt;Zero-dependency testing with built-in test runner&lt;/li&gt;
&lt;li&gt;Improve dev experience with &lt;code&gt;tsx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Easy CLI tooling with shebang + &lt;code&gt;bin&lt;/code&gt; field&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Importance of Rate Limiting
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;postToolUse&lt;/code&gt; hook is called frequently, so without rate limiting, you'd overwhelm the API. Simple file-based rate limiting was sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Using the Copilot CLI hook system, I implemented WakaTime integration with just ~200 lines of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hook configuration in &lt;code&gt;.github/hooks/*.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Receive payload from stdin&lt;/li&gt;
&lt;li&gt;Proper error handling so hooks don't break sessions&lt;/li&gt;
&lt;li&gt;Control API calls with rate limiting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding this mechanism enables you to create various extensions. Why not build your own Copilot CLI extension?&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/geeknees/copilot-cli-wakatime" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@geeknees/copilot-cli-wakatime" rel="noopener noreferrer"&gt;npm Package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://githubnext.com/projects/copilot-cli" rel="noopener noreferrer"&gt;Copilot CLI Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wakatime.com" rel="noopener noreferrer"&gt;WakaTime&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🇯🇵 This article is also available in Japanese:&lt;br&gt;
&lt;a href="https://zenn.dev/geeknees/articles/d368adea0519fc" rel="noopener noreferrer"&gt;https://zenn.dev/geeknees/articles/d368adea0519fc&lt;/a&gt;&lt;/p&gt;

</description>
      <category>githubcopilot</category>
      <category>node</category>
      <category>typescript</category>
      <category>wakatime</category>
    </item>
    <item>
      <title>Ruby::Box Shadow Execution for Rack: Observing “Shadow Behavior” Without Changing the Production Response</title>
      <dc:creator>Masumi Kawasaki 💭</dc:creator>
      <pubDate>Mon, 29 Dec 2025 06:42:03 +0000</pubDate>
      <link>https://dev.to/geeknees/rubybox-shadow-execution-for-rack-observing-shadow-behavior-without-changing-the-production-3g49</link>
      <guid>https://dev.to/geeknees/rubybox-shadow-execution-for-rack-observing-shadow-behavior-without-changing-the-production-3g49</guid>
      <description>&lt;p&gt;Ruby 4 introduced &lt;strong&gt;Ruby::Box&lt;/strong&gt;, and I built a minimal setup to run &lt;strong&gt;shadow execution&lt;/strong&gt; against a Rack app—meaning the &lt;em&gt;same request&lt;/em&gt; is evaluated a second time “behind the scenes” with an alternate implementation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real (primary path):&lt;/strong&gt; run the production app normally and &lt;strong&gt;return the real response as-is&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow (secondary path):&lt;/strong&gt; run alternate logic inside &lt;strong&gt;Ruby::Box&lt;/strong&gt; using the same input and &lt;strong&gt;log only the diffs&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;In my “shadow universe,” I intentionally play with rules like “Y2K time,” “fixed &lt;code&gt;rand&lt;/code&gt;,” “gyaru-style I18n,” and “&lt;code&gt;/coffee&lt;/code&gt; returns 418” (just for visibility)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Ruby::Box docs: &lt;a href="https://docs.ruby-lang.org/en/master/Ruby/Box.html" rel="noopener noreferrer"&gt;https://docs.ruby-lang.org/en/master/Ruby/Box.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/geeknees/ruby_box_shadow_universe" rel="noopener noreferrer"&gt;https://github.com/geeknees/ruby_box_shadow_universe&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Background: Why run shadow execution?
&lt;/h2&gt;

&lt;p&gt;When you change production behavior, there’s always a lingering fear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did I break compatibility (status / headers / body)?&lt;/li&gt;
&lt;li&gt;Did exceptions or latency increase?&lt;/li&gt;
&lt;li&gt;Does a bug only trigger on specific paths?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal of shadow execution is to evaluate “new logic” with the same inputs &lt;strong&gt;without changing the production response&lt;/strong&gt;, and to create a state where you can observe differences safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Ruby::Box (vs “just extracting code”)?
&lt;/h2&gt;

&lt;p&gt;Shadow execution itself can be done by extracting logic into another class.&lt;br&gt;
In practice, though, once you start experimenting on the shadow side, you often want to do things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apply monkey patches only on the shadow side (Time/Random/I18n/HTTP, etc.)&lt;/li&gt;
&lt;li&gt;Allow “risky dependencies” or “experimental code” only in shadow&lt;/li&gt;
&lt;li&gt;Try behavior/version differences without polluting the main app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is that within a single process, &lt;strong&gt;constants, autoload, top-level definitions, and monkey patches&lt;/strong&gt; can easily leak and contaminate the main world.&lt;/p&gt;

&lt;p&gt;Ruby::Box provides a model of separation “per box” (you &lt;code&gt;require/load&lt;/code&gt; files inside the box so their definitions live in that world).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Whether it’s “okay” to overwrite &lt;code&gt;Time.now&lt;/code&gt; or &lt;code&gt;rand&lt;/code&gt; in the first place is a separate discussion 😉&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  High-level architecture
&lt;/h2&gt;

&lt;p&gt;A Rack middleware keeps the &lt;strong&gt;Real → Response&lt;/strong&gt; path intact, runs Shadow asynchronously, and logs diffs.&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%2F0taabi24gr2u6x2by6e9.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%2F0taabi24gr2u6x2by6e9.png" alt="Flow" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How to run it
&lt;/h2&gt;

&lt;p&gt;Ruby::Box needs to be enabled at startup via an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install
&lt;/span&gt;&lt;span class="nv"&gt;RUBY_BOX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rackup &lt;span class="nt"&gt;-p&lt;/span&gt; 9292
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation (key points)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Rack middleware: return Real, run Shadow in the background
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Return the result of &lt;code&gt;@app.call(env)&lt;/code&gt; as-is&lt;/li&gt;
&lt;li&gt;On the shadow side, load &lt;code&gt;shadow_logic.rb&lt;/code&gt; into a &lt;code&gt;Ruby::Box&lt;/code&gt;, then call &lt;code&gt;ShadowLogic.call&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Compare the “shadow response” vs the “real response” and log diffs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For readability, diff collection is extracted into a small helper (status / content-type / body bytes, etc.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;
  &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference:&lt;br&gt;
&lt;a href="https://github.com/geeknees/ruby_box_shadow_universe/blob/main/shadow_box_middleware.rb#L87" rel="noopener noreferrer"&gt;https://github.com/geeknees/ruby_box_shadow_universe/blob/main/shadow_box_middleware.rb#L87&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2) Shadow logic: keep “shadow world” definitions inside a file
&lt;/h3&gt;

&lt;p&gt;What you do in shadow is up to you, but common “experiment” patterns include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trying transforms / corrections / validations only in shadow&lt;/li&gt;
&lt;li&gt;Adding extra observability data only in shadow&lt;/li&gt;
&lt;li&gt;Applying alternate rules only in shadow (e.g. certain paths return 418)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this repo, I include an intentionally obvious example: changing Time/Random/I18n rules &lt;strong&gt;only in shadow&lt;/strong&gt;, so differences are easy to observe.&lt;/p&gt;
&lt;h2&gt;
  
  
  Example logs: diffs only
&lt;/h2&gt;

&lt;p&gt;Shadow results are not returned to the client. Only differences are logged (which is easier to treat as observability).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;127.0.0.1 - - [29/Dec/2025:15:23:27 +0900] "GET /hello HTTP/1.1" 200 - 0.0029
[shadow_box] 🌚 alternate universe detected: x-shadow-universe: nil -&amp;gt; "Y2K+RAND2+GYARU", body(bytes): 11 -&amp;gt; 123
[shadow_box] x-shadow-universe Y2K+RAND2+GYARU
[shadow_box] outside: Time.now=2025-12-29T15:23:27+09:00 rand=13
[shadow_box] inside : (computed per-request in alt_body)
[shadow_box] --- shadow report ---
req: GET /hello
at:  1999-12-31T23:59:59+09:00
rand: 2
say: こんちわ〜⭐️
original bytes: 11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference:&lt;br&gt;
&lt;a href="https://github.com/geeknees/ruby_box_shadow_universe/blob/main/shadow_box_middleware.rb#L79" rel="noopener noreferrer"&gt;https://github.com/geeknees/ruby_box_shadow_universe/blob/main/shadow_box_middleware.rb#L79&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Operational notes / caveats
&lt;/h2&gt;
&lt;h3&gt;
  
  
  0) It’s still experimental
&lt;/h3&gt;

&lt;p&gt;At runtime you’ll see a warning like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;warning: Ruby::Box is experimental, and the behavior may change in the future!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1) Ruby::Box is not a sandbox
&lt;/h3&gt;

&lt;p&gt;Ruby::Box is &lt;strong&gt;not&lt;/strong&gt; OS-level isolation.&lt;br&gt;
It won’t prevent external I/O (network, files, processes). For safety, shadow logic should be designed to be &lt;strong&gt;side-effect free&lt;/strong&gt; whenever possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Shadow execution adds cost
&lt;/h3&gt;

&lt;p&gt;It increases per-request work. In practice, shadow is often used with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sampling (only a percentage of requests)&lt;/li&gt;
&lt;li&gt;Path-based targeting&lt;/li&gt;
&lt;li&gt;Time limits (timeouts)&lt;/li&gt;
&lt;li&gt;Async execution (threads, job queue, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Rack middleware can provide a solid base for shadow execution&lt;/li&gt;
&lt;li&gt;Ruby::Box makes it easier to define/override behavior &lt;em&gt;only in the shadow world&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Because you keep the production response unchanged, you can introduce changes progressively while observing diffs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/geeknees/ruby_box_shadow_universe" rel="noopener noreferrer"&gt;https://github.com/geeknees/ruby_box_shadow_universe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🇯🇵 This article is also available in Japanese:&lt;br&gt;
&lt;a href="https://zenn.dev/geeknees/articles/1b882864b0f10c" rel="noopener noreferrer"&gt;https://zenn.dev/geeknees/articles/1b882864b0f10c&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
    </item>
    <item>
      <title>SendGrid Email Logs Are Quietly Useful — So I’m Writing About Them</title>
      <dc:creator>Masumi Kawasaki 💭</dc:creator>
      <pubDate>Wed, 24 Dec 2025 08:40:39 +0000</pubDate>
      <link>https://dev.to/geeknees/sendgrid-email-logs-are-quietly-useful-so-im-writing-about-them-12ci</link>
      <guid>https://dev.to/geeknees/sendgrid-email-logs-are-quietly-useful-so-im-writing-about-them-12ci</guid>
      <description>&lt;p&gt;When you implement email delivery in a web application, you inevitably run into questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was this email actually sent?&lt;/li&gt;
&lt;li&gt;Why didn’t a specific user receive it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These investigations come up on a regular basis.&lt;/p&gt;

&lt;p&gt;In some cases, delivery stats in the admin panel are enough.&lt;br&gt;
But tracing details such as &lt;strong&gt;when an email was sent&lt;/strong&gt;, &lt;strong&gt;what state it ended up in&lt;/strong&gt;, or &lt;strong&gt;whether it was bounced or opened&lt;/strong&gt; often turns into a surprisingly tedious task.&lt;/p&gt;

&lt;p&gt;Recently, I noticed that SendGrid has added a feature called &lt;strong&gt;Email Logs&lt;/strong&gt;&lt;br&gt;
(I’m not sure exactly when it was released).&lt;/p&gt;

&lt;p&gt;Since I haven’t seen many articles covering it, this post focuses on &lt;strong&gt;how Email Logs are useful in real-world operations&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Email Logs?
&lt;/h2&gt;

&lt;p&gt;Email Logs let you inspect &lt;strong&gt;individual emails&lt;/strong&gt; sent via SendGrid and see their status directly in the dashboard, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sent&lt;/li&gt;
&lt;li&gt;Delivered&lt;/li&gt;
&lt;li&gt;Bounced&lt;/li&gt;
&lt;li&gt;Dropped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike &lt;strong&gt;Stats&lt;/strong&gt;, which are meant for aggregated data, &lt;strong&gt;Email Logs allow you to trace the history of a single email&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Difference from Activity Feed
&lt;/h3&gt;

&lt;p&gt;SendGrid already has a similar feature called &lt;strong&gt;Activity Feed&lt;/strong&gt;, but it comes with limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On the free plan, history is limited to 3 or 7 days&lt;/li&gt;
&lt;li&gt;Viewing the last 30 days requires a paid plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Email Logs, although there is a limit of &lt;strong&gt;up to 1,000 entries&lt;/strong&gt;, you can view a &lt;strong&gt;list of emails sent within the last 30 days&lt;/strong&gt; directly in the dashboard.&lt;/p&gt;

&lt;p&gt;Here’s a natural English translation that fits well as a short &lt;strong&gt;“Note” / “Additional context”&lt;/strong&gt; section on Dev.to:&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
On Twilio’s official documentation, there is no mention of a 1,000-entry limit for Email Logs.&lt;br&gt;
However, a blog post by &lt;strong&gt;KKE (Kozo Keikaku Engineering)&lt;/strong&gt;, SendGrid’s official reseller in Japan, does mention a limit of 1,000 entries.&lt;/p&gt;

&lt;p&gt;References:&lt;br&gt;
&lt;a href="https://www.twilio.com/docs/sendgrid/ui/analytics-and-reporting/email-logs" rel="noopener noreferrer"&gt;https://www.twilio.com/docs/sendgrid/ui/analytics-and-reporting/email-logs&lt;/a&gt;&lt;br&gt;
&lt;a href="https://sendgrid.kke.co.jp/blog/?p=19696" rel="noopener noreferrer"&gt;https://sendgrid.kke.co.jp/blog/?p=19696&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why This Is Useful
&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%2Ffvh4u3p6oaihz1lmrmyd.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%2Ffvh4u3p6oaihz1lmrmyd.png" alt="Dashboard" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Email Logs can be filtered using conditions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recipient email address&lt;/li&gt;
&lt;li&gt;Message ID&lt;/li&gt;
&lt;li&gt;Status (Delivered / Bounced, etc.)&lt;/li&gt;
&lt;li&gt;Time range&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it possible to find:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“That email we sent to this user yesterday”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;in just a few seconds.&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%2F07k2jhu0jc52ijpga8o0.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%2F07k2jhu0jc52ijpga8o0.png" alt="Detail" width="800" height="754"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For common questions during support or internal checks —&lt;br&gt;
&lt;em&gt;“Was that email really sent?”&lt;/em&gt; — Email Logs let you answer almost immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Email Logs aren’t a flashy new feature, but they significantly reduce friction in day-to-day operations, especially for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial investigation during incidents&lt;/li&gt;
&lt;li&gt;Quick checks between customer support and engineers&lt;/li&gt;
&lt;li&gt;Verifying email delivery in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re using SendGrid, this feature is well worth a look.&lt;/p&gt;

&lt;p&gt;🇯🇵 This article is also available in Japanese:&lt;br&gt;
&lt;a href="https://zenn.dev/geeknees/articles/5bac00fd7b87bd" rel="noopener noreferrer"&gt;https://zenn.dev/geeknees/articles/5bac00fd7b87bd&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sendgrid</category>
    </item>
    <item>
      <title>Recommended Series for Small Rails Development Teams: Error Tracking - Honeybadger</title>
      <dc:creator>Masumi Kawasaki 💭</dc:creator>
      <pubDate>Thu, 05 Sep 2024 14:26:22 +0000</pubDate>
      <link>https://dev.to/geeknees/recommended-series-for-small-rails-development-teams-error-tracking-honeybadger-5f7a</link>
      <guid>https://dev.to/geeknees/recommended-series-for-small-rails-development-teams-error-tracking-honeybadger-5f7a</guid>
      <description>&lt;p&gt;Hello, Rails engineers! Today, I’d like to recommend Honeybadger as an error-tracking tool for small development teams working on Rails projects. Honeybadger offers ease of use and a powerful feature set that can significantly improve your development efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Choose Honeybadger?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Simple Setup&lt;/strong&gt;: Easy integration into Rails projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-Time Alerts&lt;/strong&gt;: Instantly notify you of critical errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detailed Error Information&lt;/strong&gt;: Provides rich data like stack traces, environment variables, and parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance Monitoring&lt;/strong&gt;: Track application performance alongside error tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uptime Monitoring&lt;/strong&gt;: Keep an eye on your app’s availability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron Job Monitoring&lt;/strong&gt;: Ensure cron jobs are running smoothly.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setup Instructions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add this to your Gemfile:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'honeybadger'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 5.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run the bundle install command:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Execute the Honeybadger setup script:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle exec honeybadger install HONEYBADGER_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s all you need for basic setup!&lt;/p&gt;

&lt;h2&gt;
  
  
  Effective Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Custom Error Notifications
&lt;/h3&gt;

&lt;p&gt;You can set up custom notifications for specific conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="c1"&gt;# Some processing&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="no"&gt;Honeybadger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;error_class: &lt;/span&gt;&lt;span class="s2"&gt;"CustomError"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;error_message: &lt;/span&gt;&lt;span class="s2"&gt;"A critical error occurred"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;user_id: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"important_action"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adding Context
&lt;/h3&gt;

&lt;p&gt;Adding user or request data makes it easier to identify error causes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_honeybadger_context&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_honeybadger_context&lt;/span&gt;
    &lt;span class="no"&gt;Honeybadger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;user_id: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;user_email: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;plan: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plan&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Real-Time Alerts
&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%2Fn2vx12rk52bjvdm38y5y.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%2Fn2vx12rk52bjvdm38y5y.png" alt="Error Tracker" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can configure notifications to send alerts via email, Slack, or other tools. Honeybadger integrates well with platforms like GitHub, automatically creating issues for errors. Once you fix the error and deploy the change, Honeybadger will automatically mark the issue as resolved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Monitoring
&lt;/h3&gt;

&lt;p&gt;A notable feature is &lt;strong&gt;Insights&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;insights&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By enabling this, you can monitor performance data and query it using &lt;strong&gt;BadgerQL&lt;/strong&gt;, similar to a log management tool like Papertrail. However, log data fees can be high, so be mindful of this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;event_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sql.active_record'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since SQL logs can quickly consume space, disabling them can effectively save on log data costs.&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%2Fjxhl8evgvz7az7i9m5tp.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%2Fjxhl8evgvz7az7i9m5tp.png" alt="Performance Dashboard" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using this information, you can create an APM-like dashboard to monitor performance trends.&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%2F0r7kfoy3odmfqian0367.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%2F0r7kfoy3odmfqian0367.png" alt="APM like" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Uptime Monitoring
&lt;/h3&gt;

&lt;p&gt;Though a small feature, Honeybadger includes uptime monitoring. Our team migrated from Pingdom to Honeybadger's Uptime feature, simplifying our monitoring stack and reducing costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cron Job Monitoring
&lt;/h3&gt;

&lt;p&gt;We previously used Dead Man’s Snitch for cron job monitoring, but Honeybadger’s &lt;strong&gt;Check-Ins&lt;/strong&gt; feature looks promising, and we’re considering migrating to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment Tracking
&lt;/h3&gt;

&lt;p&gt;By running this command during deployment, you can track deployments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;honeybadger deploy --environment=production --revision=$(git rev-parse HEAD) --repository=git@github.com:your/repo.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Honeybadger is an excellent tool for small Rails teams. Its simple setup, rich monitoring features, and ease of use significantly improve development workflows. It helps detect and resolve errors early, contributing to application stability.&lt;/p&gt;

&lt;p&gt;Our team previously used the open-source error tracker &lt;strong&gt;Errbit&lt;/strong&gt;, which worked well for catching errors and sending notifications. However, we switched to Honeybadger when Errbit's Ruby version became outdated. While we still contribute to Errbit, Honeybadger provides more stable application operations, leading to our decision to adopt it.&lt;/p&gt;

&lt;p&gt;🇯🇵 This article is also available in Japanese:&lt;br&gt;
&lt;a href="https://zenn.dev/geeknees/articles/bd3a58bb4a758e" rel="noopener noreferrer"&gt;https://zenn.dev/geeknees/articles/bd3a58bb4a758e&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
    </item>
    <item>
      <title>Recommended Series for Small Rails Development Teams: APM - Skylight</title>
      <dc:creator>Masumi Kawasaki 💭</dc:creator>
      <pubDate>Thu, 05 Sep 2024 13:45:42 +0000</pubDate>
      <link>https://dev.to/geeknees/recommended-series-for-small-rails-development-teams-apm-skylight-o87</link>
      <guid>https://dev.to/geeknees/recommended-series-for-small-rails-development-teams-apm-skylight-o87</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hello to all small teams developing web apps using Rails! We often seek information to improve development, but there seems to be little targeted towards small teams. Hence, I decided to write a series recommending development tools for small Rails teams.&lt;/p&gt;

&lt;p&gt;For the first part, let's focus on APM (Application Performance Monitoring) and introduce "Skylight," a relatively new tool that could be an excellent choice for small Rails teams. I’ll explain why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Skylight?
&lt;/h2&gt;

&lt;p&gt;Skylight is an APM tool for monitoring and analyzing web app performance, specifically optimized for Ruby on Rails. It features an easy setup and an intuitive interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Skylight is Recommended for Small Teams
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Simple Setup
&lt;/h3&gt;

&lt;p&gt;Small teams often can’t afford to spend much time on complex configurations or infrastructure setup. Skylight has an incredibly simple setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add to Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'skylight'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run these commands&lt;/span&gt;
bundle &lt;span class="nb"&gt;install
&lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;skylight setup &amp;lt;setup token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it—basic monitoring begins instantly. It requires almost no additional settings after setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Low Cost
&lt;/h3&gt;

&lt;p&gt;Cost is always a key factor for small teams. Skylight is much more affordable than other major APM tools, with free plans available for small projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note: Skylight currently does not have a method to limit usage within the free tier. If your data transmission exceeds the free tier limit, charges will be applied automatically. If you want to remain within the free plan, please monitor your request volume to ensure it stays under the free tier limit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5nlyxedmvcpeob8r05lz.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%2F5nlyxedmvcpeob8r05lz.png" alt="Image Skylight Price" width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my company, our business app runs within a budget of just a few dozen dollars. For hobby apps, the free plan may suffice.&lt;/p&gt;

&lt;p&gt;When compared with Datadog, a widely used APM in large companies, Skylight's cost advantage becomes clear. For instance, Datadog costs $18.75 per host per month, and with multiple web servers, the costs increase accordingly. Logs also add costs. In contrast, Skylight can operate at around $20–40.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rails-Specific Display Features
&lt;/h3&gt;

&lt;p&gt;Skylight is tailored for Rails applications. For example, it provides excellent ActiveRecord query analysis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Sample query&lt;/span&gt;
&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:posts&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Skylight automatically provides the following information:&lt;/span&gt;
&lt;span class="c1"&gt;# - Query execution time&lt;/span&gt;
&lt;span class="c1"&gt;# - Index usage&lt;/span&gt;
&lt;span class="c1"&gt;# - Detection of N+1 problems&lt;/span&gt;
&lt;span class="c1"&gt;# - Optimization suggestions (e.g., counter cache usage)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This info is visually presented on the dashboard, making it easy to spot performance bottlenecks quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Intuitive UI
&lt;/h3&gt;

&lt;p&gt;Not everyone in a small team needs to be a performance expert. Skylight's intuitive UI allows all team members to understand and utilize performance data, regardless of their technical expertise.&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%2Fw4vceattzjiamdu6eoc5.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%2Fw4vceattzjiamdu6eoc5.png" alt="Image Skylight UI" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In comparison, while New Relic is excellent for power users, Skylight’s simplicity makes it more accessible for small teams. There’s no need to explain how to use it—just log in and see slow requests at a glance. &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%2Fjhikdsfehhuqd5m4eb47.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%2Fjhikdsfehhuqd5m4eb47.png" alt="Image New Relic UI" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;For small Rails development teams, Skylight is an attractive APM tool. Its easy setup, low cost, Rails-focused features, and intuitive UI offer many advantages.&lt;/p&gt;

&lt;p&gt;Performance monitoring is essential to maintain app quality and improve user experience. Skylight allows small teams to efficiently monitor and optimize performance.&lt;/p&gt;

&lt;p&gt;An interesting note: when I contacted Skylight, I learned that they are also a small team, which may explain their reasonable pricing. When small teams build tools, they often fit the needs of other small teams quite well.&lt;/p&gt;

&lt;p&gt;Finally, here’s my referral link:&lt;br&gt;&lt;br&gt;
&lt;a href="https://www.skylight.io/r/egzcaZi6HRcn" rel="noopener noreferrer"&gt;https://www.skylight.io/r/egzcaZi6HRcn&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🇯🇵 This article is also available in Japanese:&lt;br&gt;
&lt;a href="https://zenn.dev/geeknees/articles/3545755e8b2210" rel="noopener noreferrer"&gt;https://zenn.dev/geeknees/articles/3545755e8b2210&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>apm</category>
    </item>
    <item>
      <title>How to Write Tests for GraphQL Resolvers in RSpec</title>
      <dc:creator>Masumi Kawasaki 💭</dc:creator>
      <pubDate>Sat, 27 Jul 2024 16:52:31 +0000</pubDate>
      <link>https://dev.to/geeknees/how-to-write-tests-for-graphql-resolvers-in-rspec-52jo</link>
      <guid>https://dev.to/geeknees/how-to-write-tests-for-graphql-resolvers-in-rspec-52jo</guid>
      <description>&lt;p&gt;Before, we wrote resolvers and their corresponding tests in the following manner (with slight modifications to the code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Resolvers&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Users&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Resolvers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
    &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="no"&gt;ObjectTypes&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;

    &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="s1"&gt;'Find users by roles. You can pass XXX_role or [XXX_role, YYY_role].'&lt;/span&gt;

    &lt;span class="n"&gt;argument&lt;/span&gt; &lt;span class="ss"&gt;:role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;EnumTypes&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'evaluation_role'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluation_users&lt;/span&gt;
      &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'level_aptitude_role'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;level_aptitude_users&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rails_helper'&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Resolvers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Users&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'#resolve'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'when role includes evaluation_role'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'returns evaluation test users filtered by service'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;user1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s1"&gt;'evaluation_role'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;user2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s1"&gt;'evaluation_role'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:evaluation_test_users&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;and_return&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;user1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;described_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;object: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;field: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'evaluation_role'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;contain_exactly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recent Issue with Resolver&lt;/p&gt;

&lt;p&gt;After upgrading graphql-ruby from version 2.3.7 to 2.3.10, I encountered the error undefined method 'types' for nil:NilClass. This change is due to modifications in the library, and upon raising an issue and contacting the author, I was informed that resolvers are not supported outside of GraphQL queries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/rmosolgo/graphql-ruby/commit/db7a9bcaceb77dc0af78b73e0d5c2ad849c27598#diff-fdbf285d34242cb3cb7b1e56e80f5d39bc600246e72b7bb32190e555432f156cR39" rel="noopener noreferrer"&gt;https://github.com/rmosolgo/graphql-ruby/commit/db7a9bcaceb77dc0af78b73e0d5c2ad849c27598#diff-fdbf285d34242cb3cb7b1e56e80f5d39bc600246e72b7bb32190e555432f156cR39&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The author suggested the following alternatives:&lt;br&gt;
1.Using the run_graphql_field Helper:&lt;br&gt;
This method provides a complete GraphQL context.&lt;br&gt;
More details can be found here.&lt;br&gt;
&lt;a href="https://graphql-ruby.org/testing/helpers.html" rel="noopener noreferrer"&gt;https://graphql-ruby.org/testing/helpers.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2.Creating a Dummy Query::Context Instance:&lt;br&gt;
Instead of passing context: nil, create a dummy instance.&lt;br&gt;
Though not part of the public API, this workaround should work for now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;query_ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;GraphQL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;MySchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"{ __typename }"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Time I Used the First Method, But Here Are Sample Codes for Both&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rails_helper'&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Resolvers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :graphql&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'#resolve'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;run_graphql_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Query.users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;arguments: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s1"&gt;'evaluation_role'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s1"&gt;'evaluation_role'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'when role includes evaluation_role'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'evaluation_role'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rails_helper'&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Resolvers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Users&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'#resolve'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:query_ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;GraphQL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TofflerSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"{ __typename }"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:resolver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;described_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;object: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="n"&gt;query_ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;field: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s1"&gt;'evaluation_role'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s1"&gt;'evaluation_role'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'when role includes evaluation_role'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'evaluation_role'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have configured the helper as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_options&lt;/span&gt; &lt;span class="ss"&gt;type: :graphql&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;graphql_config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;graphql_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include&lt;/span&gt; &lt;span class="no"&gt;GraphQL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Testing&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;MySchema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have taken the first method of using &lt;code&gt;run_graphql_field&lt;/code&gt; this time. Using a dummy Query::Context instance is better as it requires less changes to the existing code base, but I decided it was better to use &lt;code&gt;run_graphql_field&lt;/code&gt; for future maintenance as it is not a public API.&lt;/p&gt;

&lt;p&gt;🇯🇵 This article is also available in Japanese:&lt;br&gt;
&lt;a href="https://zenn.dev/geeknees/articles/842f04ee5d48a3" rel="noopener noreferrer"&gt;https://zenn.dev/geeknees/articles/842f04ee5d48a3&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>graphql</category>
      <category>test</category>
    </item>
  </channel>
</rss>
