<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fernando Paladini</title>
    <description>The latest articles on DEV Community by Fernando Paladini (@paladini).</description>
    <link>https://dev.to/paladini</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1065831%2F79b4d650-5838-4481-a62f-8f03f4010512.jpeg</url>
      <title>DEV Community: Fernando Paladini</title>
      <link>https://dev.to/paladini</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paladini"/>
    <language>en</language>
    <item>
      <title>Cross-Post a DEV.to Tutorial to Medium with a Formatting Check</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Thu, 13 Aug 2026 15:15:09 +0000</pubDate>
      <link>https://dev.to/paladini/cross-post-a-devto-tutorial-to-medium-with-a-formatting-check-299n</link>
      <guid>https://dev.to/paladini/cross-post-a-devto-tutorial-to-medium-with-a-formatting-check-299n</guid>
      <description>&lt;p&gt;Cross-posting a technical tutorial is easy to start and surprisingly easy to get wrong. A URL import can leave code blocks split, headings as plain text, or metadata incomplete. The result may look acceptable at a glance while damaging the parts readers need most.&lt;/p&gt;

&lt;p&gt;This tutorial shows a reviewable DEV.to to Medium workflow using &lt;a href="https://github.com/paladini/publish-agents" rel="noopener noreferrer"&gt;publish-agents&lt;/a&gt;, an open-source TypeScript project by Fernando Paladini. Its &lt;code&gt;medium-publisher&lt;/code&gt; package imports a public article through Medium's import flow, checks the editor against the source Markdown, and can repair a small set of common formatting problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Checkout the stable &lt;code&gt;v0.2.3&lt;/code&gt; release, build the &lt;code&gt;@paladini/medium-publisher-mcp&lt;/code&gt; package, log in once, and create a Medium draft with &lt;code&gt;publish-devto&lt;/code&gt;. Keep the default draft behavior while you inspect the title, code blocks, headings, lists, and metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer.&lt;/li&gt;
&lt;li&gt;A published DEV.to article with a public URL.&lt;/li&gt;
&lt;li&gt;A Medium account that can create stories.&lt;/li&gt;
&lt;li&gt;A terminal that can run npm and the browser installation step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package uses Patchright browser automation and a saved browser session. It does not use a Medium write API key. The project documents Medium UI changes as a compatibility risk, so treat the browser session and the resulting draft as reviewable state rather than an unattended guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the released source
&lt;/h2&gt;

&lt;p&gt;The repository's &lt;code&gt;v0.2.3&lt;/code&gt; release is the stable reference for this walkthrough. Installing from that tag keeps the commands separate from later changes on the default branch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/paladini/publish-agents.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;publish-agents&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v0.2.3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;paladini/medium-publisher-mcp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;paladini/medium-publisher-mcp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build produces the CLI and MCP server from the package source. The package declares Node.js 20 or newer and uses &lt;code&gt;patchright&lt;/code&gt; as its browser automation dependency. Its post-install step may install the bundled Chromium browser. If that step was skipped in your environment, run the browser installation command documented by Patchright before continuing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a saved Medium session
&lt;/h2&gt;

&lt;p&gt;Log in interactively once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;medium-publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the session in JSON form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;medium-publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;session-check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project stores browser state in an operating-system data directory by default. On Windows, the documented default is &lt;code&gt;%LOCALAPPDATA%\medium-publisher\storageState.json&lt;/code&gt;. You can override it with &lt;code&gt;MEDIUM_STATE_PATH&lt;/code&gt; or &lt;code&gt;MEDIUM_PUBLISHER_HOME&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is an important boundary: the saved state contains authentication cookies. Protect the directory like any other local credential store, do not commit it, and do not copy it into a CI artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import a DEV.to article as a draft
&lt;/h2&gt;

&lt;p&gt;Use the one-shot command with &lt;code&gt;--draft&lt;/code&gt; and &lt;code&gt;--json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;medium-publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;publish-devto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://dev.to/your-name/your-tutorial"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--draft&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL must point to a public article. The package fetches the DEV.to article through its public API, then opens Medium's official import route. The &lt;code&gt;--draft&lt;/code&gt; flag matters because it leaves the final publication decision with you.&lt;/p&gt;

&lt;p&gt;The JSON result includes a Medium URL and metadata details such as whether the title was set, which topics were selected, and whether a hero image was detected. Treat that URL as a draft inspection target until you have checked the actual editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the imported editor
&lt;/h2&gt;

&lt;p&gt;Open the returned draft URL in the browser, or use the package's structured extraction command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;medium-publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;extract&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://medium.com/p/your-draft/edit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extraction workflow is useful because it reports the editor outline and formatting flags. Check at least these points manually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The title matches the DEV.to source.&lt;/li&gt;
&lt;li&gt;Headings are rendered as headings, not literal Markdown text.&lt;/li&gt;
&lt;li&gt;Each fenced code example is one code block with the expected language.&lt;/li&gt;
&lt;li&gt;Lists, links, and paragraphs remain in the expected order.&lt;/li&gt;
&lt;li&gt;The subtitle and topics describe the article without inventing claims.&lt;/li&gt;
&lt;li&gt;No secrets or private URLs were imported accidentally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package includes a source review that compares the editor content with the DEV.to Markdown. It also has a security check for secret-like values and unexpected short links. These checks reduce risk, but they cannot replace reading the draft in the Medium editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repair common formatting failures
&lt;/h2&gt;

&lt;p&gt;If extraction identifies a problem, create an actions file and apply only the targeted fixes. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"removeEmptyCodeBlocks"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mergeAdjacentCodeBlocks"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"promoteDemoteHeading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"blockIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it to the draft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;medium-publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fix-draft&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://medium.com/p/your-draft/edit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--actions-file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fixes.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;extract&lt;/code&gt; again after the change. A repair is complete only when the editor and the source agree on the affected blocks. If the editor has changed since the release, stop and inspect the draft manually instead of repeatedly applying actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What success looks like
&lt;/h2&gt;

&lt;p&gt;A successful draft run gives you a Medium editor URL, the expected title, readable code blocks, intact headings and lists, and metadata that you can explain. The command does not prove that Medium will preserve every future import. It gives you a repeatable starting point and evidence for a human review.&lt;/p&gt;

&lt;p&gt;For a final pre-publication check, compare the draft with the original DEV.to article block by block. Pay particular attention to content after the first code block because malformed fences can make later paragraphs appear to be code. The repository's own test suite covers its formatting and metadata helpers, but your article is still the source of truth for the review.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP option for agent-assisted workflows
&lt;/h2&gt;

&lt;p&gt;The same package exposes an MCP server. Register &lt;code&gt;medium-publisher-mcp&lt;/code&gt; with your MCP client and use &lt;code&gt;medium_publish_from_devto&lt;/code&gt; with &lt;code&gt;publish: false&lt;/code&gt; for a draft-first flow. The project documents stdio configuration for clients such as Cursor and Claude Code.&lt;/p&gt;

&lt;p&gt;An agent can prepare the URL and summarize the extraction result, but the account owner should approve login, external publication, and any formatting correction. Browser automation is an integration boundary, not a reason to remove human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and limitations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The session is missing or expired
&lt;/h3&gt;

&lt;p&gt;The CLI uses exit code 3 for a missing or expired session. Run &lt;code&gt;medium-publisher login&lt;/code&gt; again. A two-factor challenge or bot check may require headed browser interaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  The import hangs
&lt;/h3&gt;

&lt;p&gt;The source article must be public because the workflow reads its public DEV.to representation. Confirm that the URL loads without authentication and that the article has been published.&lt;/p&gt;

&lt;h3&gt;
  
  
  Topics or selectors do not match
&lt;/h3&gt;

&lt;p&gt;Medium's interface can change. The project waits for topic autocomplete and targets the current import and editor controls, but a future UI change can still break a selector. Use &lt;code&gt;extract&lt;/code&gt;, inspect the draft, and report reproducible failures to the repository rather than publishing blindly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The content contains sensitive data
&lt;/h3&gt;

&lt;p&gt;Stop the workflow if the source or editor contains credentials, private links, or unexpected content. Delete the draft through Medium's controls as appropriate, rotate exposed credentials, and review the local browser-state directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this need a Medium API key?
&lt;/h3&gt;

&lt;p&gt;No. The documented workflow uses browser automation and a persistent login session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can it publish immediately?
&lt;/h3&gt;

&lt;p&gt;The CLI supports publishing options, but this tutorial deliberately uses &lt;code&gt;--draft&lt;/code&gt;. Review the imported editor before choosing a live publication action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it import any URL?
&lt;/h3&gt;

&lt;p&gt;The package supports public URL import, while the one-shot &lt;code&gt;publish-devto&lt;/code&gt; path expects a public DEV.to article so it can compare the imported result with the source Markdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this a guarantee that Medium formatting will stay correct?
&lt;/h3&gt;

&lt;p&gt;No. Medium UI changes, account challenges, and article-specific content can still require manual intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize this tutorial and check its wording. Commands, version references, workflow boundaries, and limitations were checked against the public &lt;code&gt;publish-agents&lt;/code&gt; repository, its &lt;code&gt;v0.2.3&lt;/code&gt; release, and the package's documented source behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Cross-posting is safer when import, repair, and publication are separate decisions. Use &lt;code&gt;publish-devto --draft&lt;/code&gt;, inspect the real Medium editor, rerun extraction after any fix, and publish only after the source and destination agree.&lt;/p&gt;

&lt;p&gt;Have you found a Medium import failure that a block-level source review catches earlier than a visual skim?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>mcp</category>
      <category>tutorial</category>
      <category>medium</category>
    </item>
    <item>
      <title>Build a Citation-Checked Research Report in Python with Evidence Graph Studio</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Wed, 12 Aug 2026 18:28:51 +0000</pubDate>
      <link>https://dev.to/paladini/build-a-citation-checked-research-report-in-python-with-evidence-graph-studio-3pa6</link>
      <guid>https://dev.to/paladini/build-a-citation-checked-research-report-in-python-with-evidence-graph-studio-3pa6</guid>
      <description>&lt;p&gt;Polished technical writing can still hide a basic failure: a sentence has no source, a citation points to a missing record, or two sources disagree and the draft does not show it.&lt;/p&gt;

&lt;p&gt;If you are reviewing AI-assisted research, a pile of links is not enough. You need to see which source supports which claim, which claims need review, and what a writer can cite in the final report.&lt;/p&gt;

&lt;p&gt;This tutorial uses &lt;a href="https://github.com/paladini/evidence-graph-studio" rel="noopener noreferrer"&gt;Evidence Graph Studio&lt;/a&gt;, a small MIT-licensed Python project that turns a JSON research dossier into a claim-to-source graph, a Markdown report, a citation pack, JSON graph data, and Mermaid source. It is deterministic and uses only the Python standard library.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Create a dossier containing sources, claims, and evidence links. Run the validator and builder. A successful build exits with status 0 when every claim is supported. A dossier that needs review exits with status 2, so a CI job can stop before an unsupported claim reaches publication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need Python 3.10 or newer, Git, and a shell. The project does not require a database, API key, model download, or network access at runtime.&lt;/p&gt;

&lt;p&gt;Clone the repository and enter it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/paladini/evidence-graph-studio.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;evidence-graph-studio&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository currently documents version 0.1.0 in &lt;code&gt;pyproject.toml&lt;/code&gt;, requires Python 3.10 or newer, and is released under the MIT license. The commands below target the current default branch because the project has no GitHub release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the bundled example
&lt;/h2&gt;

&lt;p&gt;The repository includes a dossier about AI agent reliability. Set &lt;code&gt;PYTHONPATH&lt;/code&gt; to the source directory, then validate the input before generating output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="bp"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;\src"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;evidence_graph_studio&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--dossier&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;examples\ai-agent-reliability-dossier.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The validator checks the dossier structure without writing reports. The bundled example contains three claims and three sources.&lt;/p&gt;

&lt;p&gt;Now build the complete evidence pack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;evidence_graph_studio&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--dossier&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;examples\ai-agent-reliability-dossier.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build\evidence-report.md&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--graph-out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build\evidence-graph.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--citations-out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build\citation-pack.md&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--mermaid-out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build\evidence-graph.mmd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command writes four artifacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;build/evidence-report.md&lt;/code&gt; contains summary counts and per-claim findings.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build/evidence-graph.json&lt;/code&gt; preserves the graph for downstream tools.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build/citation-pack.md&lt;/code&gt; groups citations by source and claim.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build/evidence-graph.mmd&lt;/code&gt; can be rendered in documentation or a pull request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The documented example produces &lt;code&gt;3 supported, 0 need review&lt;/code&gt; and exits successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the dossier shape
&lt;/h2&gt;

&lt;p&gt;The input is deliberately explicit. A source has an ID, URL, summary, tags, and optional quotes. A claim has an ID, text, required tags, and evidence entries that point back to source IDs.&lt;/p&gt;

&lt;p&gt;Here is a minimal dossier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Release review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"source.release-notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Release notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/release-notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The release adds a validation command."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"documentation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"release"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"quotes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"The validation command checks the input dossier."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claims"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claim.validation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The tool can validate a dossier before building reports."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"required_tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"release"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"source_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"source.release-notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"stance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"supports"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The release notes describe the validation command."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"quote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The validation command checks the input dossier."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Evidence uses one of three stances: &lt;code&gt;supports&lt;/code&gt;, &lt;code&gt;contradicts&lt;/code&gt;, or &lt;code&gt;context&lt;/code&gt;. That distinction matters. A source that mentions a topic is not automatically support for a claim, and a contradiction should remain visible for review rather than being silently discarded.&lt;/p&gt;

&lt;p&gt;Optional links can express relationships between claims, such as &lt;code&gt;depends_on&lt;/code&gt;. The output graph therefore captures more than a flat bibliography: it records the reasoning structure that a reviewer needs to inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the result in a repeatable way
&lt;/h2&gt;

&lt;p&gt;The project includes seven unit tests. Run them from the repository root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;unittest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;discover&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tests&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a practical check, assert that the generated report and citation pack exist, inspect the summary, and review the Mermaid graph. A useful CI policy is to fail when the build exits with status 2. That preserves the distinction between a valid report and a report that was generated but still needs human review.&lt;/p&gt;

&lt;p&gt;You can also inspect the JSON graph in a later step without reparsing Markdown. That makes the output suitable for a documentation pipeline, a pull request check, or a custom review interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;The design makes support a data relationship instead of a formatting convention. Each evidence item names its source and stance. The builder can then detect missing source IDs, unsupported claims, contradictory evidence, and missing required topic tags consistently.&lt;/p&gt;

&lt;p&gt;This is especially useful for AI-assisted drafting. A language model can help propose claims or source notes, but the dossier gives a reviewer a concrete object to validate. The generated Markdown is an output of that review model, not the model's memory of where a statement came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and boundaries
&lt;/h2&gt;

