<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Posts on </title>
    <link>/posts/</link>
    <description>Recent content in Posts on </description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Sun, 29 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>ProjectDecompose: Breaking Down Complex Projects Into Deliverables</title>
      <link>/posts/augmented-resilience-posts/projectdecompose-breaking-down-complex-projects-into-deliverables/</link>
      <pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
      
      <guid>/posts/augmented-resilience-posts/projectdecompose-breaking-down-complex-projects-into-deliverables/</guid>
      <description>&lt;h1 id=&#34;projectdecompose-breaking-down-complex-projects-into-deliverables&#34;&gt;ProjectDecompose: Breaking Down Complex Projects Into Deliverables&lt;/h1&gt;
&lt;p&gt;Have you ever stared at a project and thought, &amp;ldquo;This is huge. Where do I even start?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;That moment—where the scope feels overwhelming and the path forward is unclear—is where most projects stall. Not because the vision is bad, but because it lives in your head as a vague cloud rather than a concrete set of steps.&lt;/p&gt;
&lt;p&gt;I just updated a tool to solve this: &lt;strong&gt;ProjectDecompose&lt;/strong&gt;, a reusable skill that systematically breaks down any personal or work project into structured, actionable deliverables using three complementary frameworks.&lt;/p&gt;</description>
      <content>&lt;h1 id=&#34;projectdecompose-breaking-down-complex-projects-into-deliverables&#34;&gt;ProjectDecompose: Breaking Down Complex Projects Into Deliverables&lt;/h1&gt;
