<?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: Eshan Das</title>
    <description>The latest articles on DEV Community by Eshan Das (@eshan276).</description>
    <link>https://dev.to/eshan276</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1168187%2F8f41a20b-9e86-414b-9ba8-984087adbd34.jpg</url>
      <title>DEV Community: Eshan Das</title>
      <link>https://dev.to/eshan276</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eshan276"/>
    <language>en</language>
    <item>
      <title>Observability should be a `git diff`, not a weekend: instrumenting an AI app with one command using SigNoz</title>
      <dc:creator>Eshan Das</dc:creator>
      <pubDate>Sun, 26 Jul 2026 00:48:58 +0000</pubDate>
      <link>https://dev.to/eshan276/observability-should-be-a-git-diff-not-a-weekend-instrumenting-an-ai-app-with-one-command-using-235h</link>
      <guid>https://dev.to/eshan276/observability-should-be-a-git-diff-not-a-weekend-instrumenting-an-ai-app-with-one-command-using-235h</guid>
      <description>&lt;p&gt;&lt;em&gt;Built for the Agents of SigNoz hackathon — Track 1, AI &amp;amp; Agent Observability.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/Eshan276/signoz_hackathon" rel="noopener noreferrer"&gt;https://github.com/Eshan276/signoz_hackathon&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;curl -fsSL https://raw.githubusercontent.com/Eshan276/signoz_hackathon/main/install.sh | sh&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem: flying blind
&lt;/h2&gt;

&lt;p&gt;The hackathon brief names it perfectly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI agents are chaining LLM calls, invoking tools, hitting vector DBs, and making decisions autonomously. But when latency spikes, costs explode, or an agent hallucinates in production, you're flying blind. You can't debug what you can't see.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the thing though — the telemetry to &lt;em&gt;not&lt;/em&gt; fly blind already exists. OpenTelemetry, OpenLLMetry, SigNoz's LLM dashboards: the pieces are all there. The reason most teams stay blind isn't that it's impossible. It's that wiring it all up is a &lt;em&gt;weekend&lt;/em&gt; of yak-shaving: auto-instrumentation for each language, LLM and vector-DB spans, a collector, cost math, dashboards — all before the first span shows up.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;code&gt;signoz-init&lt;/code&gt;&lt;/strong&gt;: a Go CLI that turns that weekend into &lt;strong&gt;one command&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;signoz-init init &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point it at any Docker Compose stack. It detects each service, wires up full OpenTelemetry into SigNoz — HTTP, database, &lt;strong&gt;LLM and vector-DB spans with token counts and per-request cost in dollars&lt;/strong&gt; — and confirms the telemetry actually arrived. Without touching your &lt;code&gt;docker-compose.yml&lt;/code&gt; or a single line of application code.&lt;/p&gt;

&lt;p&gt;The pitch: &lt;strong&gt;observability should be a &lt;code&gt;git diff&lt;/code&gt;, not a weekend.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;Here's the trace from a single request through a RAG service, produced with &lt;strong&gt;zero application code changes&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web:POST /ask                      ← Node/Express gateway
└─ api:POST /ask                   ← FastAPI (root server span)
   ├─ api:qdrant.search            ← vector DB
   └─ api:chat gpt-4o-mini         ← LLM call
      ├─ gen_ai.usage.input_tokens : 77
      ├─ gen_ai.usage.output_tokens: 25
      └─ gen_ai.usage.cost_usd     : 0.0000856   ← in dollars, not tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Insert screenshot: the trace waterfall in SigNoz's Traces explorer, LLM span expanded.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That last line is the whole idea. &lt;strong&gt;SigNoz tracks token counts natively, but it does not compute dollar cost&lt;/strong&gt; — its own sample dashboards scale tokens by hand. &lt;code&gt;signoz-init&lt;/code&gt; emits &lt;code&gt;gen_ai.usage.cost_usd&lt;/code&gt; from an editable pricing table, filling a real gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works: five visible phases
&lt;/h2&gt;