&lt;p&gt;An exit status of 2 is not a crash. It means the report contains claims that need review. Treat it as a gate in automation, not as proof that the sources are false.&lt;/p&gt;

&lt;p&gt;The tool checks the structure and relationships you provide. It does not fetch URLs, verify that a quote is authentic, measure source quality, or decide whether a claim is true. Those remain research and editorial responsibilities.&lt;/p&gt;

&lt;p&gt;The path is local and deterministic, but the input can still contain sensitive material. Do not place credentials, private customer data, or confidential research in a dossier that will be committed or uploaded. The MIT license permits reuse, but it does not change your obligations for the sources and data you process.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does it use an AI model?
&lt;/h3&gt;

&lt;p&gt;No. Version 0.1 is documented as deterministic and uses the Python standard library.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it need internet access?
&lt;/h3&gt;

&lt;p&gt;No runtime network access is required. The dossier stores source metadata and URLs, while the tool checks the relationships locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can it replace fact checking?
&lt;/h3&gt;

&lt;p&gt;No. It makes missing, conflicting, or incomplete support visible. A person still needs to inspect the source and decide whether the claim is accurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I delete old files automatically?
&lt;/h3&gt;

&lt;p&gt;The current project is an evidence graph builder and report exporter. It does not describe a content deletion workflow. Keep cleanup and retention policy outside the build step until you have reviewed your own requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If a research workflow ends with a Markdown draft and a loose list of URLs, the review boundary is hard to see. Evidence Graph Studio gives each claim an explicit relationship to its sources and turns unresolved support into a machine-checkable result.&lt;/p&gt;

&lt;p&gt;Start with the bundled example, then adapt the dossier format to one small article or research brief. Keep the validator in CI and require human review whenever the build reports claims that need review.&lt;/p&gt;

&lt;p&gt;I used AI assistance to organize and edit this tutorial. The commands, project behavior, version details, and limitations were checked against the public repository and a local run of its documented example.&lt;/p&gt;

&lt;p&gt;What evidence relationship would be most valuable in your own review workflow: missing support, contradictions, required topic coverage, or claim dependencies?&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>tutorial</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Build Typed Gear-List Automation with the LighterPack SDK</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Tue, 11 Aug 2026 12:37:49 +0000</pubDate>
      <link>https://dev.to/paladini/build-typed-gear-list-automation-with-the-lighterpack-sdk-184g</link>
      <guid>https://dev.to/paladini/build-typed-gear-list-automation-with-the-lighterpack-sdk-184g</guid>
      <description>&lt;p&gt;Planning a gear list in a browser is convenient until the list becomes data you need to generate, review, or update repeatedly. A spreadsheet can hold the rows, but it does not automatically create categories, preserve LighterPack's rules, or calculate the result you actually see in the app.&lt;/p&gt;

&lt;p&gt;This tutorial shows a small TypeScript workflow built with &lt;a href="https://github.com/paladini/lighterpack-sdk" rel="noopener noreferrer"&gt;&lt;code&gt;@paladini/lighterpack&lt;/code&gt;&lt;/a&gt;. It creates a complete backpacking list in one batch operation, reads the resulting totals, and demonstrates the safety boundaries you need to understand before automating writes to a personal account.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The SDK is an unofficial, ESM-only TypeScript client for LighterPack. It requires Node.js 20 or newer, uses your normal LighterPack username and password, and has no runtime dependencies. Its &lt;code&gt;batch.createListWithItems()&lt;/code&gt; method can scaffold categories and items in one save round-trip.&lt;/p&gt;

&lt;p&gt;The example below is a real API shape from the current &lt;code&gt;0.1.0&lt;/code&gt; package and repository. Replace the placeholder credentials with environment variables, run it against an account you control, and inspect the returned totals before adding more automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer&lt;/li&gt;
&lt;li&gt;A LighterPack account you are allowed to modify&lt;/li&gt;
&lt;li&gt;A new TypeScript project using ESM&lt;/li&gt;
&lt;li&gt;Credentials supplied through the environment, not committed to source control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SDK is published under the MIT license. It is not affiliated with or endorsed by LighterPack. The project documents the package as an unofficial client that talks to the web application's own API.&lt;/p&gt;

&lt;p&gt;Create a project and install the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;typed-gear-list
&lt;span class="nb"&gt;cd &lt;/span&gt;typed-gear-list
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @paladini/lighterpack
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; tsx typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package metadata declares &lt;code&gt;"type": "module"&lt;/code&gt;, exposes TypeScript declarations, and currently publishes version &lt;code&gt;0.1.0&lt;/code&gt; as the npm &lt;code&gt;latest&lt;/code&gt; tag. The repository's &lt;a href="https://github.com/paladini/lighterpack-sdk/blob/main/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; and &lt;a href="https://github.com/paladini/lighterpack-sdk/blob/main/package.json" rel="noopener noreferrer"&gt;package metadata&lt;/a&gt; are the source for those details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a complete list in one call
&lt;/h2&gt;

&lt;p&gt;Set credentials in your shell. PowerShell users can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LIGHTERPACK_USERNAME&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-username"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LIGHTERPACK_PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-password"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;index.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LighterPackClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@paladini/lighterpack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LIGHTERPACK_USERNAME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LIGHTERPACK_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Set LIGHTERPACK_USERNAME and LIGHTERPACK_PASSWORD first.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LighterPackClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createListWithItems&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Weekend shelter system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A small example generated from TypeScript.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Shelter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Two-person tent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1450&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weightUnit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;249.99&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Groundsheet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weightUnit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;24.5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sleep system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sleeping bag&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;780&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weightUnit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;189&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sleeping pad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;420&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weightUnit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;listId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;weightGrams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weightGrams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;code&gt;npx tsx index.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The expected result is an object containing the created list ID, its name, and computed totals. The four item weights sum to 2,830 grams. The price total should be 562.49 in the account's configured currency representation, subject to the service's own handling of optional fields.&lt;/p&gt;

&lt;p&gt;This example uses grams explicitly. The SDK accepts &lt;code&gt;g&lt;/code&gt;, &lt;code&gt;oz&lt;/code&gt;, &lt;code&gt;lb&lt;/code&gt;, or &lt;code&gt;kg&lt;/code&gt; as input units and returns &lt;code&gt;weightGrams&lt;/code&gt; at its output boundary. Making the unit explicit is useful when data comes from different sources and avoids silently mixing ounces and grams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the result with a fresh read
&lt;/h2&gt;

&lt;p&gt;The client keeps a library cache for the lifetime of the instance. That makes several reads efficient, but a script that needs to confirm server state can call &lt;code&gt;refresh()&lt;/code&gt; and then fetch the list again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`List &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; was not found after refresh.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Verified &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weightGrams&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;g`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a disposable test account, add a cleanup step after verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Deleted the test list.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not add that deletion to a production workflow unless the list is intentionally disposable. The SDK documents deletion as irreversible and also protects the account from deleting its only list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the batch API matters
&lt;/h2&gt;

&lt;p&gt;LighterPack does not expose a granular CRUD API for each list mutation. The SDK documentation describes the library as one JSON document saved through &lt;code&gt;POST /saveLibrary&lt;/code&gt; with an optimistic-concurrency &lt;code&gt;sync_token&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That implementation detail explains the design of the client. Every mutation goes through one sync engine that loads the library, applies a change to a clone, and saves the result. If another writer changes the library first, the engine refetches and retries once. A second conflict becomes a typed &lt;code&gt;SyncConflictError&lt;/code&gt; instead of silently overwriting newer data.&lt;/p&gt;

&lt;p&gt;Calling &lt;code&gt;items.add()&lt;/code&gt; repeatedly is still valid, but &lt;code&gt;batch.addItems()&lt;/code&gt;, &lt;code&gt;batch.createListWithItems()&lt;/code&gt;, and &lt;code&gt;batch.updateItems()&lt;/code&gt; are better fits for generated data because they group related changes into one save operation. The repository's &lt;a href="https://github.com/paladini/lighterpack-sdk/blob/main/src/api/batch.ts" rel="noopener noreferrer"&gt;batch API implementation&lt;/a&gt; is the precise reference for the accepted fields and validation behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update many items safely
&lt;/h2&gt;

&lt;p&gt;After importing a gear inventory, you may need to mark worn items or adjust quantities. Flags belong to an item's placement in a category, so the batch update accepts a &lt;code&gt;categoryId&lt;/code&gt; when flags are involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateItems&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;worn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;789&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;410&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weightUnit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weightGrams&lt;/span&gt; &lt;span class="p"&gt;})));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK enforces documented rules such as requiring &lt;code&gt;weightUnit&lt;/code&gt; whenever a weight is changed. It also models worn and consumable as mutually exclusive flags. These checks happen before the save, so an invalid input should fail without a partial library update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication and security boundaries
&lt;/h2&gt;

&lt;p&gt;There is no separate LighterPack API-key system in this SDK. Authentication uses the regular account username and password. The client keeps the resulting session cookie in memory for its lifetime and does not provide a persistent token to revoke individually. The project's README recommends changing the account password if access must be cut off.&lt;/p&gt;

&lt;p&gt;Treat credentials as secrets. Use environment variables or a local secret manager, exclude &lt;code&gt;.env&lt;/code&gt; files from Git, and never paste a real password into a tutorial or issue. For a self-hosted LighterPack deployment, the client accepts a &lt;code&gt;baseUrl&lt;/code&gt; option, but you must verify that the instance is trusted and that its API behavior matches the client assumptions.&lt;/p&gt;

&lt;p&gt;The SDK also supports image uploads and public share-link generation. Both can create external effects, so add explicit confirmation and cleanup rules before exposing them to an automated job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The command fails before any network request
&lt;/h3&gt;

&lt;p&gt;Check the environment variables and input validation first. Bad units, malformed colors, invalid stars, and missing category IDs are represented by typed validation errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  A save reports a synchronization conflict
&lt;/h3&gt;

&lt;p&gt;Another browser tab or process may have changed the library. Retry from fresh state only after deciding which writer should win. The SDK already retries one stale-token conflict; a second conflict is surfaced as &lt;code&gt;SyncConflictError&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The output is not what the web UI displays
&lt;/h3&gt;

&lt;p&gt;The SDK treats weights as grams at its input and output boundaries, while LighterPack can display another unit in the UI. Compare &lt;code&gt;weightGrams&lt;/code&gt; and the account's display settings instead of comparing formatted strings.&lt;/p&gt;

&lt;h3&gt;
  
  
  The example creates data in the wrong account
&lt;/h3&gt;

&lt;p&gt;Stop and inspect the configured username before performing another write. For repeatable tests, use a dedicated account and delete only the lists created by that test.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is this an official LighterPack SDK?
&lt;/h3&gt;

&lt;p&gt;No. It is an unofficial open-source client maintained in &lt;a href="https://github.com/paladini/lighterpack-sdk" rel="noopener noreferrer"&gt;paladini/lighterpack-sdk&lt;/a&gt;. Verify that its assumptions still match your target service before relying on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use JavaScript instead of TypeScript?
&lt;/h3&gt;

&lt;p&gt;Yes. The package ships ESM JavaScript, and the TypeScript declarations are optional at runtime. TypeScript is useful here because list, category, item, unit, and error shapes become part of the editor feedback loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the tutorial run without credentials?
&lt;/h3&gt;

&lt;p&gt;The repository's unit tests run without a live account. The end-to-end example requires credentials because it creates and modifies real remote data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The useful boundary is not a clever wrapper around HTTP. It is a typed workflow that makes list structure, units, batch writes, conflict handling, and credential boundaries visible in code. Start with a disposable account, create one small list, refresh it, verify the totals, and only then automate larger imports.&lt;/p&gt;

&lt;p&gt;Have you found a safer input format for generating backpacking lists - CSV, a typed JSON file, or a spreadsheet export? Share the validation and cleanup rules that made your workflow trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to help organize and edit this tutorial. The repository README, package metadata, implementation, tests, and published package metadata were checked against the claims and examples before publication.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>npm</category>
      <category>tutorial</category>
      <category>backpacking</category>
    </item>
    <item>
      <title>Detect AI-Built Web Pages with a Chrome Extension Heuristic Scanner</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Mon, 10 Aug 2026 12:36:53 +0000</pubDate>
      <link>https://dev.to/paladini/detect-ai-built-web-pages-with-a-chrome-extension-heuristic-scanner-5hnb</link>
      <guid>https://dev.to/paladini/detect-ai-built-web-pages-with-a-chrome-extension-heuristic-scanner-5hnb</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;You can inspect a page for signs of AI-assisted generation without uploading its HTML to a server. This tutorial walks through &lt;a href="https://github.com/paladini/vibe-code-detector" rel="noopener noreferrer"&gt;Vibe Code Detector&lt;/a&gt;, an open-source Chrome extension that runs weighted heuristics in the browser and reports a score with the detected factors.&lt;/p&gt;

&lt;p&gt;The useful engineering idea is not the label. It is the separation between observable signals, a transparent scoring model, and a user interface that lets you inspect why a result happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: a score is easy, an explainable score is harder
&lt;/h2&gt;

&lt;p&gt;Browser extensions that inspect page structure face an awkward tradeoff. A simple rule can find a marker such as &lt;code&gt;data-radix-*&lt;/code&gt;, a Lucide SVG, or a generator meta tag. But any one marker is weak evidence. Modern sites can use the same libraries for ordinary reasons, and a generated site may remove or change those markers.&lt;/p&gt;

&lt;p&gt;Vibe Code Detector combines several signals instead of treating one match as proof. Its canonical factor list includes utility-class density, Shadcn or Radix markers, Lucide iconography, AI IDE markers, platform signatures, prompt-like comments, provider metadata, placeholder text, and other structural clues. Each factor has a weight, and the result exposes the individual detections.&lt;/p&gt;

&lt;p&gt;That makes the extension useful as a forensic hint, not as an authorship detector. The distinction matters: the project documentation describes heuristic analysis, while the implementation can only observe the current document and its accessible markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or a compatible current Node.js installation&lt;/li&gt;
&lt;li&gt;npm&lt;/li&gt;
&lt;li&gt;Google Chrome or another Chromium browser that supports Manifest V3&lt;/li&gt;
&lt;li&gt;A page you are allowed to inspect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository is released under the &lt;a href="https://github.com/paladini/vibe-code-detector/blob/main/LICENSE" rel="noopener noreferrer"&gt;MIT License&lt;/a&gt;. The current &lt;code&gt;main&lt;/code&gt; branch reports version &lt;code&gt;1.0.0&lt;/code&gt; in both &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;extension/manifest.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the extension from source
&lt;/h2&gt;

&lt;p&gt;Clone the public repository and install its locked dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paladini/vibe-code-detector.git
&lt;span class="nb"&gt;cd &lt;/span&gt;vibe-code-detector
npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build the extension and run the TypeScript check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run lint
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;lint&lt;/code&gt; script runs &lt;code&gt;tsc --noEmit&lt;/code&gt;. The Vite build writes the extension assets into &lt;code&gt;extension/&lt;/code&gt;, including the popup page and bundled assets. The release workflow uses the same build command before packaging &lt;code&gt;extension/&lt;/code&gt; into a zip.&lt;/p&gt;