&lt;p&gt;Have you ever stared at a project and thought, &amp;ldquo;This is huge. Where do I even start?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;That moment—where the scope feels overwhelming and the path forward is unclear—is where most projects stall. Not because the vision is bad, but because it lives in your head as a vague cloud rather than a concrete set of steps.&lt;/p&gt;
&lt;p&gt;I just updated a tool to solve this: &lt;strong&gt;ProjectDecompose&lt;/strong&gt;, a reusable skill that systematically breaks down any personal or work project into structured, actionable deliverables using three complementary frameworks.&lt;/p&gt;
&lt;h2 id=&#34;the-problem-it-solves&#34;&gt;The Problem It Solves&lt;/h2&gt;
&lt;p&gt;When you&amp;rsquo;re building something—a SaaS app, a side project, a data pipeline, an automation system—you need clarity on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What are the actual systems?&lt;/strong&gt; (Not &amp;ldquo;an app&amp;rdquo;, but the backend, frontend, database, sync service)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What are the major pieces?&lt;/strong&gt; (The containers: web tier, data tier, cache layer, messaging)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What&amp;rsquo;s inside each piece?&lt;/strong&gt; (The components: auth, sync engine, offline support)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What&amp;rsquo;s the execution order?&lt;/strong&gt; (Which must be built first?)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How long will it take?&lt;/strong&gt; (Realistically, with your team size)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most project planning tools skip this step. They want your estimate upfront, but you can&amp;rsquo;t estimate what you haven&amp;rsquo;t named.&lt;/p&gt;
&lt;h2 id=&#34;how-it-works&#34;&gt;How It Works&lt;/h2&gt;
&lt;p&gt;ProjectDecompose combines three battle-tested frameworks:&lt;/p&gt;
&lt;h3 id=&#34;1-domain-driven-design--business-boundaries-first&#34;&gt;1. Domain-Driven Design — Business Boundaries First&lt;/h3&gt;
&lt;p&gt;Before diving into system structure, ProjectDecompose asks: &lt;em&gt;what kind of project is this, really?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For domain-rich or team-based projects, it applies DDD Strategic Design:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Subdomains&lt;/strong&gt; — split the full problem space into areas of distinct concern. Each gets classified:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Core Domain:&lt;/strong&gt; your competitive advantage. Build this in-house, invest deeply.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Supporting Subdomain:&lt;/strong&gt; necessary but not differentiating. Custom build is fine; simpler modeling is acceptable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generic Subdomain:&lt;/strong&gt; commodity functionality (auth, email, billing). Buy off-the-shelf or use SaaS.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bounded Contexts&lt;/strong&gt; — explicit boundaries within which a single model is valid and a shared vocabulary (the &lt;em&gt;Ubiquitous Language&lt;/em&gt;) applies. Where domain experts and developers speak the same words with the same meanings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context Map&lt;/strong&gt; — a map of how Bounded Contexts relate to each other: who&amp;rsquo;s upstream, who&amp;rsquo;s downstream, and what integration pattern connects them (Anticorruption Layer, Customer-Supplier, Open Host Service, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For tactical modeling inside each Bounded Context, the skill identifies &lt;strong&gt;Aggregates&lt;/strong&gt; (clusters of objects with a single consistency boundary), &lt;strong&gt;Domain Events&lt;/strong&gt; (past-tense records of things that happened — &lt;code&gt;OrderPlaced&lt;/code&gt;, &lt;code&gt;UserRegistered&lt;/code&gt;), and &lt;strong&gt;Repositories&lt;/strong&gt; (the persistence abstraction for each Aggregate).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This phase is optional and automatically skipped for simple personal projects.&lt;/em&gt; A solo habit tracker doesn&amp;rsquo;t need Bounded Contexts. An e-commerce platform absolutely does.&lt;/p&gt;
&lt;h3 id=&#34;2-the-c4-model--hierarchical-clarity&#34;&gt;2. The C4 Model — Hierarchical Clarity&lt;/h3&gt;
&lt;p&gt;The C4 Model (from c4model.com) forces you to think in layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Level 1 (System):&lt;/strong&gt; What is the overall software system you&amp;rsquo;re building?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 2 (Container):&lt;/strong&gt; What are the major building blocks? (Frontend, API, database, cache, message queue)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 3 (Component):&lt;/strong&gt; What&amp;rsquo;s inside each container? (For the API: auth service, sync engine, data validation)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each level answers a specific question and helps you think precisely. Instead of saying &amp;ldquo;I need an API&amp;rdquo;, you define what&amp;rsquo;s actually inside it.&lt;/p&gt;
&lt;h3 id=&#34;3-system-design-interview-thinking--architectural-concerns&#34;&gt;3. System Design Interview Thinking — Architectural Concerns&lt;/h3&gt;
&lt;p&gt;The second framework comes from system design interview methodology. It asks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scalability:&lt;/strong&gt; How many users? What&amp;rsquo;s the bottleneck?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layering:&lt;/strong&gt; What tier does each concern live in?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reliability:&lt;/strong&gt; What needs redundancy? What&amp;rsquo;s a single point of failure?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; What are we optimizing for (cost, speed, reliability)?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These questions reshape your decomposition. A habit tracker built for 10 users looks very different from one designed for 1M users. The frameworks force you to be explicit about what you&amp;rsquo;re optimizing for.&lt;/p&gt;
&lt;h2 id=&#34;a-concrete-example&#34;&gt;A Concrete Example&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s say you want to build a &lt;strong&gt;habit tracker app&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;ProjectDecompose asks you 7 clarifying questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How many users? (1-10 people, just for me)&lt;/li&gt;
&lt;li&gt;What platforms? (iOS + web)&lt;/li&gt;
&lt;li&gt;What&amp;rsquo;s the key constraint? (Speed to market—ship quickly)&lt;/li&gt;
&lt;li&gt;Team size? (Solo)&lt;/li&gt;
&lt;li&gt;Data sensitivity? (Personal, moderate privacy concern)&lt;/li&gt;
&lt;li&gt;What&amp;rsquo;s the core business problem this solves? (Personal productivity — habit formation)&lt;/li&gt;
&lt;li&gt;Are there distinct areas with different vocabularies or rules? (No — single domain, skip DDD)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Because this is a solo personal project with no distinct business subdomains, &lt;strong&gt;Phase 0 (DDD Strategic Analysis) is automatically skipped.&lt;/strong&gt; The decomposition goes straight to C4 and System Design.&lt;/p&gt;
&lt;p&gt;Based on your answers, it decomposes the project:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Systems:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Client app (iOS)&lt;/li&gt;
&lt;li&gt;Client app (web)&lt;/li&gt;
&lt;li&gt;Backend API&lt;/li&gt;
&lt;li&gt;Sync service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Containers:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React web app&lt;/li&gt;
&lt;li&gt;React Native mobile app&lt;/li&gt;
&lt;li&gt;Node.js API server&lt;/li&gt;
&lt;li&gt;PostgreSQL database&lt;/li&gt;
&lt;li&gt;Redis cache&lt;/li&gt;
&lt;li&gt;Message queue&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Components:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Authentication system&lt;/li&gt;
&lt;li&gt;Habit tracking engine&lt;/li&gt;
&lt;li&gt;Sync protocol&lt;/li&gt;
&lt;li&gt;Offline support&lt;/li&gt;
&lt;li&gt;Analytics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Deliverables (ordered by dependency):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DEL-001:&lt;/strong&gt; Core data model &amp;amp; API (M — 2-4 weeks)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DEL-002:&lt;/strong&gt; Authentication system (S — 1-2 weeks)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DEL-003:&lt;/strong&gt; Web UI for habit entry (M — 2-3 weeks)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DEL-004:&lt;/strong&gt; Sync mechanism (L — 4-6 weeks)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DEL-005:&lt;/strong&gt; Mobile app (L — 4-6 weeks)
&amp;hellip; and so on&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each deliverable is discrete (you can implement it independently), actionable (you know what to build), and has an effort estimate (you know how long it should take).&lt;/p&gt;
&lt;h2 id=&#34;how-to-use-it&#34;&gt;How to Use It&lt;/h2&gt;
&lt;p&gt;The skill is invokable from your terminal:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/projectdecompose &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Your project description&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or trigger with natural language:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;decompose my project&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;break down this idea&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;structure my project&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The skill then:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Asks 7 clarifying questions&lt;/li&gt;
&lt;li&gt;Applies DDD Strategic Design if the project warrants it (Subdomains, Bounded Contexts, Context Map)&lt;/li&gt;
&lt;li&gt;Applies the C4 Model hierarchically (System → Container → Component)&lt;/li&gt;
&lt;li&gt;Applies DDD Tactical modeling per Bounded Context (Aggregates, Domain Events, Repositories)&lt;/li&gt;
&lt;li&gt;Maps System Design patterns (scalability, layering, reliability)&lt;/li&gt;
&lt;li&gt;Generates a JSON decomposition&lt;/li&gt;
&lt;li&gt;Creates a markdown summary with dependencies and a timeline&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You get back:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Structured JSON&lt;/strong&gt; (machine-readable; includes a &lt;code&gt;ddd&lt;/code&gt; key with the full domain model)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Markdown summary&lt;/strong&gt; (human-readable with hierarchy)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Domain model&lt;/strong&gt; (Subdomains, Bounded Contexts, Context Map — for domain-rich projects)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency graph&lt;/strong&gt; (execution order)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effort estimates&lt;/strong&gt; (T-shirt sizing: XS, S, M, L, XL)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timeline&lt;/strong&gt; (based on your team size)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;why-this-matters-for-resilience&#34;&gt;Why This Matters for Resilience&lt;/h2&gt;
&lt;p&gt;This tool embodies what Augmented Resilience is about: &lt;strong&gt;practical systems thinking&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Resilience isn&amp;rsquo;t just bouncing back from failure—it&amp;rsquo;s building things that are robust, maintainable, and clear in their structure. A decomposed project is a resilient project because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You can hand it to someone else.&lt;/strong&gt; Clear boundaries mean easy knowledge transfer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can pause and resume.&lt;/strong&gt; Deliverables are atomic; you can stop mid-project and pick it back up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can adapt to change.&lt;/strong&gt; When requirements shift, you know which deliverables to adjust.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You don&amp;rsquo;t get stuck.&lt;/strong&gt; Breaking the fog of &amp;ldquo;huge project&amp;rdquo; into &amp;ldquo;18 clear tasks&amp;rdquo; is psychologically powerful.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most projects fail not from lack of vision, but from lack of structure. This tool builds the structure.&lt;/p&gt;
&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s Next&lt;/h2&gt;
&lt;p&gt;ProjectDecompose is live and ready to use. Try it with any project—personal, professional, side hustle, or wild idea. The frameworks work at any scale.&lt;/p&gt;
&lt;p&gt;The skill is registered as &lt;code&gt;/projectdecompose&lt;/code&gt; in the PAI skill system. From any project folder, run:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/projectdecompose &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Describe your project in one sentence&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then answer the 5 clarifying questions, and you&amp;rsquo;ll get back a complete decomposition with everything you need to move forward.&lt;/p&gt;
&lt;p&gt;Building is hard enough without unclear scope. Let&amp;rsquo;s fix that.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Have you used decomposition frameworks before? What&amp;rsquo;s your approach to breaking down big projects?&lt;/strong&gt; Feel free to share—I&amp;rsquo;m curious how others think about this.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;ProjectDecompose is built on three frameworks: the C4 Model, System Design Interview methodology, and Domain-Driven Design. Inspired by Simon Brown&amp;rsquo;s architecture thinking, the operational realism of system design interviews, and Vaughn Vernon&amp;rsquo;s&lt;/em&gt; Implementing Domain-Driven Design &lt;em&gt;(Addison-Wesley, 2013).&lt;/em&gt;&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Adding a Web UI to My PDF to Markdown Converter</title>
      <link>/posts/augmented-resilience-posts/adding-a-web-ui-to-my-pdf-to-markdown-converter/</link>
      <pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate>
      
      <guid>/posts/augmented-resilience-posts/adding-a-web-ui-to-my-pdf-to-markdown-converter/</guid>
      <description>&lt;h2 id=&#34;the-promise-i-made-to-myself&#34;&gt;The Promise I Made to Myself&lt;/h2&gt;
&lt;p&gt;In my last post about &lt;a href=&#34;https://augmentedresilience.com/posts/when-your-pdf-workflow-breaks-building-a-markdown-converter-with-claude-code/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;building the PDF to Markdown converter&lt;/a&gt;
, I listed some &amp;ldquo;what&amp;rsquo;s next&amp;rdquo; ideas at the end. One of them was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FastAPI wrapper: Create an HTTP API for web apps to use&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Well, I did it. And I went a step further — I built a full drag-and-drop web UI on top of it.&lt;/p&gt;
&lt;p&gt;The CLI still works exactly as before. This is an addition, not a replacement. But now when I want to convert a batch of PDFs without thinking about terminal commands, I just open a browser tab.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-promise-i-made-to-myself&#34;&gt;The Promise I Made to Myself&lt;/h2&gt;
&lt;p&gt;In my last post about &lt;a href=&#34;https://augmentedresilience.com/posts/when-your-pdf-workflow-breaks-building-a-markdown-converter-with-claude-code/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;building the PDF to Markdown converter&lt;/a&gt;
, I listed some &amp;ldquo;what&amp;rsquo;s next&amp;rdquo; ideas at the end. One of them was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FastAPI wrapper: Create an HTTP API for web apps to use&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Well, I did it. And I went a step further — I built a full drag-and-drop web UI on top of it.&lt;/p&gt;
&lt;p&gt;The CLI still works exactly as before. This is an addition, not a replacement. But now when I want to convert a batch of PDFs without thinking about terminal commands, I just open a browser tab.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-the-ui-does&#34;&gt;What the UI Does&lt;/h2&gt;
&lt;p&gt;The interface is intentionally minimal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Drag-and-drop zone&lt;/strong&gt; — drop one PDF or fifty onto it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browse button&lt;/strong&gt; — if you prefer clicking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convert button&lt;/strong&gt; — kicks off the conversion&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-file progress bars&lt;/strong&gt; — live updates as each file converts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Individual download&lt;/strong&gt; — each completed file gets its own Download button&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Download all as ZIP&lt;/strong&gt; — one click to grab everything&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear&lt;/strong&gt; — resets the session and cleans up temp files server-side&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything runs locally. Files go to a temp directory on your machine, get converted, and are served back to you. Nothing hits an external API.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-stack&#34;&gt;The Stack&lt;/h2&gt;
&lt;p&gt;I kept it as simple as possible:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI + uvicorn&lt;/p&gt;
&lt;p&gt;FastAPI was the obvious choice — it handles file uploads cleanly, has first-class async support, and the &lt;code&gt;python-multipart&lt;/code&gt; library makes multi-file form handling trivial. The conversion logic is unchanged from the CLI: &lt;code&gt;pymupdf4llm.to_markdown()&lt;/code&gt; doing the heavy lifting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Progress updates:&lt;/strong&gt; Server-Sent Events (SSE)&lt;/p&gt;
&lt;p&gt;This is the part I found most interesting. When you hit Convert, the browser opens a persistent connection to &lt;code&gt;/progress/{job_id}&lt;/code&gt; and receives a stream of JSON events — one every 400ms — until the job finishes. No polling loop, no WebSocket complexity. SSE is perfect for this: unidirectional, simple, and built into every modern browser.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;event_stream&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;dumps({&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;progress&amp;#34;&lt;/span&gt;: job[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;progress&amp;#34;&lt;/span&gt;], &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;: job[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;]})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;yield&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;data: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;data&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; job[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; asyncio&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;sleep(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.4&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; StreamingResponse(event_stream(), media_type&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;text/event-stream&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the frontend, consuming it is three lines:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;eventSource&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;EventSource&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;`/progress/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;jobId&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;eventSource&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;onmessage&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;progress&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt; } &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;JSON&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;parse&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// update the UI...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Threading:&lt;/strong&gt; The conversion itself is synchronous (PyMuPDF4LLM blocks while it processes pages). To keep the FastAPI event loop from freezing during conversion, each job runs in a &lt;code&gt;ThreadPoolExecutor&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;asyncio&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_event_loop()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;run_in_executor(executor, _convert_job, job_id, job_dir)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Four workers by default — enough to handle several simultaneous conversions without overloading the machine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; Vanilla JS, no build step&lt;/p&gt;
&lt;p&gt;I deliberately avoided React, Vue, or any framework. The whole UI is a single &lt;code&gt;static/index.html&lt;/code&gt; file. It loads instantly, has no dependencies to install, and is easy to read and modify. For a local tool that one person uses, this is the right call.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;project-structure&#34;&gt;Project Structure&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s what changed from the original CLI project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pdf-to-markdown/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  pdf2md          — original CLI (unchanged)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  app.py          — FastAPI server (new)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  static/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    index.html    — drag-drop UI (new)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  serve           — start script (new)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  requirements.txt — updated with FastAPI deps
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  venv/           — existing venv, three new packages added
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;serve&lt;/code&gt; script is just:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;dirname &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$0&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;source venv/bin/activate
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;uvicorn app:app --host 0.0.0.0 --port &lt;span style=&#34;color:#ae81ff&#34;&gt;8765&lt;/span&gt; --reload
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run it once, open &lt;code&gt;http://localhost:8765&lt;/code&gt;, and you have a working converter in your browser.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;one-gotcha-pymupdf4llm-is-synchronous&#34;&gt;One Gotcha: PyMuPDF4LLM is Synchronous&lt;/h2&gt;
&lt;p&gt;This tripped me up briefly. &lt;code&gt;pymupdf4llm.to_markdown()&lt;/code&gt; does not return a coroutine — it&amp;rsquo;s a blocking call that can take 10–30 seconds on a large document. If you call it directly in an async FastAPI route handler, you freeze the entire event loop while it runs. No other requests get handled. The SSE stream stops updating.&lt;/p&gt;
&lt;p&gt;The fix is the &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; pattern above — push the blocking work off the event loop entirely. The async route returns immediately, the SSE stream keeps ticking, and the conversion runs in a thread pool where it belongs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-download-endpoints&#34;&gt;The Download Endpoints&lt;/h2&gt;
&lt;p&gt;Three endpoints handle output:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;GET /download/{job_id}/{filename}    — single .md file
GET /download-all/{job_id}           — all .md files as a ZIP
DELETE /job/{job_id}                 — clean up temp files
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The ZIP is built in memory using Python&amp;rsquo;s &lt;code&gt;zipfile&lt;/code&gt; module and streamed directly to the browser — no intermediate file on disk:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;buf &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; io&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;BytesIO()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; zipfile&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ZipFile(buf, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;w&amp;#34;&lt;/span&gt;, zipfile&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ZIP_DEFLATED) &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; zf:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; f &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; md_files:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        zf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write(f, f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;buf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;seek(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; StreamingResponse(buf, media_type&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;application/zip&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;what-this-unlocks&#34;&gt;What This Unlocks&lt;/h2&gt;
&lt;p&gt;The CLI was already useful. The web UI adds a few things the CLI cannot easily do:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Non-terminal users.&lt;/strong&gt; Anyone on my network can now use this converter by visiting &lt;code&gt;http://my-machine:8765&lt;/code&gt;. No Python knowledge required.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bulk drop workflows.&lt;/strong&gt; Dragging 20 PDFs from Finder into a browser window and clicking Convert is significantly faster than constructing a &lt;code&gt;--batch&lt;/code&gt; command with the right paths.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Visual feedback.&lt;/strong&gt; The progress bars are not just cosmetic. For large PDFs that take 20–30 seconds, knowing the conversion is running (and roughly how far along it is) removes the anxiety of staring at a terminal cursor.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s Next&lt;/h2&gt;
&lt;p&gt;The original roadmap item was &amp;ldquo;FastAPI wrapper.&amp;rdquo; That&amp;rsquo;s done. The next one I&amp;rsquo;m eyeing:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auto-feed to Obsidian inbox.&lt;/strong&gt; Right now the flow is: convert in the web UI, download the ZIP, unzip, move to Obsidian. I&amp;rsquo;d like to add a toggle: &amp;ldquo;Send output directly to &lt;code&gt;~/projects/obsidian-vault/00-inbox/&lt;/code&gt;&amp;rdquo; — one less manual step.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a small addition to the backend. Coming soon.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;running-it&#34;&gt;Running It&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd ~/projects/pdf-to-markdown
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;./serve
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Open http://localhost:8765&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first run installs nothing new — the three new packages (fastapi, uvicorn, python-multipart) are already in the venv. It just works.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Claude Code Has Skills. PAI Has a Skill System. Here&#39;s the Difference.</title>
      <link>/posts/augmented-resilience-posts/claude-code-has-skills.-pai-has-a-skill-system.-heres-the-difference/</link>
      <pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate>
      
      <guid>/posts/augmented-resilience-posts/claude-code-has-skills.-pai-has-a-skill-system.-heres-the-difference/</guid>
      <description>&lt;h1 id=&#34;claude-code-has-skills-pai-has-a-skill-system-heres-the-difference&#34;&gt;Claude Code Has Skills. PAI Has a Skill System. Here&amp;rsquo;s the Difference.&lt;/h1&gt;
&lt;p&gt;There&amp;rsquo;s a word that shows up in both Claude Code&amp;rsquo;s documentation and in PAI&amp;rsquo;s architecture: &lt;strong&gt;skills&lt;/strong&gt;. And because they share the same word — and even the same file conventions — it&amp;rsquo;s easy to assume they&amp;rsquo;re roughly equivalent. One is just a slightly fancier version of the other.&lt;/p&gt;
&lt;p&gt;They&amp;rsquo;re not. The relationship is closer to HTTP and a web framework. Claude Code&amp;rsquo;s skill mechanism is the protocol. PAI is the framework built on top of it.&lt;/p&gt;</description>
      <content>&lt;h1 id=&#34;claude-code-has-skills-pai-has-a-skill-system-heres-the-difference&#34;&gt;Claude Code Has Skills. PAI Has a Skill System. Here&amp;rsquo;s the Difference.&lt;/h1&gt;
&lt;p&gt;There&amp;rsquo;s a word that shows up in both Claude Code&amp;rsquo;s documentation and in PAI&amp;rsquo;s architecture: &lt;strong&gt;skills&lt;/strong&gt;. And because they share the same word — and even the same file conventions — it&amp;rsquo;s easy to assume they&amp;rsquo;re roughly equivalent. One is just a slightly fancier version of the other.&lt;/p&gt;
&lt;p&gt;They&amp;rsquo;re not. The relationship is closer to HTTP and a web framework. Claude Code&amp;rsquo;s skill mechanism is the protocol. PAI is the framework built on top of it.&lt;/p&gt;
&lt;p&gt;Understanding that distinction changed how I think about what I&amp;rsquo;ve actually built on my machine.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;start-here-what-claude-codes-skill-mechanism-actually-is&#34;&gt;Start Here: What Claude Code&amp;rsquo;s Skill Mechanism Actually Is&lt;/h2&gt;
&lt;p&gt;Before explaining what PAI adds, it&amp;rsquo;s worth being precise about what Claude Code provides natively — because it&amp;rsquo;s both more minimal and more elegant than most people realize.&lt;/p&gt;
&lt;p&gt;Claude Code&amp;rsquo;s skill system works like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;At startup&lt;/strong&gt;, Claude reads every &lt;code&gt;SKILL.md&lt;/code&gt; file it finds under &lt;code&gt;~/.claude/skills/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;description&lt;/code&gt; field&lt;/strong&gt; in each skill&amp;rsquo;s YAML frontmatter determines when that skill activates — it&amp;rsquo;s pure intent matching. Anthropic caps this at 1024 characters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When a skill matches your request&lt;/strong&gt;, the &lt;code&gt;Skill&lt;/code&gt; tool injects the full SKILL.md content into Claude&amp;rsquo;s context window&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude follows the instructions&lt;/strong&gt; in that file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&amp;rsquo;s the entire mechanism. It&amp;rsquo;s a &lt;em&gt;context injection system with a routing layer&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;USE WHEN&lt;/code&gt; clause in a skill description is the key piece. Here&amp;rsquo;s a simplified example from my OracleHCM skill:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;OracleHCM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;description&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Expert Oracle HCM Cloud troubleshooting. USE WHEN user mentions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ae81ff&#34;&gt;Oracle HCM, HCM Cloud, HDL, HCM Data Loader, Journey, Checklist, workflow&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ae81ff&#34;&gt;approvals, autocomplete rules, fast formulas, security profiles...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When I describe an Oracle HCM problem in natural language, Claude Code matches my intent against that description and loads the skill. I never have to say &amp;ldquo;use the Oracle HCM skill.&amp;rdquo; The intent matching handles it.&lt;/p&gt;
&lt;p&gt;Elegant. Minimal. And — on its own — surprisingly limited.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-gap-between-context-injection-and-operational-capability&#34;&gt;The Gap Between &amp;ldquo;Context Injection&amp;rdquo; and &amp;ldquo;Operational Capability&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;Imagine a skill that&amp;rsquo;s just a long markdown file. When it loads, Claude reads the instructions and tries to follow them. If the instructions are clear and the task is simple, that works fine. But for anything complex — something that involves multiple steps, personalized behavior, CLI tooling, external APIs, or parallel agents — a single markdown file loaded into context starts to break down.&lt;/p&gt;
&lt;p&gt;The instructions get long. They can&amp;rsquo;t be personalized without making the skill personal (and therefore un-shareable). There&amp;rsquo;s no way to dispatch to a sub-procedure. There&amp;rsquo;s no tooling layer. There&amp;rsquo;s no way to say &amp;ldquo;if the user wants to &lt;em&gt;create&lt;/em&gt; a blog post, follow &lt;em&gt;this&lt;/em&gt; procedure; if they want to &lt;em&gt;publish&lt;/em&gt;, follow &lt;em&gt;that&lt;/em&gt; one.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;This is the gap PAI fills.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-pai-builds-on-top-nine-layers&#34;&gt;What PAI Builds on Top: Nine Layers&lt;/h2&gt;
&lt;p&gt;PAI&amp;rsquo;s &lt;a href=&#34;https://github.com/danielmiessler/PAI&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;SKILLSYSTEM.md&lt;/code&gt;&lt;/a&gt;
 defines a canonical structure that every skill must follow. It&amp;rsquo;s not a suggestion — it&amp;rsquo;s enforced by convention and by the &lt;code&gt;CreateSkill&lt;/code&gt; skill that scaffolds new ones. Here&amp;rsquo;s what each layer adds.&lt;/p&gt;
&lt;h3 id=&#34;layer-1--canonical-structure&#34;&gt;Layer 1 — Canonical Structure&lt;/h3&gt;
&lt;p&gt;Claude Code just needs a &lt;code&gt;SKILL.md&lt;/code&gt;. PAI requires a specific directory layout:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;SkillName/
├── SKILL.md          ← minimal routing only (40-50 lines)
├── Workflows/        ← execution procedures, one per task
│   ├── Create.md
│   └── Update.md
├── Tools/            ← TypeScript CLI tools (always present)
│   └── Generate.ts
└── ApiReference.md   ← context files loaded on demand
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SKILL.md stays minimal. Complexity lives in workflows and context files that load when actually needed.&lt;/p&gt;
&lt;h3 id=&#34;layer-2--workflow-routing&#34;&gt;Layer 2 — Workflow Routing&lt;/h3&gt;
&lt;p&gt;This is the most immediately useful layer. Claude Code routes &lt;em&gt;to&lt;/em&gt; a skill. PAI routes &lt;em&gt;within&lt;/em&gt; a skill.&lt;/p&gt;
&lt;p&gt;The routing table in every SKILL.md dispatches sub-tasks to specific workflow files:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| Workflow    | Trigger                    | File                      |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|-------------|----------------------------|---------------------------|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| &lt;span style=&#34;font-weight:bold&#34;&gt;**Create**&lt;/span&gt;  | &amp;#34;write a post&amp;#34;             | &lt;span style=&#34;color:#e6db74&#34;&gt;`Workflows/Create.md`&lt;/span&gt;     |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| &lt;span style=&#34;font-weight:bold&#34;&gt;**Publish**&lt;/span&gt; | &amp;#34;publish&amp;#34;, &amp;#34;deploy&amp;#34;        | &lt;span style=&#34;color:#e6db74&#34;&gt;`Workflows/Publish.md`&lt;/span&gt;    |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;| &lt;span style=&#34;font-weight:bold&#34;&gt;**Header**&lt;/span&gt;  | &amp;#34;create header image&amp;#34;      | &lt;span style=&#34;color:#e6db74&#34;&gt;`Workflows/Header.md`&lt;/span&gt;     |
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&amp;ldquo;Write a post&amp;rdquo; and &amp;ldquo;publish the site&amp;rdquo; both activate the same skill, but they route to completely different procedures. Without this, a skill that handles multiple operations becomes one giant file Claude has to navigate by itself.&lt;/p&gt;
&lt;h3 id=&#34;layer-3--the-personalization-layer&#34;&gt;Layer 3 — The Personalization Layer&lt;/h3&gt;
&lt;p&gt;Every system skill in PAI checks for user overrides before executing:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;~/.claude/skills/PAI/USER/SKILLCUSTOMIZATIONS/{SkillName}/
├── EXTEND.yaml        ← merge strategy (append | override | deep_merge)
└── PREFERENCES.md     ← user-specific behavior
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The system skill stays generic and shareable. My preferences — color palettes for the Art skill, voice configurations for the Agents skill, output format defaults for Research — live separately and merge in at runtime. The skill author never needs to know about my preferences. I never need to fork the skill to add my own.&lt;/p&gt;
&lt;h3 id=&#34;layer-4--system-vs-personal-skill-separation&#34;&gt;Layer 4 — System vs Personal Skill Separation&lt;/h3&gt;
&lt;p&gt;PAI enforces a hard naming convention that determines portability:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TitleCase&lt;/strong&gt; (&lt;code&gt;Research&lt;/code&gt;, &lt;code&gt;Browser&lt;/code&gt;, &lt;code&gt;OracleHCM&lt;/code&gt;) = system skills, no personal data, shareable via PAI Packs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;_ALLCAPS&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;_BLOGGING&lt;/code&gt;, &lt;code&gt;_MAQINA&lt;/code&gt;) = personal skills, private by convention, never exported&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Claude Code has no concept of skill visibility. PAI makes it structural.&lt;/p&gt;
&lt;h3 id=&#34;layer-5--cli-tooling-convention&#34;&gt;Layer 5 — CLI Tooling Convention&lt;/h3&gt;
&lt;p&gt;Every PAI skill has a &lt;code&gt;Tools/&lt;/code&gt; directory. When a workflow needs to do something repeatable — generate an image, manage a server, sync a repository — it calls a TypeScript CLI tool instead of embedding logic in the workflow markdown itself.&lt;/p&gt;
&lt;p&gt;Tools use &lt;code&gt;#!/usr/bin/env bun&lt;/code&gt;, expose configuration via flags, and have &lt;code&gt;.help.md&lt;/code&gt; documentation files. This keeps workflows simple (intent routing) and tools encapsulated (execution). You can test a tool independently of its workflow.&lt;/p&gt;
&lt;h3 id=&#34;layer-6--ai-powered-hooks&#34;&gt;Layer 6 — AI-Powered Hooks&lt;/h3&gt;
&lt;p&gt;PAI runs 17 event hooks that fire at specific moments: session start, prompt submission, pre-tool, post-tool, and others. The most important one for response quality is &lt;code&gt;FormatReminder&lt;/code&gt; — it runs AI inference on your &lt;em&gt;raw prompt&lt;/em&gt; before Claude even starts responding, classifies the depth required (FULL / ITERATION / MINIMAL), and injects that classification as authoritative context.&lt;/p&gt;
&lt;p&gt;This is hooks doing real work, not just shell scripts appending text to prompts.&lt;/p&gt;
&lt;h3 id=&#34;layer-7--the-algorithm&#34;&gt;Layer 7 — The Algorithm&lt;/h3&gt;
&lt;p&gt;Every response PAI generates runs through a 7-phase problem-solving framework: &lt;strong&gt;OBSERVE → THINK → PLAN → BUILD → EXECUTE → VERIFY → LEARN&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t decorative structure. The OBSERVE phase reverse-engineers your actual intent. The THINK phase selects capabilities and validates skill choices against the problem. The VERIFY phase uses &lt;code&gt;TaskCreate&lt;/code&gt;/&lt;code&gt;TaskUpdate&lt;/code&gt; to track measurable success criteria. The LEARN phase captures what to improve next time.&lt;/p&gt;
&lt;p&gt;Skills feed into this framework — they&amp;rsquo;re not parallel to it. When a skill activates, it executes inside the Algorithm, with its results held accountable to the ISC criteria created in OBSERVE.&lt;/p&gt;
&lt;h3 id=&#34;layer-8--agent-composition-patterns&#34;&gt;Layer 8 — Agent Composition Patterns&lt;/h3&gt;
&lt;p&gt;PAI skills can spawn specialized subagents and compose them using named patterns:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Pattern&lt;/th&gt;
          &lt;th&gt;Shape&lt;/th&gt;
          &lt;th&gt;When&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Pipeline&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;A → B → C&lt;/td&gt;
          &lt;td&gt;Sequential domain handoff&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;TDD Loop&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Engineer ↔ QATester&lt;/td&gt;
          &lt;td&gt;Build-verify cycle&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Fan-out&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;→ [A, B, C]&lt;/td&gt;
          &lt;td&gt;Multiple perspectives needed&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Gate&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;A → check → B or retry&lt;/td&gt;
          &lt;td&gt;Quality gate before progression&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A skill that just loads into context can&amp;rsquo;t orchestrate parallel agents. A PAI skill that routes to a workflow that invokes a Fan-out pattern can research, build, and verify in parallel — with a spotcheck agent at the end synthesizing results.&lt;/p&gt;
&lt;h3 id=&#34;layer-9--dynamic-loading&#34;&gt;Layer 9 — Dynamic Loading&lt;/h3&gt;
&lt;p&gt;Large skills use deferred loading. Only the SKILL.md loads on invocation. Reference documents, API guides, and style specs load only when the specific workflow that needs them runs. This actively manages token budget rather than blowing it on context that might not be needed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-feature-gap-in-one-table&#34;&gt;The Feature Gap in One Table&lt;/h2&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Feature&lt;/th&gt;
          &lt;th&gt;Claude Code (Native)&lt;/th&gt;
          &lt;th&gt;PAI System&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Skill discovery&lt;/td&gt;
          &lt;td&gt;YAML &lt;code&gt;description&lt;/code&gt; at startup&lt;/td&gt;
          &lt;td&gt;Same + &lt;code&gt;USE WHEN&lt;/code&gt; intent parsing&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Sub-routing&lt;/td&gt;
          &lt;td&gt;None&lt;/td&gt;
          &lt;td&gt;Workflow routing table&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Personalization&lt;/td&gt;
          &lt;td&gt;None&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;SKILLCUSTOMIZATIONS&lt;/code&gt; layer&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Skill visibility&lt;/td&gt;
          &lt;td&gt;All equal&lt;/td&gt;
          &lt;td&gt;System vs Personal convention&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Tooling&lt;/td&gt;
          &lt;td&gt;None&lt;/td&gt;
          &lt;td&gt;TypeScript CLI tools&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Hooks&lt;/td&gt;
          &lt;td&gt;Basic&lt;/td&gt;
          &lt;td&gt;17 AI-powered hooks&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Response structure&lt;/td&gt;
          &lt;td&gt;Free-form&lt;/td&gt;
          &lt;td&gt;Algorithm (7 phases, ISC, verify)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Agents&lt;/td&gt;
          &lt;td&gt;None&lt;/td&gt;
          &lt;td&gt;15+ specialized subagent types&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Memory&lt;/td&gt;
          &lt;td&gt;None&lt;/td&gt;
          &lt;td&gt;File-based cross-session memory&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Dynamic loading&lt;/td&gt;
          &lt;td&gt;Full file loaded&lt;/td&gt;
          &lt;td&gt;Context files on demand&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Portability&lt;/td&gt;
          &lt;td&gt;No convention&lt;/td&gt;
          &lt;td&gt;PAI Packs&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;why-this-matters-practically&#34;&gt;Why This Matters Practically&lt;/h2&gt;
&lt;p&gt;The single most useful shift in mental model: &lt;strong&gt;Claude Code skills are context&lt;/strong&gt;. PAI skills are &lt;strong&gt;operational units&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When I ask my system to publish a blog post, the publishing skill doesn&amp;rsquo;t just remind Claude how publishing works. It dispatches to the Publish workflow, which runs image conversion, calls &lt;code&gt;hugo&lt;/code&gt;, commits, pushes to GitHub, and triggers the Actions pipeline that deploys to Namecheap FTP — all as a structured procedure with steps that can fail, be verified, and be corrected independently.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not context injection. That&amp;rsquo;s execution.&lt;/p&gt;
&lt;p&gt;The 34 skills on my system aren&amp;rsquo;t 34 long markdown files. They&amp;rsquo;re 34 capabilities, each with their own routing logic, personalization layer, tooling, and agent integration. Claude Code&amp;rsquo;s mechanism made them possible. PAI&amp;rsquo;s framework made them reliable.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;where-to-go-from-here&#34;&gt;Where to Go from Here&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re new to PAI and want to understand the broader architecture this sits inside — the memory system, the agent tiers, how RAG ties everything together — the prior post &lt;a href=&#34;/posts/rag-agents-and-skills-the-three-pillars-inside-my-personal-ai&#34;&gt;&lt;em&gt;RAG, Agents, and Skills: The Three Pillars Inside My Personal AI&lt;/em&gt;&lt;/a&gt;
 covers the full picture.&lt;/p&gt;
&lt;p&gt;If you want to go deeper on the skill system itself, the canonical reference is &lt;code&gt;~/.claude/skills/PAI/SYSTEM/SKILLSYSTEM.md&lt;/code&gt; — it&amp;rsquo;s the document all skills are built against, and it explains every convention described here in precise detail.&lt;/p&gt;
&lt;p&gt;PAI is open source at &lt;a href=&#34;https://github.com/danielmiessler/PAI&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;github.com/danielmiessler/PAI&lt;/a&gt;
.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Building an AI Conference Directory That Populates Itself</title>
      <link>/posts/augmented-resilience-posts/building-an-ai-conference-directory-that-populates-itself/</link>
      <pubDate>Sat, 14 Mar 2026 00:00:00 +0000</pubDate>
      
      <guid>/posts/augmented-resilience-posts/building-an-ai-conference-directory-that-populates-itself/</guid>
      <description>&lt;h2 id=&#34;the-problem-ai-conferences-are-everywhere-and-nowhere&#34;&gt;The Problem: AI Conferences Are Everywhere and Nowhere&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;ve ever tried to find a comprehensive list of upcoming AI conferences, you know the pain. There&amp;rsquo;s no single source. AAAI has their page. NeurIPS has theirs. ICML posts deadlines on OpenReview. Half the emerging summits only exist on LinkedIn event pages or buried in Reddit threads.&lt;/p&gt;
&lt;p&gt;I wanted a simple, searchable directory of AI conferences — one site where I could see what&amp;rsquo;s coming up, filter by topic, and get the key details. But I didn&amp;rsquo;t want to manually curate it. I&amp;rsquo;ve seen too many &amp;ldquo;awesome lists&amp;rdquo; on GitHub that are lovingly maintained for three months and then abandoned.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem-ai-conferences-are-everywhere-and-nowhere&#34;&gt;The Problem: AI Conferences Are Everywhere and Nowhere&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;ve ever tried to find a comprehensive list of upcoming AI conferences, you know the pain. There&amp;rsquo;s no single source. AAAI has their page. NeurIPS has theirs. ICML posts deadlines on OpenReview. Half the emerging summits only exist on LinkedIn event pages or buried in Reddit threads.&lt;/p&gt;
&lt;p&gt;I wanted a simple, searchable directory of AI conferences — one site where I could see what&amp;rsquo;s coming up, filter by topic, and get the key details. But I didn&amp;rsquo;t want to manually curate it. I&amp;rsquo;ve seen too many &amp;ldquo;awesome lists&amp;rdquo; on GitHub that are lovingly maintained for three months and then abandoned.&lt;/p&gt;
&lt;p&gt;What I wanted was a system that populates itself.&lt;/p&gt;
&lt;p&gt;So I built one. And with Claude Code running through my PAI system, the whole pipeline — from search to database to website — came together over a few focused sessions.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the full story.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-architecture-three-layers-zero-manual-data-entry&#34;&gt;The Architecture: Three Layers, Zero Manual Data Entry&lt;/h2&gt;
&lt;p&gt;The final system has three layers, each handling a distinct responsibility:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;SearXNG (search engine)
    → conference_tracker.py (discovery)
        → Airtable (database)
            → fetch-events.mjs (build-time fetch)
                → React + Vite site on Netlify
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Each layer is independently useful, loosely coupled, and replaceable. Let&amp;rsquo;s walk through them.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;layer-1-the-tracker--finding-conferences-automatically&#34;&gt;Layer 1: The Tracker — Finding Conferences Automatically&lt;/h2&gt;
&lt;p&gt;The foundation is a Python script called &lt;code&gt;conference_tracker.py&lt;/code&gt;. Its job is simple: search the web for AI conferences and store what it finds.&lt;/p&gt;
&lt;h3 id=&#34;search-searxng-instead-of-google&#34;&gt;Search: SearXNG Instead of Google&lt;/h3&gt;
&lt;p&gt;Rather than hitting the Google API (with its quotas and billing), I use &lt;a href=&#34;https://github.com/searxng/searxng&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;SearXNG&lt;/a&gt;
 — an open-source, self-hosted meta-search engine. It aggregates results from Google, Bing, DuckDuckGo, and others without API keys or rate limits.&lt;/p&gt;
&lt;p&gt;The tracker runs a curated list of search queries defined in &lt;code&gt;config.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;search_queries&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;AI conference 2026&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;artificial intelligence conference 2026&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;machine learning conference 2026&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;NeurIPS 2026&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ICML 2026&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;AAAI 2026&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;AI summit 2026&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;deep learning conference 2026&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;computer vision conference 2026 CVPR&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;natural language processing conference 2026&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each query returns up to 10 results. The tracker extracts the title, URL, and snippet from each result, deduplicates against what&amp;rsquo;s already in the database, and stores new finds.&lt;/p&gt;
&lt;h3 id=&#34;storage-airtable-as-the-source-of-truth&#34;&gt;Storage: Airtable as the Source of Truth&lt;/h3&gt;
&lt;p&gt;Why Airtable? Because it&amp;rsquo;s a real database with an API, but it also has a spreadsheet-like UI for manual review. When you&amp;rsquo;re building a pipeline that discovers data automatically, you want a way to eyeball the results and clean up noise — and Airtable is perfect for that.&lt;/p&gt;
&lt;p&gt;The tracker writes five fields per record: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;websiteUrl&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;Source Query&lt;/code&gt;, and &lt;code&gt;Date Found&lt;/code&gt;. That&amp;rsquo;s it. Just the raw discovery data. The structured details come later.&lt;/p&gt;
&lt;p&gt;The deduplication is URL-based — normalized and lowercased. If we&amp;rsquo;ve already stored &lt;code&gt;neurips.cc/2026&lt;/code&gt;, we don&amp;rsquo;t store it again even if it appears in a different search query.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;extract_conference_info&lt;/span&gt;(result, source_query):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;: result[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;][:&lt;span style=&#34;color:#ae81ff&#34;&gt;200&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;websiteUrl&amp;#34;&lt;/span&gt;: result[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;: result[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;snippet&amp;#34;&lt;/span&gt;][:&lt;span style=&#34;color:#ae81ff&#34;&gt;1000&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Source Query&amp;#34;&lt;/span&gt;: source_query,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Date Found&amp;#34;&lt;/span&gt;: datetime&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;now(timezone&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;utc)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strftime(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;%Y-%m-&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%d&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After one run, we had 87 unique conference records. The real stuff — NeurIPS, ICML, CVPR, AAAI — alongside smaller but interesting events like the Quantum AI and NLP Conference, Deep Learning Indaba, and the Wharton Human-AI Research summit.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;layer-2-the-website--react--vite-on-netlify&#34;&gt;Layer 2: The Website — React + Vite on Netlify&lt;/h2&gt;
&lt;p&gt;The directory itself is a React app built with Vite and deployed on Netlify. It&amp;rsquo;s a single-page app with search, tag filtering, and individual event pages.&lt;/p&gt;
&lt;p&gt;The key architectural decision: &lt;strong&gt;data is fetched at build time, not runtime.&lt;/strong&gt; A prebuild script (&lt;code&gt;fetch-events.mjs&lt;/code&gt;) pulls conference data from the database and writes it to a &lt;code&gt;data.ts&lt;/code&gt; file that Vite bundles into the site. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No API keys exposed in the browser&lt;/li&gt;
&lt;li&gt;No CORS issues&lt;/li&gt;
&lt;li&gt;Instant page loads (data is already in the bundle)&lt;/li&gt;
&lt;li&gt;The site works even if Airtable is temporarily down&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The prebuild hook in &lt;code&gt;package.json&lt;/code&gt; makes this automatic:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;fetch-events&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;bun scripts/fetch-events.mjs&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;prebuild&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;bun scripts/fetch-events.mjs&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;build&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;vite build&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every time Netlify builds the site, it automatically fetches the latest data from Airtable. Fresh data on every deploy.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-middleman-problem-cutting-google-sheets&#34;&gt;The Middleman Problem: Cutting Google Sheets&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s where the story gets interesting.&lt;/p&gt;
&lt;p&gt;The original pipeline had an extra step: Airtable → Google Sheets → website. The &lt;code&gt;fetch-events.mjs&lt;/code&gt; script was pulling from a published Google Sheet CSV. Why? Because when I first prototyped the site, I started with a spreadsheet. It was quick and easy.&lt;/p&gt;
&lt;p&gt;But once the conference tracker was writing directly to Airtable, Google Sheets became a middleman with no purpose. Data had to be synced from Airtable to Sheets (manually or via Zapier), and that sync was another thing that could break.&lt;/p&gt;
&lt;p&gt;The fix was straightforward: teach &lt;code&gt;fetch-events.mjs&lt;/code&gt; to talk directly to the Airtable API.&lt;/p&gt;
&lt;h3 id=&#34;airtables-rest-api&#34;&gt;Airtable&amp;rsquo;s REST API&lt;/h3&gt;
&lt;p&gt;The Airtable API is clean. A single GET request returns records as JSON:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;URL&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;`https://api.airtable.com/v0/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;baseId&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;tableId&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;resp&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;toString&lt;/span&gt;(), {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;headers&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;Authorization&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`Bearer &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;pat&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;resp&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// data.records = [{ id, fields: { title, date, ... } }]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The one gotcha: Airtable paginates at 100 records. You need to follow the &lt;code&gt;offset&lt;/code&gt; token:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetchFromAirtable&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;pat&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;baseId&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tableId&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;allRecords&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;offset&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;URL&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;`https://api.airtable.com/v0/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;baseId&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;tableId&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;offset&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;searchParams&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;set&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;offset&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;offset&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;resp&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;toString&lt;/span&gt;(), {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;headers&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;Authorization&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`Bearer &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;pat&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;resp&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;allRecords&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;push&lt;/span&gt;(...&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;records&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;offset&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;offset&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  } &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;offset&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;allRecords&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;graceful-fallback&#34;&gt;Graceful Fallback&lt;/h3&gt;
&lt;p&gt;I kept the Google Sheets path as a fallback. The &lt;code&gt;main()&lt;/code&gt; function uses a priority chain:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Airtable&lt;/strong&gt; — if &lt;code&gt;AIRTABLE_PAT&lt;/code&gt;, &lt;code&gt;AIRTABLE_BASE_ID&lt;/code&gt;, &lt;code&gt;AIRTABLE_TABLE_ID&lt;/code&gt; are set&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Sheets&lt;/strong&gt; — if &lt;code&gt;GOOGLE_SHEET_CSV_URL&lt;/code&gt; is set&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fallback events&lt;/strong&gt; — hardcoded sample data so the build never fails&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This means you can&amp;rsquo;t break the site by misconfiguring a data source. The build always succeeds.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;layer-3-the-enrichment--ai-powered-data-extraction&#34;&gt;Layer 3: The Enrichment — AI-Powered Data Extraction&lt;/h2&gt;
&lt;p&gt;This is where things got really interesting.&lt;/p&gt;
&lt;p&gt;After cutting Google Sheets, I had 87 conference records in Airtable. But they only had three useful fields: title, description, and URL. No dates. No locations. No tags. The site worked, but every event card was sparse — no way to filter by date or location, no tags to browse by topic.&lt;/p&gt;
&lt;p&gt;Filling in 87 records by hand? No thanks.&lt;/p&gt;
&lt;h3 id=&#34;the-idea-visit-each-url-and-ask-ai-to-extract-the-data&#34;&gt;The Idea: Visit Each URL and Ask AI to Extract the Data&lt;/h3&gt;
&lt;p&gt;The approach: for each conference record, fetch its web page, extract the text content, and use AI inference to pull out structured fields like date, location, organizer, and tags.&lt;/p&gt;
&lt;p&gt;I built an enrichment script — &lt;code&gt;enrich_conferences.py&lt;/code&gt; — that sits alongside the tracker in the same project.&lt;/p&gt;
&lt;h3 id=&#34;step-1-fetch-and-clean-the-page&#34;&gt;Step 1: Fetch and Clean the Page&lt;/h3&gt;
&lt;p&gt;Each conference URL gets fetched with &lt;code&gt;requests&lt;/code&gt;, then cleaned with BeautifulSoup. Navigation, footers, scripts, and styling get stripped, leaving just the text content:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch_page_text&lt;/span&gt;(url, timeout&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    resp &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; requests&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(url, headers&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;headers, timeout&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;timeout)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    soup &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; BeautifulSoup(resp&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;text, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;html.parser&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; tag &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; soup([&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;script&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;style&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;nav&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;footer&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;header&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;aside&amp;#34;&lt;/span&gt;]):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        tag&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;decompose()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    text &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; soup&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_text(separator&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;, strip&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    lines &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [line&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strip() &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; line &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; text&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;splitlines() &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; line&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strip()]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;join(lines)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;step-2-ai-extraction-via-pai-inference&#34;&gt;Step 2: AI Extraction via PAI Inference&lt;/h3&gt;
&lt;p&gt;The cleaned text gets sent to Claude (via PAI&amp;rsquo;s Inference tool) with a structured extraction prompt. The prompt is specific about what to extract and what format to use:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Given text from a conference web page, extract these fields as JSON:
{
  &amp;#34;date&amp;#34;: &amp;#34;human-readable date like &amp;#39;May 5-6, 2026&amp;#39;&amp;#34;,
  &amp;#34;endDate&amp;#34;: &amp;#34;ISO end date like &amp;#39;2026-05-06&amp;#39;&amp;#34;,
  &amp;#34;location&amp;#34;: &amp;#34;City, State/Country&amp;#34;,
  &amp;#34;venue&amp;#34;: &amp;#34;venue name&amp;#34;,
  &amp;#34;price&amp;#34;: &amp;#34;ticket price or &amp;#39;Free&amp;#39;&amp;#34;,
  &amp;#34;organizer&amp;#34;: &amp;#34;organizing body&amp;#34;,
  &amp;#34;tags&amp;#34;: &amp;#34;comma-separated topic tags (max 4)&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;One critical addition: if the page is a &lt;strong&gt;list of conferences&lt;/strong&gt; (like &amp;ldquo;Top 10 AI Conferences of 2026&amp;rdquo;), the AI returns &lt;code&gt;{&amp;quot;is_list_page&amp;quot;: true}&lt;/code&gt; and the script skips it. This was essential — about 15% of our URLs were aggregator pages, not individual conference pages.&lt;/p&gt;
&lt;h3 id=&#34;step-3-write-back-to-airtable&#34;&gt;Step 3: Write Back to Airtable&lt;/h3&gt;
&lt;p&gt;Non-empty extracted fields get PATCHed back to Airtable. The script only writes fields that actually exist in the table schema — a lesson learned the hard way when &lt;code&gt;venue&lt;/code&gt; and &lt;code&gt;imageUrl&lt;/code&gt; threw 422 errors because those columns hadn&amp;rsquo;t been created yet.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;build_patch_fields&lt;/span&gt;(extracted, allowed_fields):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; extracted&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;is_list_page&amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    patch &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; key &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;date&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;endDate&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;location&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;venue&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;price&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;organizer&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; key &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; allowed_fields:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        val &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; extracted&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(key, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; isinstance(val, str) &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; val&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strip():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            patch[key] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; val&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strip()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; patch &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; patch &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;the-results&#34;&gt;The Results&lt;/h3&gt;
&lt;p&gt;Running the enrichment script across all 87 records:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Outcome&lt;/th&gt;
          &lt;th&gt;Count&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Records enriched&lt;/td&gt;
          &lt;td&gt;48&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;List/aggregator pages (correctly skipped)&lt;/td&gt;
          &lt;td&gt;12&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;No extractable fields (social media, OpenReview, etc.)&lt;/td&gt;
          &lt;td&gt;11&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Errors (timeouts, HTTP 403s)&lt;/td&gt;
          &lt;td&gt;16&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;After enrichment:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Field&lt;/th&gt;
          &lt;th&gt;Records populated&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Date&lt;/td&gt;
          &lt;td&gt;42&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Location&lt;/td&gt;
          &lt;td&gt;41&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Tags&lt;/td&gt;
          &lt;td&gt;47&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Organizer&lt;/td&gt;
          &lt;td&gt;27&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Price&lt;/td&gt;
          &lt;td&gt;4&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;From zero structured data to a directory where most events have dates, locations, and topic tags — without opening a single conference website manually.&lt;/p&gt;
&lt;p&gt;Some highlights from the extraction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NeurIPS 2026:&lt;/strong&gt; December 6-12, Sydney, Australia — Deep Learning, Research, Algorithms, LLMs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CVPR 2026:&lt;/strong&gt; June 3-7, Denver, CO — Computer Vision, Deep Learning, Research&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ICML 2026:&lt;/strong&gt; July 6-11, Seoul, South Korea — LLMs, Computer Vision, NLP, Robotics&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Council 2026:&lt;/strong&gt; May 12-14, San Francisco, CA — Generative AI, ML Ops, AI Safety&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MIDL 2026:&lt;/strong&gt; July 8-10, Taipei — Deep Learning, Healthcare AI, Computer Vision&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-pipeline-today&#34;&gt;The Pipeline Today&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s what the full system looks like now:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;SearXNG (self-hosted search)
  → conference_tracker.py (Python — discovers conferences)
    → Airtable (source of truth — 87 records)
      → enrich_conferences.py (Python — AI-powered field extraction)
        → Airtable (now with dates, locations, tags)
          → fetch-events.mjs (Node — build-time data fetch)
            → data.ts (bundled into the site)
              → React + Vite app on Netlify
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The tracker discovers. The enricher structures. The fetcher delivers. The site displays. Each piece runs independently and can be re-run at any time.&lt;/p&gt;
&lt;p&gt;The enrichment script is idempotent — it only processes records where the &lt;code&gt;date&lt;/code&gt; field is empty, so running it again only touches new or previously-failed records.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-id-do-differently-and-whats-next&#34;&gt;What I&amp;rsquo;d Do Differently (And What&amp;rsquo;s Next)&lt;/h2&gt;
&lt;h3 id=&#34;the-timeout-problem&#34;&gt;The Timeout Problem&lt;/h3&gt;
&lt;p&gt;About 16 records hit the 25-second inference timeout. The fast tier (Haiku) is quick but occasionally chokes on pages with dense, complex content. A retry mechanism using the standard tier (Sonnet) for failed records would catch most of these.&lt;/p&gt;
&lt;h3 id=&#34;missing-table-columns&#34;&gt;Missing Table Columns&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;venue&lt;/code&gt; and &lt;code&gt;imageUrl&lt;/code&gt; fields don&amp;rsquo;t exist in the Airtable table yet. The enrichment script extracts venue names beautifully (The Venetian for Ai4, COEX Convention Center for ICML, Dongguk University for AAAI Summer), but the data gets dropped because the columns aren&amp;rsquo;t there. A quick table schema update in the Airtable UI fixes this.&lt;/p&gt;
&lt;h3 id=&#34;scheduled-runs&#34;&gt;Scheduled Runs&lt;/h3&gt;
&lt;p&gt;Right now, both the tracker and enricher are manual. The natural next step is scheduling — run the tracker daily to discover new conferences, the enricher on new records, and trigger a Netlify deploy afterward. The Netlify build hook is already configured; it just needs a cron job or GitHub Action to call it.&lt;/p&gt;
&lt;h3 id=&#34;data-quality&#34;&gt;Data Quality&lt;/h3&gt;
&lt;p&gt;Some records are noise — Reddit discussion threads, Amazon Science blog posts, Twitter/X profiles. A quality filter (either rule-based on URL patterns or AI-powered) would clean the dataset before enrichment runs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;lessons-learned&#34;&gt;Lessons Learned&lt;/h2&gt;
&lt;h3 id=&#34;1-eliminate-middlemen-early&#34;&gt;1. Eliminate Middlemen Early&lt;/h3&gt;
&lt;p&gt;Google Sheets added zero value once Airtable was in the picture. But it lingered because it was the &amp;ldquo;original&amp;rdquo; approach. Every extra hop in a pipeline is a thing that can break, a thing that needs syncing, and a thing that slows you down. Cut it.&lt;/p&gt;
&lt;h3 id=&#34;2-build-time-data-fetching-is-underrated&#34;&gt;2. Build-Time Data Fetching Is Underrated&lt;/h3&gt;
&lt;p&gt;Pulling data at build time instead of runtime means no API keys in the browser, no loading spinners, and no CORS headaches. For data that changes daily (not per-second), this is the right architecture.&lt;/p&gt;
&lt;h3 id=&#34;3-ai-extraction-beats-manual-curation&#34;&gt;3. AI Extraction Beats Manual Curation&lt;/h3&gt;
&lt;p&gt;Using AI to extract structured data from unstructured web pages isn&amp;rsquo;t perfect — we got 48 out of 87 records enriched, not 87 out of 87. But it took 20 minutes of runtime versus what would have been hours of manual work. And the script is re-runnable. Improvement is incremental.&lt;/p&gt;
&lt;h3 id=&#34;4-detect-your-datas-shape-before-writing&#34;&gt;4. Detect Your Data&amp;rsquo;s Shape Before Writing&lt;/h3&gt;
&lt;p&gt;The Airtable 422 errors on &lt;code&gt;venue&lt;/code&gt; were entirely preventable. The enrichment script now probes the table schema at startup and only writes to fields that exist. Defensive coding at system boundaries saves debugging time.&lt;/p&gt;
&lt;h3 id=&#34;5-list-page-detection-is-essential-for-web-scraping-pipelines&#34;&gt;5. List Page Detection Is Essential for Web Scraping Pipelines&lt;/h3&gt;
&lt;p&gt;When you&amp;rsquo;re scraping URLs from search results, a significant percentage will be aggregator pages (&amp;ldquo;Top 10 Best AI Conferences&amp;rdquo;) rather than individual event pages. If you don&amp;rsquo;t detect and skip these, you&amp;rsquo;ll corrupt your dataset with merged data from multiple events. The &lt;code&gt;is_list_page&lt;/code&gt; flag in the AI extraction prompt was one of the highest-value additions to the whole pipeline.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-bigger-picture&#34;&gt;The Bigger Picture&lt;/h2&gt;
&lt;p&gt;This project is a miniature version of a pattern I keep coming back to: &lt;strong&gt;systems that compound.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The tracker runs once and discovers 87 conferences. The enricher runs once and structures 48 of them. The next time the tracker runs, it discovers only &lt;em&gt;new&lt;/em&gt; conferences (deduplication handles the rest). The next time the enricher runs, it only processes records it hasn&amp;rsquo;t touched yet.&lt;/p&gt;
&lt;p&gt;Every run makes the dataset better without redoing previous work. That&amp;rsquo;s the whole point of building infrastructure instead of doing things manually — you invest upfront so the system improves over time with minimal additional effort.&lt;/p&gt;
&lt;p&gt;Working with Claude through PAI made each layer come together faster than I expected. The tracker, the Airtable integration, the Google Sheets elimination, the enrichment script — each was a focused session where the AI handled the implementation details while I focused on architecture decisions.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the augmented part of Augmented Resilience. Not replacing the thinking — amplifying it.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>RAG, Agents, and Skills: The Three Pillars Inside My Personal AI</title>
      <link>/posts/augmented-resilience-posts/rag-agents-and-skills---the-three-pillars-inside-my-personal-ai/</link>
      <pubDate>Tue, 24 Feb 2026 00:00:00 +0000</pubDate>
      
      <guid>/posts/augmented-resilience-posts/rag-agents-and-skills---the-three-pillars-inside-my-personal-ai/</guid>
      <description>&lt;h1 id=&#34;rag-agents-and-skills-the-three-pillars-inside-my-personal-ai&#34;&gt;RAG, Agents, and Skills: The Three Pillars Inside My Personal AI&lt;/h1&gt;
&lt;p&gt;This site — Augmented Resilience — didn&amp;rsquo;t get built the way most blogs do. There was no staring at blank Hugo config files, no manually hunting down Namecheap SSH docs, no scrambling to remember whether the deploy script needed the &lt;code&gt;public/&lt;/code&gt; folder cleaned before each build.&lt;/p&gt;
&lt;p&gt;Instead, I described what I wanted. The AI knew my hosting setup (Namecheap shared hosting), my stack (Hugo with the re-terminal theme), my repo (GitHub, SSH-keyed), and my editor (Obsidian). When a build error surfaced — a theme name mismatch between &lt;code&gt;hugo.toml&lt;/code&gt; and the actual directory — it was diagnosed and fixed before I had time to Google it. When the deploy script needed writing, it was scaffolded against my specific environment. When I accidentally left sensitive data in an early draft, it caught it before the commit.&lt;/p&gt;</description>
      <content>&lt;h1 id=&#34;rag-agents-and-skills-the-three-pillars-inside-my-personal-ai&#34;&gt;RAG, Agents, and Skills: The Three Pillars Inside My Personal AI&lt;/h1&gt;
&lt;p&gt;This site — Augmented Resilience — didn&amp;rsquo;t get built the way most blogs do. There was no staring at blank Hugo config files, no manually hunting down Namecheap SSH docs, no scrambling to remember whether the deploy script needed the &lt;code&gt;public/&lt;/code&gt; folder cleaned before each build.&lt;/p&gt;
&lt;p&gt;Instead, I described what I wanted. The AI knew my hosting setup (Namecheap shared hosting), my stack (Hugo with the re-terminal theme), my repo (GitHub, SSH-keyed), and my editor (Obsidian). When a build error surfaced — a theme name mismatch between &lt;code&gt;hugo.toml&lt;/code&gt; and the actual directory — it was diagnosed and fixed before I had time to Google it. When the deploy script needed writing, it was scaffolded against my specific environment. When I accidentally left sensitive data in an early draft, it caught it before the commit.&lt;/p&gt;
&lt;p&gt;None of that context lived in the prompt. It lived in the infrastructure.&lt;/p&gt;
&lt;p&gt;The system behind it is called &lt;strong&gt;PAI (Personal AI Infrastructure)&lt;/strong&gt; — an open-source framework I run locally on top of Claude Code. And the reason it could handle an entire site build end-to-end without constant hand-holding comes down to three architectural pillars: &lt;strong&gt;RAG&lt;/strong&gt;, &lt;strong&gt;Agents&lt;/strong&gt;, and &lt;strong&gt;Skills&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-is-pai&#34;&gt;What Is PAI?&lt;/h2&gt;
&lt;p&gt;PAI is an open-source personal AI infrastructure system that runs on top of Claude Code. It&amp;rsquo;s not a SaaS product — it&amp;rsquo;s a framework you install on your own machine. The system is built around a central idea: &lt;strong&gt;AI systems need structure to be reliable&lt;/strong&gt;. Like scaffolding supports construction, PAI provides the architectural patterns that make AI assistance consistent, contextual, and capable of compounding over time.&lt;/p&gt;
&lt;p&gt;There are 34 skills installed on my system, 17 event hooks, 141 workflows, and a memory system that learns from every interaction. But none of that would matter without three core mechanisms working in concert.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/images/2026-02-24_18-07-16.png&#34; alt=&#34;Image Description&#34;&gt;
&lt;em&gt;The PAI statusline — live system stats showing version (v2.4), algorithm (ALG:v0.2.25), skill count (SK: 34), workflows (WF: 141), hooks (17), context usage (48%), memory signals (144 ratings), and a rolling quality score trend.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;pillar-1-rag--your-personal-knowledge-base-in-every-response&#34;&gt;Pillar 1: RAG — Your Personal Knowledge Base In Every Response&lt;/h2&gt;
&lt;p&gt;RAG (Retrieval-Augmented Generation) is the pattern of &lt;em&gt;retrieving&lt;/em&gt; relevant documents from a knowledge store and &lt;em&gt;augmenting&lt;/em&gt; the AI&amp;rsquo;s prompt with that context before generating a response. In enterprise AI, this is how you get a chatbot that can answer questions about your internal policies without hallucinating.&lt;/p&gt;
&lt;p&gt;In PAI, RAG is the engine that makes the AI feel like it &lt;em&gt;knows&lt;/em&gt; you.&lt;/p&gt;
&lt;h3 id=&#34;how-it-works-in-pai&#34;&gt;How It Works in PAI&lt;/h3&gt;
&lt;p&gt;When a session starts, PAI&amp;rsquo;s hook system loads a foundational context layer: my identity, my name, the current date, and the core behavioral rules (the Algorithm). This is the retrieval index — a lightweight map of everything the system knows how to find.&lt;/p&gt;
&lt;p&gt;When I make a request, the system retrieves additional context on demand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Skills frontmatter&lt;/strong&gt; — Each of the 34 skills has a &lt;code&gt;description&lt;/code&gt; field with a &lt;code&gt;USE WHEN&lt;/code&gt; clause. These descriptions load at startup as a routing index. When my request matches a skill&amp;rsquo;s intent, the full skill content loads. This is retrieval — pulling in the right expertise document for the task.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;USER/ context files&lt;/strong&gt; — There&amp;rsquo;s a structured personal knowledge base living at &lt;code&gt;~/.claude/skills/PAI/USER/&lt;/code&gt;. It contains my resume, my TELOS life goals, my contacts, my projects, my tech stack preferences. When I ask a question where my professional background is relevant, that context gets retrieved and injected.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MEMORY/ directory&lt;/strong&gt; — Every session, every correction, every insight gets captured in a structured memory system organized into &lt;code&gt;WORK/&lt;/code&gt;, &lt;code&gt;LEARNING/&lt;/code&gt;, &lt;code&gt;SIGNALS/&lt;/code&gt;, and &lt;code&gt;RESEARCH/&lt;/code&gt; directories. Past work items, completed tasks, and quality signals from previous interactions can all be retrieved to inform the current one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hook-injected context&lt;/strong&gt; — Event hooks fire at specific moments (session start, before each prompt, after tool use) and inject dynamic context — things like the current depth classification, relevant behavioral rules, or system state.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;practical-scenario-building-augmented-resilience&#34;&gt;Practical Scenario: Building Augmented Resilience&lt;/h3&gt;
&lt;p&gt;When I was setting up this site and ran into the Hugo theme mismatch error, here&amp;rsquo;s what PAI retrieved without me explaining any of it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;My tech stack preferences from the USER context — Hugo, GitHub, Namecheap, Obsidian as editor&lt;/li&gt;
&lt;li&gt;The WebSavant skill loaded automatically (matched &amp;ldquo;Hugo site&amp;rdquo;, &amp;ldquo;deployment&amp;rdquo; intent), bringing with it Hugo-specific knowledge about theme configuration, &lt;code&gt;hugo.toml&lt;/code&gt; structure, and build pipelines&lt;/li&gt;
&lt;li&gt;My project context from MEMORY — the repo name, the hosting environment, decisions made in prior sessions about the deploy workflow&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By the time I described the error, PAI already knew the environment it was debugging. It didn&amp;rsquo;t need me to explain what kind of hosting I had, which theme I was using, or what my folder structure looked like. The retrieval layer had already assembled that context before a single word of the solution was written.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the difference RAG makes — not smarter AI, but &lt;em&gt;contextually equipped&lt;/em&gt; AI.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;pillar-2-skills--domain-expertise-that-activates-itself&#34;&gt;Pillar 2: Skills — Domain Expertise That Activates Itself&lt;/h2&gt;
&lt;p&gt;If RAG is how PAI &lt;em&gt;knows&lt;/em&gt; context, Skills are how PAI &lt;em&gt;does&lt;/em&gt; work. A skill is a self-contained expertise module that activates automatically based on intent, routes to the right workflow, and executes a structured procedure.&lt;/p&gt;
&lt;p&gt;Think of each skill as a senior specialist on call — and you never have to explicitly page them.&lt;/p&gt;
&lt;h3 id=&#34;the-anatomy-of-a-skill&#34;&gt;The Anatomy of a Skill&lt;/h3&gt;
&lt;p&gt;Every skill follows the same structure:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;SkillName/
├── SKILL.md          ← Routing layer (loads on invocation)
├── Workflows/        ← Step-by-step execution procedures
│   └── Create.md
│   └── Update.md
└── Tools/            ← CLI automation scripts (TypeScript)
    └── Generate.ts
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;SKILL.md&lt;/code&gt; file has two parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;YAML frontmatter&lt;/strong&gt; with a &lt;code&gt;USE WHEN&lt;/code&gt; clause — this is how Claude Code knows when to activate the skill&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow routing table&lt;/strong&gt; — once activated, this routes the request to the correct workflow file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The magic is in &lt;code&gt;USE WHEN&lt;/code&gt;. Here&amp;rsquo;s a simplified example from the OracleHCM skill:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;description&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Expert Oracle HCM Cloud troubleshooting and guidance. USE WHEN user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ae81ff&#34;&gt;mentions Oracle HCM, HCM Cloud, HDL, HCM Data Loader, Journey, Checklist,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ae81ff&#34;&gt;workflow approvals, autocomplete rules, fast formulas, security profiles...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I never have to say &amp;ldquo;use the Oracle HCM skill.&amp;rdquo; I just describe my problem in natural language. The intent matching system routes it.&lt;/p&gt;
&lt;h3 id=&#34;practical-scenario-configuring-seo-and-geo-for-augmentedresiliencecom&#34;&gt;Practical Scenario: Configuring SEO and GEO for augmentedresilience.com&lt;/h3&gt;
&lt;p&gt;Before the site went live, I needed proper SEO and GEO — Open Graph tags for social sharing, meta descriptions for search, canonical URLs, a sitemap, and schema.org structured data so AI-powered search engines like Perplexity, ChatGPT, and Claude could understand and cite the content accurately. None of that comes configured out of the box with the re-terminal theme. I said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Set up SEO and apply Generative Engine Optimization to augmentedresilience.com.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The system:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Activated the WebSavant skill&lt;/strong&gt; (matched &amp;ldquo;SEO&amp;rdquo; + &amp;ldquo;GEO&amp;rdquo; + &amp;ldquo;site&amp;rdquo; intent — no skill named, no flags set)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Routed to the SEO and AddSchema workflows&lt;/strong&gt; inside that skill&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Created the correct Hugo partial override&lt;/strong&gt; at &lt;code&gt;layouts/partials/extended_head.html&lt;/code&gt; — the exact injection point the re-terminal theme exposes without touching any theme files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Added the full Open Graph tag set&lt;/strong&gt; (&lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt;, &lt;code&gt;og:url&lt;/code&gt;, &lt;code&gt;og:type&lt;/code&gt;) wired to Hugo&amp;rsquo;s page variables&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Injected schema.org JSON-LD&lt;/strong&gt; for every page type: &lt;code&gt;WebSite&lt;/code&gt; and &lt;code&gt;Person&lt;/code&gt; on the homepage, &lt;code&gt;Article&lt;/code&gt; and &lt;code&gt;BreadcrumbList&lt;/code&gt; on every post, and &lt;code&gt;AboutPage&lt;/code&gt; on &lt;code&gt;/about&lt;/code&gt; — giving AI crawlers a machine-readable knowledge graph of the site&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Created &lt;code&gt;robots.txt&lt;/code&gt;&lt;/strong&gt; explicitly permitting GPTBot, ClaudeBot, PerplexityBot, and other AI crawlers — with the sitemap URL wired in&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configured &lt;code&gt;hugo.toml&lt;/code&gt;&lt;/strong&gt; for canonical URL generation and enabled the built-in sitemap output&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Without the skill, this is a day of Hugo documentation, schema.org spec-reading, and trial-and-error. With it, the full SEO and GEO stack was complete in a single pass — because the skill had already encoded where everything goes in Hugo&amp;rsquo;s directory structure, which schema types matter for which page contexts, and how to wire Hugo&amp;rsquo;s template variables into valid JSON-LD that AI search engines can actually parse.&lt;/p&gt;
&lt;h3 id=&#34;the-34-skills-i-have-installed&#34;&gt;The 34 Skills I Have Installed&lt;/h3&gt;
&lt;p&gt;My current skill roster includes tools for Oracle HCM support, security recon, OSINT research, browser automation, art generation, document processing, code generation, red teaming, and more. Each one is a packaged capability that activates without friction.&lt;/p&gt;
&lt;p&gt;The system is also designed to be extended — building a new skill means writing a &lt;code&gt;SKILL.md&lt;/code&gt; with a &lt;code&gt;USE WHEN&lt;/code&gt; clause, a workflow routing table, and the workflow files. The CreateSkill skill handles the scaffolding automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;pillar-3-agents--parallel-specialized-brains&#34;&gt;Pillar 3: Agents — Parallel Specialized Brains&lt;/h2&gt;
&lt;p&gt;Skills handle individual domain expertise. Agents handle &lt;em&gt;scale&lt;/em&gt; and &lt;em&gt;specialization&lt;/em&gt; when a task is too complex for a single pass or requires multiple perspectives simultaneously.&lt;/p&gt;
&lt;p&gt;PAI has a three-tier agent system:&lt;/p&gt;
&lt;h3 id=&#34;tier-1-task-tool-subagents-internal-workhorses&#34;&gt;Tier 1: Task Tool Subagents (Internal Workhorses)&lt;/h3&gt;
&lt;p&gt;These are pre-built specialist agents that skills and workflows invoke internally: &lt;code&gt;Engineer&lt;/code&gt;, &lt;code&gt;Architect&lt;/code&gt;, &lt;code&gt;Explore&lt;/code&gt;, &lt;code&gt;QATester&lt;/code&gt;, &lt;code&gt;Pentester&lt;/code&gt;, &lt;code&gt;ClaudeResearcher&lt;/code&gt;, &lt;code&gt;GeminiResearcher&lt;/code&gt;, &lt;code&gt;GrokResearcher&lt;/code&gt;, and others.&lt;/p&gt;
&lt;p&gt;When I ask PAI to research something deeply, it doesn&amp;rsquo;t just run one search. It can fan out to multiple research agents simultaneously — Claude, Gemini, and Grok each investigating from different angles — then synthesize the results with a &amp;ldquo;spotcheck&amp;rdquo; agent that verifies consistency.&lt;/p&gt;
&lt;p&gt;This is parallel processing that would take me hours of manual work, running in minutes.&lt;/p&gt;
&lt;h3 id=&#34;tier-2-named-agents-persistent-specialists&#34;&gt;Tier 2: Named Agents (Persistent Specialists)&lt;/h3&gt;
&lt;p&gt;Named agents are recurring characters with rich backstories, persistent identities, and unique voices via ElevenLabs text-to-speech. They build relationship continuity across sessions.&lt;/p&gt;
&lt;p&gt;My installed named agents include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Serena Blackwood&lt;/strong&gt; — Architect. Long-term system design decisions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Marcus Webb&lt;/strong&gt; — Engineer. Strategic technical leadership.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rook Blackburn&lt;/strong&gt; — Pentester. Security testing with a distinct personality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ava Sterling&lt;/strong&gt; — Researcher (Claude). Strategic deep-dive analysis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alex Rivera&lt;/strong&gt; — Researcher (Gemini). Multi-perspective comprehensive analysis.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When Rook runs a security assessment, he doesn&amp;rsquo;t just return findings — he announces them in his own voice through my speakers. It sounds minor. It&amp;rsquo;s not. Distinct voices make it cognitively easier to understand &lt;em&gt;who&lt;/em&gt; did what work and &lt;em&gt;why&lt;/em&gt; you should trust it.&lt;/p&gt;
&lt;h3 id=&#34;tier-3-custom-agents-on-demand-compositions&#34;&gt;Tier 3: Custom Agents (On-Demand Compositions)&lt;/h3&gt;
&lt;p&gt;For tasks that don&amp;rsquo;t fit a named agent, PAI can compose agents dynamically from trait combinations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Expertise traits:&lt;/strong&gt; &lt;code&gt;security&lt;/code&gt;, &lt;code&gt;legal&lt;/code&gt;, &lt;code&gt;finance&lt;/code&gt;, &lt;code&gt;medical&lt;/code&gt;, &lt;code&gt;research&lt;/code&gt;, &lt;code&gt;technical&lt;/code&gt;, &lt;code&gt;creative&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Personality traits:&lt;/strong&gt; &lt;code&gt;skeptical&lt;/code&gt;, &lt;code&gt;enthusiastic&lt;/code&gt;, &lt;code&gt;analytical&lt;/code&gt;, &lt;code&gt;contrarian&lt;/code&gt;, &lt;code&gt;meticulous&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Approach traits:&lt;/strong&gt; &lt;code&gt;thorough&lt;/code&gt;, &lt;code&gt;rapid&lt;/code&gt;, &lt;code&gt;systematic&lt;/code&gt;, &lt;code&gt;adversarial&lt;/code&gt;, &lt;code&gt;synthesizing&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each unique trait combination maps to a different ElevenLabs voice. A &lt;code&gt;security + adversarial&lt;/code&gt; agent gets Callum&amp;rsquo;s edgy voice. An &lt;code&gt;analytical + meticulous&lt;/code&gt; agent gets Charlotte&amp;rsquo;s precise cadence.&lt;/p&gt;
&lt;p&gt;The trait system means I can spin up a custom agent for any edge case without writing a new agent from scratch.&lt;/p&gt;
&lt;h3 id=&#34;practical-scenario-pre-launch-validation-of-augmented-resilience&#34;&gt;Practical Scenario: Pre-Launch Validation of Augmented Resilience&lt;/h3&gt;
&lt;p&gt;Before pushing the first real commit to augmentedresilience.com, I wasn&amp;rsquo;t going to just cross my fingers and run &lt;code&gt;deploy.py&lt;/code&gt;. I asked PAI to validate the site was actually ready. What happened next wasn&amp;rsquo;t a single check — it was a parallel review board.&lt;/p&gt;
&lt;p&gt;PAI spawned three agents simultaneously:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Rook Blackburn&lt;/strong&gt; (Pentester) scanned the entire repo for credentials, API keys, and sensitive data I might have accidentally left in a config file or draft post — and announced his findings in his own voice through my speakers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A QA agent&lt;/strong&gt; opened the Hugo local preview, walked every page, verified links weren&amp;rsquo;t broken, images loaded, and the deploy pipeline produced a clean &lt;code&gt;public/&lt;/code&gt; build&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A Researcher agent&lt;/strong&gt; audited the site&amp;rsquo;s meta tags, Open Graph data, and &lt;code&gt;hugo.toml&lt;/code&gt; settings against SEO best practices for a new blog&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A fourth &lt;strong&gt;spotcheck agent&lt;/strong&gt; then reviewed all three outputs for conflicts — did Rook&amp;rsquo;s findings overlap with anything the QA agent flagged? Were there config issues that touched both SEO and security?&lt;/p&gt;
&lt;p&gt;The result was a single consolidated pre-launch checklist. Two issues surfaced: a leftover draft post with personal notes still marked &lt;code&gt;draft: false&lt;/code&gt;, and a missing &lt;code&gt;og:image&lt;/code&gt; tag. Both fixed before the first visitor ever landed.&lt;/p&gt;
&lt;p&gt;The site you&amp;rsquo;re reading right now went live clean because three agents checked it in parallel before I touched the deploy button. That&amp;rsquo;s the difference between asking a question and deploying intelligence.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;when-all-three-work-together&#34;&gt;When All Three Work Together&lt;/h2&gt;
&lt;p&gt;The real power of PAI isn&amp;rsquo;t any single pillar — it&amp;rsquo;s the composition.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what happened when I needed to go from &amp;ldquo;Obsidian draft&amp;rdquo; to &amp;ldquo;live on augmentedresilience.com&amp;rdquo; without a manual process I&amp;rsquo;d eventually forget or skip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RAG&lt;/strong&gt; assembled the context before I described the problem. From MEMORY it already knew: Namecheap shared hosting doesn&amp;rsquo;t support native &lt;code&gt;git pull&lt;/code&gt; — content has to be pushed via FTP through GitHub Actions. It knew the Obsidian vault was the content source, that &lt;code&gt;images.py&lt;/code&gt; had to run before &lt;code&gt;hugo&lt;/code&gt; to convert image links, and that Python was the right tool for the orchestration script. Not one of those constraints was in my prompt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skills&lt;/strong&gt; handled the architecture. WebSavant recognized a Hugo deployment pipeline request and routed to a workflow already aware of the full delivery chain: sync posts from Obsidian → convert images → &lt;code&gt;hugo&lt;/code&gt; build → &lt;code&gt;git commit&lt;/code&gt; → &lt;code&gt;git push&lt;/code&gt; → GitHub Actions → Namecheap FTP. It knew the sequence. It knew why each step had to happen in that order.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agents&lt;/strong&gt; built it. An Engineer agent wrote &lt;code&gt;deploy.py&lt;/code&gt; — the script that runs the whole sequence in a single command. An Architect agent designed the GitHub Actions workflow that picks up after the push and handles the Namecheap delivery step automatically. Two agents, two distinct responsibilities, running the job that a solo developer would have spent an afternoon piecing together from Stack Overflow answers.&lt;/p&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python3 deploy.py &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Add new post&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. One command. Every post that has ever gone live on this site — including this one — passed through a pipeline that RAG, Skills, and Agents built together. It&amp;rsquo;s not running because I set it up manually. It&amp;rsquo;s running because three systems knew what the job required before I finished explaining it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-compounding-effect&#34;&gt;The Compounding Effect&lt;/h2&gt;
&lt;p&gt;What makes this architecture meaningful over time isn&amp;rsquo;t any single interaction — it&amp;rsquo;s that the system gets better at helping you with every session.&lt;/p&gt;
&lt;p&gt;The MEMORY system captures learnings. The SIGNALS directory tracks your implicit feedback. When something goes wrong, it logs the full context under LEARNING/FAILURES. When a workflow produces a 9-10 response, that signal is captured too. The system adjusts.&lt;/p&gt;
&lt;p&gt;Generic AI starts fresh every time. PAI compounds.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m still early in this — the personal profile files still have template placeholders I haven&amp;rsquo;t filled in, and there are skills I&amp;rsquo;ve barely touched. But even at partial configuration, the system already thinks more like a senior colleague than a search engine.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what RAG, Agents, and Skills make possible together: &lt;strong&gt;an AI that knows your context, activates the right expertise automatically, and scales parallel intelligence for complex work&lt;/strong&gt; — all without you having to manage the machinery.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>When Your PDF Workflow Breaks - Building a Markdown Converter with Claude Code</title>
      <link>/posts/augmented-resilience-posts/building-a-pdf-to-markdown-converter-with-claude-code/</link>
      <pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate>
      
      <guid>/posts/augmented-resilience-posts/building-a-pdf-to-markdown-converter-with-claude-code/</guid>
      <description>&lt;h2 id=&#34;the-problem-pdfs-are-knowledge-prisons&#34;&gt;The Problem: PDFs Are Knowledge Prisons&lt;/h2&gt;
&lt;p&gt;You know that feeling when you download a brilliant research paper, only to realize you can&amp;rsquo;t easily feed it into your AI workflow? Or when you want to add documentation to your knowledge base, but it&amp;rsquo;s locked in a format that doesn&amp;rsquo;t play well with version control or LLM tools?&lt;/p&gt;
&lt;p&gt;Yeah, I was there last week.&lt;/p&gt;
&lt;p&gt;I had just downloaded a fascinating 1.3MB research paper on Generative Engine Optimization and wanted to process it with my AI tools. But PDFs are terrible for this. They&amp;rsquo;re designed for &lt;em&gt;printing&lt;/em&gt;, not for &lt;em&gt;processing&lt;/em&gt;. What I needed was Markdown—clean, portable, AI-friendly Markdown.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem-pdfs-are-knowledge-prisons&#34;&gt;The Problem: PDFs Are Knowledge Prisons&lt;/h2&gt;
&lt;p&gt;You know that feeling when you download a brilliant research paper, only to realize you can&amp;rsquo;t easily feed it into your AI workflow? Or when you want to add documentation to your knowledge base, but it&amp;rsquo;s locked in a format that doesn&amp;rsquo;t play well with version control or LLM tools?&lt;/p&gt;
&lt;p&gt;Yeah, I was there last week.&lt;/p&gt;
&lt;p&gt;I had just downloaded a fascinating 1.3MB research paper on Generative Engine Optimization and wanted to process it with my AI tools. But PDFs are terrible for this. They&amp;rsquo;re designed for &lt;em&gt;printing&lt;/em&gt;, not for &lt;em&gt;processing&lt;/em&gt;. What I needed was Markdown—clean, portable, AI-friendly Markdown.&lt;/p&gt;
&lt;p&gt;So I built a converter. And with Claude Code as my copilot through the PAI (Personal AI Infrastructure) system, the whole thing took less than 30 minutes.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how it went down.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;why-markdown-is-better-than-pdf-for-llms&#34;&gt;Why Markdown is Better Than PDF for LLMs&lt;/h2&gt;
&lt;p&gt;Before diving into the build, let&amp;rsquo;s answer the obvious question: &lt;em&gt;why bother converting?&lt;/em&gt; Can&amp;rsquo;t LLMs just read PDFs directly?&lt;/p&gt;
&lt;p&gt;Technically, yes. But the results are significantly worse, and the reasons are fundamental to how PDFs work.&lt;/p&gt;
&lt;h3 id=&#34;pdfs-are-layout-first-not-structure-first&#34;&gt;PDFs Are Layout-First, Not Structure-First&lt;/h3&gt;
&lt;p&gt;PDFs were designed to describe &lt;em&gt;where things appear on a page&lt;/em&gt;, not &lt;em&gt;what they mean&lt;/em&gt;. As Steven Howard explains in &lt;a href=&#34;https://untetheredai.substack.com/p/why-pdfs-fail-under-llm-parsing&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Why PDFs Fail Under LLM Parsing&lt;/a&gt;
:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Table cells with wrapped text insert hard line breaks that fragment token continuity and break logical row recognition. Headers and footers simply add noise to the context when used with LLMs. Sentences are split with arbitrary CR/LFs making it very difficult to find paragraph boundaries.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This architectural mismatch — a format designed for printing being fed into a system designed for understanding — causes cascading problems downstream.&lt;/p&gt;
&lt;h3 id=&#34;the-token-efficiency-problem&#34;&gt;The Token Efficiency Problem&lt;/h3&gt;
&lt;p&gt;Every token your LLM processes costs money and consumes context window space. PDF extraction wastes both.&lt;/p&gt;
&lt;p&gt;According to analysis from &lt;a href=&#34;https://markdownconverters.com/blog/pdf-vs-markdown-ai-tokens&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;MarkdownConverters&lt;/a&gt;
, &lt;strong&gt;Markdown saves up to 70% more tokens compared to extracted PDF text&lt;/strong&gt; for the same content. The culprit: PDF extraction introduces formatting artifacts, metadata noise, headers/footers, and encoding remnants that all consume tokens without adding semantic value.&lt;/p&gt;
&lt;p&gt;To put that in practical terms: a PDF that would use 10,000 tokens might only need 3,000 tokens when properly converted to Markdown. At scale, this compounds dramatically.&lt;/p&gt;
&lt;h3 id=&#34;the-rag-performance-problem&#34;&gt;The RAG Performance Problem&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re building Retrieval Augmented Generation (RAG) systems — using documents as a knowledge base for AI — document format directly impacts answer quality.&lt;/p&gt;
&lt;p&gt;The research here is compelling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Academic validation&lt;/strong&gt;: A 2024 paper on arXiv (&lt;a href=&#34;https://arxiv.org/abs/2401.12599&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Revolutionizing RAG with Enhanced PDF Structure Recognition&lt;/a&gt;
) found that &amp;ldquo;the low accuracy of PDF parsing significantly impacts the effectiveness of professional knowledge-based QA.&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Industry validation&lt;/strong&gt;: NVIDIA&amp;rsquo;s technical blog documents how their NeMo Retriever pipeline converts extracted content to Markdown specifically because it &amp;ldquo;preserves row/column relationships in an LLM-native format, significantly reducing numeric hallucination&amp;rdquo; — and &lt;strong&gt;reduces incorrect answers by 50%&lt;/strong&gt;. (&lt;a href=&#34;https://developer.nvidia.com/blog/approaches-to-pdf-data-extraction-for-information-retrieval/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;NVIDIA: Approaches to PDF Data Extraction for Information Retrieval&lt;/a&gt;
)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Chunking quality&lt;/strong&gt;: Analysis from &lt;a href=&#34;https://medium.com/data-science/improved-rag-document-processing-with-markdown-426a2e0dd82b&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Towards Data Science&lt;/a&gt;
 shows that Markdown&amp;rsquo;s heading structure (&lt;code&gt;#&lt;/code&gt;, &lt;code&gt;##&lt;/code&gt;, &lt;code&gt;###&lt;/code&gt;) produces semantically meaningful chunks, while PDF-based chunking relies on arbitrary page breaks and heuristics.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Retrieval failure rates&lt;/strong&gt;: Unstructured.io&amp;rsquo;s &lt;a href=&#34;https://unstructured.io/blog/contextual-chunking-in-unstructured-platform-boost-your-rag-retrieval-accuracy&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;research on contextual chunking&lt;/a&gt;
 — tested across 5,563 question-answer pairs — showed an &lt;strong&gt;84% reduction in retrieval failure rates&lt;/strong&gt; when using structure-aware chunking (the kind Markdown enables natively).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Real-world outcomes&lt;/strong&gt;: The 2025 Semrush AI Index, cited by &lt;a href=&#34;https://developer.webex.com/blog/boosting-ai-performance-the-power-of-llm-friendly-content-in-markdown&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Webex Developers Blog&lt;/a&gt;
, found that 72% of top AI-indexed articles used Markdown or Markdown-like structures, achieving &lt;strong&gt;34% higher retrieval accuracy&lt;/strong&gt; across ChatGPT, Perplexity, and Gemini.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;the-bottom-line&#34;&gt;The Bottom Line&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Metric&lt;/th&gt;
          &lt;th&gt;Impact&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Token reduction&lt;/td&gt;
          &lt;td&gt;Up to 70% fewer tokens vs PDF extraction&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Incorrect answers in RAG&lt;/td&gt;
          &lt;td&gt;50% reduction (NVIDIA NeMo)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Retrieval failure rates&lt;/td&gt;
          &lt;td&gt;84% reduction (Unstructured.io)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Retrieval accuracy&lt;/td&gt;
          &lt;td&gt;34% higher (Semrush AI Index 2025)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Markdown isn&amp;rsquo;t just more convenient — it&amp;rsquo;s meaningfully better for AI. Converting your document libraries is one of the highest-ROI steps you can take before building any LLM-powered workflow.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-first-failure-when-bleeding-edge-python-bites-back&#34;&gt;The First Failure: When Bleeding-Edge Python Bites Back&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m running Python 3.14.2—the latest release, barely a few weeks old. Modern, shiny, cutting-edge. Perfect, right?&lt;/p&gt;
&lt;p&gt;Not quite.&lt;/p&gt;
&lt;p&gt;My first instinct was to use &lt;code&gt;marker-pdf&lt;/code&gt;, a high-performance converter optimized for scientific papers and books. It looked perfect on paper (pun intended). But when I tried to install it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Building wheel for Pillow (pyproject.toml): finished with status &amp;#39;error&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ugh.&lt;/p&gt;
&lt;p&gt;Turns out, &lt;code&gt;marker-pdf&lt;/code&gt; depends on Pillow (the Python imaging library), and Pillow hasn&amp;rsquo;t built binary wheels for Python 3.14 yet. I could have downgraded Python. I could have fought with source compilation. But why?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is where working with Claude Code really shines.&lt;/strong&gt; Instead of going down a rabbit hole trying to force marker-pdf to work, Claude suggested pivoting to &lt;strong&gt;PyMuPDF4LLM&lt;/strong&gt;—a mature, actively maintained library specifically designed for AI/LLM workflows.&lt;/p&gt;
&lt;p&gt;And it just worked.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-solution-pymupdf4llm&#34;&gt;The Solution: PyMuPDF4LLM&lt;/h2&gt;
&lt;p&gt;PyMuPDF4LLM turned out to be exactly what I needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Works flawlessly with Python 3.14 (no compilation errors)&lt;/li&gt;
&lt;li&gt;Fast and accurate conversion&lt;/li&gt;
&lt;li&gt;Built specifically for feeding documents into LLMs&lt;/li&gt;
&lt;li&gt;Clean, simple API&lt;/li&gt;
&lt;li&gt;Actively maintained by the PyMuPDF team&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The installation was literally:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install pymupdf4llm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Five seconds later, I was ready to go.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;building-the-tool-first-principles-thinking&#34;&gt;Building the Tool: First Principles Thinking&lt;/h2&gt;
&lt;p&gt;As someone new to the CLI world, I&amp;rsquo;ve been learning to think through project structure from first principles. Where should this live? How should it be organized?&lt;/p&gt;
&lt;p&gt;With Claude&amp;rsquo;s guidance, I chose &lt;code&gt;/Users/dsa/projects/pdf-to-markdown/&lt;/code&gt; for a few key reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Separation of Concerns:&lt;/strong&gt; Tool projects should be separate from my main workspace&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discoverability:&lt;/strong&gt; Clear, descriptive naming means I&amp;rsquo;ll find it again in 6 months&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reusability:&lt;/strong&gt; This structure works both as a CLI tool AND as a library I could import later&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The project structure ended up simple but complete:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pdf-to-markdown/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── README.md              # Documentation
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── venv/                  # Isolated Python environment
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── input/                 # Test PDFs
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── output/                # Generated markdown
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── pdf2md                 # CLI wrapper script
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;└── requirements.txt       # Dependencies
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;the-code-a-simple-but-powerful-cli&#34;&gt;The Code: A Simple but Powerful CLI&lt;/h2&gt;
&lt;p&gt;I wanted a tool I could actually use—something with a clean command-line interface that handles the common cases elegantly. Working with Claude through PAI, we created a Python script that does exactly that:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;PDF to Markdown Converter
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;A simple CLI tool to convert PDF files to Markdown using PyMuPDF4LLM
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; sys
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; os
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; pathlib &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Path
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; pymupdf4llm
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; pymupdf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; tqdm &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; tqdm
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;convert_pdf_to_markdown&lt;/span&gt;(pdf_path: str, output_path: str &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Convert a PDF file to Markdown format.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; os&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;exists(pdf_path):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;FileNotFoundError&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;PDF file not found: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;pdf_path&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# Get page count for progress bar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    doc &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pymupdf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;open(pdf_path)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page_count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; doc&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;page_count
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    doc&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;close()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Converting: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;pdf_path&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; tqdm(total&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;page_count, unit&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;page&amp;#34;&lt;/span&gt;, desc&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Processing&amp;#34;&lt;/span&gt;, colour&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;blue&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; bar:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        md_text &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pymupdf4llm&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;to_markdown(pdf_path, page_chunks&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        bar&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;n &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; page_count
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        bar&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;refresh()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; output_path &lt;span style=&#34;color:#f92672&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        output_path &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Path(pdf_path)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;with_suffix(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;.md&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; open(output_path, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;w&amp;#39;&lt;/span&gt;, encoding&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;utf-8&amp;#39;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; f:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write(md_text)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;✓ Done: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;output_path&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;  (&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;len(md_text)&lt;span style=&#34;color:#e6db74&#34;&gt;:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;,&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; characters)&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; str(output_path)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;batch_convert&lt;/span&gt;(input_dir: str, output_dir: str &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Convert all PDFs in a directory to Markdown.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    input_path &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Path(input_dir)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; input_path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;is_dir():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;NotADirectoryError&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Not a directory: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;input_dir&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pdfs &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; sorted(input_path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;glob(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;*.pdf&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; pdfs:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;No PDF files found in: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;input_dir&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        sys&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;exit(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; output_dir:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        output_dir &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Path(output_dir)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        output_dir &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; input_path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parent &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;output&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    output_dir&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;mkdir(parents&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;, exist_ok&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    total &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; len(pdfs)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    succeeded &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    failed &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;Batch mode: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;total&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; PDF(s) found in &amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;input_dir&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Output folder: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;output_dir&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i, pdf_path &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; enumerate(pdfs, start&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;i&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;total&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;] &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;pdf_path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        output_path &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; output_dir &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; pdf_path&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;with_suffix(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;.md&amp;#39;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            convert_pdf_to_markdown(str(pdf_path), str(output_path))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            succeeded &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Exception&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;  ✗ Failed: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;e&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            failed &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;─&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;40&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Batch complete: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;succeeded&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; converted, &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;failed&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; failed&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Output folder: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;output_dir&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Main CLI entry point&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    args &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; sys&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;argv[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;:]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; args:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Usage:&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;  pdf2md &amp;lt;input.pdf&amp;gt; [output.md]                    # Convert a single PDF&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;  pdf2md --batch &amp;lt;folder/&amp;gt;                          # Convert all PDFs in a folder&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;  pdf2md --batch &amp;lt;folder/&amp;gt; --output &amp;lt;out_folder/&amp;gt;   # Batch with custom output dir&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;Examples:&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;  pdf2md document.pdf                               # Creates document.md&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;  pdf2md document.pdf custom.md                     # Creates custom.md&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;  pdf2md --batch input/                             # Converts all PDFs in input/&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;  pdf2md --batch ~/documents/pdfs/ --output ~/knowledge-base/docs/&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        sys&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;exit(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; args[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--batch&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        input_dir &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; args[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        output_dir &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--output&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; args:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            idx &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; args&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;index(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--output&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            output_dir &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; args[idx &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        batch_convert(input_dir, output_dir)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        pdf_path &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; args[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        output_path &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; args[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;] &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; len(args) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        convert_pdf_to_markdown(pdf_path, output_path)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; __name__ &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    main()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What I love about this code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Smart defaults:&lt;/strong&gt; If you don&amp;rsquo;t specify an output path, it just replaces &lt;code&gt;.pdf&lt;/code&gt; with &lt;code&gt;.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Progress bars:&lt;/strong&gt; &lt;code&gt;tqdm&lt;/code&gt; gives you a blue progress bar with page count&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch mode:&lt;/strong&gt; &lt;code&gt;--batch&lt;/code&gt; processes an entire folder at once, with optional &lt;code&gt;--output&lt;/code&gt; target&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Helpful errors:&lt;/strong&gt; Clear messages when things go wrong&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible usage:&lt;/strong&gt; Works with relative paths, absolute paths, custom output names&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make it executable:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;chmod +x pdf2md
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And now it&amp;rsquo;s a proper command-line tool.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-moment-of-truth-testing-with-real-data&#34;&gt;The Moment of Truth: Testing with Real Data&lt;/h2&gt;
&lt;p&gt;Theory is great. But does it actually work?&lt;/p&gt;
&lt;p&gt;I grabbed that 1.3MB research paper on Generative Engine Optimization and ran:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python pdf2md input/test.pdf output/test.md
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The output:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Converting input/test.pdf to Markdown...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Processing: 100%|████████████████| 12/12 [00:02&amp;lt;00:00,  5.8 pages/s]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;✓ Done: output/test.md  (73,463 characters)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;1.3MB PDF → 74KB of clean Markdown in seconds.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I opened the output file, and there it was—perfectly formatted markdown:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## **GEO: Generative Engine Optimization**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Pranjal Aggarwal [∗]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Indian Institute of Technology Delhi
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;New Delhi, India
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pranjal2041@gmail.com
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Ashwin Kalyan
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Independent
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Seattle, USA
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;asaavashwin@gmail.com
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Headers, formatting, structure—all preserved. No manual cleanup needed.&lt;/p&gt;
&lt;p&gt;Success.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-this-unlocks&#34;&gt;What This Unlocks&lt;/h2&gt;
&lt;p&gt;Now that I have PDFs converting to Markdown reliably, a whole world of possibilities opens up:&lt;/p&gt;
&lt;h3 id=&#34;ai-workflows&#34;&gt;AI Workflows&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Feed research papers and documentation directly into Claude or other LLMs&lt;/li&gt;
&lt;li&gt;Build RAG (Retrieval Augmented Generation) pipelines backed by your document library&lt;/li&gt;
&lt;li&gt;Process technical documentation at scale without losing structure&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;knowledge-management&#34;&gt;Knowledge Management&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Import PDFs into your Obsidian vault automatically&lt;/li&gt;
&lt;li&gt;Version control document content (because it&amp;rsquo;s now plain text in git)&lt;/li&gt;
&lt;li&gt;Full-text search across your entire converted document library&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;automation-ideas&#34;&gt;Automation Ideas&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Watch folder that auto-converts any dropped PDFs&lt;/li&gt;
&lt;li&gt;Batch process entire directories of reports, papers, or manuals&lt;/li&gt;
&lt;li&gt;Feed converted markdown directly into a vector database&lt;/li&gt;
&lt;li&gt;API wrapper to convert PDFs via HTTP requests&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;lessons-learned-especially-for-cli-beginners&#34;&gt;Lessons Learned (Especially for CLI Beginners)&lt;/h2&gt;
&lt;h3 id=&#34;1-virtual-environments-are-non-negotiable&#34;&gt;1. Virtual Environments Are Non-Negotiable&lt;/h3&gt;
&lt;p&gt;Every Python project should live in its own virtual environment. Always:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python3 -m venv venv
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;source venv/bin/activate
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install --upgrade pip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This keeps dependencies isolated and projects reproducible.&lt;/p&gt;
&lt;h3 id=&#34;2-bleeding-edge-isnt-always-better&#34;&gt;2. Bleeding-Edge Isn&amp;rsquo;t Always Better&lt;/h3&gt;
&lt;p&gt;Python 3.14 is awesome, but sometimes mature tooling (like PyMuPDF) that &amp;ldquo;just works&amp;rdquo; beats bleeding-edge alternatives. Don&amp;rsquo;t be afraid to pivot when something doesn&amp;rsquo;t work.&lt;/p&gt;
&lt;h3 id=&#34;3-test-with-real-data&#34;&gt;3. Test With Real Data&lt;/h3&gt;
&lt;p&gt;I didn&amp;rsquo;t test with &amp;ldquo;hello.pdf&amp;rdquo; containing two sentences. I tested with a 1.3MB research paper. Real data reveals real issues (or in this case, confirms it works beautifully).&lt;/p&gt;
&lt;h3 id=&#34;4-document-as-you-build&#34;&gt;4. Document As You Build&lt;/h3&gt;
&lt;p&gt;Writing the README alongside the code made the project immediately understandable. Future-me will thank present-me.&lt;/p&gt;
&lt;h3 id=&#34;5-claude-code--pai--superpowers&#34;&gt;5. Claude Code + PAI = Superpowers&lt;/h3&gt;
&lt;p&gt;Working with Claude through the PAI infrastructure meant I had a senior developer helping me think through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Project structure (first principles)&lt;/li&gt;
&lt;li&gt;Library selection (when to pivot)&lt;/li&gt;
&lt;li&gt;Code organization (clean, maintainable)&lt;/li&gt;
&lt;li&gt;Real-world usage patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This wasn&amp;rsquo;t just coding faster—it was learning better patterns while building.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;usage-examples&#34;&gt;Usage Examples&lt;/h2&gt;
&lt;h3 id=&#34;basic-conversion&#34;&gt;Basic Conversion&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Activate environment first (always!)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;source venv/bin/activate
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Convert a PDF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python pdf2md document.pdf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Custom output name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python pdf2md research.pdf my-notes.md
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Full paths&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python pdf2md ~/Downloads/paper.pdf ~/Documents/notes.md
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;batch-processing&#34;&gt;Batch Processing&lt;/h3&gt;
&lt;p&gt;Convert an entire folder of PDFs:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;source venv/bin/activate
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Convert all PDFs in a folder (output goes to output/ by default)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python pdf2md --batch ~/documents/pdfs/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Convert to a specific knowledge base directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python pdf2md --batch ~/documents/pdfs/ --output ~/knowledge-base/docs/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;add-to-path-optional&#34;&gt;Add to PATH (Optional)&lt;/h3&gt;
&lt;p&gt;To use &lt;code&gt;pdf2md&lt;/code&gt; from anywhere:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Add to ~/.zshrc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;export PATH&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/Users/dsa/projects/pdf-to-markdown:&lt;/span&gt;$PATH&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Then run from anywhere&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pdf2md ~/Downloads/paper.pdf ~/Documents/paper.md
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s Next?&lt;/h2&gt;
&lt;p&gt;This tool works great as-is, but there are some exciting enhancements on the roadmap:&lt;/p&gt;
&lt;h3 id=&#34;immediate-improvements&#34;&gt;Immediate Improvements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Better layout analysis:&lt;/strong&gt; Install &lt;code&gt;pymupdf_layout&lt;/code&gt; for improved structure detection on complex documents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recursive batch mode:&lt;/strong&gt; Process nested folder structures, not just flat directories&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;future-integrations&#34;&gt;Future Integrations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RAG pipeline:&lt;/strong&gt; Auto-feed converted markdown into a vector database&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Obsidian plugin:&lt;/strong&gt; Detect PDFs in vault and convert automatically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FastAPI wrapper:&lt;/strong&gt; Create an HTTP API for web apps to use&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Electron/Tauri app:&lt;/strong&gt; Build a desktop GUI for non-technical users&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-bigger-picture-why-this-matters&#34;&gt;The Bigger Picture: Why This Matters&lt;/h2&gt;
&lt;p&gt;This project is tiny—roughly 100 lines of Python, 30 minutes of work. But it represents something bigger:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The ability to build tools that solve your actual problems.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I had a workflow friction (PDFs don&amp;rsquo;t work well with AI tools). I built a solution. Now that friction is gone, and I can focus on higher-level work.&lt;/p&gt;
&lt;p&gt;And the data is clear: converting your document library to Markdown isn&amp;rsquo;t a nice-to-have. It&amp;rsquo;s a multiplier on every AI workflow that follows. Up to 70% fewer tokens consumed. 84% fewer retrieval failures. 50% fewer incorrect answers. These aren&amp;rsquo;t marginal improvements—they&amp;rsquo;re transformational.&lt;/p&gt;
&lt;p&gt;Working with Claude Code through PAI accelerated all of this. It&amp;rsquo;s like having a patient senior developer sitting next to you, suggesting better approaches, catching errors before they happen, and explaining &lt;em&gt;why&lt;/em&gt; certain patterns work.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;resources&#34;&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PyMuPDF4LLM Docs:&lt;/strong&gt; &lt;a href=&#34;https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyMuPDF GitHub:&lt;/strong&gt; &lt;a href=&#34;https://github.com/pymupdf/PyMuPDF&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;https://github.com/pymupdf/PyMuPDF&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;citations-markdown-vs-pdf-for-llms&#34;&gt;Citations: Markdown vs PDF for LLMs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why PDFs Fail Under LLM Parsing&lt;/strong&gt; — Steven Howard, Untethered AI: &lt;a href=&#34;https://untetheredai.substack.com/p/why-pdfs-fail-under-llm-parsing&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;https://untetheredai.substack.com/p/why-pdfs-fail-under-llm-parsing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PDF vs Markdown for AI: Token Efficiency&lt;/strong&gt; — MarkdownConverters: &lt;a href=&#34;https://markdownconverters.com/blog/pdf-vs-markdown-ai-tokens&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;https://markdownconverters.com/blog/pdf-vs-markdown-ai-tokens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revolutionizing RAG with Enhanced PDF Structure Recognition&lt;/strong&gt; — arXiv:2401.12599 (2024): &lt;a href=&#34;https://arxiv.org/abs/2401.12599&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;https://arxiv.org/abs/2401.12599&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Approaches to PDF Data Extraction for Information Retrieval&lt;/strong&gt; — NVIDIA Technical Blog: &lt;a href=&#34;https://developer.nvidia.com/blog/approaches-to-pdf-data-extraction-for-information-retrieval/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;https://developer.nvidia.com/blog/approaches-to-pdf-data-extraction-for-information-retrieval/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improved RAG Document Processing With Markdown&lt;/strong&gt; — Dr. Leon Eversberg, Towards Data Science: &lt;a href=&#34;https://medium.com/data-science/improved-rag-document-processing-with-markdown-426a2e0dd82b&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;https://medium.com/data-science/improved-rag-document-processing-with-markdown-426a2e0dd82b&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contextual Chunking: Boost Your RAG Retrieval Accuracy&lt;/strong&gt; — Unstructured.io: &lt;a href=&#34;https://unstructured.io/blog/contextual-chunking-in-unstructured-platform-boost-your-rag-retrieval-accuracy&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;https://unstructured.io/blog/contextual-chunking-in-unstructured-platform-boost-your-rag-retrieval-accuracy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Boosting AI Performance: The Power of LLM-Friendly Content in Markdown&lt;/strong&gt; — Webex Developers Blog: &lt;a href=&#34;https://developer.webex.com/blog/boosting-ai-performance-the-power-of-llm-friendly-content-in-markdown&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;https://developer.webex.com/blog/boosting-ai-performance-the-power-of-llm-friendly-content-in-markdown&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Happy converting!&lt;/strong&gt;&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Deploying a Hugo Site to Namecheap with PAI</title>
      <link>/posts/augmented-resilience-posts/deploying-a-hugo-site-to-namecheap-with-pai/</link>
      <pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate>
      
      <guid>/posts/augmented-resilience-posts/deploying-a-hugo-site-to-namecheap-with-pai/</guid>
      <description>&lt;p&gt;I recently deployed my Hugo blog to Namecheap shared hosting, using Obsidian as my content editor and Claude Code with PAI (Personal AI) as my copilot. Here&amp;rsquo;s a walkthrough of every step, from fixing build errors to setting up a fully automated pipeline that goes from Obsidian to live site in a single command.&lt;/p&gt;
&lt;h2 id=&#34;the-starting-point&#34;&gt;The Starting Point&lt;/h2&gt;
&lt;p&gt;I created a Hugo blog project called &lt;strong&gt;Augmented Resilience&lt;/strong&gt; and used the &lt;a href=&#34;https://github.com/mirus-ua/hugo-theme-re-terminal&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;re-terminal&lt;/a&gt;
 theme, a Namecheap shared hosting account, and a GitHub repository. I used Claude Code in VS Code editor and leveraged Daniel Miessler&amp;rsquo;s Personal AI infrastructure. The goal: get the site live at &lt;a href=&#34;https://augmentedresilience.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;augmentedresilience.com&lt;/a&gt;
 with a push-to-deploy workflow.&lt;/p&gt;</description>
      <content>&lt;p&gt;I recently deployed my Hugo blog to Namecheap shared hosting, using Obsidian as my content editor and Claude Code with PAI (Personal AI) as my copilot. Here&amp;rsquo;s a walkthrough of every step, from fixing build errors to setting up a fully automated pipeline that goes from Obsidian to live site in a single command.&lt;/p&gt;
&lt;h2 id=&#34;the-starting-point&#34;&gt;The Starting Point&lt;/h2&gt;
&lt;p&gt;I created a Hugo blog project called &lt;strong&gt;Augmented Resilience&lt;/strong&gt; and used the &lt;a href=&#34;https://github.com/mirus-ua/hugo-theme-re-terminal&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;re-terminal&lt;/a&gt;
 theme, a Namecheap shared hosting account, and a GitHub repository. I used Claude Code in VS Code editor and leveraged Daniel Miessler&amp;rsquo;s Personal AI infrastructure. The goal: get the site live at &lt;a href=&#34;https://augmentedresilience.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;augmentedresilience.com&lt;/a&gt;
 with a push-to-deploy workflow.&lt;/p&gt;
&lt;p&gt;For context, the Personal AI Infrastructure System (PAI) from Daniel Miessler (see resources below) is an open-source framework that wraps around Claude Code and turns it into a structured problem-solving system. Instead of just chatting with an AI, PAI runs every request through a 7-phase algorithm — observe, think, plan, build, execute, verify, learn — so nothing gets skipped. It maintains persistent memory across sessions (so it remembers my project structure, preferences, and past decisions), automatically selects specialized agents for different tasks (security review, architecture, engineering), and enforces verification criteria before declaring anything &amp;ldquo;done.&amp;rdquo; For this project, PAI handled everything from debugging Hugo build errors to writing the deploy script to catching sensitive data I accidentally left in this blog post before it went live. It wasn&amp;rsquo;t just an AI assistant — it was the entire workflow engine. I found it easier to use it within VS Code (&lt;em&gt;still getting used to using the command line interface&lt;/em&gt;).&lt;/p&gt;
&lt;h2 id=&#34;step-1-fixing-the-hugo-build&#34;&gt;Step 1: Fixing the Hugo Build&lt;/h2&gt;
&lt;p&gt;The first issue was a build error:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;module &amp;#34;hugo-theme-re-terminal&amp;#34; not found
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The problem was a mismatch between the theme name in &lt;code&gt;hugo.toml&lt;/code&gt; and the actual directory name. The theme was installed as a git submodule at &lt;code&gt;themes/re-terminal/&lt;/code&gt;, but the config referenced &lt;code&gt;hugo-theme-re-terminal&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Change the theme name in &lt;code&gt;hugo.toml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;theme&lt;/span&gt; = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;re-terminal&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After that, &lt;code&gt;hugo&lt;/code&gt; built the site successfully, generating the &lt;code&gt;public/&lt;/code&gt; folder with all the static files.&lt;/p&gt;
&lt;h2 id=&#34;step-2-setting-up-the-github-repository&#34;&gt;Step 2: Setting Up the GitHub Repository&lt;/h2&gt;
&lt;p&gt;I initialized the repo and connected it to GitHub:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git init
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git remote add origin git@github.com:dsacosta/Augmented-Resilience.git
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git add .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git commit -m &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;my first commit&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git push origin main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One gotcha: I initially typed &lt;code&gt;orgin&lt;/code&gt; instead of &lt;code&gt;origin&lt;/code&gt; in the remote add command. Typos happen — double-check your remote names with &lt;code&gt;git remote -v&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;step-3-connecting-namecheap-to-github-via-ssh&#34;&gt;Step 3: Connecting Namecheap to GitHub via SSH&lt;/h2&gt;
&lt;p&gt;This was the trickiest part. Namecheap shared hosting needs an SSH key to clone from a private GitHub repo. Here&amp;rsquo;s what worked:&lt;/p&gt;
&lt;h3 id=&#34;generate-an-ssh-key-on-namecheap&#34;&gt;Generate an SSH Key on Namecheap&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Log into &lt;strong&gt;cPanel&lt;/strong&gt; on Namecheap&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;SSH Access&lt;/strong&gt; → &lt;strong&gt;Manage SSH Keys&lt;/strong&gt; → &lt;strong&gt;Generate a New Key&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Generate an RSA key (I used the default settings)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;remove-the-passphrase&#34;&gt;Remove the Passphrase&lt;/h3&gt;
&lt;p&gt;This is critical. cPanel&amp;rsquo;s Git Version Control runs non-interactively, so it can&amp;rsquo;t prompt for a passphrase. I opened &lt;strong&gt;cPanel Terminal&lt;/strong&gt; and ran:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ssh-keygen -p -f ~/.ssh/id_rsa
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Enter the old passphrase, then press Enter twice for no new passphrase.&lt;/p&gt;
&lt;h3 id=&#34;add-the-public-key-to-github&#34;&gt;Add the Public Key to GitHub&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;On Namecheap&amp;rsquo;s cPanel Terminal, run: &lt;code&gt;cat ~/.ssh/id_rsa.pub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Copy the output&lt;/li&gt;
&lt;li&gt;Go to your GitHub repo → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Deploy Keys&lt;/strong&gt; → &lt;strong&gt;Add deploy key&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Paste the public key and save&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;verify-the-connection&#34;&gt;Verify the Connection&lt;/h3&gt;
&lt;p&gt;From cPanel Terminal:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ssh -T git@github.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see: &lt;code&gt;Hi dsacosta/Augmented-Resilience! You&#39;ve successfully authenticated...&lt;/code&gt;&lt;/p&gt;
&lt;h2 id=&#34;step-4-clone-the-repo-on-namecheap&#34;&gt;Step 4: Clone the Repo on Namecheap&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;In cPanel, go to &lt;strong&gt;Git Version Control&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Toggle &lt;strong&gt;Clone a Repository&lt;/strong&gt; on&lt;/li&gt;
&lt;li&gt;Enter the clone URL: &lt;code&gt;git@github.com:dsacosta/Augmented-Resilience.git&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set the repository path (I used &lt;code&gt;/home/yourusername/your-repo&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Important: Don&amp;rsquo;t clone directly into &lt;code&gt;public_html&lt;/code&gt; or your domain folder — it likely already has files and will error out. Clone to a separate directory and use deployment to copy files over.&lt;/p&gt;
&lt;h2 id=&#34;step-5-auto-deployment-with-cpanelyml&#34;&gt;Step 5: Auto-Deployment with .cpanel.yml&lt;/h2&gt;
&lt;p&gt;cPanel supports automatic deployment tasks via a &lt;code&gt;.cpanel.yml&lt;/code&gt; file in the repo root. This file tells cPanel what to do after each pull:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;deployment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;tasks&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;export DEPLOYPATH=/home/yourusername/yourdomain.com/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;/bin/cp -R public/* $DEPLOYPATH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This copies everything from the &lt;code&gt;public/&lt;/code&gt; folder (Hugo&amp;rsquo;s build output) into the live site directory.&lt;/p&gt;
&lt;p&gt;After pushing this file to GitHub:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Git Version Control&lt;/strong&gt; → &lt;strong&gt;Manage&lt;/strong&gt; your repo&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;Pull or Deploy&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Update from Remote&lt;/strong&gt; to pull the latest&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy HEAD Commit&lt;/strong&gt; to trigger the &lt;code&gt;.cpanel.yml&lt;/code&gt; tasks&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your site should now be live.&lt;/p&gt;
&lt;h2 id=&#34;step-6-fully-automated-deploys-with-github-actions&#34;&gt;Step 6: Fully Automated Deploys with GitHub Actions&lt;/h2&gt;
&lt;p&gt;To eliminate the manual &amp;ldquo;pull and deploy&amp;rdquo; step in cPanel, I set up a GitHub Actions workflow that SSHs into Namecheap and triggers the pull automatically on every push.&lt;/p&gt;
&lt;h3 id=&#34;generate-a-deploy-key&#34;&gt;Generate a Deploy Key&lt;/h3&gt;
&lt;p&gt;On your local machine, generate a key pair with no passphrase:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ssh-keygen -t ed25519 -C &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;github-actions-deploy&amp;#34;&lt;/span&gt; -f ~/.ssh/deploy_key -N &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add the &lt;strong&gt;public key&lt;/strong&gt; to Namecheap:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# In cPanel Terminal on Namecheap:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ssh-ed25519 AAAA...your-key-here github-actions-deploy&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; ~/.ssh/authorized_keys
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;add-secrets-to-github&#34;&gt;Add Secrets to GitHub&lt;/h3&gt;
&lt;p&gt;Go to your repo → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Secrets and variables&lt;/strong&gt; → &lt;strong&gt;Actions&lt;/strong&gt; and add:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Secret&lt;/th&gt;
          &lt;th&gt;Value&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;NC_HOST&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;augmentedresilience.com&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;NC_USER&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Your cPanel username&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;NC_PORT&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Your SSH port (check cPanel)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;NC_SSH_KEY&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;The full private key (including BEGIN/END lines)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;create-the-workflow&#34;&gt;Create the Workflow&lt;/h3&gt;
&lt;p&gt;Add &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt; to your repo:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Deploy to Namecheap&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;push&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;branches&lt;/span&gt;: [&lt;span style=&#34;color:#ae81ff&#34;&gt;main]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;jobs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;deploy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;runs-on&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ubuntu-latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Deploy via SSH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;uses&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;appleboy/ssh-action@v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;host&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;${{ secrets.NC_HOST }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;username&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;${{ secrets.NC_USER }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;key&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;${{ secrets.NC_SSH_KEY }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;port&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;${{ secrets.NC_PORT }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;script&lt;/span&gt;: |&lt;span style=&#34;color:#e6db74&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;            cd ~/your-repo &amp;amp;&amp;amp; git pull origin main &amp;amp;&amp;amp; /bin/cp -R public/* ~/yourdomain.com/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now every push to &lt;code&gt;main&lt;/code&gt; automatically deploys to your live site.&lt;/p&gt;
&lt;h2 id=&#34;step-7-one-command-deploy-script&#34;&gt;Step 7: One-Command Deploy Script&lt;/h2&gt;
&lt;p&gt;Five manual commands every time you publish? That&amp;rsquo;s not a workflow — that&amp;rsquo;s a chore. I had Claude write a Python script that handles everything in one shot:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;One-command deploy: Obsidian → Hugo → GitHub → Live site.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; subprocess
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; sys
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; datetime &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; datetime
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PROJECT_DIR &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;~/Documents/Augmented-Resilience&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;OBSIDIAN_POSTS &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;~/projects/obsidian-vault/30-projects/augmented-resilience-posts&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;HUGO_POSTS &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;PROJECT_DIR&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/content/posts&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(cmd, description, cwd&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;PROJECT_DIR):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Run a command and print status.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;=&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;  &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;description&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;=&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; subprocess&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;run(cmd, shell&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;, cwd&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;cwd)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; result&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;returncode &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;  FAILED: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;description&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        sys&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;exit(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; result
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    msg &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;join(sys&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;argv[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;:]) &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; len(sys&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;argv) &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Site update &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;datetime&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;now()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strftime(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;%Y-%m-&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%d&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; %H:%M&amp;#39;&lt;/span&gt;)&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    run(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;rsync -av --delete &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;OBSIDIAN_POSTS&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;HUGO_POSTS&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Syncing posts from Obsidian&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    run(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;python3 &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;PROJECT_DIR&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/images.py&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Processing images&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    run(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hugo&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Building site with Hugo&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    run(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;git add .&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Staging changes&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; subprocess&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;run(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;git diff --cached --quiet&amp;#34;&lt;/span&gt;, shell&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;, cwd&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;PROJECT_DIR)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; result&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;returncode &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;  No changes to commit. Site is up to date.&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    run(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;git commit -m &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;msg&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Committing&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    run(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;git push origin main&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Pushing to GitHub&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;=&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;  DEPLOYED! Your site will be live shortly.&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;=&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; __name__ &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    main()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Save this as &lt;code&gt;deploy.py&lt;/code&gt; in your project root. Now the entire workflow is:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Default timestamped commit message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python3 deploy.py
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Or with a custom message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python3 deploy.py &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Add new blog post about deployment&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The script runs every step in sequence — syncs from Obsidian, converts image links, builds with Hugo, commits, and pushes. If any step fails, it stops immediately so you don&amp;rsquo;t push a broken build. Combined with the GitHub Actions workflow from Step 6, pushing triggers the auto-deploy to Namecheap. One command, fully live.&lt;/p&gt;
&lt;h2 id=&#34;lessons-learned&#34;&gt;Lessons Learned&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SSH passphrases break cPanel automation.&lt;/strong&gt; Always remove the passphrase from keys used by cPanel&amp;rsquo;s Git Version Control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Theme names must match directory names.&lt;/strong&gt; Hugo looks for the theme in &lt;code&gt;themes/&amp;lt;theme-name&amp;gt;/&lt;/code&gt;, so the &lt;code&gt;theme&lt;/code&gt; value in your config must match exactly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;rsquo;t clone into the live site directory.&lt;/strong&gt; Clone to a separate folder and use &lt;code&gt;.cpanel.yml&lt;/code&gt; to copy the built files over.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Actions + SSH is the cleanest auto-deploy for shared hosting.&lt;/strong&gt; No webhooks, no cron jobs — just a simple SSH action that runs on every push.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code with PAI made this possible in a single session.&lt;/strong&gt; From debugging build errors to SSH key troubleshooting to writing GitHub Actions workflows, having an AI pair programmer turned what could have been hours of Stack Overflow rabbit holes into a smooth, guided process.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;tools-used&#34;&gt;Tools Used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://gohugo.io/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Hugo&lt;/a&gt;
 — Static site generator&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/mirus-ua/hugo-theme-re-terminal&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;re-terminal theme&lt;/a&gt;
 — Hugo theme&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.namecheap.com/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Namecheap&lt;/a&gt;
 — Shared hosting with cPanel&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/features/actions&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;GitHub Actions&lt;/a&gt;
 — CI/CD automation&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://claude.ai/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Claude Code&lt;/a&gt;
 — AI pair programmer&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://danielmiessler.com/blog/personal-ai-infrastructure&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Personal AI Infrastructure (PAI)&lt;/a&gt;
 — Workflow engine&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://obsidian.md/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Obsidian&lt;/a&gt;
 — Content authoring&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://code.visualstudio.com/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;VS Code&lt;/a&gt;
 - Development environment; used as the workspace for Claude Code sessions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;resources&#34;&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://youtu.be/dnE7c0ELEH8?si=YEDoaoiekMYJIe3U&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;I started a blog&amp;hellip;..in 2024 (why you should too)&lt;/a&gt;
 — The YouTube video that inspired me to start this blog&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://danielmiessler.com/blog/personal-ai-infrastructure&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Building a Personal AI Infrastructure (PAI)&lt;/a&gt;
 — Daniel Miessler&amp;rsquo;s guide to building your own Personal AI system&lt;/li&gt;
&lt;/ul&gt;
</content>
    </item>
    
  </channel>
</rss>
