2026-01-23T08:11:08Zhttps://github.com/bolasblack/BlogPosts/c4605's blogc4605bolasblack@gmail.comhttps://github.com/bolasblackI Made a Skill That Converts MCP to Skill2026-01-23T00:00:00Z2026-01-23T08:11:08Zhttps://github.com/bolasblack/BlogPosts/blob/master/2026-01-23-mcp-skill-generator<p>I recently made a skill that converts MCP servers into Claude Code skills. Sounds a bit meta, but it's actually quite useful.</p><h2 id="why-convert?">Why Convert?</h2><p>MCP itself is great. It provides a unified, discoverable interaction protocol, and the adoption rate is impressive. RESTful APIs are supported by many providers, sure, but they're too loosely constrained—honestly, "discoverability" is something many big companies (especially quite a few Chinese tech giants) do terribly. GraphQL, gRPC and the like have limited adoption and are quite fragmented. MCP solves a lot of these problems.</p><p>But it still has some issues.</p><h3 id="context-window-consumption-is-too-heavy">Context Window Consumption Is Too Heavy</h3><p>MCP works by dumping all tool definitions into the context at once. If you've connected several MCP servers, each with a bunch of tools, the tool definitions alone take up a huge chunk of your context window. For example, I just checked—the GitHub MCP takes up 10k tokens (5%) of my context.</p><p>Claude Code skills have a mechanism called progressive disclosure: the AI first sees a brief description, and only loads the full content when it actually needs to use that skill. This way the context window doesn't get stuffed with tool definitions that might never be used.</p><p>Although this skill currently can't convert MCP services that require OAuth (technically it's doable, just a bit more work—I'm considering adding it when I have time...), at least every bit of context saved counts...</p><h3 id="ai-tool-calling-is-too-inefficient">AI Tool Calling Is Too Inefficient</h3><p>This is the main reason I wanted to build this converter.</p><p>The traditional approach has the AI call tools one by one: call A, get the result, call B, call C... Each call goes through an "AI decides → make call → get result → AI decides again" loop.</p><p>The problem is this process is unreliable. The AI might skip a step, might suddenly go off the rails mid-way, and each call result has to be stuffed into the context, making token consumption skyrocket. It might even fail because the response is too long, forcing the AI to figure out how to paginate through the content.</p><p>A better approach is to have the AI write a script that completes all calls at once (or preprocesses the responses). Code control flow is much more reliable than AI "chain of thought", and Anthropic is officially researching this direction—they published an article called <a href="https://www.anthropic.com/engineering/code-execution-with-mcp">Code execution with MCP</a>, showing token consumption dropping from 150k to 2k, a 98.7% reduction.</p><p>The skill I made generates an <code>api.mjs</code> that the AI can directly import and use for scripting:</p><pre><code class="language-javascript">import { callTool } from '/<mcp-skill-path>/scripts/api.mjs';
// Search issues across multiple repos in parallel
const repos = ['facebook/react', 'vuejs/vue', 'sveltejs/svelte'];
const results = await Promise.all(
repos.map(repo => callTool('search_issues', {
repo,
query: 'memory leak'
}))
);
// Summarize results, return only needed fields
const summary = results.flatMap((r, i) =>
r.content[0].text.items?.slice(0, 3).map(issue => ({
repo: repos[i],
title: issue.title,
url: issue.html_url
})) ?? []
);
console.log(JSON.stringify(summary, null, 2));
</code></pre><p>With traditional tool calling, the AI would need to call <code>search_issues</code> 3 times, with each call's full result stuffed into context, then the AI "thinks" again about how to summarize. With a script, it executes once and returns the processed, concise result to the AI.</p><h3 id="version-locking">Version Locking</h3><p>MCP servers are typically run like <code>npx @some/mcp-server</code>, pulling the latest version every time. This means the server maintainer could push a malicious update at any moment, and you'd have no idea.</p><p>When converting to a skill, you can lock the version number—at least providing some defense against supply chain attacks.</p><h2 id="quick-example">Quick Example</h2><p>Usage looks something like this:</p><pre><code>> /mcp-skill-generator convert @anthropic/mcp-server-filesystem
> /mcp-skill-generator convert https://docs.devin.ai/work-with-devin/deepwiki-mcp
</code></pre><p>It will ask where you want to save it (project directory or personal directory), then automatically extract the schema and generate files. The resulting directory structure is:</p><pre><code>mcp-filesystem/
├── SKILL.md
├── config.toml
├── tools/
│ ├── read_file.md
│ ├── write_file.md
│ └── ...
└── scripts/
├── mcp-caller.mjs
├── ...
└── api.mjs # AI can use this for scripting
</code></pre><h2 id="finally">Finally</h2><p>The code is in <a href="https://github.com/user/claude-skills">my claude-skills repo</a> if you're interested.</p><p>To be honest, this skill itself was written using Claude Code. During the process, I deeply experienced how much more reliable "having AI write code" is compared to "having AI call tools"—at least when code doesn't run, you can see error messages, instead of the AI quietly going off track without you knowing.</p>c4605bolasblack@gmail.comhttps://github.com/bolasblackshadow-cljs-vite-plugin v0.0.6: Fixing HMR and ES Module Compatibility2026-01-22T00:00:00Z2026-01-22T13:45:44Zhttps://github.com/bolasblack/BlogPosts/blob/master/2026-01-22-shadow-cljs-vite-plugin-v0.0.6<p>Two weeks after the initial release of <a href="https://github.com/bolasblack/shadow-cljs-vite-plugin">shadow-cljs-vite-plugin</a>, v0.0.6 brings some important fixes that make the development experience much smoother.</p><h2 id="problem-1:-es-module-imports">Problem 1: ES Module Imports</h2><p>When I first released the plugin, there were edge cases where ClojureScript code wouldn't import correctly in Vite's ESM environment. This was particularly problematic for users deploying to Cloudflare Workers.</p><p>The fix required coordinating with shadow-cljs upstream. We submitted a <a href="https://github.com/thheller/shadow-cljs/pull/1249">PR #1249</a> to address the root cause, which was merged in v3.3.5. Thanks to <a href="https://github.com/thheller">@thheller</a> for working through it with me!</p><h2 id="problem-2:-hmr-"namespace-already-declared"-error">Problem 2: HMR "Namespace already declared" Error</h2><p>This was a frustrating one. During hot module replacement, Vite re-executes module code, but global state (like <code>goog.loadedModules_</code>) persists across HMR updates. Google Closure Library assumes modules are loaded exactly once and throws errors on duplicate registration.</p><p>The solution was to patch <code>goog.provide</code> and <code>goog.module</code> to be idempotent:</p><pre><code class="language-javascript">goog.provide = function(name) {
return goog.isProvided_(name) ? undefined : goog.constructNamespace_.call(this, name);
};
goog.module = Object.assign(function(name) {
if (name in goog.loadedModules_) {
goog.moduleLoaderState_.moduleName = name;
return;
}
return origModule.call(this, name);
}, origModule);
</code></pre><h2 id="better-hmr-batching">Better HMR Batching</h2><p>Previously, if you saved multiple files quickly (or your editor formatted on save), each change triggered a separate HMR update. Now the plugin batches multiple file changes into a single update using a debounce mechanism.</p><p>Additionally, changes to <code>shadow-cljs.edn</code> now automatically restart Vite, so you don't have to manually restart when modifying your build configuration.</p><h2 id="tailwind-css-support">Tailwind CSS Support</h2><p>The plugin works great with Tailwind CSS. I've been using this combination on <a href="https://blog.c4605.com/en/">my blog</a> frontend with no issues.</p><h2 id="try-it-out">Try It Out</h2><pre><code class="language-bash">npm install shadow-cljs-vite-plugin
</code></pre><p>The plugin is stable and I'm using it in production. Feedback and contributions are always welcome on <a href="https://github.com/bolasblack/shadow-cljs-vite-plugin">GitHub</a>.</p>c4605bolasblack@gmail.comhttps://github.com/bolasblackBitcoin Wallet Connector: Probably the Best Bitcoin Wallet Adapter Currently2026-01-06T00:00:00Z2026-01-11T18:56:04Zhttps://github.com/bolasblack/BlogPosts/blob/master/2026-01-06-bitcoin-wallet-connector-introduction<p>Clickbait intended 🤪</p><p>In the <a href="https://blog.c4605.com/goto/articles/2026-01-03-bitcoin-wallet-ecosystem-is-a-mess.en.md">previous article</a>, I ranted about how chaotic the Bitcoin wallet ecosystem is: WBIPs standards that nobody implements, sats-connect compatibility issues, wallets each doing their own thing with APIs...</p><p>This article introduces the library I built: <a href="https://github.com/bolasblack/bitcoin-wallet-connector">bitcoin-wallet-connector</a>.</p><p>Try the <strong><a href="https://bitcoin-wallet-connector.netlify.app/iframe.html?id=components-bitcoinconnectionprovider--default&viewMode=story">Live Demo</a></strong> first, or check out the <strong><a href="https://bitcoin-wallet-connector.netlify.app/">Storybook</a></strong>.</p><h2 id="what-this-library-focuses-on">What This Library Focuses On</h2><h3 id="smoothing-out-the-ridiculous-differences">Smoothing Out the Ridiculous Differences</h3><p><code>bitcoin-wallet-connector</code> provides a unified API that lets you connect to all supported wallets with the same code:</p><pre><code class="language-typescript">import {
BitcoinWalletConnector,
UnisatWalletAdapterFactory,
XverseWalletAdapterFactory,
LeatherWalletAdapterFactory,
} from "bitcoin-wallet-connector";
// Register the wallets you want to support
const connector = new BitcoinWalletConnector([
UnisatWalletAdapterFactory(),
XverseWalletAdapterFactory(),
LeatherWalletAdapterFactory(),
]);
// Subscribe to available wallet changes
// Note: The timing of wallet extension API injection is unpredictable
// (some inject at DOMContentLoaded, others after the load event),
// so subscribing is recommended over getting
connector.subscribeAvailableAdapters((availableAdapters) => {
console.log(
"Available wallets:",
availableAdapters.map(([id]) => id)
);
// => ['unisat', 'xverse', ...]
});
// Connect wallet - same API for all wallets
const [adapterId, adapter] = availableAdapters[0];
await connector.connect(adapterId, adapter);
// Get addresses, sign, send transactions - unified interface
const addresses = await adapter.getAddresses();
const result = await adapter.signMessage(
addresses[0].address,
"Hello Bitcoin!"
);
</code></pre><p>That's it. Write the code once, support all wallets.</p><p>Currently supported wallets:</p><table><thead><tr><th>Wallet</th><th>Adapter</th><th>Extra Dependencies</th></tr></thead><tbody><tr><td><a href="https://unisat.io/">UniSat</a></td><td><code>UnisatWalletAdapterFactory</code></td><td>-</td></tr><tr><td><a href="https://www.xverse.app/">Xverse</a></td><td><code>XverseWalletAdapterFactory</code></td><td><code>sats-connect</code></td></tr><tr><td><a href="https://www.okx.com/web3">OKX</a></td><td><code>OkxWalletAdapterFactory</code></td><td>-</td></tr><tr><td><a href="https://leather.io/">Leather</a></td><td><code>LeatherWalletAdapterFactory</code></td><td><code>@leather.io/rpc</code></td></tr><tr><td><a href="https://web3.bitget.com/">Bitget</a></td><td><code>BitgetWalletAdapterFactory</code></td><td>-</td></tr><tr><td><a href="https://wallet.magiceden.io/">Magic Eden</a></td><td><code>MagicEdenWalletAdapterFactory</code></td><td><code>sats-connect</code></td></tr></tbody></table><p>All adapters implement the same interface:</p><pre><code class="language-typescript">interface WalletAdapter {
// Connect/Disconnect
connect(): Promise<void>;
disconnect(): Promise<void>;
// Get addresses
getAddresses(): Promise<WalletAdapterAddress[]>;
// Message signing
signMessage(address: string, message: string): Promise<SignMessageResult>;
// Send BTC
sendBitcoin(
fromAddress: string,
receiverAddress: string,
satoshiAmount: bigint
): Promise<{ txid: string }>;
// PSBT signing
signAndFinalizePsbt(
psbtHex: string,
signIndices: [address: string, signIndex: number][]
): Promise<{ signedPsbtHex: string }>;
// Listen for address changes
onAddressesChanged(callback): { unsubscribe: () => void };
}
</code></pre><p>No matter which wallet users choose, your business logic stays the same.</p><h4 id="about-sendrunes/sendinscriptions/sendbrc20">About sendRunes/sendInscriptions/sendBRC20</h4><p>Currently, this library only supports <code>signMessage</code>, <code>sendBitcoin</code>, and <code>signPsbt</code>. It doesn't support <code>sendRunes</code>, <code>sendInscriptions</code>, or <code>sendBRC20</code>.</p><p>Because these involve more complex dependencies (like needing an Ordinals Indexer, a BRC20 Indexer, etc.). These would make a <code>Connector</code> overly complicated.</p><p>In my view, this should be the responsibility of a <code>Transaction Builder</code>. The <code>Transaction Builder</code> handles transaction construction, then passes it to the <code>Connector</code> for signing and broadcasting.</p><h3 id="security-matters">Security Matters</h3><p>When designing this library, I prioritized <strong>dependency security</strong>. The reason is simple: wallet libraries directly handle user assets, and any security vulnerability could result in real financial losses.</p><h4 id="peer-dependencies">Peer Dependencies</h4><p>I declared important dependencies as peer dependencies rather than bundling them:</p><pre><code class="language-bash">pnpm add bitcoin-wallet-connector @scure/base @scure/btc-signer
</code></pre><p>This means:</p><ul><li>You directly control the versions of these dependencies</li><li>If a dependency has a security vulnerability, you can <strong>upgrade immediately</strong> without waiting for this library to release a new version</li><li>You won't end up with two versions of <code>@scure/btc-signer</code> bundled in your final build</li></ul><h4 id="optional-dependencies:-install-only-what-you-need">Optional Dependencies: Install Only What You Need</h4><p>Wallet SDKs (like <code>sats-connect</code>, <code>@leather.io/rpc</code>) are <strong>optional</strong> peer dependencies:</p><pre><code class="language-bash"># Only supporting UniSat and OKX? No extra dependencies needed
# Need Xverse support?
pnpm add sats-connect
# Need Leather support?
pnpm add @leather.io/rpc
</code></pre><p>You only install what you need, <strong>reducing your exposure to malicious package scripts</strong>.</p><h4 id="dynamic-imports:-lazy-loading">Dynamic Imports: Lazy Loading</h4><p>This is another important security design: <strong>Wallet SDKs are lazy-loaded via <code>dynamic import()</code></strong>.</p><pre><code class="language-typescript">// Internal implementation sketch
const availability = createAvailability({
getPrecondition: () => window.unisat ?? null,
initializer: async () => {
// Only when user actually wants to connect this wallet
// will the corresponding implementation be loaded
const { UnisatWalletAdapterImpl } = await import(
"./UnisatWalletAdapter.impl"
);
return new UnisatWalletAdapterImpl();
},
});
</code></pre><p>Suppose <code>sats-connect</code> gets compromised in a supply chain attack (not uncommon in the npm ecosystem). If your users only use UniSat wallet, <strong>the malicious code won't be loaded or executed</strong>, because <code>sats-connect</code> is only imported when users click "Connect Xverse".</p><p>This should <strong>reduce users' exposure to supply chain attacks</strong>.</p><h2 id="framework-integration">Framework Integration</h2><p>Currently React integration is provided with an out-of-the-box Context Provider:</p><pre><code class="language-tsx">import {
BitcoinConnectionProvider,
useBitcoinConnectionContext,
} from "bitcoin-wallet-connector/react";
import {
UnisatWalletAdapterFactory,
XverseWalletAdapterFactory,
} from "bitcoin-wallet-connector/adapters";
const adapterFactories = [
UnisatWalletAdapterFactory(),
XverseWalletAdapterFactory(),
];
function App() {
return (
<BitcoinConnectionProvider
adapterFactories={adapterFactories}
onWalletConnected={(session) => console.log("Connected:", session)}
onWalletDisconnected={() => console.log("Disconnected")}
>
<WalletUI />
</BitcoinConnectionProvider>
);
}
function WalletUI() {
const { walletSession, availableAdapters, connect, disconnect } =
useBitcoinConnectionContext();
if (walletSession) {
return (
<div>
<p>Connected: {walletSession.adapterId}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
}
return (
<div>
{availableAdapters.map(([adapterId, adapter]) => (
<button key={adapterId} onClick={() => connect(adapterId, adapter)}>
Connect {adapterId}
</button>
))}
</div>
);
}
</code></pre><p>The core <code>BitcoinWalletConnector</code> is framework-agnostic. If you're using Vue, Svelte, Solid, or other frameworks, wrapping it into corresponding hooks/composables should be straightforward. Contributions are welcome!</p><h2 id="get-started-and-contribute">Get Started and Contribute</h2><h3 id="quick-start">Quick Start</h3><pre><code class="language-bash">pnpm add bitcoin-wallet-connector @scure/base @scure/btc-signer
# Install wallet SDKs as needed
pnpm add sats-connect # Xverse / Magic Eden
pnpm add @leather.io/rpc # Leather
</code></pre><p>Or get the demo running in 5 minutes:</p><ol start="1"><li>Clone the repo</li><li><code>pnpm install</code></li><li><code>pnpm storybook</code></li><li>Open <a href="http://localhost:6006">http://localhost:6006</a></li></ol><p>You can test wallet connections, signing, and other features in Storybook.</p><h3 id="contributing">Contributing</h3><p>This is an open source project, and community contributions are very welcome!</p><p>If you want to add support for a new wallet, there's one small preference: <strong>try to avoid depending on the wallet's official SDK</strong>.</p><p>Why? Because many wallet APIs are just mounted on the <code>window</code> object and can be called directly without introducing an additional SDK. For example, the UniSat, OKX, and Bitget adapters have zero external dependencies.</p><p>Introducing an SDK means one more potential supply chain attack vector, one more package users need to install, and potentially unnecessary bundle size. Of course, if a wallet can only be accessed through its SDK (like Xverse's <code>sats-connect</code>), that's fine — we can make it an optional peer dependency.</p><p>See <a href="https://github.com/bolasblack/bitcoin-wallet-connector/blob/master/CONTRIBUTING.md">CONTRIBUTING.md</a> for detailed contribution guidelines.</p><hr /><p>The project is fully open source (MIT). Stars and contributions are welcome!</p><ul><li>GitHub: <a href="https://github.com/bolasblack/bitcoin-wallet-connector">https://github.com/bolasblack/bitcoin-wallet-connector</a></li><li>Demo: <a href="https://bitcoin-wallet-connector.netlify.app/">https://bitcoin-wallet-connector.netlify.app/</a></li><li>npm: <a href="https://www.npmjs.com/package/bitcoin-wallet-connector">https://www.npmjs.com/package/bitcoin-wallet-connector</a></li></ul>c4605bolasblack@gmail.comhttps://github.com/bolasblackThe Bitcoin Wallet Extensions Ecosystem is a Mess: A Developer's Rant2026-01-03T00:00:00Z2026-01-05T10:05:48Zhttps://github.com/bolasblack/BlogPosts/blob/master/2026-01-03-bitcoin-wallet-ecosystem-is-a-mess<p>If you're building a Bitcoin dApp, you've probably already discovered that the Bitcoin community's ecosystem is an absolute dumpster fire.</p><p>The simplest example is Bitcoin wallets. Everyone seems eager to create standards for others, but nobody seems eager to actually implement them.</p><h2 id=""standards"?-what-standards?">"Standards"? What Standards?</h2><p>Let me be more specific:</p><h3 id="wbips:-even-the-standard-makers-don't-follow-their-own-standards">WBIPs: Even the Standard Makers Don't Follow Their Own Standards</h3><p>Leather and Xverse created <a href="https://wbips.netlify.app/">WBIPs</a>, supposedly to establish a unified Bitcoin wallet API standard, but:</p><p><strong>The signPsbt finalize parameter</strong></p><p>The <a href="https://github.com/wbips/wbips-docs/blob/a7862572785686da8c0097125366086fea3c70d6/pages/request_api/signPsbt.md"><code>signPsbt</code></a> standard defines a <code>finalize</code> parameter, but neither of the initiators implemented it:</p><ul><li>Leather's <a href="https://github.com/leather-io/mono/blob/2570b8e89e1a7d67ff2ec469c42b321f5d956e92/packages/rpc/src/methods/bitcoin/sign-psbt.ts#L25-L35">code</a></li><li>Xverse's <a href="https://github.com/secretkeylabs/sats-connect-core/blob/cfbbdfde28a646f1b5114dad39ec76da85c3ffe9/src/request/types/btcMethods.ts#L178-L192">code</a></li></ul><p><strong>The wallet auto-discovery mechanism is completely wrong</strong></p><p><a href="https://github.com/wbips/wbips-docs/blob/a7862572785686da8c0097125366086fea3c70d6/pages/wbips/WBIP004.md">WBIP004</a> defines a wallet plugin auto-discovery mechanism, where the first step is to inject the wallet provider into <code>wbip_providers</code>.</p><p>But in reality, both Xverse and Leather inject into <code>window.btc_providers</code>:</p><ul><li>Leather's <a href="https://github.com/leather-io/mono/blob/2570b8e89e1a7d67ff2ec469c42b321f5d956e92/packages/provider/src/add-leather-to-providers.ts#L21-L52">code</a></li><li>Xverse's <a href="https://github.com/secretkeylabs/xverse-web-extension/blob/bdbd6a008fd3d8f04ea18eca0374fae67d90114c/src/inpage/index.ts#L53">code</a></li></ul><p><strong>Two other major wallets don't care at all</strong></p><p>Not to mention that <strong>UniSat</strong> and <strong>OKX Wallet</strong>, which actually have massive traffic, seem to have zero interest in supporting WBIPs.</p><h3 id="sats-connect:-a-standard-on-top-of-standards">sats-connect: A Standard on Top of Standards</h3><p>Then, beyond WBIPs, Xverse also created <a href="https://github.com/xverseapp/sats-connect">sats-connect</a>, apparently wanting to extend some of their own APIs beyond WBIPs, implementing a standard on top of a standard.</p><p>This isn't bad, especially since it also provides compatibility with the UniSat API. Magic Eden even claims to be "sats-connect compatible."</p><p>"This sounds absolutely amazing! sats-connect is the meta! The savior of the world!" Right? That's what you're thinking.</p><p>But there are still "two small clouds" hovering over this perfect world:</p><ol start="1"><li><strong>sats-connect doesn't support OKX Wallet</strong> (even though OKX Wallet's API has now migrated to be UniSat-compatible, sats-connect can't auto-discover it)</li><li><strong>Magic Eden's auto-discovery follows a different standard</strong>: <a href="https://github.com/ExodusMovement/bitcoin-wallet-standard"><code>ExodusMovement/bitcoin-wallet-standard</code></a>, not the WBIPs standard. So you can't rely on sats-connect to auto-discover it.</li></ol><h3 id="wait-what,-another-standard?">Wait What, Another Standard?</h3><p>"Wait, what the hell is this <code>ExodusMovement/bitcoin-wallet-standard</code>? I've never heard of it!"</p><p>Right, before I tried to integrate Magic Eden, I hadn't heard of it either. Now I've heard of it, and so have you.</p><h3 id="more-fun-facts">More Fun Facts</h3><p>Oh, here's a "fun fact": if your users have both Xverse and Magic Eden installed, and you don't handle it properly, when they click "Connect Wallet," they might <strong>randomly receive a confirmation request from one of the two wallets</strong>. Life is full of surprises.</p><p>Oh, and another "fun fact": although Magic Eden claims to be "sats-connect compatible," it's actually compatible with <strong>an older version of the API</strong>.</p><h2 id="show-some-code">Show Some Code</h2><p>You might think, "Fine, I'll just integrate them all myself." So let's look at the code differences for implementing a simple "connect wallet" feature:</p><pre><code class="language-typescript">// UniSat - Direct window object call
const unisatResponse = await window.unisat.requestAccounts()
// Leather - Yet another API
const leatherResponse = await window.LeatherProvider.request("getAddresses")
// Xverse - From official docs
import { request } from "sats-connect"
const xverseResponse = await request("getAddresses", null)
// Magic Eden - From official docs
import { getAddress, AddressPurpose, BitcoinNetworkType } from "sats-connect"
await getAddress({
getProvider: getBtcProvider,
payload: {
purposes: [AddressPurpose.Ordinals, AddressPurpose.Payment],
message: "Address for receiving Ordinals and payments",
network: {
type: BitcoinNetworkType.Mainnet,
},
},
onFinish: response => {
console.log("onFinish response, ", response.addresses)
connectionStatus?.setAccounts(response.addresses as unknown as Account[])
},
onCancel: () => {
alert("Request canceled")
},
})
</code></pre><p>Four wallets, four different implementations.</p><p>What if you need to integrate 6 wallets in your project? Congratulations, you'll need to go through all the issues I mentioned above, and enjoy wasting a huge chunk of your life along with me.</p><h2 id="to-sum-it-up">To Sum It Up</h2><p>"F**k the stupid ecosystem, what a dumpster fire!" — you/I thought.</p><h2 id="so-i-built-a-library">So I Built a Library</h2><p>So I built <a href="https://github.com/bolasblack/bitcoin-wallet-connector">bitcoin-wallet-connector</a> to help others waste less time on this crap.</p><p>In the <strong>next article</strong>, I'll detail the usage and design philosophy of this library. If you can't wait, check out:</p><ul><li><strong><a href="https://bitcoin-wallet-connector.netlify.app/iframe.html?id=components-bitcoinconnectionprovider--default&viewMode=story">Live Demo</a></strong></li><li><strong><a href="https://github.com/bolasblack/bitcoin-wallet-connector">GitHub Repository</a></strong></li></ul>c4605bolasblack@gmail.comhttps://github.com/bolasblackI Moved My Config Sharing Repo to LLM-oriented Mode2026-01-02T00:00:00Z2026-01-05T10:05:48Zhttps://github.com/bolasblack/BlogPosts/blob/master/2026-01-02-moved-to-llm-oriented<h2 id="what-is-"llm-oriented-config-sharing"?">What is "LLM-oriented Config Sharing"?</h2><p>Simply put, config sharing projects provide <a href="https://llmstxt.org/">llms.txt</a>, and consumers use it to let LLMs download/update project config files to keep them up to date—instead of distributing configs through npm registry.</p><h2 id="so-why-not-use-npm-anymore?">So Why Not Use npm Anymore?</h2><p>Traditional ways of sharing tool configurations—whether through <code>extends</code> or presets—all face a fundamental contradiction: <strong>the trade-off between convenience and flexibility</strong>.</p><h3 id="the-pros-of-inheritance-mode">The Pros of Inheritance Mode</h3><p>When you use something like <code>extends: "@company/eslint-config"</code>, the benefits are obvious:</p><ul><li><strong>Easy upgrades</strong>: Just <code>npm update</code>, and all projects using this config get the updates</li><li><strong>Consistency guaranteed</strong>: All projects in the team use the same rules</li><li><strong>Centralized maintenance</strong>: Improvements and bug fixes only need to happen in one place</li></ul><h3 id="but-reality-is-harsh">But Reality is Harsh</h3><p>Problems arise when you need to do anything "non-standard":</p><ol start="1"><li><strong>Custom requirements</strong>: Your project has special needs and requires overriding certain rules</li><li><strong>Dependency version conflicts</strong>: You need to upgrade ESLint, but the upstream config package hasn't caught up yet (e.g., ESLint moved to flat config mode, and you need the latest version, but I'm lazy and haven't updated yet 🤪)</li><li><strong>Rule disagreements</strong>: Upstream updated a rule, but it doesn't fit your project (the problem is you might not know at first, and only realize "damn, I got burned" when things break)</li></ol><p>At this point, you have two choices:</p><p><strong>Option A: Patch on top of inheritance</strong></p><pre><code class="language-javascript">// eslint.config.js
export default [
...baseConfig,
{
rules: {
// Override one rule
"some-rule": "off",
// Override another
"another-rule": ["error", { different: "options" }],
},
},
// Still need to handle plugin version conflicts...
];
</code></pre><p>As patches pile up, your "inheritance" becomes layer upon layer of overrides, ending up harder to maintain than writing from scratch.</p><p>ESLint is actually not too bad. Tools like Prettier and lint-staged that don't have extends/merge mechanisms are even more troublesome. I previously wrote a super complicated lint-staged config (see <a href="https://github.com/bolasblack/js-metarepo/blob/0270b291651bcbc75a3ecd81d391f4ad836a814e/packages/toolconfs/lint-staged.config.js"><code>lint-staged.config.js</code></a> and <a href="https://github.com/bolasblack/js-metarepo/blob/0270b291651bcbc75a3ecd81d391f4ad836a814e/packages/toolconfs/lint-staged.helpers.js"><code>lint-staged.helpers.js</code></a>). It looks decent (well, at least I think so), but it's actually super hard to use and makes a very simple thing extremely complex.</p><p><strong>Option B: Eject</strong></p><p>Many scaffold tools provide an <code>eject</code> command that copies all config files into your project, giving you full control.</p><p>But after ejecting:</p><ul><li>You lose the ability to auto-upgrade; you have to update yourself and can't benefit from upstream's tested plugin compatibility</li><li>When upstream has important updates, you need to manually diff and merge</li></ul><p><strong>This is the dilemma</strong>: In inheritance mode you're constrained by upstream; after ejecting you bear the maintenance cost.</p><h2 id="llm-is-a-game-changer">LLM is a Game Changer</h2><p>Now we have LLMs. Times have changed.</p><h3 id="the-new-workflow">The New Workflow</h3><ol start="1"><li><strong>Get latest configs</strong>: LLM fetches the latest recommended configs directly from llms.md/llms.txt</li><li><strong>Smart merging</strong>: LLM understands your project context and merges new configs into existing ones</li><li><strong>Preserve customizations</strong>: Your custom modifications are recognized and preserved</li><li><strong>Update on demand</strong>: When you need to upgrade, let LLM handle the merge conflicts</li></ol><h3 id="why-is-this-better-than-traditional-approaches?">Why is This Better Than Traditional Approaches?</h3><p><strong>All configs live in your codebase</strong></p><p>No hidden config files deep in <code>node_modules</code>, no confusion about "where did this rule come from". Every line of config is clearly visible in your project.</p><p><strong>Upgrades become controllable</strong></p><p>Upgrading is no longer an "all or nothing" decision. LLM can help you:</p><ul><li>See what changed upstream</li><li>Understand the impact of each change</li><li>Selectively apply updates</li><li>Automatically handle merge conflicts</li></ul><p><strong>Customization is a first-class citizen</strong></p><p>Your customizations are no longer "patches"—they're part of the config. LLM understands and respects these customizations when updating.</p><p><strong>Dependency version freedom</strong></p><p>You can upgrade any dependency anytime without waiting for upstream config packages to update. Run into issues? LLM can help adjust the config (and configs are easier for them to read now too).</p><p><strong>Upstream config files become simpler</strong></p><p>Let's look at my lint-staged config files again.</p><p>Previous version:</p><ul><li><a href="https://github.com/bolasblack/js-metarepo/blob/0270b291651bcbc75a3ecd81d391f4ad836a814e/packages/toolconfs/lint-staged.config.js"><code>lint-staged.config.js</code></a></li><li>Helper functions I had to write so downstream could reuse and customize the lint-staged config: <a href="https://github.com/bolasblack/js-metarepo/blob/0270b291651bcbc75a3ecd81d391f4ad836a814e/packages/toolconfs/lint-staged.helpers.js"><code>lint-staged.helpers.js</code></a></li></ul><p>Current version:</p><ul><li><a href="https://github.com/bolasblack/js-metarepo/blob/25d4495adcba67bda9daf65b04a204acc01cb795/packages/toolconfs/lint-staged.config.js"><code>lint-staged.config.js</code></a> Actually, this is so simple now, let me just inline it:<pre><code class="language-javascript">module.exports = {
"*.{ts,tsx,js,jsx}": ["prettier --write", "eslint"],
"*.{css,scss,sass,less,md,mdx}": ["prettier --write"],
};
</code></pre></li><li><code>lint-staged.helpers.js</code> no longer exists—because it's no longer needed</li></ul><h2 id="you've-said-a-lot,-but-doesn't-this-still-look-like-a-step-backward?">You've Said a Lot, But Doesn't This Still Look Like a Step Backward?</h2><p>Yes and no.</p><ul><li><strong>Yes</strong>: Config files do exist independently in each project, just like the old days</li><li><strong>No</strong>: Because the maintenance cost has fundamentally changed:<ul><li><strong>Before</strong>: Manually read CHANGELOG or diff against upstream config -> Figure out what changed -> Manually modify -> Test and verify</li><li><strong>Now</strong>: Tell LLM "upgrade to the latest recommended config" -> Review LLM's changes -> Done</li></ul></li></ul><p>The cognitive burden of maintenance shifts from "I need to understand all these configs" to "I need to review LLM's suggestions". This is what's different from the early days.</p><h2 id="summary-(ai-version,-couldn't-write-this-myself)">Summary (AI version, couldn't write this myself)</h2><p>Traditional config sharing forces a choice between "one-size-fits-all convenience" and "full autonomy with complexity". The LLM-oriented approach lets us have both:</p><ul><li>Maintain full ownership and transparency of configs</li><li>While enjoying intelligent assistance for upgrades and maintenance</li></ul><p>This isn't a step backward in technology—it's redefining best practices with new tools.</p>c4605bolasblack@gmail.comhttps://github.com/bolasblackI built (another) Elm-style useEffectReducer hook for React2025-11-12T00:00:00Z2026-01-05T10:05:48Zhttps://github.com/bolasblack/BlogPosts/blob/master/2025-11-12-elm-style-useeffectreducer-hook-for-react<p><strong>GitHub:</strong> <a href="https://github.com/bolasblack/react-components/tree/develop/packages/useEffectReducer">bolasblack/react-components/tree/develop/packages/useEffectReducer</a></p><h2 id="what-is-"elm-style"?">What is "Elm-style"?</h2><p>Elm popularized the <strong>Model-View-Update</strong> pattern, where each update returns both the next model and the commands to run:</p><pre><code class="language-elm">update : Msg -> Model -> ( Model, Cmd Msg )
</code></pre><p>This design keeps all logic about <em>"what happens when event X occurs"</em> in <strong>one place</strong> — the update function — instead of scattering side effects across random <code>useEffect</code>s.</p><p>It also keeps reducers pure while making <strong>when and what side effects happen</strong> explicit and interpretable by a runtime.</p><p>React's docs echo this philosophy: effects should be an <em>escape hatch</em>, not the default — see <a href="https://react.dev/learn/you-might-not-need-an-effect">You Might Not Need an Effect</a>.</p><blockquote><p>If you want to see how this kind of modeling makes UI logic elegant and maintainable, check out David Khourshid's classic post <a href="https://dev.to/davidkpiano/no-disabling-a-button-is-not-app-logic-598i"><strong>"No, disabling a button is not app logic."</strong></a></p></blockquote><h2 id="so-why-another-elm-style-reducer?">So why <em>another</em> Elm-style reducer?</h2><p>The author wanted a variant with different trade-offs:</p><ul><li><p><strong>Some are archived.</strong> For example, <a href="https://github.com/davidkpiano/useEffectReducer"><code>useEffectReducer</code></a> and <a href="https://github.com/soywod/react-use-bireducer"><code>react-use-bireducer</code></a> are now read-only.</p></li><li><p><strong>Keep it tiny.</strong> Fits in one file with almost no dependencies — copy, tweak, or delete it whenever you want.</p></li><li><p><strong>Effects as plain objects + separate interpreter.</strong> Returns serializable effect descriptors and implements the actual effect logic in one dedicated place. It's easier to test reducers (assert on descriptors) without invoking real side effects.</p></li><li><p><strong>Lower the barrier.</strong> The goal is to make the Elm-style approach approachable without requiring deep knowledge of Elm's <code>Cmd Msg</code> system or learning a full-blown state machine library like <a href="https://xstate.js.org/">XState</a>.</p></li></ul><h2 id="example-usage">Example usage</h2><p>The article reimplements the example from David Khourshid's article <a href="https://dev.to/davidkpiano/no-disabling-a-button-is-not-app-logic-598i"><strong>"No, disabling a button is not app logic."</strong></a> using <code>useEffectReducer</code>.</p><p>Both implementations are available on GitHub:</p><ul><li><strong>useEffectReducer version:</strong> <a href="https://github.com/bolasblack/react-components/blob/eb47a2416e8cc95bb4fa7e6fbf776ac2432a2468/packages/useEffectReducer/src/useEffectReducer.stories.tsx#L121-L203">useEffectReducer.stories.tsx → L121–203</a></li><li><strong>useReducer version:</strong> <a href="https://github.com/bolasblack/react-components/blob/eb47a2416e8cc95bb4fa7e6fbf776ac2432a2468/packages/useEffectReducer/src/useEffectReducer.stories.tsx#L24-L119">useEffectReducer.stories.tsx → L24–119</a></li></ul><h2 id="related-work">Related work</h2><p>Excellent existing takes on bringing <strong>Elm-style "state + effects" reducers</strong> into React:</p><ul><li><strong><a href="https://github.com/davidkpiano/useEffectReducer"><code>davidkpiano/useEffectReducer</code></a></strong> — by <em>David Khourshid</em>; archived</li><li><strong><a href="https://github.com/soywod/react-use-bireducer"><code>soywod/react-use-bireducer</code></a></strong> — returns <code>[state, effects]</code> and processes effects through a separate effect reducer; archived</li><li><strong><a href="https://github.com/ncthbrt/react-use-elmish"><code>ncthbrt/react-use-elmish</code></a></strong> — Elmish-style hook combining reducer logic with async helpers</li><li><strong><a href="https://github.com/redux-loop/redux-loop"><code>redux-loop</code></a></strong> — Redux enhancer adding Elm-like effect tuples</li><li><strong><a href="https://github.com/dai-shi/use-reducer-async"><code>dai-shi/use-reducer-async</code></a></strong> — extends <code>dispatch</code> for async actions</li><li><strong><a href="https://gist.github.com/sophiebits/145c47544430c82abd617c9cdebefee8"><code>useReducerWithEmitEffect</code></a></strong> — Sophie Alpert's gist that inspired much of this work</li></ul><h2 id="good-articles">Good Articles</h2><ul><li>Christian Ekrem - "<a href="https://cekrem.github.io/posts/chapter-2-take-2/">Chapter 2, Take 2: Why I Changed Course</a>"</li><li>David Khourshid – "<a href="https://dev.to/davidkpiano/redux-is-half-of-a-pattern-1-2-1hd7">Redux is half of a pattern (1/2)</a>"</li><li>David Khourshid – "<a href="https://medium.com/@DavidKPiano/there-are-so-many-fundamental-misunderstandings-about-xstate-and-state-machines-in-general-in-13aec57d2f85">There are so many fundamental misunderstandings about XState (and state machines in general)</a>"</li><li>David Khourshid – "<a href="https://dev.to/davidkpiano/no-disabling-a-button-is-not-app-logic-598i">No, disabling a button is not app logic.</a>"</li><li>React docs – "<a href="https://react.dev/learn/you-might-not-need-an-effect">You Might Not Need an Effect</a>"</li></ul>c4605bolasblack@gmail.comhttps://github.com/bolasblack