&lt;p&gt;The CLI runs five phases, and every one is something you can see and confirm:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Detect&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Parses &lt;code&gt;docker-compose.yml&lt;/code&gt;, classifies each service by layered signals — build-context manifests → image name → command → ports. Reports &lt;code&gt;unknown&lt;/code&gt; honestly rather than guessing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Confirm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shows a detection table. Low-confidence guesses are &lt;em&gt;flagged&lt;/em&gt;, not stated as fact.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Writes &lt;code&gt;docker-compose.override.yml&lt;/code&gt; + &lt;code&gt;.signoz/&lt;/code&gt; assets. &lt;strong&gt;Shows a full diff first.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Apply&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rebuilds and restarts the stack.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Verify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Polls SigNoz until your services report, then prints what actually arrived.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last phase is what makes it a &lt;em&gt;product&lt;/em&gt; rather than a YAML generator. Anything can write config and hope. Confirming telemetry landed is the difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ 2 services reporting, 168 spans

  api  123 spans
    POST /ask, qdrant.search, chat gpt-4o-mini
  web  45 spans
    POST /ask, tcp.connect

  7 LLM spans with token counts
  7 spans with cost attribution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The zero-touch injection trick
&lt;/h3&gt;

&lt;p&gt;The magic is getting instrumentation into a running container without editing code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node&lt;/strong&gt; is genuinely zero-touch: &lt;code&gt;NODE_OPTIONS=--require /otel/otel-bootstrap.js&lt;/code&gt; plus env vars, all in the override file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; mounts a &lt;code&gt;sitecustomize.py&lt;/code&gt; on &lt;code&gt;PYTHONPATH&lt;/code&gt;. Python auto-imports &lt;code&gt;sitecustomize&lt;/code&gt; at interpreter startup, &lt;em&gt;before&lt;/em&gt; your app — the same mechanism the OpenTelemetry Operator uses for Kubernetes auto-injection. It installs the instrumentation and the cost processor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything lands in &lt;code&gt;docker-compose.override.yml&lt;/code&gt;, which Compose merges automatically. Reverting the entire thing is a single &lt;code&gt;rm docker-compose.override.yml&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where SigNoz comes in
&lt;/h2&gt;

&lt;p&gt;SigNoz is the observability backend the whole tool targets, and I leaned on it at every layer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install via Foundry.&lt;/strong&gt; SigNoz self-hosts through &lt;code&gt;foundryctl&lt;/code&gt; and an 8-line &lt;code&gt;casting.yaml&lt;/code&gt; (committed in the repo, so judges can re-run it). UI on &lt;code&gt;:8080&lt;/code&gt;, OTLP on &lt;code&gt;:4317&lt;/code&gt;/&lt;code&gt;:4318&lt;/code&gt;, with its own bundled OTel collector.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traces.&lt;/strong&gt; Every instrumented request becomes a distributed trace in SigNoz spanning &lt;code&gt;web → api → qdrant → LLM&lt;/code&gt;, viewable as a full waterfall in the Traces explorer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost &amp;amp; tokens.&lt;/strong&gt; By emitting canonical &lt;code&gt;gen_ai.*&lt;/code&gt; attributes, SigNoz's built-in LLM views and Cost Meter light up for free — and I ship a custom 12-widget dashboard imported through SigNoz's dashboards API: cost over time, tokens by model, cost by service, LLM p95 latency, vector-search latency.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Insert screenshot: the LLM Cost dashboard.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification.&lt;/strong&gt; The CLI queries SigNoz to prove spans landed — ClickHouse directly for self-hosted (no credentials needed), or the &lt;code&gt;/api/v2/services&lt;/code&gt; API for SigNoz Cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  The first-run gotcha SigNoz taught me
&lt;/h3&gt;

&lt;p&gt;The single most valuable thing I learned: &lt;strong&gt;"SigNoz is running" is not the same as "SigNoz can accept telemetry."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Freshly cast, SigNoz's ingester logs &lt;code&gt;cannot create agent without orgId&lt;/code&gt; and &lt;strong&gt;never opens its OTLP ports&lt;/strong&gt; — because the OpAMP server won't hand it a config until an organization exists, which only happens after first-run signup. Connections are &lt;em&gt;refused&lt;/em&gt;, &lt;code&gt;curl&lt;/code&gt; returns exit 56, and it looks exactly like a networking bug. It isn't. You just have to register the first admin, and OTLP opens within ~30 seconds.&lt;/p&gt;

&lt;p&gt;This is a product requirement, not just my local workaround. So &lt;code&gt;signoz-init&lt;/code&gt; detects the no-org state and guides you through it — because otherwise the first thing a new user sees is a silent black hole.&lt;/p&gt;




&lt;h2&gt;
  
  
  Going beyond cost: RAG quality and per-conversation rollups
&lt;/h2&gt;