&lt;p&gt;To load the result locally, open &lt;code&gt;chrome://extensions&lt;/code&gt;, enable Developer mode, choose &lt;strong&gt;Load unpacked&lt;/strong&gt;, and select the repository's &lt;code&gt;extension/&lt;/code&gt; directory. The manual installation path is also documented in the &lt;a href="https://github.com/paladini/vibe-code-detector#installation" rel="noopener noreferrer"&gt;project README&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the scanner works
&lt;/h2&gt;

&lt;p&gt;The implementation keeps its factor definitions in &lt;a href="https://github.com/paladini/vibe-code-detector/blob/main/src/lib/vibe-detector.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/lib/vibe-detector.ts&lt;/code&gt;&lt;/a&gt;. The same analysis function is used by the web preview and popup, while the content script contains an inlined copy because browser content scripts do not use the project's module imports directly.&lt;/p&gt;

&lt;p&gt;The main path is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the active document's HTML and element structure.&lt;/li&gt;
&lt;li&gt;Count utility classes and compare the count with the number of elements.&lt;/li&gt;
&lt;li&gt;Check for known Shadcn, Radix, Lucide, IDE, and platform markers.&lt;/li&gt;
&lt;li&gt;Walk comment nodes for prompt-like or generated-by text.&lt;/li&gt;
&lt;li&gt;Add the weights of detected factors.&lt;/li&gt;
&lt;li&gt;Clamp the score to 100 and return the factor list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, the utility-density rule counts whitespace-separated classes on elements and marks the factor when the average exceeds &lt;code&gt;4.5&lt;/code&gt;, or when the HTML includes the &lt;code&gt;tw-&lt;/code&gt; prefix. The Shadcn and Radix rule requires at least three markers from a small list that includes &lt;code&gt;bg-background&lt;/code&gt;, &lt;code&gt;text-foreground&lt;/code&gt;, &lt;code&gt;data-radix-&lt;/code&gt;, &lt;code&gt;data-state=&lt;/code&gt;, &lt;code&gt;rounded-xl&lt;/code&gt;, and &lt;code&gt;border-input&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result is intentionally inspectable. A caller receives a score plus factors shaped like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;VibeResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;factors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VibeFactor&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure is more valuable than a bare boolean because it supports review. Someone examining a high score can ask which signals fired and whether those signals make sense for the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the behavior without trusting the label
&lt;/h2&gt;

&lt;p&gt;Use two pages with different characteristics. First, inspect a normal documentation or application page that uses a component library. Then inspect a page that clearly contains one or more generator markers. Compare the factor list, not just the final score.&lt;/p&gt;

&lt;p&gt;The project has a practical constraint: &lt;code&gt;extension/content.js&lt;/code&gt; must stay synchronized with &lt;code&gt;src/lib/vibe-detector.ts&lt;/code&gt;. If you change a heuristic, update both paths and rebuild. Otherwise the preview and the loaded extension can disagree even though the TypeScript source looks correct.&lt;/p&gt;

&lt;p&gt;You can also verify the packaged output by checking that the build produced &lt;code&gt;extension/popup.html&lt;/code&gt; and the generated JavaScript and CSS assets. The repository's release workflow runs &lt;code&gt;npm ci&lt;/code&gt;, &lt;code&gt;npm run build&lt;/code&gt;, checks that the package and manifest versions match, then creates a zip and SHA-256 checksum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and security boundaries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A high score is not proof
&lt;/h3&gt;

&lt;p&gt;Tailwind, Radix, Lucide, and motion libraries are popular outside AI-generated projects. Conversely, a site can be generated with an AI tool and remove the visible markers. Treat the score as a collection of clues. Do not use it as evidence of who wrote a page or as an automated moderation decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  The scan is document-local
&lt;/h3&gt;

&lt;p&gt;The core function receives a &lt;code&gt;Document&lt;/code&gt; and reads its DOM, HTML, and comments. It does not establish provenance, inspect a private repository, or recover an author's workflow. Results can change after client-side rendering, navigation, or a site redesign.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissions still matter
&lt;/h3&gt;

&lt;p&gt;The manifest requests &lt;code&gt;activeTab&lt;/code&gt; and &lt;code&gt;scripting&lt;/code&gt;, and its content script matches &lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt;. Install the extension only from a source you trust, review permission changes, and inspect the code before loading an unpacked build. The repository recommends the Chrome Web Store listing for normal installation, while source loading is useful for development and review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency health is separate from scanner logic
&lt;/h3&gt;

&lt;p&gt;In a fresh checkout on August 10, 2026, &lt;code&gt;npm ci&lt;/code&gt; completed and reported 19 audit findings: 2 low, 7 moderate, 9 high, and 1 critical. That result is not proof that the extension is exploitable, but it is a reason to review the dependency tree before distributing a build. Run &lt;code&gt;npm audit&lt;/code&gt; yourself and reassess the result as dependencies change.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does it detect whether a human or AI wrote a page?
&lt;/h3&gt;

&lt;p&gt;No. It detects implementation and markup signals that may correlate with AI-assisted tools. The score is heuristic evidence about the page, not an authorship verdict.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the extension upload the page?
&lt;/h3&gt;

&lt;p&gt;The documented analysis function operates on the active page's &lt;code&gt;Document&lt;/code&gt;. Review the current repository and manifest before installing any version, especially if future releases add network features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I add another AI tool signature?
&lt;/h3&gt;

&lt;p&gt;Yes. Add a narrowly defined marker or factor to the canonical TypeScript implementation, mirror the change in &lt;code&gt;extension/content.js&lt;/code&gt;, then run &lt;code&gt;npm run lint&lt;/code&gt; and &lt;code&gt;npm run build&lt;/code&gt;. A good contribution should include a reason the signal is useful and a note about false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The strongest part of this project is its boundary: it turns a vague question about a web page into a visible list of weighted, testable signals. That does not make the conclusion certain. It makes the reasoning inspectable, which is the better property for a browser tool that deals with ambiguous evidence.&lt;/p&gt;

&lt;p&gt;AI assistance disclosure: I used an AI assistant to help organize this tutorial and check the documented build path. The repository behavior, commands, permissions, and limitations were verified against the public source and a fresh local build.&lt;/p&gt;

&lt;p&gt;What additional page signal would you test first, and how would you measure whether it adds useful evidence without increasing false positives?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>chrome</category>
      <category>tutorial</category>
      <category>browserextension</category>
    </item>
    <item>
      <title>Build an Offline Spatial Reasoning Trainer with OpenFold</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Sun, 09 Aug 2026 12:40:25 +0000</pubDate>
      <link>https://dev.to/paladini/build-an-offline-spatial-reasoning-trainer-with-openfold-2901</link>
      <guid>https://dev.to/paladini/build-an-offline-spatial-reasoning-trainer-with-openfold-2901</guid>
      <description>&lt;h1&gt;
  
  
  Build an Offline Spatial Reasoning Trainer with OpenFold
&lt;/h1&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you want to practice mental rotation without uploading attempts to a hosted service, OpenFold is an interesting open-source example. It generates cube-net folding problems locally, renders them with Three.js, stores practice telemetry in IndexedDB, and exposes a React web app. This tutorial checks out the current &lt;code&gt;main&lt;/code&gt; branch, builds the app, runs its test suites, and explains where its offline boundary comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: spatial practice is often difficult to inspect
&lt;/h2&gt;

&lt;p&gt;Spatial reasoning practice needs more than a collection of static images. A useful exercise should generate new problems, distinguish equivalent cube rotations from genuinely different answers, give feedback, and let you review progress.&lt;/p&gt;

&lt;p&gt;That creates an engineering problem: the generator, 3D view, answer validation, and local history must agree. If they use different geometry rules, a visual answer can look correct while the checker rejects it. If the app sends attempts to a server, the learner also loses a simple privacy boundary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/paladini/OpenFold" rel="noopener noreferrer"&gt;OpenFold&lt;/a&gt; addresses that problem as a local-first educational simulator. Its README describes procedural cube-net exercises, a TypeScript core, a Three.js renderer, a React web app, IndexedDB telemetry, and a Rust desktop shell. The repository is public and MIT licensed.&lt;/p&gt;

&lt;p&gt;This walkthrough uses the repository's current &lt;code&gt;main&lt;/code&gt; branch. The README and changelog describe a v1.0.0 scope, but the &lt;code&gt;v1.0.0&lt;/code&gt; Git ref was not resolvable during verification, so do not treat that label as a downloadable release here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer.&lt;/li&gt;
&lt;li&gt;pnpm 10 or newer.&lt;/li&gt;
&lt;li&gt;Git.&lt;/li&gt;
&lt;li&gt;A modern browser if you want to run the web app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Node and pnpm requirements come from the repository README and root package metadata. The desktop wrapper additionally requires a Rust toolchain, but it is not needed for the web build.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Check out and install the project
&lt;/h2&gt;

&lt;p&gt;Clone the repository and install its pinned dependency graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paladini/OpenFold.git
&lt;span class="nb"&gt;cd &lt;/span&gt;OpenFold
pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workspace contains three relevant packages: &lt;code&gt;@openfold/core&lt;/code&gt;, &lt;code&gt;@openfold/render&lt;/code&gt;, and &lt;code&gt;@openfold/web&lt;/code&gt;. The core and render packages are private workspace packages rather than published npm packages, so a source checkout is the normal path for this tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Build the browser application
&lt;/h2&gt;

&lt;p&gt;Run the workspace build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;-w&lt;/span&gt; build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles the TypeScript packages and creates the Vite production bundle for the web app. To try the application locally, start the documented development server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; @openfold/web dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the local URL printed by Vite. The browser version does not require an account or an API key. The app's storage layer uses IndexedDB for local sessions and attempts, and the desktop bridge is a no-op in a plain browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Understand the generated problem
&lt;/h2&gt;

&lt;p&gt;The most useful boundary in OpenFold is the core package. Its public &lt;code&gt;generateProblem&lt;/code&gt; function accepts a seed and either generation parameters or a difficulty preset. Internally, the current implementation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;validates the seed and resolves the preset;&lt;/li&gt;
&lt;li&gt;creates a deterministic random-number generator;&lt;/li&gt;
&lt;li&gt;generates a cube net;&lt;/li&gt;
&lt;li&gt;folds the net into a cube state;&lt;/li&gt;
&lt;li&gt;creates non-equivalent distractors;&lt;/li&gt;
&lt;li&gt;shuffles the answer pool; and&lt;/li&gt;
&lt;li&gt;returns the correct answer index plus metadata for the distractors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sequence matters because the renderer and the answer checker can share the same folded representation. The code also retries generation when a distractor set cannot be built, then throws a &lt;code&gt;GenerationError&lt;/code&gt; after its bounded redraw limit. A bounded failure is easier to test and diagnose than an infinite retry loop.&lt;/p&gt;

&lt;p&gt;The current core API is documented directly in &lt;a href="https://github.com/paladini/OpenFold/blob/main/packages/core/src/index.ts" rel="noopener noreferrer"&gt;&lt;code&gt;packages/core/src/index.ts&lt;/code&gt;&lt;/a&gt;. The implementation exports helpers for canonicalization, equivalence checks, presets, net generation, and seeded random numbers, which makes the geometry rules testable without opening a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Verify the important behavior
&lt;/h2&gt;

&lt;p&gt;Run the checks used by the repository's workspace scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;-w&lt;/span&gt; typecheck
pnpm &lt;span class="nt"&gt;-w&lt;/span&gt; lint
pnpm &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my source checkout of the current &lt;code&gt;main&lt;/code&gt; branch, the checks completed successfully. The core package reported 112 passing tests, the render package reported 76, and the web package reported 264.&lt;/p&gt;

&lt;p&gt;The tests cover more than whether functions return objects. The core suite checks that alternatives are pairwise non-equivalent, that the correct answer is not positionally biased across seeded samples, and that generated problems remain valid across difficulty presets. The render suite compares its folded state with the core result. The web suite exercises rounds, keyboard-only play, local persistence, dashboards, and training explanations.&lt;/p&gt;

&lt;p&gt;The repository also defines a Playwright configuration for browser tests and an accessibility audit. Those checks are separate from the fast workspace test command and require the browser test setup, so a green unit and integration test run should not be described as a complete cross-browser or WCAG certification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the offline boundary is useful
&lt;/h2&gt;

&lt;p&gt;OpenFold's local-first design is concrete rather than just a marketing label:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The web app persists attempts through IndexedDB.&lt;/li&gt;
&lt;li&gt;The README describes no network calls for problem generation and local training data.&lt;/li&gt;
&lt;li&gt;There are no user accounts or cloud-sync features in the documented v1 scope.&lt;/li&gt;
&lt;li&gt;The desktop bridge exists for the native shell, while a browser can run without it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives a learner a clear operational model: install the code, open the app, practice, and keep the resulting history in the local browser profile. The tradeoff is equally important. Local storage is not a backup service. Clearing browser data, changing profiles, or uninstalling a portable desktop build can make history unavailable unless you export it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and limitations
&lt;/h2&gt;

&lt;p&gt;The project documentation lists several limits that should shape your expectations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The exercise type is cube-net folding. It is not a general spatial-ability trainer.&lt;/li&gt;
&lt;li&gt;Difficulty uses deterministic tiers rather than adaptive IRT.&lt;/li&gt;
&lt;li&gt;The web experience is English-only at the documented scope.&lt;/li&gt;
&lt;li&gt;The desktop distribution is portable rather than a signed native installer.&lt;/li&gt;
&lt;li&gt;Cross-engine behavior on macOS and Linux webviews still needs manual verification according to the release notes.&lt;/li&gt;
&lt;li&gt;The production web bundle emits a large-chunk warning during Vite build. That is a packaging optimization issue, not evidence that the build failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The security boundary is also narrow. Offline generation and local IndexedDB storage reduce the need to send practice data elsewhere, but they do not protect a compromised machine or browser profile. The repository's own release checklist calls for reviewing credentials, path traversal, XSS, dependency licenses, and audit results. Those are review tasks, not guarantees supplied by the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is OpenFold available as an npm package?
&lt;/h3&gt;

&lt;p&gt;The workspace packages are marked private in their package metadata. Use the repository checkout and pnpm workspace commands for development.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Rust to try the app?
&lt;/h3&gt;

&lt;p&gt;No. Rust is required for the desktop wrapper. The React and Vite web app can be built and served with Node.js and pnpm.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a seed make every visual result identical everywhere?
&lt;/h3&gt;

&lt;p&gt;The core generator is seeded and tested for deterministic geometry, but the changelog distinguishes web verification from manual verification of other webview engines. Treat cross-platform visual equivalence as a claim to test on the target platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is local storage the same as private storage?
&lt;/h3&gt;