&lt;p&gt;Cost and latency tell you &lt;em&gt;that&lt;/em&gt; something is wrong, not &lt;em&gt;what&lt;/em&gt;. So I added two signals that directly attack the "an agent hallucinates in production" line from the brief — signals &lt;strong&gt;auto-instrumentation fundamentally cannot produce, because only the application knows them.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Groundedness — a cheap hallucination proxy
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;gen_ai.response.groundedness&lt;/code&gt; is a 0–1 score of how much of the model's answer actually came from the retrieved context. It's a deliberately cheap lexical heuristic — &lt;strong&gt;no second LLM call, no API key, no added latency&lt;/strong&gt; — but it catches the failure that matters: an answer that wandered off the retrieved chunks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;groundedness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;answer_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;meaningful_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;source_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;meaningful_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer_tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;source_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A grounded answer scores ~1.0; an answer drawing on parametric knowledge scores ~0. It rides the same trace as the LLM span and rolls up natively in SigNoz — average groundedness, groundedness over time, the works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-conversation rollups
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;session.id&lt;/code&gt; on every span means cost, latency, and groundedness aggregate &lt;strong&gt;per conversation&lt;/strong&gt;, not just per call. That turns "this request cost $0.00004" into "this &lt;em&gt;conversation&lt;/em&gt; cost $0.40" — the number a budget owner actually cares about.&lt;/p&gt;