&lt;p&gt;No. It means the application is designed to keep data on the local device. Anyone or any software with access to that browser profile or machine may still access it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;OpenFold is a compact case study in making an educational simulator inspectable: deterministic generation, shared geometry rules, local telemetry, and tests that connect the core to the UI. Start with the web workspace, run the typecheck, lint, build, and test commands, then inspect the core package before changing the visual layer.&lt;/p&gt;

&lt;p&gt;The project describes a v1.0.0 scope, but its release tag was not verifiable in this run. If you use OpenFold, pin the commit you tested and keep that distinction visible in your own notes.&lt;/p&gt;

&lt;p&gt;What would you improve first: adaptive difficulty, additional spatial exercise types, or a more portable desktop release process?&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;This tutorial was researched and edited with AI assistance. Repository facts, commands, source links, and verification results were checked against the public OpenFold repository and a local source checkout before publication.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>react</category>
      <category>tutorial</category>
      <category>threejs</category>
    </item>
    <item>
      <title>Fetch Daily Reflections from Node.js with an Unofficial AA API Client</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Sat, 08 Aug 2026 12:36:43 +0000</pubDate>
      <link>https://dev.to/paladini/fetch-daily-reflections-from-nodejs-with-an-unofficial-aa-api-client-1g0o</link>
      <guid>https://dev.to/paladini/fetch-daily-reflections-from-nodejs-with-an-unofficial-aa-api-client-1g0o</guid>
      <description>&lt;p&gt;Many small Node.js scripts start as one HTTP request and become awkward as soon as they need a date, a language, a reusable type, and a useful error message.&lt;/p&gt;

&lt;p&gt;This tutorial uses &lt;a href="https://github.com/paladini/aa-daily-reflections-api" rel="noopener noreferrer"&gt;aa-daily-reflections&lt;/a&gt;, an MIT-licensed open-source Node.js library, to build that boundary in a few minutes. The package exposes a typed &lt;code&gt;DailyReflections&lt;/code&gt; class and an &lt;code&gt;aa-daily&lt;/code&gt; command. It fetches Daily Reflections content from the public AA.org API, so the useful engineering lesson is not just how to print a result. It is how to keep input validation, network access, parsing, and attribution visible in a small client.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Install &lt;code&gt;aa-daily-reflections&lt;/code&gt;, use &lt;code&gt;getReflection(month, day)&lt;/code&gt; for a deterministic date, choose &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;es&lt;/code&gt;, or &lt;code&gt;fr&lt;/code&gt;, and treat the upstream service and its content rules as part of your application boundary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 16 or newer and npm 8 or newer.&lt;/li&gt;
&lt;li&gt;A terminal with network access.&lt;/li&gt;
&lt;li&gt;A legitimate personal, educational, or recovery-support use for the returned content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package metadata currently declares version &lt;code&gt;1.0.1&lt;/code&gt;, MIT licensing, and support for Windows, macOS, and Linux on x64 or arm64. Node.js 18 or newer is a simpler choice because it provides the native &lt;code&gt;fetch&lt;/code&gt; implementation used by the library. On Node.js 16, the project can fall back to &lt;code&gt;node-fetch&lt;/code&gt; when it is available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and inspect the CLI
&lt;/h2&gt;

&lt;p&gt;Create a small project and install the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;daily-reflection-demo
&lt;span class="nb"&gt;cd &lt;/span&gt;daily-reflection-demo
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;aa-daily-reflections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package includes the &lt;code&gt;aa-daily&lt;/code&gt; executable. Before making a request, inspect its documented options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx aa-daily &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI supports today's reflection, a specific &lt;code&gt;MM/DD&lt;/code&gt; date, and the &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;es&lt;/code&gt;, and &lt;code&gt;fr&lt;/code&gt; language codes. The help command is a useful first smoke test because it exercises the installed package without contacting the upstream service.&lt;/p&gt;

&lt;p&gt;To fetch a specific date in English, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx aa-daily 06/25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is formatted for a terminal and may include a title, quote, reference, reflection text, and copyright information. The command is a convenience layer. The class API is the better fit when another program needs structured data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the typed API from JavaScript
&lt;/h2&gt;

&lt;p&gt;Add a file named &lt;code&gt;index.cjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DailyReflections&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aa-daily-reflections&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DailyReflections&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reflection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReflection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;copyright&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;copyright&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node index.cjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important choice is &lt;code&gt;getReflection(6, 25)&lt;/code&gt;, rather than embedding a URL in your application. The library validates the month and day, builds the upstream URL, performs the request, parses the response, and returns a &lt;code&gt;DailyReflection&lt;/code&gt; object. That gives a caller one narrow async boundary and one predictable place to handle failure.&lt;/p&gt;

&lt;p&gt;For a date-driven application, &lt;code&gt;getToday()&lt;/code&gt; uses the machine's current local date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DailyReflections&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getToday&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;monthName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your application runs in a different timezone from the person reading it, pass an explicit month and day instead. The library's &lt;code&gt;getToday()&lt;/code&gt; reads the JavaScript runtime's current date; it does not accept a timezone argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switch languages without duplicating clients
&lt;/h2&gt;

&lt;p&gt;The supported language type is intentionally small: &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;es&lt;/code&gt;, or &lt;code&gt;fr&lt;/code&gt;. You can select a language at construction time or change it before a later request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DailyReflections&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aa-daily-reflections&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;printTitles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DailyReflections&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reflection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReflection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;printTitles&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Could not load reflections: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example makes the package's stateful design visible. &lt;code&gt;setLanguage()&lt;/code&gt; changes the language for future requests on that instance. If concurrent requests need different languages, use separate instances so one operation cannot change another operation's configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a failure boundary
&lt;/h2&gt;