&lt;p&gt;This one taught me a subtle lesson (see gotcha #10 below): cost lives on the &lt;em&gt;LLM&lt;/em&gt; span, but the session id starts on the &lt;em&gt;request&lt;/em&gt; span. To make "cost by conversation" work, a span processor has to stamp the session onto &lt;em&gt;every&lt;/em&gt; span in the request. I only caught that the dashboard widget was silently empty because I tested the actual query, not just the happy path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ten things that cost me real debugging time
&lt;/h2&gt;

&lt;p&gt;This is the part I'm proudest of, and the best evidence the tool is worth building — because a normal developer hits these and gives up. Every one is verified, hands-on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TRACELOOP_BASE_URL&lt;/code&gt; routes traces, not the &lt;code&gt;api_endpoint&lt;/code&gt; kwarg.&lt;/strong&gt; &lt;code&gt;Traceloop.init(api_endpoint=...)&lt;/code&gt; alone exports nowhere, silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenLLMetry does NOT instrument web frameworks.&lt;/strong&gt; It covers LLM providers, vector DBs, and HTTP &lt;em&gt;clients&lt;/em&gt; — but not FastAPI. Without the framework instrumentor there's no root span and every LLM span is an orphan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't double-instrument.&lt;/strong&gt; Re-instrumenting what Traceloop already patched breaks the existing patch — and silently killed my Qdrant spans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceloop's AI instrumentor init swallows every exception.&lt;/strong&gt; Qdrant never got patched, with zero logging. Fix: call the instrumentors explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;async def&lt;/code&gt; + &lt;code&gt;run_in_threadpool&lt;/code&gt; loses OTel context&lt;/strong&gt; → orphaned spans. A plain &lt;code&gt;def&lt;/code&gt; endpoint works, because Starlette copies contextvars. Counter-intuitive, verified both ways.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost can't be attached in &lt;code&gt;SpanProcessor.on_end&lt;/code&gt;&lt;/strong&gt; — the exporter has already read the attributes, so it exports as 0. You have to wrap the &lt;em&gt;exporter&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BatchSpanProcessor.span_exporter&lt;/code&gt; is read-only.&lt;/strong&gt; Assign through &lt;code&gt;_batch_processor._exporter&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild, don't restart.&lt;/strong&gt; The Dockerfiles &lt;code&gt;COPY&lt;/code&gt; source in, so &lt;code&gt;docker compose restart&lt;/code&gt; runs stale code. Several confusing results traced back to this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenLLMetry's Qdrant instrumentor wraps &lt;code&gt;search&lt;/code&gt; but NOT &lt;code&gt;query_points&lt;/code&gt;&lt;/strong&gt; — the modern API produces no span. Also: build clients lazily, or they capture unpatched methods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceloop never sets the global tracer provider.&lt;/strong&gt; So &lt;code&gt;trace.get_tracer()&lt;/code&gt; returns a proxy and every hand-written span is a &lt;code&gt;NonRecordingSpan&lt;/code&gt; that silently vanishes. Instrumented libraries emit spans while your own code emits nothing, with no error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every one of these is now documented and handled in the tool, so its users never have to learn them.&lt;/p&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# install (no Go needed — prebuilt binary)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Eshan276/signoz_hackathon/main/install.sh | sh

&lt;span class="c"&gt;# stand up SigNoz once (foundryctl), then:&lt;/span&gt;
signoz-init init ./demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo is a real RAG stack — Node gateway → FastAPI → Qdrant → LLM — and it &lt;strong&gt;runs with or without an API key.&lt;/strong&gt; No key uses a mock LLM that still emits proper &lt;code&gt;gen_ai.*&lt;/code&gt; spans with realistic tokens, so anyone can reproduce the full pipeline, cost included. With a key it calls a real model — and because it uses the OpenAI SDK, it works against any OpenAI-compatible endpoint (I verified it against Gemini via a single &lt;code&gt;LLM_BASE_URL&lt;/code&gt; change).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;~2,600 lines of Go, 16 tests green, one command from blind to observable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Observability should be a &lt;code&gt;git diff&lt;/code&gt;, not a weekend.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with SigNoz for the Agents of SigNoz hackathon. Repo: &lt;a href="https://github.com/Eshan276/signoz_hackathon" rel="noopener noreferrer"&gt;https://github.com/Eshan276/signoz_hackathon&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>monitoring</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Visual Novel Engine in TypeScript — Here’s Why (and How You Can Join Me)</title>
      <dc:creator>Eshan Das</dc:creator>
      <pubDate>Sat, 19 Jul 2025 07:04:23 +0000</pubDate>
      <link>https://dev.to/eshan276/i-built-a-visual-novel-engine-in-typescript-heres-why-and-how-you-can-join-me-35jl</link>
      <guid>https://dev.to/eshan276/i-built-a-visual-novel-engine-in-typescript-heres-why-and-how-you-can-join-me-35jl</guid>
      <description>&lt;p&gt;Ever wanted to create a visual novel like Doki Doki Literature Club, Steins;Gate, or Ace Attorney—but with web technologies?&lt;/p&gt;

&lt;p&gt;Well, I just released an open-source visual novel engine powered by TypeScript, and I’m building it for developers like you who want to bring stories to life using modern, composable code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repo&lt;/strong&gt;: &lt;a href="https://github.com/Eshan276/easyvn" rel="noopener noreferrer"&gt;https://github.com/Eshan276/easyvn&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;NPM&lt;/strong&gt;: &lt;code&gt;npx create-vn my-story&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Join me&lt;/strong&gt;: Looking for contributors &amp;amp; maintainers!&lt;/p&gt;

&lt;p&gt;Why Build a Visual Novel Engine?&lt;br&gt;
As a dev and a fan of interactive storytelling, I always wanted to create visual novels that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run smoothly in the browser&lt;/li&gt;
&lt;li&gt;Use TypeScript for strong typing and modern dev flow&lt;/li&gt;
&lt;li&gt;Allow simple scripting, branching paths, and rich dialogues&lt;/li&gt;
&lt;li&gt;Are modular, open-source, and easy to extend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of waiting for the perfect engine, I built one.&lt;br&gt;
What Makes This Engine &lt;strong&gt;Different&lt;/strong&gt;?&lt;br&gt;
Unlike Ren’Py (Python-based) or RPG Maker (closed source), this engine is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Web-native&lt;/strong&gt;: Built in TypeScript, runs in the browser with zero install&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Developer-friendly&lt;/strong&gt;: Uses JSON or declarative scripting&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Composable&lt;/strong&gt;: Custom themes, and event logic supported&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Lightweight&lt;/strong&gt;: Tiny bundle, quick to learn&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Open source&lt;/strong&gt;: Contributions welcome!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Website&lt;/strong&gt;- &lt;a href="https://easyvn.iameshan.tech/" rel="noopener noreferrer"&gt;easyvn&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why I Need You&lt;br&gt;
This engine is just getting started. I’ve laid the minimal groundwork, but I’d love your help to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build more scripting features (conditions, labels, sounds)&lt;/li&gt;
&lt;li&gt;Create visual themes and UI templates&lt;/li&gt;
&lt;li&gt;Optimize performance&lt;/li&gt;
&lt;li&gt;Write tutorials, docs, or create demo games&lt;/li&gt;
&lt;li&gt;Port to Electron / mobile / PWA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Want to Contribute?&lt;br&gt;
I’ve made it super easy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fork the repo: &lt;a href="https://github.com/Eshan276/easyvn" rel="noopener noreferrer"&gt;https://github.com/Eshan276/easyvn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Run npm install &amp;amp;&amp;amp; npm run dev&lt;/li&gt;
&lt;li&gt;Join the fun! Add your ideas, open issues, and send PRs 💖&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m also happy to mentor first-time OSS contributors!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>visualnovel</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