&lt;p&gt;There are two different kinds of invalid input to handle. A month outside 1 through 12 is rejected, and a day greater than the valid number for a month is rejected before the network request. Network and upstream HTTP failures happen later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DailyReflections&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aa-daily-reflections&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DailyReflections&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReflection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not valid for month&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Choose a real calendar date.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`The reflection service could not be reached: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;readDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project wraps fetch failures with &lt;code&gt;Failed to fetch daily reflection&lt;/code&gt;. Its HTTP client sends an &lt;code&gt;Accept: application/json&lt;/code&gt; header and throws when the upstream response is not successful. That is enough for a small script, but a production integration may want retry policy, timeout handling, structured logging, and a cache owned by the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the result reproducibly
&lt;/h2&gt;

&lt;p&gt;Use a fixed date for a smoke test rather than today's date. That lets you verify the same input path repeatedly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx aa-daily &lt;span class="nt"&gt;-d&lt;/span&gt; 06/25 &lt;span class="nt"&gt;-l&lt;/span&gt; es
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify the local validation path without depending on a successful network request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx aa-daily &lt;span class="nt"&gt;-d&lt;/span&gt; 02/30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command should fail with an invalid-date error before a valid reflection is printed. Do not assert an exact quote or reflection body in an automated test unless you have permission to store that content and are prepared for upstream changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this boundary is useful
&lt;/h2&gt;

&lt;p&gt;The library separates four concerns: date helpers validate inputs, URL helpers construct the request, an HTTP client handles fetch and status errors, and a parser converts the upstream response into the &lt;code&gt;DailyReflection&lt;/code&gt; shape. The public class then exposes only the operations an application needs: get today's item, get a date, change language, and inspect the current language.&lt;/p&gt;

&lt;p&gt;That structure is small enough to understand and useful enough to reuse. It also leaves the right responsibilities with the caller. Your application decides how to schedule requests, cache results, display content, and respond when the upstream service is unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and security boundaries
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This is an unofficial client. It is not endorsed by Alcoholics Anonymous.&lt;/li&gt;
&lt;li&gt;The library accesses content from the public AA.org API. Respect the source, its terms, copyright, and reasonable request rates.&lt;/li&gt;
&lt;li&gt;The software is MIT licensed, but the Daily Reflections content is separately copyrighted by Alcoholics Anonymous World Services, Inc. The library license does not grant permission to republish that content.&lt;/li&gt;
&lt;li&gt;The package does not provide authentication, an API key, retries, caching, or a documented service-level guarantee.&lt;/li&gt;
&lt;li&gt;Never put account credentials or unrelated secrets into this client. It only needs the public upstream request described by the package.&lt;/li&gt;
&lt;li&gt;Treat returned text as untrusted external data. Escape it for HTML, logs, or other output formats as appropriate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this package need an API key?
&lt;/h3&gt;

&lt;p&gt;No API key is documented. It requests the public upstream service, so rate limits and availability still apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use TypeScript?
&lt;/h3&gt;

&lt;p&gt;Yes. The package publishes declarations and exports &lt;code&gt;DailyReflection&lt;/code&gt; and &lt;code&gt;Language&lt;/code&gt; types.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;getToday()&lt;/code&gt; use UTC?
&lt;/h3&gt;

&lt;p&gt;It uses the JavaScript runtime's local date. Use an explicit month and day when timezone behavior matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the content safe to republish in my app?
&lt;/h3&gt;

&lt;p&gt;Do not assume that. Review the source's terms and copyright position. The repository explicitly separates its MIT software license from the copyrighted AA content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aa-daily-reflections&lt;/code&gt; is a compact example of a good application boundary: validate inputs before I/O, return typed data, expose the CLI and API separately, and document the legal and operational limits around upstream content. Start with a fixed-date smoke test, then add your own timeout, cache, and output policy if the workflow grows.&lt;/p&gt;

&lt;p&gt;Have you built a small API wrapper where the hardest part was not the request itself, but making the upstream limits clear to every caller?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI assistance disclosure: This tutorial was researched and drafted with AI assistance. The repository documentation, package metadata, source files, and CLI help were checked against the current public project before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>Build a Deterministic Multi-Agent Pipeline with A2A in Python</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Thu, 06 Aug 2026 12:36:58 +0000</pubDate>
      <link>https://dev.to/paladini/build-a-deterministic-multi-agent-pipeline-with-a2a-in-python-1gh</link>
      <guid>https://dev.to/paladini/build-a-deterministic-multi-agent-pipeline-with-a2a-in-python-1gh</guid>
      <description>&lt;p&gt;Multi-agent examples often jump straight to models, tools, and production claims. That makes it difficult to see what the protocol is doing. Before adding an LLM, it is useful to watch a small system discover specialists, delegate a task, and return a result that you can inspect.&lt;/p&gt;

&lt;p&gt;This tutorial uses &lt;a href="https://github.com/paladini/a2a-orchestration-lab" rel="noopener noreferrer"&gt;A2A Orchestration Lab&lt;/a&gt;, an open-source Python project by Fernando Paladini. It starts three local agents: an orchestrator, a researcher, and a writer. The researcher and writer are deterministic stubs, so the example isolates the Agent2Agent (A2A) communication flow from model behavior.&lt;/p&gt;

&lt;p&gt;The result is a runnable research-to-write pipeline that helps explain where A2A fits next to the Model Context Protocol (MCP).&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Install the lab with &lt;code&gt;uv&lt;/code&gt;, run its &lt;code&gt;demo&lt;/code&gt; command, and inspect the three local Agent Cards and the delegated result. The project is a learning lab, not a production runtime. That is a feature for this tutorial because every moving part remains visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.12 or newer.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; for environment and dependency management.&lt;/li&gt;
&lt;li&gt;A terminal with network access for the initial dependency download.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository declares version &lt;code&gt;0.1.0&lt;/code&gt;, requires Python &lt;code&gt;&amp;gt;=3.12&lt;/code&gt;, and depends on the A2A Python SDK, &lt;code&gt;httpx&lt;/code&gt;, and &lt;code&gt;uvicorn&lt;/code&gt;. It is licensed under MIT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create and run the lab
&lt;/h2&gt;

&lt;p&gt;Clone the public repository and let &lt;code&gt;uv&lt;/code&gt; create the environment from the locked dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paladini/a2a-orchestration-lab.git
&lt;span class="nb"&gt;cd &lt;/span&gt;a2a-orchestration-lab
uv &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the bundled end-to-end demo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run a2a-lab demo &lt;span class="s2"&gt;"Explain A2A and how it relates to MCP"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI starts the three agents as subprocesses, waits for their Agent Cards, sends a message to the orchestrator, prints the response, and terminates the child processes. The default prompt is the same explanation used by the repository README, but using your own prompt makes the delegation easier to recognize.&lt;/p&gt;

&lt;p&gt;On a successful run, the output contains sections similar to these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[demo] asking orchestrator: 'Explain A2A and how it relates to MCP'

## Orchestrator result

**User:** Explain A2A and how it relates to MCP

### Step 1 - Researcher (A2A)
# Research notes

### Step 2 - Writer (A2A)
# Brief
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact wording is generated by the local stubs and can change with repository updates. The important result is the sequence: the orchestrator receives the prompt, delegates research, then gives the research result to the writer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect the three agents
&lt;/h2&gt;

&lt;p&gt;The lab can also run each service independently. Open three terminals in the repository and start one command in each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run a2a-lab run researcher
uv run a2a-lab run writer
uv run a2a-lab run orchestrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The services listen on loopback addresses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orchestrator: &lt;code&gt;http://127.0.0.1:9100&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Researcher: &lt;code&gt;http://127.0.0.1:9101&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Writer: &lt;code&gt;http://127.0.0.1:9102&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each exposes an Agent Card at &lt;code&gt;/.well-known/agent-card.json&lt;/code&gt;. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:9100/.well-known/agent-card.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An Agent Card is the discovery surface. It tells a client what an agent is, where it is available, and which skills or interfaces it advertises. In this lab, opening the cards is a practical way to connect the protocol concept to a real HTTP response.&lt;/p&gt;

&lt;p&gt;Stop the three processes with &lt;code&gt;Ctrl+C&lt;/code&gt; when you are finished. The one-shot &lt;code&gt;demo&lt;/code&gt; command handles child-process cleanup in its &lt;code&gt;finally&lt;/code&gt; block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow the delegation path in the source
&lt;/h2&gt;

&lt;p&gt;The most useful source-reading path is short:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with &lt;a href="https://github.com/paladini/a2a-orchestration-lab/blob/main/src/a2a_lab/cli.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/a2a_lab/cli.py&lt;/code&gt;&lt;/a&gt;. The &lt;code&gt;cmd_demo&lt;/code&gt; function starts the agents and waits for their cards. &lt;code&gt;_ask_orchestrator&lt;/code&gt; resolves the orchestrator card, creates an A2A client, and sends a text message.&lt;/li&gt;
&lt;li&gt;Read the agent server and executor modules to see how a local process becomes an A2A service and how task lifecycle states are handled.&lt;/li&gt;
&lt;li&gt;Read the orchestrator agent to see the client-side delegation to the researcher and writer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The CLI uses the A2A Python SDK types for a &lt;code&gt;SendMessageRequest&lt;/code&gt;, then collects the response stream into text. This is a useful separation: the CLI manages the demo lifecycle, while the agent modules implement the roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  A2A and MCP solve different boundaries
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://a2a-protocol.org/latest/specification/" rel="noopener noreferrer"&gt;A2A describes itself as a protocol for agent discovery and task-oriented communication&lt;/a&gt;. Its core concepts include messages, tasks, artifacts, Agent Cards, and task updates. In the lab, A2A is the horizontal connection between the orchestrator and its specialist agents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/specification/2025-06-18" rel="noopener noreferrer"&gt;MCP standardizes connections between LLM applications and external data sources or tools&lt;/a&gt;. Its server features include resources, prompts, and tools. MCP is therefore a natural boundary when one agent needs a capability such as filesystem access, search, or a code analysis tool.&lt;/p&gt;

&lt;p&gt;A simple mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user -&amp;gt; A2A orchestrator -&amp;gt; A2A researcher
                       -&amp;gt; A2A writer
                              |
                              -&amp;gt; MCP tools, when a specialist needs them
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lab does not implement MCP. Its README explicitly treats MCP as a later addition after the A2A learning path. That makes the project a good place to understand agent-to-agent delegation before introducing another protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the demo proves, and what it does not
&lt;/h2&gt;

&lt;p&gt;The smoke test proves that the declared Python environment resolves, the three services start, Agent Cards become reachable, a client can send a message to the orchestrator, and the pipeline returns a result. It does not prove model quality, distributed reliability, authentication, or production readiness.&lt;/p&gt;

&lt;p&gt;The project intentionally lists several missing production concerns: portable task checkpointing, capability tokens, strong sandboxing, multi-vendor identity and trust, budgets, audit as a primitive, and governed business context. Treat that list as an implementation boundary, not as a roadmap promise.&lt;/p&gt;

&lt;p&gt;The lab also uses loopback HTTP services and local knowledge-base stubs. Do not expose the three ports to a network and assume that localhost implies authorization. If you replace the stubs with real tools or models, add authentication, authorization, timeouts, rate limits, structured logs, and explicit data handling rules before handling sensitive input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and troubleshooting
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;uv sync&lt;/code&gt; fails, verify the Python version with &lt;code&gt;python --version&lt;/code&gt; and the tool with &lt;code&gt;uv --version&lt;/code&gt;. The project requires Python 3.12 or newer.&lt;/p&gt;

&lt;p&gt;If the demo reports that an agent is not ready, check whether ports 9100, 9101, or 9102 are already in use. The CLI waits up to 15 seconds for each Agent Card. Stop stale processes and try again.&lt;/p&gt;

&lt;p&gt;If the demo prints a result but the content is unexpectedly short, remember that the researcher and writer are deterministic stubs. Change the local knowledge base or prompt to explore the flow. Adding an LLM is a separate experiment, not a prerequisite for understanding A2A.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this lab require an API key?
&lt;/h3&gt;

&lt;p&gt;No. The README describes the first-day workflow as local and without API keys. Its current agents use deterministic stubs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is A2A a replacement for MCP?
&lt;/h3&gt;

&lt;p&gt;No. A2A connects agents to agents. MCP connects an AI application to tools and data sources. A system can use both, with an A2A specialist invoking MCP tools when that capability belongs inside the specialist boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the demo run forever?
&lt;/h3&gt;

&lt;p&gt;No. &lt;code&gt;uv run a2a-lab demo&lt;/code&gt; starts the agents, runs one task, prints the result, and shuts them down. Use the separate &lt;code&gt;run&lt;/code&gt; commands when you want to inspect services manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this ready for production?
&lt;/h3&gt;

&lt;p&gt;No. The project explicitly describes itself as a learning lab and names the lifecycle, identity, capability, budget, audit, and governance gaps that remain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A2A becomes easier to reason about when the first example is deterministic. The Orchestration Lab gives you three inspectable local services, discoverable Agent Cards, a real delegation path, and a clear place to add MCP later.&lt;/p&gt;

&lt;p&gt;I used AI assistance to organize and edit this tutorial. The repository README, &lt;code&gt;pyproject.toml&lt;/code&gt;, learning notes, CLI source, MIT license, official A2A specification, official MCP specification, and a live &lt;code&gt;uv sync&lt;/code&gt; plus demo run were checked separately.&lt;/p&gt;

&lt;p&gt;What would you replace first in this lab: the researcher stub, the writer stub, or the orchestrator's routing logic?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
      <category>multiagent</category>
    </item>
    <item>
      <title>Build a Backpacking Gear List with an MCP Server</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Wed, 05 Aug 2026 12:35:51 +0000</pubDate>
      <link>https://dev.to/paladini/build-a-backpacking-gear-list-with-an-mcp-server-1o65</link>
      <guid>https://dev.to/paladini/build-a-backpacking-gear-list-with-an-mcp-server-1o65</guid>
      <description>&lt;h1&gt;
  
  
  Build a Backpacking Gear List with an MCP Server
&lt;/h1&gt;

&lt;p&gt;Packing lists are easy to start and surprisingly hard to maintain. A spreadsheet can track a tent and a stove, but it does not give an AI assistant a useful interface for creating categories, updating item quantities, or generating a share link.&lt;/p&gt;

&lt;p&gt;This tutorial shows how to run &lt;a href="https://github.com/paladini/lighterpack-mcp" rel="noopener noreferrer"&gt;lighterpack-mcp&lt;/a&gt; from its public TypeScript repository and connect it to an MCP-compatible client. The server exposes LighterPack operations for lists, categories, items, weights, flags, and sharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Clone the repository, install its dependencies, build the server, and point your MCP client at the generated &lt;code&gt;dist/index.js&lt;/code&gt; file. Provide a LighterPack username and password through environment variables, then ask the client to create a list with categories and gear.&lt;/p&gt;

&lt;p&gt;The current GitHub repository reports version &lt;code&gt;0.1.0&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;, and the repository has no release tag. The public npm registry also returned 404 for &lt;code&gt;lighterpack-mcp&lt;/code&gt; during verification on August 5, 2026, so this walkthrough deliberately uses the source checkout instead of the README's &lt;code&gt;npx lighterpack-mcp&lt;/code&gt; example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer.&lt;/li&gt;
&lt;li&gt;Git.&lt;/li&gt;
&lt;li&gt;An existing LighterPack account.&lt;/li&gt;
&lt;li&gt;An MCP client that can launch a local stdio server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is MIT licensed. It is an unofficial integration and is not affiliated with or endorsed by LighterPack.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Clone and verify the source
&lt;/h2&gt;

&lt;p&gt;Clone the public repository and install the locked dependency tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paladini/lighterpack-mcp.git
&lt;span class="nb"&gt;cd &lt;/span&gt;lighterpack-mcp
npm ci
npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run typecheck
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository's current scripts define &lt;code&gt;npm test&lt;/code&gt; as the unit-test suite, &lt;code&gt;npm run typecheck&lt;/code&gt; as a TypeScript check, and &lt;code&gt;npm run build&lt;/code&gt; as the production compilation step. During this tutorial's verification, 75 unit tests passed, type checking passed, and the build completed successfully.&lt;/p&gt;

&lt;p&gt;The build creates &lt;code&gt;dist/index.js&lt;/code&gt;, which is the executable configured in &lt;code&gt;package.json&lt;/code&gt;. Keep the checkout in a stable location because the absolute path will be part of your MCP client configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Configure the MCP server
&lt;/h2&gt;

&lt;p&gt;The server reads credentials from &lt;code&gt;LIGHTERPACK_USERNAME&lt;/code&gt; and &lt;code&gt;LIGHTERPACK_PASSWORD&lt;/code&gt;. It also accepts an optional &lt;code&gt;LIGHTERPACK_BASE_URL&lt;/code&gt;, which defaults to &lt;code&gt;https://lighterpack.com&lt;/code&gt; and can point to a self-hosted LighterPack instance.&lt;/p&gt;

&lt;p&gt;A generic MCP configuration looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lighterpack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/lighterpack-mcp/dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"LIGHTERPACK_USERNAME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-username"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"LIGHTERPACK_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-password"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;/absolute/path/to&lt;/code&gt; with the checkout path on your machine. On Windows, use the path style required by your client, such as &lt;code&gt;C:\\src\\lighterpack-mcp\\dist\\index.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not commit this configuration when it contains real credentials. Prefer the host's secret or environment-variable support when available, and treat the password as a credential with access to the complete LighterPack account.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Ask the agent to build a list
&lt;/h2&gt;

&lt;p&gt;After restarting the MCP client, verify that it discovers the &lt;code&gt;lighterpack&lt;/code&gt; server. A useful first request is intentionally small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use lighterpack to create a list named "Weekend Trail". Add Shelter and Kitchen categories. Add a tent weighing 1200 grams to Shelter and a stove weighing 450 grams to Kitchen. Show the resulting totals, but do not delete or overwrite any existing list.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server's tool boundary uses grams for item weights and returns &lt;code&gt;weightGrams&lt;/code&gt; values, even if the LighterPack interface displays another unit. The tool descriptions also expose business rules such as mutually exclusive &lt;code&gt;worn&lt;/code&gt; and &lt;code&gt;consumable&lt;/code&gt; flags.&lt;/p&gt;

&lt;p&gt;Once the list looks correct, a second request can create a public read-only link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use lighterpack to generate a share link for "Weekend Trail". Do not change any items.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected result is a LighterPack list with the requested categories and items, calculated totals, and a link in the form &lt;code&gt;lighterpack.com/r/&amp;lt;code&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;The server is not translating each action into a granular LighterPack REST request. LighterPack stores an account's library as one JSON document. The server's &lt;code&gt;SyncEngine&lt;/code&gt; fetches that library, clones it, applies one mutation, and saves it through &lt;code&gt;POST /saveLibrary&lt;/code&gt; with a &lt;code&gt;sync_token&lt;/code&gt; for optimistic concurrency.&lt;/p&gt;

&lt;p&gt;That design explains two important behaviors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every write must preserve the rest of the account library. A small-looking item edit still participates in a whole-library synchronization cycle.&lt;/li&gt;
&lt;li&gt;If another writer saves between reads, the server retries once after detecting the concurrency conflict. If you edit the same list in a browser while an agent session is active, call &lt;code&gt;refresh_library&lt;/code&gt; before continuing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The project keeps mutation rules in &lt;code&gt;src/lighterpack/mutations.ts&lt;/code&gt;. Its tests cover cases such as item sharing, ID sequencing, quantity changes, star levels, the last-category guard, and worn versus consumable flags. This separation gives the MCP tools a smaller and more reviewable business-logic surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and safe boundaries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The client cannot start the server
&lt;/h3&gt;

&lt;p&gt;Confirm that Node.js is version 20 or newer, that &lt;code&gt;npm run build&lt;/code&gt; created &lt;code&gt;dist/index.js&lt;/code&gt;, and that the configured path points to that file. Run the built entry point directly to inspect startup diagnostics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node dist/index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process communicates over stdout using the MCP protocol, so diagnostics are written to stderr. Do not add ordinary logging to stdout in a local modification.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication fails
&lt;/h3&gt;

&lt;p&gt;Check the environment-variable names exactly. The server does not provide account registration, account deletion, password changes, email changes, or password recovery tools. Those omissions are deliberate limits on what an agent can do with the integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Changes appear stale
&lt;/h3&gt;

&lt;p&gt;There is no realtime push channel. If the LighterPack website or another server instance changed the account, call &lt;code&gt;refresh_library&lt;/code&gt; before making another edit. One server instance is designed for one LighterPack account.&lt;/p&gt;

&lt;h3&gt;
  
  
  A destructive action is requested
&lt;/h3&gt;

&lt;p&gt;Treat &lt;code&gt;delete_item&lt;/code&gt;, list deletion, category removal, and batch updates as actions requiring explicit confirmation. The README documents that &lt;code&gt;delete_item&lt;/code&gt; cannot be undone, and some operations guard against removing the only list or category. An MCP client can expose a tool, but it cannot make an irreversible operation safe by itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  You want a self-hosted endpoint
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;LIGHTERPACK_BASE_URL&lt;/code&gt; supports a different LighterPack endpoint, but changing it changes the trust boundary. Validate the URL before use, use HTTPS where appropriate, and understand the server's security policy before sending credentials to a non-default host.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducible verification
&lt;/h2&gt;

&lt;p&gt;The repository includes a network-free unit suite and an opt-in live integration suite. The unit suite can be repeated without an account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run typecheck
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The live test is intentionally not part of the default test command. The contributing guide says it exercises create, edit, share, and delete against a real LighterPack account and requires &lt;code&gt;RUN_INTEGRATION=1&lt;/code&gt; plus real credentials. Do not enable it against an account containing data you cannot afford to change. Use a disposable account if you choose to run the live smoke test.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is lighterpack-mcp an official LighterPack integration?
&lt;/h3&gt;

&lt;p&gt;No. The repository describes it as unofficial. It talks to the web service's own API, while the LighterPack application remains a separate project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I install it with npm today?
&lt;/h3&gt;

&lt;p&gt;The repository README contains an &lt;code&gt;npx&lt;/code&gt; quick start, but the public npm registry returned 404 for &lt;code&gt;lighterpack-mcp&lt;/code&gt; during this verification. Use the documented source checkout and build path until a published package is verifiably available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it store my password in a database?
&lt;/h3&gt;

&lt;p&gt;The security policy says the password is supplied through an environment variable and the authenticated session cookie is held in memory for the process lifetime. Review the code and deployment host before treating that as a complete security assessment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it support multiple accounts in one server process?
&lt;/h3&gt;

&lt;p&gt;No. The README documents one LighterPack account per server instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;lighterpack-mcp turns a packing list into a tool interface an MCP client can understand, but the useful engineering detail is its synchronization boundary: one authenticated server instance manages one whole library document with optimistic concurrency. Build and test it locally, keep credentials outside source control, refresh before working after browser edits, and confirm destructive actions before they reach the account.&lt;/p&gt;

&lt;p&gt;What other personal data model would benefit from a small, typed MCP layer instead of a general-purpose assistant guessing at a web UI?&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize this tutorial and edit its prose. The repository commands, configuration names, version details, limitations, and security boundaries were checked against the public project sources and the local verification run described above.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>backpacking</category>
    </item>
    <item>
      <title>Give AI Coding Agents LSP Navigation with Locus MCP</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:37:55 +0000</pubDate>
      <link>https://dev.to/paladini/give-ai-coding-agents-lsp-navigation-with-locus-mcp-3hk8</link>
      <guid>https://dev.to/paladini/give-ai-coding-agents-lsp-navigation-with-locus-mcp-3hk8</guid>
      <description>&lt;h1&gt;
  
  
  Give AI Coding Agents LSP Navigation with Locus MCP
&lt;/h1&gt;

&lt;p&gt;An AI coding agent can edit a file quickly and still misunderstand the codebase. Text search finds matching strings, but it does not reliably tell you which definition is active, which callers will be affected, or what type a value has.&lt;/p&gt;

&lt;p&gt;This tutorial shows how to connect an MCP-capable coding agent to &lt;a href="https://github.com/paladini/locus-mcp" rel="noopener noreferrer"&gt;Locus MCP&lt;/a&gt;, an open-source server that delegates semantic code questions to language servers. You will install the published v0.1.5 package, create project configuration, add a host entry, and verify the connection with a real prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Install a language server, run Locus setup in the project root, and add an MCP server entry that points &lt;code&gt;cwd\&lt;/code&gt; at that project:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
npm install -g typescript-language-server typescript&lt;br&gt;
npx @paladini/locus-mcp init&lt;br&gt;
npx @paladini/locus-mcp check&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then configure your agent host to run &lt;code&gt;npx -y @paladini/locus-mcp@0.1.5 serve\&lt;/code&gt;. Ask the agent to use the &lt;code&gt;locate\&lt;/code&gt;, &lt;code&gt;refs\&lt;/code&gt;, or &lt;code&gt;hover\&lt;/code&gt; tool instead of guessing from grep output.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem does this solve?
&lt;/h2&gt;

&lt;p&gt;Suppose an agent must rename &lt;code&gt;parseConfig\&lt;/code&gt;. Grep can list every occurrence of that text, including comments, strings, tests, generated files, and unrelated symbols. An LSP can answer a narrower question: which declaration does this identifier refer to, and which source locations reference that declaration?&lt;/p&gt;

&lt;p&gt;Locus is a small MCP layer between the agent host and those language servers. The released documentation describes six tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;locate\&lt;/code&gt; finds a symbol or lists symbols in a file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;refs\&lt;/code&gt; finds references or implementations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hover\&lt;/code&gt; returns type information and documentation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;diagnostics\&lt;/code&gt; reports errors and warnings.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status\&lt;/code&gt; reports language-server readiness.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rename\&lt;/code&gt; previews the impact of a rename without applying edits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction matters. Locus reads code structure and diagnostics; your agent still performs edits, and grep remains useful for plain text such as logs, comments, and configuration keys. See the &lt;a href="https://github.com/paladini/locus-mcp/blob/v0.1.5/docs/usage.md" rel="noopener noreferrer"&gt;Locus usage guide&lt;/a&gt; for the supported workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22 or newer.&lt;/li&gt;
&lt;li&gt;An MCP-capable host such as Cursor, Codex, or Claude Code.&lt;/li&gt;
&lt;li&gt;A language server installed for the language you want to inspect.&lt;/li&gt;
&lt;li&gt;A project directory where you can create configuration files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a TypeScript or JavaScript project, install the TypeScript language server and compiler:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
npm install -g typescript-language-server typescript&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For Python, the documented option is &lt;code&gt;pip install pyright\&lt;/code&gt;. The &lt;a href="https://github.com/paladini/locus-mcp/blob/v0.1.5/docs/getting-started.md#step-2-install-language-servers" rel="noopener noreferrer"&gt;getting started guide&lt;/a&gt; also lists &lt;code&gt;gopls\&lt;/code&gt; for Go and &lt;code&gt;rust-analyzer\&lt;/code&gt; for Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and check Locus
&lt;/h2&gt;

&lt;p&gt;Open a terminal in the root of the project you want the agent to understand. Run the published package explicitly so the example is tied to the stable v0.1.5 release:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
npx @paladini/locus-mcp init&lt;br&gt;
npx @paladini/locus-mcp check&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;init\&lt;/code&gt; creates &lt;code&gt;locus.toml\&lt;/code&gt; and &lt;code&gt;locus.json\&lt;/code&gt;. &lt;code&gt;check\&lt;/code&gt; verifies that the language-server binaries are available on &lt;code&gt;PATH\&lt;/code&gt;. A successful TypeScript check should identify &lt;code&gt;typescript-language-server\&lt;/code&gt;; a missing binary is a setup problem, not an MCP problem.&lt;/p&gt;

&lt;p&gt;If your project uses more than one language, install only the servers it needs and configure the warm languages in &lt;code&gt;locus.toml\&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;toml&lt;br&gt;
root = "."&lt;br&gt;
warm = ["typescript", "python"]&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/paladini/locus-mcp/blob/v0.1.5/docs/configuration.md" rel="noopener noreferrer"&gt;configuration reference&lt;/a&gt; documents the file priority and custom server fields. Locus looks for &lt;code&gt;locus.toml\&lt;/code&gt;, then &lt;code&gt;locus.json\&lt;/code&gt;, then &lt;code&gt;.lsp.json\&lt;/code&gt; while walking up from the current directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the MCP server to your host
&lt;/h2&gt;

&lt;p&gt;The host starts the server on demand. You normally do not run &lt;code&gt;serve\&lt;/code&gt; manually in a terminal.&lt;/p&gt;

&lt;p&gt;For Cursor, create &lt;code&gt;.cursor/mcp.json\&lt;/code&gt; in the project:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "mcpServers": {&lt;br&gt;
    "locus": {&lt;br&gt;
      "command": "npx",&lt;br&gt;
      "args": ["-y", "@paladini/locus-mcp", "serve"],&lt;br&gt;
      "cwd": "/absolute/path/to/your/project"&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For Codex, the equivalent project-scoped entry is TOML:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;toml&lt;br&gt;
[mcp_servers.locus]&lt;br&gt;
command = "npx"&lt;br&gt;
args = ["-y", "@paladini/locus-mcp", "serve"]&lt;br&gt;
cwd = "/absolute/path/to/your/project"&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Replace the placeholder with the absolute project path. The &lt;code&gt;cwd\&lt;/code&gt; value is important because it tells Locus which codebase and configuration to inspect. After saving the file, reload MCP servers or restart the host.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/paladini/locus-mcp/blob/v0.1.5/docs/getting-started.md#step-4-add-locus-to-your-agent" rel="noopener noreferrer"&gt;official setup walkthrough&lt;/a&gt; shows the corresponding Claude Code shape and the host-specific reload step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the complete path
&lt;/h2&gt;

&lt;p&gt;First ask the agent to call &lt;code&gt;status\&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Call the Locus MCP tool &lt;code&gt;status\&lt;/code&gt; and tell me which language servers are ready.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A readiness response confirms that the host can start Locus and that Locus can see the configured language server. If the response says &lt;code&gt;server_starting\&lt;/code&gt;, wait briefly and retry. If it says &lt;code&gt;server_unavailable\&lt;/code&gt;, run &lt;code&gt;check\&lt;/code&gt; again and inspect the binary path.&lt;/p&gt;

&lt;p&gt;Then test a semantic lookup in a project with a known symbol:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use Locus &lt;code&gt;locate\&lt;/code&gt; to find where &lt;code&gt;UserService.authenticate\&lt;/code&gt; is defined. Do not use grep.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a refactor review, use a two-step request:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before changing &lt;code&gt;parseConfig\&lt;/code&gt;, use Locus to find its definition and list every reference. Show me the files and lines before editing anything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finally, ask for diagnostics on a changed file:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Run Locus diagnostics on &lt;code&gt;src/api/handler.ts\&lt;/code&gt; and report errors and warnings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These prompts test different boundaries: server readiness, symbol resolution, reference discovery, and diagnostics. They also make the agent's intended tool visible in the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;Locus does not implement a parser for every language. It starts configured language-server processes and translates MCP tool calls into language-server requests. This lets the agent use the same general semantic services that IDEs expose, while keeping the MCP surface focused.&lt;/p&gt;

&lt;p&gt;The configuration separates project settings from server definitions. &lt;code&gt;locus.toml\&lt;/code&gt; is convenient for the root and warm languages; &lt;code&gt;locus.json\&lt;/code&gt; can describe commands, arguments, language IDs, and file extensions. When no custom &lt;code&gt;servers\&lt;/code&gt; array is supplied, Locus uses its built-in defaults for TypeScript, Python, Go, and Rust.&lt;/p&gt;

&lt;p&gt;That architecture also explains the limits. Results depend on the language server, its project configuration, its indexing state, and the files visible from the selected root. A green &lt;code&gt;check\&lt;/code&gt; means the executable exists on &lt;code&gt;PATH\&lt;/code&gt;; it does not guarantee that every workspace can produce a useful answer immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and safe boundaries
&lt;/h2&gt;

&lt;p&gt;If the MCP server does not appear, confirm that the host configuration is valid, &lt;code&gt;cwd\&lt;/code&gt; is an absolute project path, and the host was reloaded. If a language is missing, install its server and rerun &lt;code&gt;check\&lt;/code&gt;. If indexing is still in progress, retry after &lt;code&gt;server_starting\&lt;/code&gt; rather than treating it as a definitive failure.&lt;/p&gt;

&lt;p&gt;Review &lt;code&gt;locus.json\&lt;/code&gt; and &lt;code&gt;.lsp.json\&lt;/code&gt; before trusting a project. Locus runs language servers as child processes and spawns the configured command and arguments. Its security policy explicitly warns that configuration should be reviewed before use. A language server is also a separate dependency with its own security and trust boundary.&lt;/p&gt;

&lt;p&gt;Locus does not edit code, store agent memory, replace grep, or promise complete IDE coverage. &lt;code&gt;rename\&lt;/code&gt; is a preview; the agent applies any actual change. For symbolic editing, persistent memory, or a much broader toolkit, the project documentation points readers to alternatives such as Serena.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need to understand LSP?
&lt;/h3&gt;

&lt;p&gt;No. You need a compatible language server and the documented setup commands. Locus exposes the agent-facing MCP tools while the language server handles semantic analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I have to install Locus globally?
&lt;/h3&gt;

&lt;p&gt;No. The documented recommended path uses &lt;code&gt;npx\&lt;/code&gt;, so the host can start the package without a global Locus installation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I keep using grep?
&lt;/h3&gt;

&lt;p&gt;Yes. Use Locus for symbols, references, types, and diagnostics. Use grep for strings, comments, logs, and configuration text.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if my language is not listed?
&lt;/h3&gt;

&lt;p&gt;Check whether your language server can be described in &lt;code&gt;locus.json\&lt;/code&gt; or &lt;code&gt;.lsp.json\&lt;/code&gt;. The configuration reference documents custom server entries, but compatibility still depends on the language server's behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The useful change is not adding another search command to an agent. It is giving the agent a deliberate boundary between text search and semantic code navigation. Install the language server, run &lt;code&gt;init\&lt;/code&gt; and &lt;code&gt;check\&lt;/code&gt;, point the MCP host at the correct project root, and ask for &lt;code&gt;status\&lt;/code&gt; before relying on &lt;code&gt;locate\&lt;/code&gt;, &lt;code&gt;refs\&lt;/code&gt;, &lt;code&gt;hover\&lt;/code&gt;, or &lt;code&gt;diagnostics\&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This article was prepared with AI assistance. The repository, v0.1.5 package metadata, release-tagged documentation, security policy, and &lt;code&gt;--help\&lt;/code&gt; output were checked before publication; the article does not claim results beyond those sources and smoke checks.&lt;/p&gt;

&lt;p&gt;What is the first code-navigation task you would delegate to an MCP server: finding definitions, reviewing references before a refactor, or checking diagnostics after an edit?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>tutorial</category>
      <category>lsp</category>
    </item>
    <item>
      <title>Run Local Audio Transcription with EchoTranscribe and Whisper</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:35:57 +0000</pubDate>
      <link>https://dev.to/paladini/run-local-audio-transcription-with-echotranscribe-and-whisper-2hii</link>
      <guid>https://dev.to/paladini/run-local-audio-transcription-with-echotranscribe-and-whisper-2hii</guid>
      <description>&lt;p&gt;Sending a recording to a hosted transcription service is convenient, but it also creates a data-handling decision. Meeting audio, interviews, research notes, and draft content may be easier to process when the files stay on the computer that owns them.&lt;/p&gt;

&lt;p&gt;This tutorial walks through the documented development path for &lt;a href="https://github.com/paladini/echo-transcribe" rel="noopener noreferrer"&gt;EchoTranscribe&lt;/a&gt;, an MIT-licensed desktop application by Fernando Paladini. It combines a Tauri desktop shell, a React and TypeScript frontend, and a local FastAPI backend that loads Whisper models through faster-whisper.&lt;/p&gt;

&lt;p&gt;The goal is not to claim that local transcription is automatically more accurate or faster. The useful outcome is a reproducible local workflow: start the backend, open the desktop app, choose a model, transcribe supported audio, inspect timestamps, and export TXT, SRT, or JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Use the stable &lt;code&gt;v0.1.1&lt;/code&gt; release, install the frontend dependencies, start the backend, and then run the Tauri development app in a second terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v0.1.1 https://github.com/paladini/echo-transcribe.git
&lt;span class="nb"&gt;cd &lt;/span&gt;echo-transcribe
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the first terminal, start the Python backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;src-tauri/backend
python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the second terminal, from the repository root, start Tauri:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run tauri dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend documents its API at &lt;a href="http://localhost:8000/docs" rel="noopener noreferrer"&gt;http://localhost:8000/docs&lt;/a&gt;, and the Tauri window should open the frontend automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;v0.1.1&lt;/code&gt; README lists these prerequisites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 18 or newer&lt;/li&gt;
&lt;li&gt;Python 3.8 or newer&lt;/li&gt;
&lt;li&gt;Rust for Tauri compilation&lt;/li&gt;
&lt;li&gt;Microsoft Visual Studio C++ Build Tools on Windows&lt;/li&gt;
&lt;li&gt;The Linux system packages documented in the README when developing on Ubuntu or Debian&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project also has macOS setup guidance using Homebrew. Tauri compilation adds platform-specific requirements, so a successful Python installation alone is not enough to build the desktop application.&lt;/p&gt;

&lt;p&gt;The backend requirements pin FastAPI 0.104.1, Uvicorn 0.24.0, faster-whisper 0.9.0, Pydantic 2.5.0, and supporting packages. PyTorch and torchaudio are specified as version 2.0.0 or newer. Read the repository's current dependency files before installing if you are working from &lt;code&gt;main&lt;/code&gt; instead of the stable tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start the local backend
&lt;/h2&gt;

&lt;p&gt;The README provides a startup script for each platform, but the manual path makes the process easier to inspect. From the repository root, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;src-tauri/backend
python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend creates model and temporary directories below &lt;code&gt;~/.echo-transcribe&lt;/code&gt;. It searches ports 8000 through 8004 and writes the selected port to &lt;code&gt;backend_port.txt&lt;/code&gt;. When port 8000 is available, the documented API address is &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before launching the desktop shell, check the backend's health endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A healthy response includes &lt;code&gt;status&lt;/code&gt; set to &lt;code&gt;healthy&lt;/code&gt;. You can also open the generated OpenAPI documentation at &lt;a href="http://localhost:8000/docs" rel="noopener noreferrer"&gt;http://localhost:8000/docs&lt;/a&gt; to inspect the available routes.&lt;/p&gt;

&lt;p&gt;The first transcription may take longer because the selected Whisper model is downloaded when it is not already present. The source stores models under the &lt;code&gt;.echo-transcribe/models&lt;/code&gt; directory in your home folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Launch the Tauri desktop app
&lt;/h2&gt;

&lt;p&gt;Keep the backend terminal running. Open another terminal at the repository root and run the exact command documented for Tauri development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run tauri dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Tauri configuration uses &lt;code&gt;http://localhost:1420&lt;/code&gt; as the development URL and starts the Vite frontend with &lt;code&gt;npm run dev&lt;/code&gt;. The desktop window is configured with a 1200 by 800 initial size and can be resized.&lt;/p&gt;

&lt;p&gt;The application is not a remote client. Its frontend is paired with the backend running on your machine, and the backend's CORS configuration allows the local Vite origin and &lt;code&gt;tauri://localhost&lt;/code&gt;. That local arrangement is part of the privacy boundary, but it is not authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transcribe a file
&lt;/h2&gt;

&lt;p&gt;In the app, select one audio file or a batch. The README documents MP3, WAV, FLAC, M4A, OGG, and WebM input, with a maximum of 10 files selected at once for batch transcription.&lt;/p&gt;

&lt;p&gt;Choose one of the models exposed by the backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tiny&lt;/code&gt;: 39 MB, faster with lower expected accuracy&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;base&lt;/code&gt;: 74 MB, a balance between speed and precision&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;small&lt;/code&gt;: 244 MB, better quality with medium speed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;medium&lt;/code&gt;: 769 MB, higher quality with slower processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These sizes are the values in the tagged source's model metadata, not a benchmark for your machine. Runtime depends on the audio, model, dependency versions, and whether the environment can use an available accelerator.&lt;/p&gt;

&lt;p&gt;Leave automatic language detection enabled for a first test, or choose a language manually. The backend first validates the extension and model name. It then writes the upload to a temporary file, loads the selected Whisper model, transcribes with word timestamps enabled, and schedules cleanup of the temporary file.&lt;/p&gt;

&lt;p&gt;The result can be reviewed in the interface and exported as TXT, SRT, or JSON. Word-level timestamps are included in the backend response when the transcription model returns them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the path without transcribing a large recording
&lt;/h2&gt;

&lt;p&gt;Use a short audio file that you are allowed to process. Verify the workflow in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm that &lt;code&gt;/health&lt;/code&gt; returns a healthy status.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;/models&lt;/code&gt; and inspect the four model entries.&lt;/li&gt;
&lt;li&gt;Start with the &lt;code&gt;tiny&lt;/code&gt; or &lt;code&gt;base&lt;/code&gt; model to reduce the initial download and wait time.&lt;/li&gt;
&lt;li&gt;Transcribe a short file in one of the documented formats.&lt;/li&gt;
&lt;li&gt;Confirm that text and timestamps appear in the result.&lt;/li&gt;
&lt;li&gt;Export one result as TXT, SRT, or JSON and open the exported file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The backend also exposes &lt;code&gt;POST /transcribe&lt;/code&gt; and &lt;code&gt;POST /transcribe-batch&lt;/code&gt;. The interactive Swagger page at &lt;code&gt;/docs&lt;/code&gt; is the safest place to inspect the current multipart field names instead of guessing at a curl command. The single-file route accepts an uploaded &lt;code&gt;file&lt;/code&gt;, a &lt;code&gt;model&lt;/code&gt;, an optional &lt;code&gt;language&lt;/code&gt;, and the &lt;code&gt;auto_detect_language&lt;/code&gt; flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the local architecture works
&lt;/h2&gt;

&lt;p&gt;The project separates three responsibilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tauri packages the desktop experience and connects the frontend to the local application.&lt;/li&gt;
&lt;li&gt;React, TypeScript, and Vite provide the interface and development server.&lt;/li&gt;
&lt;li&gt;FastAPI handles uploads, model loading, transcription, language detection, timestamps, and cleanup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This separation keeps model execution out of the browser UI. It also makes the backend inspectable through OpenAPI while keeping the default network path on loopback. The model cache avoids downloading the same model for every request, and the temporary directory gives the backend a controlled place to write uploaded audio during processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and security boundaries
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The desktop window cannot load the backend.&lt;/strong&gt; Check that the Python process is still running and that port 8000 is available. If the backend selected another port, inspect &lt;code&gt;backend_port.txt&lt;/code&gt; and the terminal log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first request appears stuck.&lt;/strong&gt; Model loading and download happen before transcription. Check the network connection and backend logs. Do not use a long recording as the first test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The file is rejected.&lt;/strong&gt; Confirm that its extension is one of MP3, WAV, FLAC, M4A, OGG, or WebM. The backend validates the suffix, not the contents of every possible container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The build fails on a platform dependency.&lt;/strong&gt; Install the platform prerequisites listed by the README, including Rust and the required Linux packages or Windows C++ Build Tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want to expose the API to another machine.&lt;/strong&gt; Stop and design that boundary first. The current backend allows broad methods and headers for its two local origins, and the repository does not document user authentication, quotas, or a production reverse proxy. Local execution limits where the default app sends data, but it does not turn an unauthenticated API into a safe public service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You process someone else's recording.&lt;/strong&gt; Local execution does not replace consent, retention, copyright, or organizational data-handling requirements. Only transcribe audio you are authorized to process.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does EchoTranscribe require a cloud API key?
&lt;/h3&gt;

&lt;p&gt;The documented backend loads Whisper models locally through faster-whisper and downloads missing models to the local model directory. The tutorial does not require a hosted transcription key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use it without a GPU?
&lt;/h3&gt;

&lt;p&gt;The repository documents model choices and local execution, but it does not promise a particular hardware configuration or processing speed. Start with &lt;code&gt;tiny&lt;/code&gt; or &lt;code&gt;base&lt;/code&gt; and check the backend logs on your machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is &lt;code&gt;main&lt;/code&gt; the same as the release?
&lt;/h3&gt;

&lt;p&gt;No. This tutorial uses the stable &lt;code&gt;v0.1.1&lt;/code&gt; tag. The repository's default branch can change, so pin the tag when you need the commands and model list described here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it ready to expose as a shared service?
&lt;/h3&gt;

&lt;p&gt;That is outside the documented guarantee. Add authentication, request limits, storage controls, logging decisions, and a deliberate network boundary before considering shared access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;EchoTranscribe is a useful pattern for local AI desktop tooling: a Tauri shell, an inspectable FastAPI service, a persistent model directory, and explicit export formats. Pin &lt;code&gt;v0.1.1&lt;/code&gt;, verify the health endpoint, test with a short authorized recording, and treat the lack of authentication as a real deployment boundary.&lt;/p&gt;

&lt;p&gt;Which local transcription feature would you verify next: speaker separation, stronger export controls, or authenticated access for a small team?&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize this tutorial and review its wording. The release, commands, versions, routes, model names, file paths, limitations, and security notes were checked against the public &lt;code&gt;v0.1.1&lt;/code&gt; repository sources linked above. No performance benchmark or personal usage claim is implied.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
      <category>whisper</category>
    </item>
    <item>
      <title>Self-Host Audio Stem Separation with Docker and Demucs</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Sun, 02 Aug 2026 12:36:20 +0000</pubDate>
      <link>https://dev.to/paladini/self-host-audio-stem-separation-with-docker-and-demucs-2hmk</link>
      <guid>https://dev.to/paladini/self-host-audio-stem-separation-with-docker-and-demucs-2hmk</guid>
      <description>&lt;p&gt;Turning a song into a vocal track, an instrumental, or separate drums and bass often leads to a choice between a hosted service and a complicated local machine setup. Hosted services can create privacy and upload concerns. A local setup can require Python, FFmpeg, PyTorch, model downloads, and a way to retrieve generated files.&lt;/p&gt;

&lt;p&gt;This tutorial uses &lt;a href="https://github.com/paladini/voice-separator-demucs" rel="noopener noreferrer"&gt;voice-separator-demucs&lt;/a&gt;, an MIT-licensed self-hosted application by Fernando Paladini. It provides a browser interface and a FastAPI backend for separating audio with Demucs. The Docker path keeps the model cache persistent and maps generated files to a directory on your computer.&lt;/p&gt;

&lt;p&gt;The result is a local service at &lt;code&gt;http://localhost:7860&lt;/code&gt;. You can upload an MP3, WAV, FLAC, M4A, or AAC file, choose stems such as vocals or drums, and download the generated MP3 files without sending the audio to a hosted application.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Clone the repository, start the Docker Compose service, open the local web interface, and upload an audio file. The current Compose file maps &lt;code&gt;./static/output&lt;/code&gt; to the container output directory and stores the model cache in a named Docker volume.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paladini/voice-separator-demucs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;voice-separator-demucs
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:7860" rel="noopener noreferrer"&gt;http://localhost:7860&lt;/a&gt;. The first separation downloads the selected model, so the initial run takes longer than later runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker with the Compose plugin&lt;/li&gt;
&lt;li&gt;A machine with enough disk space for Python dependencies and the Demucs model cache&lt;/li&gt;
&lt;li&gt;An audio file you are allowed to process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository also documents a Python path that needs Python 3.8 or newer and FFmpeg. Docker is the more reproducible starting point because the Dockerfile installs FFmpeg and uses Python 3.9 inside the image. The Dockerfile currently installs the unpinned dependencies from &lt;code&gt;requirements.txt&lt;/code&gt;, so this is a current-branch setup rather than a fully lockfile-reproducible build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start the local service
&lt;/h2&gt;

&lt;p&gt;Clone the repository and start the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paladini/voice-separator-demucs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;voice-separator-demucs
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Compose configuration exposes port &lt;code&gt;7860&lt;/code&gt;, mounts &lt;code&gt;./static/output&lt;/code&gt; at &lt;code&gt;/app/static/output&lt;/code&gt;, and persists &lt;code&gt;/root/.cache&lt;/code&gt; in a named volume called &lt;code&gt;model-cache&lt;/code&gt;. The host output mount is useful because files created by the container remain available in the repository's &lt;code&gt;static/output&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Check that the API is alive before uploading anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:7860/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application defines this endpoint as a simple health check. A successful response has the following shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Voice Separator API is running"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also open the interactive FastAPI documentation at &lt;a href="http://localhost:7860/docs" rel="noopener noreferrer"&gt;http://localhost:7860/docs&lt;/a&gt;. The documented API includes &lt;code&gt;/api/stems&lt;/code&gt;, &lt;code&gt;/api/separate&lt;/code&gt;, and &lt;code&gt;/api/separate-youtube&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate vocals with the API
&lt;/h2&gt;

&lt;p&gt;The browser interface is the easiest path, but the API makes the workflow scriptable. The upload endpoint accepts a multipart file, a comma-separated &lt;code&gt;stems&lt;/code&gt; value, and an optional model name. The default model is &lt;code&gt;mdx_extra_q&lt;/code&gt; and the default stem is &lt;code&gt;vocals&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The following command uses the repository's current endpoint and parameter names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:7860/api/separate"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@./example.mp3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"stems=vocals"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On success, the response includes &lt;code&gt;success: true&lt;/code&gt;, the processed stems, an estimated processing time, and a file URL such as &lt;code&gt;/static/output/vocals_&amp;lt;id&amp;gt;.mp3&lt;/code&gt;. Because the output directory is mounted by Compose, the same file is also available under &lt;code&gt;static/output&lt;/code&gt; on the host.&lt;/p&gt;

&lt;p&gt;To request more than one stem, pass a comma-separated list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:7860/api/separate"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@./example.wav"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"stems=vocals,instrumental"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The available selections in the current implementation are &lt;code&gt;drums&lt;/code&gt;, &lt;code&gt;bass&lt;/code&gt;, &lt;code&gt;other&lt;/code&gt;, &lt;code&gt;vocals&lt;/code&gt;, and &lt;code&gt;instrumental&lt;/code&gt;. The instrumental result combines drums, bass, and other. The backend validates both the model name and the selected stems before processing the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the model choices
&lt;/h2&gt;

&lt;p&gt;The interface exposes four model names: &lt;code&gt;mdx_extra_q&lt;/code&gt;, &lt;code&gt;mdx&lt;/code&gt;, &lt;code&gt;htdemucs&lt;/code&gt;, and &lt;code&gt;htdemucs_ft&lt;/code&gt;. The application selects CPU for &lt;code&gt;mdx_extra_q&lt;/code&gt; and &lt;code&gt;mdx&lt;/code&gt; when a GPU is unavailable. The two &lt;code&gt;htdemucs&lt;/code&gt; variants require a CUDA-capable GPU in the current separator implementation.&lt;/p&gt;

&lt;p&gt;For a first local test, keep the default &lt;code&gt;mdx_extra_q&lt;/code&gt; model and request only vocals. The README describes it as the CPU-oriented default. It also notes that the first model download is about 200 MB, while exact disk usage and runtime depend on the dependency versions, hardware, audio length, and selected model.&lt;/p&gt;

&lt;p&gt;Do not treat the README's rough processing times as a benchmark. They are operational guidance, not a guarantee. A longer file, several selected stems, or CPU-only processing can take substantially longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Compose mounts matter
&lt;/h2&gt;

&lt;p&gt;There are two different persistence concerns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The named &lt;code&gt;model-cache&lt;/code&gt; volume prevents the model cache from disappearing when the container is recreated.&lt;/li&gt;
&lt;li&gt;The bind mount keeps generated audio in &lt;code&gt;./static/output&lt;/code&gt; on the host.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can inspect the container and output files with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"name=voice-separator"&lt;/span&gt;
Get-ChildItem .&lt;span class="se"&gt;\s&lt;/span&gt;tatic&lt;span class="se"&gt;\o&lt;/span&gt;utput
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command is for PowerShell. On macOS or Linux, use &lt;code&gt;ls -lah static/output&lt;/code&gt; instead. If you use the single &lt;code&gt;docker run&lt;/code&gt; command from the README without a bind mount, output files stay inside the container and must be copied out with &lt;code&gt;docker cp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you are done, stop the service without deleting the cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To remove the persistent model volume as well, use &lt;code&gt;docker compose down -v&lt;/code&gt;. That forces a future startup to download the model again.&lt;/p&gt;

&lt;h2&gt;
  
  
  YouTube input and its boundary
&lt;/h2&gt;

&lt;p&gt;The application also exposes &lt;code&gt;/api/separate-youtube&lt;/code&gt; and uses &lt;code&gt;yt-dlp&lt;/code&gt; to download audio before separation. The current route validates a YouTube URL and rejects videos longer than 10 minutes. It then removes the temporary downloaded audio after processing.&lt;/p&gt;

&lt;p&gt;This feature does not remove copyright or platform obligations. Process only material you have permission to download and transform. A public URL is not automatically a license to copy its audio.&lt;/p&gt;

&lt;p&gt;For a privacy-focused local workflow, prefer direct file upload. Both routes write output into the local application's static directory, and the app does not provide authentication in the current implementation. Do not expose port 7860 to the public internet without adding an access-control and deployment boundary appropriate for your environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes to check first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;FFmpeg errors:&lt;/strong&gt; The Docker image installs FFmpeg. If you use the Python path, install FFmpeg separately as described in the repository README and confirm that &lt;code&gt;ffmpeg -version&lt;/code&gt; works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow first request:&lt;/strong&gt; Model loading and the initial model download happen before separation. Check the container logs with &lt;code&gt;docker compose logs -f&lt;/code&gt; and wait for the model to finish loading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Out of memory:&lt;/strong&gt; Use a smaller input, select fewer stems, close competing workloads, or use the CPU-oriented default. GPU model choices are not a universal speed-up if the machine lacks the required GPU memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No files on the host:&lt;/strong&gt; Confirm that you started with &lt;code&gt;docker compose up -d&lt;/code&gt; from the repository directory and that &lt;code&gt;static/output&lt;/code&gt; is the directory mounted by the Compose file. A plain &lt;code&gt;docker run&lt;/code&gt; without &lt;code&gt;-v&lt;/code&gt; keeps output inside the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser security warnings:&lt;/strong&gt; The documented default is plain HTTP on localhost. The README includes an optional self-signed HTTPS example for local development. A self-signed certificate is not a production trust model, and the current application has no authentication layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducible verification
&lt;/h2&gt;

&lt;p&gt;After the health check, verify the core path in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;/api/stems&lt;/code&gt; and confirm the available stem names.&lt;/li&gt;
&lt;li&gt;Upload a short audio file with &lt;code&gt;stems=vocals&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm the response contains &lt;code&gt;success: true&lt;/code&gt; and a generated output URL.&lt;/li&gt;
&lt;li&gt;Confirm a new MP3 appears in &lt;code&gt;static/output&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Play the output and compare its duration with the input.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The repository includes unit tests for mono-to-stereo tensor normalization. You can run the lightweight test module in a Python environment with its available dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; unittest tests/test_audio_tensor_utils.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test suite does not prove that every model, codec, GPU, or long audio file works. It verifies a focused preprocessing behavior used by the separator.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does the audio leave my computer?
&lt;/h3&gt;

&lt;p&gt;The Docker workflow runs the application locally. If you use the YouTube route, the app downloads the requested source first. Local execution does not make the application safe to expose publicly, because the current API has no authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run it without a GPU?
&lt;/h3&gt;

&lt;p&gt;Yes, the current implementation selects CPU for &lt;code&gt;mdx_extra_q&lt;/code&gt; and &lt;code&gt;mdx&lt;/code&gt;. The &lt;code&gt;htdemucs&lt;/code&gt; variants require a CUDA GPU according to the source code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this a production-ready hosted service?
&lt;/h3&gt;

&lt;p&gt;No claim like that is supported by the repository. Treat it as a local or controlled self-hosted application. Add authentication, resource limits, storage cleanup, and a deliberate reverse-proxy boundary before considering a shared deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The useful pattern is not only the separation model. It is the local boundary around it: Docker packages the runtime, a named volume preserves expensive model downloads, and a host bind mount makes generated files easy to retrieve. Start with the default CPU-oriented model and a short file, verify the health and output paths, then decide whether your hardware and usage rights support larger jobs.&lt;/p&gt;

&lt;p&gt;Have you found a reliable way to add authentication and per-job storage limits to a local audio-processing API without making the setup too complex?&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize this tutorial and review its wording. The commands, endpoint names, model names, file paths, limitations, and security notes were checked against the current public repository sources linked above. No performance benchmark or personal usage claim is implied.&lt;/p&gt;

</description>
      <category>python</category>
      <category>docker</category>
      <category>tutorial</category>
      <category>audio</category>
    </item>
    <item>
      <title>Style Active Admin 3 and 4 with One Rails Theme Gem</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Fri, 31 Jul 2026 12:37:32 +0000</pubDate>
      <link>https://dev.to/paladini/style-active-admin-3-and-4-with-one-rails-theme-gem-32g7</link>
      <guid>https://dev.to/paladini/style-active-admin-3-and-4-with-one-rails-theme-gem-32g7</guid>
      <description>&lt;p&gt;Active Admin gives Rails applications a productive admin surface, but its styling depends on which major version your application uses. Active Admin 3 expects a Sass and Sprockets workflow. Active Admin 4 uses Tailwind CSS v4 and a different source-to-build path. A theme that treats them as the same problem can leave you with a successful generator run and an unstyled page.&lt;/p&gt;

&lt;p&gt;This tutorial uses &lt;a href="https://github.com/paladini/activeadmin-claude-theme" rel="noopener noreferrer"&gt;activeadmin-claude-theme&lt;/a&gt;, an MIT-licensed Rails engine gem, to install the same warm visual language on both Active Admin branches. The current &lt;code&gt;0.2.0&lt;/code&gt; release includes a version-aware generator, Active Admin 3 support, and Active Admin 4 support. The theme is a community project inspired by Claude aesthetics. It is not affiliated with or endorsed by Anthropic.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Add the gem, run its installer, and then use the asset step that belongs to your Active Admin version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add activeadmin-claude-theme
rails generate activeadmin_claude_theme:install

&lt;span class="c"&gt;# Active Admin 4 only&lt;/span&gt;
npm run build:css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Active Admin 4, the generator keeps Tailwind source in &lt;code&gt;app/assets/tailwind/active_admin.css&lt;/code&gt; and builds the served file into &lt;code&gt;app/assets/builds/active_admin.css&lt;/code&gt;. For Active Admin 3, it writes the Sass entry point at &lt;code&gt;app/assets/stylesheets/active_admin.scss&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need an existing Rails application with Active Admin installed, Ruby 3.2 or newer, and Rails 7.2 or newer. The theme's release notes document support for Active Admin 3.2 through the 3.x line and Active Admin 4.0.0.beta22 or newer. Active Admin 4 also requires the Node-based Tailwind build used by its asset setup.&lt;/p&gt;

&lt;p&gt;The commands below assume you have already run Active Admin's installer. If you are starting from an empty Rails application, follow the &lt;a href="https://activeadmin.info/documentation.html" rel="noopener noreferrer"&gt;Active Admin installation guide&lt;/a&gt; first, then return here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the theme
&lt;/h2&gt;

&lt;p&gt;Add the gem to your application's &lt;code&gt;Gemfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"activeadmin-claude-theme"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies and run the generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install
&lt;/span&gt;rails generate activeadmin_claude_theme:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator detects the installed Active Admin major version. It does not ask you to choose a stylesheet manually, because the integration points are different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Active Admin 4: build the Tailwind output
&lt;/h2&gt;

&lt;p&gt;Active Admin 4 uses Tailwind v4. The theme generator vendors its CSS into the application, imports it from the Tailwind source, and updates the build script when the standard setup is present.&lt;/p&gt;

&lt;p&gt;Run the CSS build after installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build:css
bin/rails server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important distinction is source versus served output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/assets/tailwind/active_admin.css   # Tailwind source
app/assets/builds/active_admin.css     # compiled output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source file is outside the Propshaft-served path. Serving the uncompiled source instead of the build output is a common reason for seeing an admin page with little or none of the expected styling.&lt;/p&gt;

&lt;p&gt;The generator also adds the theme gem's view directory to the Tailwind content configuration when &lt;code&gt;tailwind-active_admin.config.js&lt;/code&gt; exists. That lets Tailwind see the engine templates that contain utility classes. The implementation uses &lt;code&gt;bundle show activeadmin-claude-theme&lt;/code&gt; to locate the installed gem rather than asking you to hard-code a machine-specific path.&lt;/p&gt;

&lt;p&gt;Active Admin's own dark-mode toggle remains part of the AA4 setup. The theme remaps color scales and adds semantic variables while preserving the existing hooks instead of replacing the whole plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Active Admin 3: use the Sass entry point
&lt;/h2&gt;

&lt;p&gt;Active Admin 3 follows a Sprockets and Sass path. After running the same generator, check &lt;code&gt;app/assets/stylesheets/active_admin.scss&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"activeadmin_claude_theme/aa3/base"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then make sure your application includes a Sass pipeline such as &lt;code&gt;sassc-rails&lt;/code&gt; or &lt;code&gt;dartsass-rails&lt;/code&gt;, and restart Rails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install
&lt;/span&gt;bin/rails server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AA3 integration replaces the legacy &lt;code&gt;active_admin/base&lt;/code&gt; import when it finds one. If the stylesheet has a different structure, the generator appends the theme import so you can review the result before committing it.&lt;/p&gt;

&lt;p&gt;There is an intentional feature difference here: Active Admin 3 receives the light theme only. Native dark mode is documented for the AA4 path because Active Admin 3 does not provide the same dark-mode mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customize the palette without replacing the theme
&lt;/h2&gt;

&lt;p&gt;The theme exposes semantic tokens for the common colors. On Active Admin 4, override CSS variables after the theme import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--claude-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#d4845f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--claude-canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.dark&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--claude-canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#121110&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Active Admin 3, set Sass variables before importing the base theme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nv"&gt;$claude-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#d4845f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$claude-canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fff8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"activeadmin_claude_theme/aa3/base"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is safer than editing generated gem files. The repository's &lt;a href="https://github.com/paladini/activeadmin-claude-theme/blob/main/DESIGN.md" rel="noopener noreferrer"&gt;design token reference&lt;/a&gt; lists the available roles, including the accent, canvas, ink, and body colors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the installation
&lt;/h2&gt;

&lt;p&gt;Use a short checklist rather than relying only on a successful generator exit code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the gem is locked to the version you reviewed. For this tutorial, that is &lt;code&gt;0.2.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On AA4, confirm the Tailwind source exists under &lt;code&gt;app/assets/tailwind/&lt;/code&gt; and the compiled CSS exists under &lt;code&gt;app/assets/builds/&lt;/code&gt; after &lt;code&gt;npm run build:css&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On AA3, confirm the Sass entry imports &lt;code&gt;activeadmin_claude_theme/aa3/base&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open an Active Admin dashboard and an index page. Check the header, navigation, table, form, and login screen.&lt;/li&gt;
&lt;li&gt;On AA4, toggle dark mode and verify that the existing control still works.&lt;/li&gt;
&lt;li&gt;Change one semantic token, rebuild the relevant assets, and confirm the rendered color changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a repository-level verification, the project documents separate dummy applications and integration commands for AA4 and AA3. Running those tests is useful when upgrading the theme or changing its overrides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# AA4&lt;/span&gt;
&lt;span class="nb"&gt;cd test&lt;/span&gt;/dummy
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run build:css
ruby bin/rails db:setup db:seed
&lt;span class="nb"&gt;cd&lt;/span&gt; ../..
ruby &lt;span class="nt"&gt;-Itest&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/activeadmin_claude_theme_test.rb
ruby &lt;span class="nt"&gt;-Itest&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/integration/theme_integration_test.rb

&lt;span class="c"&gt;# AA3&lt;/span&gt;
&lt;span class="nv"&gt;BUNDLE_GEMFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemfiles/activeadmin_3.gemfile bundle &lt;span class="nb"&gt;install
cd test&lt;/span&gt;/dummy_aa3
bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rails db:setup db:seed
&lt;span class="nb"&gt;cd&lt;/span&gt; ../..
&lt;span class="nv"&gt;DUMMY_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dummy_aa3 &lt;span class="nv"&gt;BUNDLE_GEMFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemfiles/activeadmin_3.gemfile &lt;span class="se"&gt;\&lt;/span&gt;
  ruby &lt;span class="nt"&gt;-Itest&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/activeadmin_claude_theme_test.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why the version-aware generator matters
&lt;/h2&gt;

&lt;p&gt;The visual goal is shared, but the integration boundary is not. AA4 needs Tailwind source discovery, a build command, and view content paths. AA3 needs a Sass import and a Sprockets-compatible manifest. The generator makes that branch explicit in code through Active Admin version detection.&lt;/p&gt;

&lt;p&gt;It also handles a subtle AA4 failure mode: a legacy &lt;code&gt;app/assets/stylesheets/active_admin.css&lt;/code&gt; file can be moved to &lt;code&gt;app/assets/tailwind/active_admin.css&lt;/code&gt; before compilation. That keeps the source from being treated as the final served asset by Propshaft.&lt;/p&gt;

&lt;p&gt;The result is not magic CSS. It is a small Rails engine with two integration paths, shared design tokens, and limited view overrides. That makes the generated changes inspectable in your own application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and security boundaries
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;rails generate activeadmin_claude_theme:install&lt;/code&gt; reports an unsupported Active Admin version, check the resolved bundle. Active Admin 2.x is outside the documented support range. If AA4 looks unstyled, rebuild CSS and check the source and build locations before changing templates. If AA3 fails to compile, verify the Sass dependency and import path.&lt;/p&gt;

&lt;p&gt;The theme changes presentation. It does not provide authentication, authorization, CSRF protection, or a security review of your admin resources. Active Admin's authorization and Devise configuration remain application responsibilities. Keep the admin surface behind your existing authentication and authorization controls, and review generated template changes before deploying them.&lt;/p&gt;

&lt;p&gt;Also treat the name accurately: this is a community theme, not an Anthropic product. The visual inspiration is not a security or compatibility guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does one installation command support both Active Admin versions?
&lt;/h3&gt;

&lt;p&gt;Yes. The generator detects Active Admin 3 or 4 and applies the corresponding asset setup. The follow-up verification differs because AA4 needs a CSS build while AA3 uses Sass.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Node.js for Active Admin 3?
&lt;/h3&gt;

&lt;p&gt;Not for the theme's documented AA3 path. AA3 uses Sass through Sprockets. Node.js is required for the AA4 Tailwind workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use the theme with Propshaft?
&lt;/h3&gt;

&lt;p&gt;Yes for the documented Active Admin 4 setup, provided Tailwind source stays under &lt;code&gt;app/assets/tailwind/&lt;/code&gt; and the compiled file is emitted under &lt;code&gt;app/assets/builds/&lt;/code&gt;. AA3 typically uses Sprockets instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I keep my own brand colors?
&lt;/h3&gt;

&lt;p&gt;Yes. Override the semantic CSS variables for AA4 or Sass variables for AA3, then verify the generated CSS in the browser. Use the design token reference to choose the right role instead of changing many selectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The useful lesson is not the palette. It is respecting the boundary between Active Admin 3's Sass pipeline and Active Admin 4's Tailwind pipeline. A version-aware installer can keep the setup small, but you still need to verify the actual source path, compiled asset, and rendered admin pages.&lt;/p&gt;

&lt;p&gt;Have you kept one Rails admin theme working across a major asset-pipeline change? I would be interested in which compatibility boundary caused the most maintenance in your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize this tutorial and review its wording. The technical details and commands were checked against the project's current README, generator implementation, gemspec, changelog, FAQ, override map, and the Active Admin documentation linked above.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tutorial</category>
      <category>activeadmin</category>
    </item>
  </channel>
</rss>
