<?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: Dmitry</title>
    <description>The latest articles on DEV Community by Dmitry (@dmitryganin).</description>
    <link>https://dev.to/dmitryganin</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%2F3947986%2F36e70035-3747-40c7-8392-2a6bdce2a4ea.jpg</url>
      <title>DEV Community: Dmitry</title>
      <link>https://dev.to/dmitryganin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmitryganin"/>
    <language>en</language>
    <item>
      <title>Рабочий VPN-конфиг за пару секунд без покупки сервера: генератор Cloudflare WARP с обходом DPI</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Tue, 30 Jun 2026 07:14:30 +0000</pubDate>
      <link>https://dev.to/dmitryganin/rabochii-vpn-konfigh-za-paru-siekund-biez-pokupki-sierviera-ghienierator-cloudflare-warp-s-obkhodom-dpi-339a</link>
      <guid>https://dev.to/dmitryganin/rabochii-vpn-konfigh-za-paru-siekund-biez-pokupki-sierviera-ghienierator-cloudflare-warp-s-obkhodom-dpi-339a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;открываешь сайт → выбираешь формат → жмёшь кнопку → получаешь готовый рабочий конфиг со свежими ключами. Никакой регистрации, никакой оплаты, всё с открытым кодом.&lt;/p&gt;

&lt;p&gt;🌐 &lt;strong&gt;Генератор:&lt;/strong&gt; &lt;a href="https://warp-ru.vercel.app" rel="noopener noreferrer"&gt;https://warp-ru.vercel.app&lt;/a&gt;&lt;br&gt;
🤖 &lt;strong&gt;Telegram-бот:&lt;/strong&gt; &lt;a href="https://t.me/warpgenrubot" rel="noopener noreferrer"&gt;@warpgenrubot&lt;/a&gt;&lt;br&gt;
⭐ &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AppsGanin/warp-config-ru" rel="noopener noreferrer"&gt;AppsGanin/warp-config-ru&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Проблема, знакомая каждому
&lt;/h2&gt;

&lt;p&gt;В 2024–2026 годах «просто открыть нужный сайт» стало квестом. Привычные VPN то платные, то медленные, то сами заблокированы. А бесплатные часто оказываются помойкой, которая сливает твой трафик кому попало.&lt;/p&gt;

&lt;p&gt;Есть красивое решение — &lt;strong&gt;Cloudflare WARP&lt;/strong&gt;. Это бесплатный сервис от Cloudflare поверх протокола WireGuard. Быстрый, стабильный, ничего не стоит. Но у него две беды для новичка:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Официальное приложение WARP часто не помогает обойти блокировки&lt;/strong&gt; — его трафик легко распознаёт DPI (система глубокого анализа пакетов у провайдера) и режет.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Сделать «правильный» конфиг руками — боль.&lt;/strong&gt; Надо генерировать ключи, регистрироваться в API Cloudflare, собирать &lt;code&gt;.conf&lt;/code&gt;-файл, разбираться в параметрах. Для не-программиста — нереально.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Я сделал инструмент, который решает обе проблемы за один клик. Рассказываю, что это и как пользоваться, даже если ты вообще далёк от консоли.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что это такое
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/AppsGanin/warp-config-ru" rel="noopener noreferrer"&gt;&lt;strong&gt;WARP Config RU&lt;/strong&gt;&lt;/a&gt; — это минималистичный сайт, который выдаёт &lt;strong&gt;рабочий конфиг Cloudflare WARP&lt;/strong&gt; в одном из двух форматов:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AmneziaWG&lt;/strong&gt; (&lt;code&gt;.conf&lt;/code&gt;) — это WireGuard с маскировкой. Снаружи трафик выглядит как обычный шум, а не как «о, тут кто-то поднял VPN». Именно это и нужно для обхода DPI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clash&lt;/strong&gt; (&lt;code&gt;.yaml&lt;/code&gt;) — формат для популярных кросс-платформенных клиентов (Clash Verge, Clash Meta и т.п.), с готовыми правилами под компьютер, телефон или роутер.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ключевая фишка: &lt;strong&gt;для каждой генерации создаётся новая пара ключей и регистрируется через официальный WARP API.&lt;/strong&gt; Поэтому конфиг рабочий «из коробки» — его не надо ничего «активировать», он сразу подключается.&lt;/p&gt;

&lt;h2&gt;
  
  
  Как этим пользоваться (для новичков)
&lt;/h2&gt;

&lt;p&gt;Никакого терминала. Три шага.&lt;/p&gt;

&lt;h3&gt;
  
  
  Шаг 1. Получи конфиг
&lt;/h3&gt;

&lt;p&gt;Открой генератор:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://warp-ru.vercel.app" rel="noopener noreferrer"&gt;https://warp-ru.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://warp-ru.netlify.app" rel="noopener noreferrer"&gt;https://warp-ru.netlify.app&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Выбери:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;формат&lt;/strong&gt; — оба про обход блокировок, просто по-разному. &lt;strong&gt;AmneziaWG&lt;/strong&gt; маскирует сам трафик под «шум» — это сильнее всего против жёсткого DPI и проще для новичка (один профиль — подключился и забыл). &lt;strong&gt;Clash&lt;/strong&gt; обходит блокировки маршрутизацией по правилам (что гнать через WARP, что напрямую) и удобен, если хочешь рулить трафиком — есть готовые профили под компьютер, телефон и роутер. Не уверен — бери &lt;strong&gt;AmneziaWG&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS&lt;/strong&gt; — какой DNS-сервер пропишется в конфиг. По умолчанию выбраны DNS, которые честно резолвят заблокированные домены, а не подсовывают заглушку, как это часто делает DNS провайдера;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;эндпоинт&lt;/strong&gt; (для AmneziaWG) — адрес сервера Cloudflare, к которому ты подключаешься. По умолчанию трогать не надо; если основной вдруг не подключается — переключись на запасной. В формате &lt;strong&gt;Clash&lt;/strong&gt; вместо эндпоинта выбираешь устройство — компьютер, телефон или роутер.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Нажми кнопку — через пару секунд откроется окно с готовым конфигом. Его можно &lt;strong&gt;скопировать&lt;/strong&gt; или &lt;strong&gt;скачать файлом&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Шаг 2. Поставь клиент
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Для &lt;strong&gt;AmneziaWG&lt;/strong&gt; — приложение &lt;strong&gt;AmneziaWG / AmneziaVPN&lt;/strong&gt; (есть под Windows, macOS, Linux, Android, iOS). Именно оно умеет читать маскировку, обычный WireGuard-клиент этот формат не поймёт.&lt;/li&gt;
&lt;li&gt;Для &lt;strong&gt;Clash&lt;/strong&gt; — любой Clash-клиент (например, Clash Verge на десктопе).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Шаг 3. Импортируй и подключайся
&lt;/h3&gt;

&lt;p&gt;Открываешь в приложении пункт «Импортировать из файла» (или вставить из буфера) → выбираешь скачанный конфиг → жмёшь «Подключиться». Всё. Интернет снова открытый.&lt;/p&gt;

&lt;h2&gt;
  
  
  А если сайт недоступен? Telegram-бот
&lt;/h2&gt;

&lt;p&gt;Сайт могут заблокировать, поэтому у проекта есть &lt;strong&gt;тот же генератор в виде Telegram-бота&lt;/strong&gt;: &lt;a href="https://t.me/warpgenrubot" rel="noopener noreferrer"&gt;@warpgenrubot&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Пишешь боту &lt;code&gt;/start&lt;/code&gt;, он по шагам спрашивает «формат → устройство → DNS» и присылает готовый &lt;code&gt;.conf&lt;/code&gt;/&lt;code&gt;.yaml&lt;/code&gt; &lt;strong&gt;файлом прямо в чат&lt;/strong&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Чуть-чуть про то, как это устроено внутри
&lt;/h2&gt;

&lt;p&gt;Если тебе интересно «а не магия ли это» — нет, всё честно и с открытым кодом. Весь пайплайн — буквально три шага.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Генерируем свежую пару ключей.&lt;/strong&gt; Без внешних крипто-библиотек, на нативном &lt;code&gt;node:crypto&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;generateKeyPairSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateKeyPairSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x25519&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// WireGuard-ключи — это «сырые» 32 байта в base64,&lt;/span&gt;
&lt;span class="c1"&gt;// которые лежат в хвосте DER-экспорта. Их и берём.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Регистрируем публичный ключ в Cloudflare WARP API&lt;/strong&gt; (&lt;code&gt;POST /reg&lt;/code&gt;, затем &lt;code&gt;PATCH /reg/{id}&lt;/code&gt; с &lt;code&gt;warp_enabled: true&lt;/code&gt;). В ответ Cloudflare отдаёт публичный ключ своего пира и выданные тебе IP-адреса.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Собираем конфиг.&lt;/strong&gt; Для AmneziaWG к обычным полям WireGuard добавляются параметры маскировки awg1.5 — «мусорные» пакеты и фейковые заголовки, плюс случайная &lt;code&gt;I1&lt;/code&gt;-сигнатура, которая мимикрирует под QUIC-рукопожатие:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
&lt;span class="py"&gt;Jc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;4&lt;/span&gt;
&lt;span class="py"&gt;Jmin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;40&lt;/span&gt;
&lt;span class="py"&gt;Jmax&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;70&lt;/span&gt;
&lt;span class="py"&gt;H1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;H2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;H3&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;H4&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;4&lt;/span&gt;
&lt;span class="py"&gt;I1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;b 0x...&amp;gt;   # подмешиваем случайную сигнатуру под QUIC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Именно эти параметры и делают трафик «незаметным» для DPI: для системы анализа он перестаёт быть похожим на WireGuard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Стек:&lt;/strong&gt; Next.js 15 (App Router) + React 19 + TypeScript. Без UI-фреймворков, без тяжёлых зависимостей. Деплоится в один клик на Vercel или Netlify — это обычная serverless-функция.&lt;/p&gt;

&lt;h2&gt;
  
  
  Почему open source — это важно для VPN
&lt;/h2&gt;

&lt;p&gt;Когда речь про инструмент, через который идёт &lt;strong&gt;весь твой трафик&lt;/strong&gt;, доверие решает всё. Поэтому код полностью открыт под &lt;strong&gt;MIT&lt;/strong&gt;: можно прочитать каждую строчку, убедиться, что ничего не логируется и не утекает, и при желании &lt;strong&gt;поднять свою копию&lt;/strong&gt; одной кнопкой:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://vercel.com/new/clone?repository-url=https://github.com/AppsGanin/warp-config-ru" rel="noopener noreferrer"&gt;Развернуть на Vercel →&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;a href="https://app.netlify.com/start/deploy?repository=https://github.com/AppsGanin/warp-config-ru" rel="noopener noreferrer"&gt;Развернуть на Netlify →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Свой инстанс = свой адрес, который ещё не успели заблокировать, и полный контроль.&lt;/p&gt;

&lt;h2&gt;
  
  
  Небольшой дисклеймер
&lt;/h2&gt;

&lt;p&gt;Инструмент создан для &lt;strong&gt;свободного доступа к информации и защиты приватности&lt;/strong&gt; — это законное и нормальное желание. Используй ответственно и в рамках законов своей страны. WARP API иногда ограничивает регистрацию с IP облачных провайдеров — если на чужом деплое вдруг полезли ошибки 429/403, это лимит со стороны Cloudflare, а не баг.&lt;/p&gt;

&lt;h2&gt;
  
  
  Итог
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Бесплатно&lt;/strong&gt;, без регистрации и подписок.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Рабочие конфиги&lt;/strong&gt; со свежими ключами за пару секунд.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Обход DPI&lt;/strong&gt; через маскировку AmneziaWG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram-бот&lt;/strong&gt; на случай блокировки сайта.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Открытый код&lt;/strong&gt; под MIT — деплой своей копии в один клик.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Попробовать: &lt;strong&gt;&lt;a href="https://warp-ru.vercel.app" rel="noopener noreferrer"&gt;https://warp-ru.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
🤖 Бот: &lt;a href="https://t.me/warpgenrubot" rel="noopener noreferrer"&gt;@warpgenrubot&lt;/a&gt;&lt;br&gt;
⭐ Если зашло — &lt;strong&gt;поставь звезду на GitHub&lt;/strong&gt;, это бесплатно и реально мотивирует развивать проект дальше: &lt;a href="https://github.com/AppsGanin/warp-config-ru" rel="noopener noreferrer"&gt;AppsGanin/warp-config-ru&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Вопросы, баги, идеи — пишите в комментарии или открывайте issue. Stay free 🛡️&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>opensource</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Лёгкая панель для управления личным VPN-сервером на Xray</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Sun, 21 Jun 2026 21:44:58 +0000</pubDate>
      <link>https://dev.to/dmitryganin/lioghkaia-paniel-dlia-upravlieniia-lichnym-vpn-siervierom-na-xray-ef9</link>
      <guid>https://dev.to/dmitryganin/lioghkaia-paniel-dlia-upravlieniia-lichnym-vpn-siervierom-na-xray-ef9</guid>
      <description>&lt;p&gt;У большинства self-hosted VPN-панелей одна и та же боль: Docker-стек, внешняя БД, реверс-прокси и куча конфигов, которые надо связать между собой, прежде чем хоть что-то заработает. Мне хотелось наоборот — что-то, что можно закинуть на свежий VPS и поднять меньше чем за минуту.&lt;/p&gt;

&lt;p&gt;Так появилась &lt;strong&gt;&lt;a href="https://github.com/AppsGanin/rospanel" rel="noopener noreferrer"&gt;РосПанель&lt;/a&gt;&lt;/strong&gt;: self-hosted панель для администрирования личного VPN-сервера на &lt;a href="https://github.com/XTLS/Xray-core" rel="noopener noreferrer"&gt;Xray-core&lt;/a&gt;, который поставляется &lt;strong&gt;одним статическим бинарником&lt;/strong&gt;. React-фронтенд вшит через &lt;code&gt;go:embed&lt;/code&gt;, база — встроенный SQLite, отдельного веб-сервера нет. Поставил, открыл, добавил юзера.&lt;/p&gt;

&lt;h2&gt;
  
  
  Главная идея: один бинарник, ничего лишнего
&lt;/h2&gt;

&lt;p&gt;Цель, которая определила всё остальное, — радикальная простота. В отличие от Marzban и 3x-ui, у РосПанели нет Docker-обвязки, нет внешней БД и нет отдельного веб-сервера, который надо настраивать.&lt;/p&gt;

&lt;p&gt;Всё живёт в одном исполняемом файле:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Веб-интерфейс собирается в &lt;code&gt;web/dist&lt;/code&gt; и вшивается в Go-бинарник на этапе сборки.&lt;/li&gt;
&lt;li&gt;Состояние хранится в SQLite (чистый Go-драйвер &lt;code&gt;modernc&lt;/code&gt;, то есть &lt;strong&gt;без CGO&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Конфиг Xray всегда генерируется из базы и применяется супервизором — SQLite это единственный источник правды, а не JSON, который правят руками.
В итоге деплой — это просто положить бинарник и systemd-юнит. Никакой оркестрации, ничего не надо держать в синхроне.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Что она на самом деле делает
&lt;/h2&gt;

&lt;p&gt;РосПанель — это &lt;strong&gt;панель управления&lt;/strong&gt; (control plane), а не VPN-клиент. Она настраивает и обслуживает ваш собственный сервер: генерирует конфиг Xray, выдаёт ссылки на подписки и показывает статистику.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Протоколы из коробки&lt;/strong&gt; — один конфиг Xray, один набор учёток:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VLESS-Vision (TCP/443 + uTLS-fingerprint)&lt;/li&gt;
&lt;li&gt;Trojan-WS (через fallback на 443)&lt;/li&gt;
&lt;li&gt;Hysteria2 (UDP с port-hopping)&lt;/li&gt;
&lt;li&gt;VLESS-gRPC-REALITY (отдельный порт, маскировка под чужой TLS)
&lt;strong&gt;Маскировка&lt;/strong&gt; — панель спрятана за секретным путём. Любой другой путь отдаёт сайт-заглушку (11 готовых шаблонов), так что сервер неотличим от обычного хостинга. Без знания &lt;code&gt;/&amp;lt;secret&amp;gt;/&lt;/code&gt; форму логина не найти.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TLS, который просто работает&lt;/strong&gt; — ACME через Let's Encrypt или ZeroSSL, авто-продление и self-signed fallback, пока не выпустится настоящий сертификат.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Маршрутизация и выходы&lt;/strong&gt; — категории block / direct / proxy / WARP / Opera с настраиваемым приоритетом, geosite/geoip-пресеты с авто-загрузкой баз, Cloudflare WARP (WireGuard) как выход по правилам, Opera VPN как бесплатный региональный выход и пул прокси с health-check через Observatory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Пользователи и статистика&lt;/strong&gt; — лимиты трафика и срок действия с авто-отключением, авто-сброс по дням/неделям/месяцам/годам, учёт трафика через Xray Stats API, онлайн-статус и дашборд в реальном времени (CPU / RAM / swap / диск / сеть / VPN-трафик / аптайм).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Подписки&lt;/strong&gt; — base64-список плюс страница с QR-кодами, deep-links и кнопками импорта в популярные клиенты.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Эксплуатация&lt;/strong&gt; — бэкап/восстановление и сброс к заводским настройкам из дашборда и из CLI, логи приложения с ротацией и просмотрщиком прямо в панели, брутфорс-защита (бан по iptables за перебор на SOCKS/HTTP-инбаунде) и самообновление прямо из GitHub Releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Стек
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Go 1.26 · Xray-core · SQLite (modernc, без CGO) · React + Vite + Tailwind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Внутренняя структура разбита по зонам ответственности — типизированный генератор конфига Xray и супервизор, ACME-менеджер, роутер заглушки, port-hopping на nftables для Hysteria2, хелперы WARP и Opera, пул прокси, сборщики ссылок и подписок и сервисный слой, который сводит желаемое состояние (квоты, TLS, роутинг) с тем, что Xray реально крутит.&lt;/p&gt;

&lt;h2&gt;
  
  
  Попробовать
&lt;/h2&gt;

&lt;p&gt;Одна команда скачает последний релиз, поставит systemd-сервис, запустит его и напечатает логин:&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;-Ls&lt;/span&gt; https://raw.githubusercontent.com/AppsGanin/rospanel/main/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Скрипт спросит домен (запросит TLS-сертификат через ACME) — или можно оставить пустым и работать по IP. Xray, geo-базы и сертификат подтянутся автоматически. Поддерживаются &lt;code&gt;linux/amd64&lt;/code&gt; и &lt;code&gt;linux/arm64&lt;/code&gt; — скрипт сам выберет нужный бинарь.&lt;/p&gt;

&lt;p&gt;Есть также вариант с Docker-образом (&lt;code&gt;ghcr.io/appsganin/rospanel:latest&lt;/code&gt;), если так удобнее.&lt;/p&gt;

&lt;p&gt;При первом входе мастер настройки принудительно потребует сменить пароль и предложит заменить путь панели на случайный секрет — после этого корневой URL всегда отдаёт только заглушку.&lt;/p&gt;

&lt;h2&gt;
  
  
  Про назначение
&lt;/h2&gt;

&lt;p&gt;РосПанель создана для &lt;strong&gt;образовательных и исследовательских целей&lt;/strong&gt;: изучения того, как работают сетевые протоколы, TLS и прокси-технологии, проведения CTF-лаб, авторизованного пентеста и управления собственной инфраструктурой. Механизмы маскировки существуют для изучения этих техник и проверки устойчивости сервисов в рамках санкционированного тестирования. Ответственность за соблюдение законов своей юрисдикции несёт оператор сервера.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source
&lt;/h2&gt;

&lt;p&gt;Лицензия — &lt;strong&gt;AGPL-3.0&lt;/strong&gt;, проект полностью открыт. Релизы автоматизированы через release-please на основе Conventional Commits: публикуются и бинарник, и Docker-образ в GHCR.&lt;/p&gt;

&lt;p&gt;Если панель окажется полезной — ⭐ на &lt;a href="https://github.com/AppsGanin/rospanel" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; реально помогает, а PR и issue очень приветствуются.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Репозиторий:&lt;/strong&gt; &lt;a href="https://github.com/AppsGanin/rospanel" rel="noopener noreferrer"&gt;https://github.com/AppsGanin/rospanel&lt;/a&gt;&lt;/p&gt;

</description>
      <category>xray</category>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>go</category>
    </item>
    <item>
      <title>Your Mac Has 80GB of Hidden Junk. Here's How to Find It.</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Thu, 11 Jun 2026 07:44:54 +0000</pubDate>
      <link>https://dev.to/dmitryganin/your-mac-has-80gb-of-hidden-junk-heres-how-to-find-it-543g</link>
      <guid>https://dev.to/dmitryganin/your-mac-has-80gb-of-hidden-junk-heres-how-to-find-it-543g</guid>
      <description>&lt;p&gt;After years of using CleanMyMac and watching it beg me for a paid upgrade every time I clicked something useful, I decided to build my own. Meet &lt;strong&gt;Trashly&lt;/strong&gt; — a macOS disk cleaner written in Rust with a React frontend, open-source (AGPL-3), and with zero upsells.&lt;/p&gt;

&lt;p&gt;This post is about why I built it, the interesting technical choices I made, and the one thing most cleaners get completely wrong.&lt;/p&gt;




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

&lt;h2&gt;
  
  
  The Problem: Cleaners Don't Know About Developers
&lt;/h2&gt;

&lt;p&gt;Mainstream Mac cleaners do a decent job on browser caches and basic junk. But if you're a developer? They leave gigabytes on the table.&lt;/p&gt;

&lt;p&gt;On my MacBook after about a year of development, I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~40 GB&lt;/strong&gt; of Xcode &lt;code&gt;DerivedData&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~15 GB&lt;/strong&gt; of iOS DeviceSupport (archived device frameworks for debugging)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~20 GB&lt;/strong&gt; across &lt;code&gt;node_modules&lt;/code&gt; folders in abandoned projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~8 GB&lt;/strong&gt; of Android emulator images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~6 GB&lt;/strong&gt; of JetBrains caches&lt;/li&gt;
&lt;li&gt;Docker volumes I forgot existed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generic cleaners found maybe 2 GB of browser cache and called it a day.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;I wanted something that felt native and was actually fast. The choice was easy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; for the backend — memory safety, real parallelism, direct OS access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tauri 2&lt;/strong&gt; as the bridge — gives you a native window with WebView, IPC, tray icon, and auto-updates with no Electron overhead (~8 MB app bundle vs ~200 MB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React 19 + TypeScript&lt;/strong&gt; for the UI — familiar tooling, great ecosystem, and Tauri's WebView means I don't have to learn SwiftUI&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Interesting Parts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Safety-First Deletion
&lt;/h3&gt;

&lt;p&gt;The thing that terrifies me about disk cleaners is "what if it deletes the wrong thing." So the first thing I built was a strict allowlist guard in Rust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_safe_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;allowed_roots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;home_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Library/Caches"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;home_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Library/Logs"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;home_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".Trash"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// developer paths...&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;allowed_roots&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.any&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="nf"&gt;.starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="nf"&gt;.as_path&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail: &lt;code&gt;path != root&lt;/code&gt; — you can't delete the root itself, only things &lt;em&gt;inside&lt;/em&gt; it. And &lt;code&gt;..&lt;/code&gt; escapes are rejected before this check even runs. This guard lives in Rust, not JavaScript. Even if a malicious path somehow came from the UI layer, the backend would refuse it.&lt;/p&gt;

&lt;p&gt;By default, everything goes to the Trash via &lt;code&gt;NSFileManager&lt;/code&gt; — permanent deletion is opt-in.&lt;/p&gt;




&lt;h3&gt;
  
  
  Streaming Scan Results
&lt;/h3&gt;

&lt;p&gt;Nobody wants to stare at a spinner for 30 seconds before seeing results. The scan fans out across all categories in parallel using &lt;code&gt;rayon&lt;/code&gt;, and results stream to the UI as they're found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;scan_caches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ScanResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;rayon&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SCANNERS&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="nf"&gt;.scan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.blocking_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&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="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nn"&gt;ReceiverStream&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rx&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 UI renders file entries the moment they arrive. A 40 GB &lt;code&gt;DerivedData&lt;/code&gt; scan starts showing results within 200ms.&lt;/p&gt;




&lt;h3&gt;
  
  
  Finding Dead &lt;code&gt;node_modules&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is where Trashly gets opinionated. Rather than scanning for &lt;code&gt;node_modules&lt;/code&gt; anywhere on the disk, it looks for project manifests first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;find_project_artifacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArtifactResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;WalkDir&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.filter_map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;is_manifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.path&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="c1"&gt;// package.json, Cargo.toml, etc.&lt;/span&gt;
        &lt;span class="nf"&gt;.flat_map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;project_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="nf"&gt;.path&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.parent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;ARTIFACT_DIRS&lt;/span&gt;&lt;span class="nf"&gt;.iter&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;project_dir&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="nf"&gt;.exists&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="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ArtifactResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="py"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;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;.collect&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;It finds &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;target&lt;/code&gt;, &lt;code&gt;dist&lt;/code&gt;, &lt;code&gt;.next&lt;/code&gt; — but only when they sit next to a real &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;Cargo.toml&lt;/code&gt;, &lt;code&gt;build.gradle&lt;/code&gt;, etc. No false positives from unrelated folders with the same name.&lt;/p&gt;




&lt;h3&gt;
  
  
  Perceptual Photo Deduplication
&lt;/h3&gt;

&lt;p&gt;Exact duplicate detection (BLAKE3 hash matching) is table stakes. The interesting problem is &lt;em&gt;similar&lt;/em&gt; photos — the same screenshot at two export sizes, the same photo edited in Lightroom, burst shots that are 95% identical.&lt;/p&gt;

&lt;p&gt;I implemented dHash clustering:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode image to 9×8 grayscale (via the &lt;code&gt;image&lt;/code&gt; crate; HEIC files go through system &lt;code&gt;sips&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;For each row, compute whether each pixel is brighter than its right neighbor → 64-bit hash&lt;/li&gt;
&lt;li&gt;Group images where Hamming distance &amp;lt; threshold into clusters&lt;/li&gt;
&lt;li&gt;Within each cluster, keep the largest file (assumed best quality) and offer the rest for deletion
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;dhash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;DynamicImage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="nf"&gt;.resize_exact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;FilterType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Lanczos3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="nf"&gt;.grayscale&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt;&lt;span class="nf"&gt;.to_luma8&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;x&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="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&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="n"&gt;hash&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a photo library of 12,000 images, this runs in about 8 seconds on an M1.&lt;/p&gt;




&lt;h3&gt;
  
  
  System Metrics That Actually Work on Recent macOS
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;sysinfo&lt;/code&gt; crate — which most Rust system monitor tools reach for — reports zero memory pressure on macOS 14+. The kernel API it uses was deprecated.&lt;/p&gt;

&lt;p&gt;I ended up parsing &lt;code&gt;top -l 1 -n 0&lt;/code&gt; and &lt;code&gt;netstat -i -b&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;memory_pressure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MemPressure&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"memory_pressure"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// parses "System-wide memory free percentage: 42%"&lt;/span&gt;
    &lt;span class="c1"&gt;// and "The system memory pressure is: CRITICAL"&lt;/span&gt;
    &lt;span class="nf"&gt;parse_memory_pressure_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="py"&gt;.stdout&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;Ugly? A little. But it returns the same values macOS shows in Activity Monitor, which is what users expect.&lt;/p&gt;




&lt;h2&gt;
  
  
  The App Uninstaller
&lt;/h2&gt;

&lt;p&gt;This is the feature I'm most proud of. When you drag an app to Trash on macOS, you leave behind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/Library/Application Support/&amp;lt;AppName&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/Library/Caches/com.example.app&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/Library/Containers/com.example.app&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/Library/Preferences/com.example.app.plist&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Launch Agents&lt;/li&gt;
&lt;li&gt;Browser extensions and profiles&lt;/li&gt;
&lt;li&gt;Crash reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trashly hunts all of these down by bundle ID and developer name, lets you preview them, and optionally keep the app binary while wiping just its data (great for resetting an app to factory state).&lt;/p&gt;

&lt;p&gt;For developer tools (Xcode, Android Studio, JetBrains, Docker), there are dedicated cleanup routines that know exactly where each tool stores its caches and how much space each component takes.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Tauri is genuinely great.&lt;/strong&gt; The IPC ergonomics are excellent — you define a Rust function with &lt;code&gt;#[tauri::command]&lt;/code&gt;, and the TypeScript side gets a typed async function. Hot reload works. The app bundle is tiny.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rust's safety guarantees matter for this use case.&lt;/strong&gt; There are code paths in Trashly that walk the entire home directory and make deletion decisions. Having the compiler verify that I'm not accidentally aliasing mutable state across threads, and that every error is handled, is worth the learning curve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most disk usage surprises people.&lt;/strong&gt; The feedback I consistently get is: "I had no idea DerivedData was this big." Xcode will accumulate build artifacts for every simulator OS version and every version of your app it's ever compiled. 50 GB is not unusual for active iOS developers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to Find It
&lt;/h2&gt;

&lt;p&gt;Trashly is open-source under AGPL-3 on GitHub. No telemetry, no accounts, no upsells. The cleanup history is a local-only audit log so you can always see what was removed and when.&lt;/p&gt;

&lt;p&gt;If you're macOS user and you're regularly running out of disk space, give it a try. The first scan usually finds 30–80 GB on a machine that's been used for a year or more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AppsGanin/trashly" rel="noopener noreferrer"&gt;github.com/AppsGanin/trashly&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Rust, Tauri 2, React 19. Feedback welcome — especially if you know a cache directory I haven't found yet.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>opensource</category>
      <category>tauri</category>
      <category>programming</category>
    </item>
    <item>
      <title>Вечер на автоматизацию GitHub-ачивок</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Fri, 29 May 2026 15:20:58 +0000</pubDate>
      <link>https://dev.to/dmitryganin/viechier-na-avtomatizatsiiu-github-achivok-chto-poshlo-nie-tak-i-pochiemu-eto-intieriesno-3o5o</link>
      <guid>https://dev.to/dmitryganin/viechier-na-avtomatizatsiiu-github-achivok-chto-poshlo-nie-tak-i-pochiemu-eto-intieriesno-3o5o</guid>
      <description>&lt;p&gt;Началось всё банально: хотел собрать ачивки на GitHub-профиле.&lt;/p&gt;

&lt;p&gt;Процесс оказался несложным по сути, но раздражающим по исполнению — куча однотипных ручных шагов, ожидание ревьюеров, непрозрачные условия засчитывания. Типичная задача на «автоматизировать и забыть».&lt;/p&gt;

&lt;p&gt;Я взял её как мини-проект на вечер. Вечер растянулся.&lt;/p&gt;




&lt;h2&gt;
  
  
  Что хотелось получить
&lt;/h2&gt;

&lt;p&gt;Публичный репозиторий, куда любой может открыть PR, и он автоматически мёрджится — без мейнтейнера, без ожидания, без магии.&lt;/p&gt;

&lt;p&gt;Звучит просто. Но сразу встал вопрос: как сделать это &lt;strong&gt;безопасно&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Открытый авто-мёрдж без ограничений — это либо спам, либо кто-то пропушит что-то лишнее. Нужна была система проверок.&lt;/p&gt;




&lt;h2&gt;
  
  
  Первое нетривиальное решение: &lt;code&gt;pull_request_target&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Для авто-мёрджа форков нужен доступ к секретам репозитория. Обычный &lt;code&gt;pull_request&lt;/code&gt; такого доступа не даёт — он намеренно изолирован для безопасности.&lt;/p&gt;

&lt;p&gt;Правильный ответ — &lt;code&gt;pull_request_target&lt;/code&gt;. Он запускается в контексте &lt;strong&gt;целевого&lt;/strong&gt; репозитория, а не форка, поэтому имеет доступ к токенам. Но это же делает его опасным, если не выстроить валидацию до любых действий.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contributors/**"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Вся логика валидации идёт &lt;strong&gt;первой&lt;/strong&gt;, до мёрджа — иначе это дыра.&lt;/p&gt;




&lt;h2&gt;
  
  
  Валидация: динамический regex из логина автора
&lt;/h2&gt;

&lt;p&gt;Я хотел, чтобы каждый пользователь мог добавить &lt;strong&gt;только свой&lt;/strong&gt; файл. Не чужой, не случайный — именно &lt;code&gt;username.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Жёсткий список заранее не составишь. Поэтому имя файла проверяется динамически: regex строится прямо из логина автора PR.&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;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;validPattern&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;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`^contributors/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(-&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;d+)?&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.md$`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&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;invalidFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;validPattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Паттерн допускает &lt;code&gt;username.md&lt;/code&gt;, &lt;code&gt;username-2.md&lt;/code&gt;, &lt;code&gt;username-3.md&lt;/code&gt; — для случаев, когда нужно несколько PR от одного пользователя. Всё остальное — отказ с понятным сообщением прямо в комментарии к PR.&lt;/p&gt;




&lt;h2&gt;
  
  
  Galaxy Brain: правильный ответ спрятан в HTML-комментарии
&lt;/h2&gt;

&lt;p&gt;Для ачивки Galaxy Brain нужно, чтобы кто-то ответил на вопрос в Discussions и ответ был помечен как правильный.&lt;/p&gt;

&lt;p&gt;Автоматизировать проверку ответа — задача нетривиальная. Хранить правильные ответы в отдельном файле/базе? Лишние сущности. Сравнивать с внешним API в момент ответа? Медленно и ненадёжно.&lt;/p&gt;

&lt;p&gt;Элегантное решение: правильный ответ прячется прямо в теле discussion в HTML-комментарии, который пользователь не видит, а workflow читает.&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="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="nx"&gt;ANSWER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JavaScript&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Когда кто-то отвечает в треде, workflow проверяет, содержит ли комментарий нужную строку:&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;answerMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;discussion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;!--&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*ANSWER:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.+&lt;/span&gt;&lt;span class="se"&gt;?)\s&lt;/span&gt;&lt;span class="sr"&gt;*--&amp;gt;/i&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;correctAnswer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;answerMatch&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="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;commentBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;commentBody&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="nx"&gt;correctAnswer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// неправильный ответ — ничего не делаем&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Никакой внешней базы. Данные живут там, где они нужны.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussions API: только GraphQL
&lt;/h2&gt;

&lt;p&gt;GitHub REST API не поддерживает принятие ответа в Discussions. Это доступно только через GraphQL — &lt;code&gt;markDiscussionCommentAsAnswer&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="nx"&gt;mutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  mutation($commentId: ID!) {
    markDiscussionCommentAsAnswer(input: { id: $commentId }) {
      discussion { number url }
    }
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;graphql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;commentId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Аналогично для создания самих вопросов и добавления реакций — всё через GraphQL. REST здесь просто не работает.&lt;/p&gt;




&lt;h2&gt;
  
  
  Идемпотентность: не создавать вопросы, пока есть неотвеченные
&lt;/h2&gt;

&lt;p&gt;Workflow создания вопросов запускается три раза в день. Но создавать новые вопросы, пока старые висят без ответа — бессмысленно и засоряет Discussions.&lt;/p&gt;

&lt;p&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="nx"&gt;unanswered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;discussions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalCount&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;unanswered&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// пропускаем этот запуск&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Запуск идемпотентен: повторный прогон не ломает состояние.&lt;/p&gt;




&lt;h2&gt;
  
  
  Схема всего
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PR pipeline:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User opens PR
  → pull_request_target
  → Validation
      ✅ pass → Squash merge
      ❌ fail → Comment with reason
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Galaxy Brain pipeline:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cron 3x/day
  → Unanswered questions exist?
      yes → skip
      no  → fetch from OpenTDB
           → create discussions with hidden ANSWER tag

New discussion comment
  → correct answer? → markDiscussionCommentAsAnswer
  → wrong answer?   → ignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Что получилось в итоге
&lt;/h2&gt;

&lt;p&gt;Репозиторий, где:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🦈 &lt;strong&gt;Pull Shark&lt;/strong&gt; — PR в &lt;code&gt;contributors/&lt;/code&gt; → авто-мёрдж&lt;/li&gt;
&lt;li&gt;👥 &lt;strong&gt;Pair Extraordinaire&lt;/strong&gt; — PR в &lt;code&gt;pairs/&lt;/code&gt; → авто-мёрдж с &lt;code&gt;Co-authored-by&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🧠 &lt;strong&gt;Galaxy Brain&lt;/strong&gt; — отвечай на вопросы в Discussions → бот принимает правильные&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Quickdraw&lt;/strong&gt; и 🤠 &lt;strong&gt;YOLO&lt;/strong&gt; — инструкции за 2 клика&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Без ожидания, без мейнтейнера, без магии.&lt;/p&gt;




&lt;h2&gt;
  
  
  Дисклеймер
&lt;/h2&gt;

&lt;p&gt;Проект создан в образовательных и развлекательных целях.&lt;br&gt;
Ачивки — косметика профиля, не метрика уровня инженера.&lt;/p&gt;

&lt;p&gt;Используй в рамках &lt;a href="https://docs.github.com/en/site-policy/github-terms/github-terms-of-service" rel="noopener noreferrer"&gt;GitHub Terms of Service&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Если идея понравилась — посмотри &lt;strong&gt;&lt;a href="https://github.com/AppsGanin/achievement-github-farm" rel="noopener noreferrer"&gt;GitHub Achievement Farm&lt;/a&gt;&lt;/strong&gt; и поставь ⭐, если зашло.&lt;/p&gt;

&lt;p&gt;Если есть вопросы по деталям реализации или нашёл edge-case — welcome в &lt;a href="https://github.com/AppsGanin/achievement-github-farm/issues" rel="noopener noreferrer"&gt;issues&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>github</category>
      <category>githubactions</category>
      <category>opensource</category>
      <category>automation</category>
    </item>
    <item>
      <title>Ultimate 1-Minute Xray/3x-ui Setup: VLESS, Hysteria2, Caddy Self-Steal &amp; Smart Outbounds in One Script</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Wed, 27 May 2026 20:51:45 +0000</pubDate>
      <link>https://dev.to/dmitryganin/ultimate-1-minute-xray3x-ui-setup-vless-hysteria2-caddy-self-steal-smart-outbounds-in-one-594d</link>
      <guid>https://dev.to/dmitryganin/ultimate-1-minute-xray3x-ui-setup-vless-hysteria2-caddy-self-steal-smart-outbounds-in-one-594d</guid>
      <description>&lt;p&gt;Setting up a secure internet circumvention gateway using Xray usually turns into a tedious chore. You have to install Docker, configure the 3x-ui web panel, generate TLS certificates, tweak Reality settings against active probing, and manually set up routing tables. &lt;/p&gt;

&lt;p&gt;If you frequently cycle your VPS providers or manage servers for friends, doing this manually every time is a massive waste of time.&lt;/p&gt;

&lt;p&gt;That’s why I created &lt;strong&gt;3xui-fast-install&lt;/strong&gt; — a lightweight, automated bash script that deploys a fully production-ready, security-hardened Xray node in under a minute.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s Under the Hood? 🛠️
&lt;/h2&gt;

&lt;p&gt;This is &lt;strong&gt;NOT just a blank panel installation&lt;/strong&gt;. The script sets up an optimized, batteries-included network ecosystem wrapped in Docker. Here is exactly what gets deployed and configured out of the box:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3x-ui Panel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Xray web management interface running on custom non-standard ports.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VLESS + Reality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Industry-standard transport layer mimicking legitimate TLS handshakes (Port 443).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hysteria 2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High-performance UDP-based protocol (Port 63000) optimized for unstable or mobile networks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Caddy (Self-Steal)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Acts as a TLS terminator on port 443, handling automated, legitimate Let's Encrypt certificates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare WARP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Configured as a local SOCKS5 outbound proxy for specific fallback routing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Opera Proxy &amp;amp; Tor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Additional local SOCKS5 outbounds ready for geo-blocked services and &lt;code&gt;.onion&lt;/code&gt; routing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BBR &amp;amp; Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automated TCP BBR congestion control activation, UFW firewall configuration, and Fail2ban protection.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Smart Server-Side Routing Architecture 🗺️
&lt;/h2&gt;

&lt;p&gt;One of the highlights of this setup is how it handles outbound traffic directly inside Xray on the server. Instead of just passing everything blindly (&lt;code&gt;direct&lt;/code&gt;), the backend follows strict routing rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adblock &amp;amp; Malware:&lt;/strong&gt; Automatically dropped (&lt;code&gt;blocked&lt;/code&gt;) at the server level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domestic/RU Domains &amp;amp; GeoIP:&lt;/strong&gt; Routed via &lt;strong&gt;Cloudflare WARP&lt;/strong&gt;. Ideally, your clients should use local routing rules (like &lt;code&gt;roscomvpn-routing&lt;/code&gt;) so domestic traffic never leaves their device. However, if a domestic packet &lt;em&gt;does&lt;/em&gt; leak to your VPS, WARP intercepts it and routes it out, masking your actual VPS server IP address from domestic logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming &amp;amp; Privacy:&lt;/strong&gt; Popular global services (like Disney+ or Reddit) are routed via &lt;strong&gt;Opera Proxy&lt;/strong&gt;, while &lt;code&gt;.onion&lt;/code&gt; addresses go directly through the &lt;strong&gt;Tor outbound network&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Note on Customization:&lt;/strong&gt; The outbound routing rules listed above (such as Opera Proxy, Tor, or specific domain rules) are configured &lt;strong&gt;as examples&lt;/strong&gt; to showcase the full capabilities of the script. You can easily modify, remove, or add your own custom routing parameters in the 3x-ui panel settings anytime after the installation is complete.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Prerequisites &amp;amp; Installation 🚀
&lt;/h2&gt;

&lt;p&gt;The deployment process requires minimal preparation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requirements:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A clean &lt;strong&gt;Ubuntu/Debian VPS&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A registered &lt;strong&gt;domain name&lt;/strong&gt; with an &lt;strong&gt;A-record&lt;/strong&gt; already pointed to your VPS IP address.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The 1-Command Deployment:
&lt;/h3&gt;

&lt;p&gt;Run the following command on your clean server root 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 https://github.com/AppsGanin/3xui-fast-install 3xui-personal
&lt;span class="nb"&gt;cd &lt;/span&gt;3xui-personal

&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vpn.example.com bash deploy.sh 1.2.3.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What happens next?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The script detects and installs Docker/Compose if missing.&lt;/li&gt;
&lt;li&gt;It automatically generates unique, high-entropy credentials and secure ports for your 3x-ui panel dashboard.&lt;/li&gt;
&lt;li&gt;It configures your firewall, opens only the necessary ports (22, 80, 443, 63000 UDP), and locks down the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once finished, the console will print your &lt;strong&gt;ready-to-use admin panel URL, username, and password&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Log in, create your client configs in the web UI, and you are good to go!&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; Open Source
&lt;/h2&gt;

&lt;p&gt;This setup cuts down deployment time from an hour of manual configuration to literally 60 seconds, while maintaining best-practice security baselines out-of-the-box.&lt;/p&gt;

&lt;p&gt;The project is completely open-source. Feel free to review the bash script logic, submit issues, or contribute improvements.&lt;/p&gt;

&lt;p&gt;⭐️ &lt;strong&gt;Check out the repository on GitHub:&lt;/strong&gt; 👉 &lt;a href="https://github.com/AppsGanin/3xui-fast-install" rel="noopener noreferrer"&gt;https://github.com/AppsGanin/3xui-fast-install&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>privacy</category>
      <category>network</category>
    </item>
    <item>
      <title>Zero-Cost AI in VS Code</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Sat, 23 May 2026 17:02:39 +0000</pubDate>
      <link>https://dev.to/dmitryganin/zero-cost-ai-in-vs-code-2g24</link>
      <guid>https://dev.to/dmitryganin/zero-cost-ai-in-vs-code-2g24</guid>
      <description>&lt;h1&gt;
  
  
  Zero-Cost AI: Accessing Premium Models in VS Code Without API Keys
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;How I built a VS Code extension that gives you free access to Qwen-Max and DeepSeek models using only your existing web account — no billing, no tokens, no limits.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 The Problem with Modern AI Tools
&lt;/h2&gt;

&lt;p&gt;The state of AI development has become expensive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API keys&lt;/strong&gt; needed for every provider&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-token pricing&lt;/strong&gt; that adds up quickly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits&lt;/strong&gt; blocking your workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple subscriptions&lt;/strong&gt; to different services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Premium AI services charge significant monthly fees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT Plus&lt;/strong&gt;: $20/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Pro&lt;/strong&gt;: $20/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini Advanced&lt;/strong&gt;: $20/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's over $600/year for basic access! And you still need separate accounts for each service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if there was another way?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 The Solution: Browser-Based Authentication
&lt;/h2&gt;

&lt;p&gt;I developed &lt;strong&gt;AI Free VSCode&lt;/strong&gt; — an open-source extension that leverages your existing &lt;strong&gt;free tier accounts&lt;/strong&gt; from AI providers through browser automation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Innovation
&lt;/h3&gt;

&lt;p&gt;Instead of requiring API keys (which often have strict rate limits), the extension:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Uses &lt;strong&gt;Playwright&lt;/strong&gt; to automate a real Chromium browser session&lt;/li&gt;
&lt;li&gt;Stores authentication cookies locally&lt;/li&gt;
&lt;li&gt;Makes requests through the official web APIs&lt;/li&gt;
&lt;li&gt;Gives you full access to the &lt;strong&gt;same free tier&lt;/strong&gt; available on their websites&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Result?
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Zero cost&lt;/strong&gt; - uses existing free accounts&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;No API keys&lt;/strong&gt; - just sign in once&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Higher limits&lt;/strong&gt; - same as browsing the website&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Native integration&lt;/strong&gt; - works directly in Copilot Chat&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Agent mode&lt;/strong&gt; - full tool calling support&lt;/p&gt;


&lt;h2&gt;
  
  
  🏗 Architecture Overview
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Extension Structure
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ai-free-vscode/
├── src/
│   ├── extension.mjs          # Entry point &amp;amp; commands
│   ├── lmProvider.mjs         # Unified LM provider interface
│   ├── deepseek/
│   │   ├── auth.mjs           # Browser login with Playwright
│   │   ├── client.mjs         # API client implementation
│   │   ├── provider.mjs       # Model logic &amp;amp; session management
│   │   └── config.mjs         # Configuration constants
│   ├── qwen/
│   │   ├── auth.mjs           # Qwen authentication
│   │   ├── client.mjs         # Qwen API client
│   │   └── provider.mjs       # Qwen model implementation
│   ├── utils/
│   │   ├── logger.mjs         # Debug logging
│   │   ├── rateLimiter.mjs    # Rate limiting protection
│   │   ├── responseValidator.mjs
│   │   └── tokenValidator.mjs
│   └── promptUtils.mjs        # Message formatting
├── package.json               # Extension manifest
└── README.md                  # Documentation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Core Components
&lt;/h3&gt;
&lt;h4&gt;
  
  
  1. Authentication Flow
&lt;/h4&gt;

&lt;p&gt;The extension registers commands for users to authenticate:&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="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deepseek.login&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="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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;clearProfileSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Clear old session&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loginAndSaveAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// New login via Playwright&lt;/span&gt;
    &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookieHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookieHeader&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Process:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Opens Chromium browser via Playwright&lt;/li&gt;
&lt;li&gt;User signs into provider normally&lt;/li&gt;
&lt;li&gt;Session cookies captured and stored locally&lt;/li&gt;
&lt;li&gt;Cookies used for subsequent API requests&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Unified Provider Interface
&lt;/h4&gt;

&lt;p&gt;All models are unified under a single vendor namespace:&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;class&lt;/span&gt; &lt;span class="nc"&gt;AiFreeVscodeChatModelProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;provideLanguageModelChatResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Convert VS Code messages to API format&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;convertedMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convertMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&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;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convertToolSchemas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;tools&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;messagesToPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;convertedMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Route to appropriate provider&lt;/span&gt;
    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;family&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deepseek&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;deepseekComplete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qwen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;qwenComplete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;onText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;onThinking&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;break&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Process:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routes VS Code chat requests to appropriate provider&lt;/li&gt;
&lt;li&gt;Handles both DeepSeek and Qwen models&lt;/li&gt;
&lt;li&gt;Converts messages to API format&lt;/li&gt;
&lt;li&gt;Manages streaming responses&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Smart Session Management
&lt;/h4&gt;

&lt;p&gt;Maintains conversation continuity with session caching:&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;sessionIdCache&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;Map&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;runComplete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;threadKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;messagesCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Start fresh session for first message in thread&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;messagesCount&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;span class="nx"&gt;sessionIdCache&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;threadKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Try cached session first (for conversation continuity)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cachedSessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sessionIdCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threadKey&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;cachedSessionId&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;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedSessionId&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;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Success!&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Retry with new session&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&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;createSession&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;sessionIdCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threadKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔧 Installation &amp;amp; Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Install the Extension
&lt;/h3&gt;

&lt;p&gt;Download the latest &lt;code&gt;.vsix&lt;/code&gt; file from &lt;a href="https://github.com/AppsGanin/ai-free-vscode/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt; and install via VS Code Extensions panel.&lt;/p&gt;

&lt;p&gt;Or develop locally:&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/AppsGanin/ai-free-vscode
&lt;span class="nb"&gt;cd &lt;/span&gt;ai-free-vscode
npm &lt;span class="nb"&gt;install&lt;/span&gt;  &lt;span class="c"&gt;# installs dependencies + Playwright Chromium&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;F5&lt;/code&gt; to launch in Extension Development Host.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Sign In to Provider
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open Command Palette (&lt;code&gt;Cmd+Shift+P&lt;/code&gt; / &lt;code&gt;Ctrl+Shift+P&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Run &lt;strong&gt;"AI Free VSCode: DeepSeek: Sign In (Playwright)"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A browser window opens automatically&lt;/li&gt;
&lt;li&gt;Log in to your account normally&lt;/li&gt;
&lt;li&gt;Window closes when session is saved&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeat for &lt;strong&gt;Qwen&lt;/strong&gt; or other supported providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Start Chatting
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Open Copilot Chat panel (⌘+L)&lt;/li&gt;
&lt;li&gt;Select your preferred model from dropdown&lt;/li&gt;
&lt;li&gt;Start asking questions!&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Supported Models
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek V4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deepseek-default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;General purpose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek V4 Expert&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deepseek-expert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complex reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen2.5-Max&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen-max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Powerful tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3.6-Plus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen-plus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Long documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3-Max&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen3-max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Flagship quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3-Coder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen-coder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Code generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3.5-Flash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;qwen-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fastest responses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All models support &lt;strong&gt;tool calling&lt;/strong&gt; for Agent mode operations like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File reading/writing&lt;/li&gt;
&lt;li&gt;Terminal execution&lt;/li&gt;
&lt;li&gt;Multi-step debugging&lt;/li&gt;
&lt;li&gt;Code refactoring&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠 Technical Deep Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How Messages Are Processed
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Message Conversion
&lt;/h4&gt;

&lt;p&gt;VS Code messages are converted to API-compatible format:&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;function&lt;/span&gt; &lt;span class="nf"&gt;convertMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;messages&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;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Handle text content&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&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;part&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;part&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;LanguageModelTextPart&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Extract tool calls from assistant&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;LanguageModelToolCallPart&lt;/span&gt;&lt;span class="p"&gt;)&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;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;function&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="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}));&lt;/span&gt;

      &lt;span class="c1"&gt;// Generate separate "tool" messages for results&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;LanguageModelToolResultPart&lt;/span&gt;&lt;span class="p"&gt;)&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;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}));&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;toolResults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;toolCalls&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="nf"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Tool Call Detection
&lt;/h4&gt;

&lt;p&gt;The system detects markdown fences indicating tool calls:&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;TOOL_FENCES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`tool_call&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="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;tool_call&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_call&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findFence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&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="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;fence&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;TOOL_FENCES&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;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fence&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;idx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Stream processing&lt;/span&gt;
&lt;span class="nx"&gt;streamBuf&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;text&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;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findFence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamBuf&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;idx&lt;/span&gt; &lt;span class="o"&gt;!==&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;span class="c1"&gt;// Emit text before fence, suppress tool call block&lt;/span&gt;
  &lt;span class="nf"&gt;flushStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamBuf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;streamBuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;inToolCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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 prevents raw tool call blocks from appearing in the chat UI while still executing them properly.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Thinking Mode Support
&lt;/h4&gt;

&lt;p&gt;For models with explicit reasoning phases:&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;let&lt;/span&gt; &lt;span class="nx"&gt;thinkingStarted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;thinkingText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onThinking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;thinkingText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;thinkingStarted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// When content starts, emit thinking as collapsible block&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;thinkingStarted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LanguageModelThinkingPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;thinkingText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;thinking-0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VS Code displays this as a native collapsible &lt;strong&gt;"💭 Thinking"&lt;/strong&gt; section above responses.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 Security &amp;amp; Privacy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Happens to Your Data?
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;Cookies stored locally&lt;/strong&gt; - Only your machine, encrypted by OS&lt;br&gt;
✅ &lt;strong&gt;No cloud storage&lt;/strong&gt; - We never transmit your credentials&lt;br&gt;
✅ &lt;strong&gt;Session isolation&lt;/strong&gt; - Each provider maintains separate sessions&lt;br&gt;
✅ &lt;strong&gt;No telemetry&lt;/strong&gt; - No usage statistics sent anywhere&lt;/p&gt;
&lt;h3&gt;
  
  
  Error Handling
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&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;complete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Graceful handling of various errors&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isNotSignedIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showErrorMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please sign in 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="k"&gt;else&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isAuthError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Cookie expired - force re-login&lt;/span&gt;
    &lt;span class="nf"&gt;clearProfileSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isBizError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Business logic error with formatted message&lt;/span&gt;
    &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatBizError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bizCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bizMsg&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  ⚠️ Limitations &amp;amp; Caveats
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Important Considerations
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terms of Service&lt;/strong&gt; - Automating browser sessions may violate provider ToS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account Risk&lt;/strong&gt; - Your account could be restricted (use at your own risk)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stability&lt;/strong&gt; - Providers can change APIs without notice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single Session&lt;/strong&gt; - Only one active user session at a time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Enterprise Support&lt;/strong&gt; - Not suitable for corporate compliance requirements&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Mitigation Strategies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use separate accounts from main email&lt;/li&gt;
&lt;li&gt;Don't abuse the service (reasonable usage only)&lt;/li&gt;
&lt;li&gt;Keep extension updated for API changes&lt;/li&gt;
&lt;li&gt;Maintain backups of important code/settings&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🚀 Real-World Use Cases
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Scenario 1: Code Review Assistant
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Ask about potential bugs
&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review this Python function for memory leaks:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;pastes&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;Assistant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Analyzes&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;identifies&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="n"&gt;leaks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;suggests&lt;/span&gt; &lt;span class="n"&gt;fixes&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;explanations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Scenario 2: Database Query Optimization
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Paste slow query&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Assistant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Suggests&lt;/span&gt; &lt;span class="n"&gt;indexing&lt;/span&gt; &lt;span class="n"&gt;strategies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;rewriting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;alternative&lt;/span&gt; &lt;span class="n"&gt;approaches&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Scenario 3: Full Stack Debugging
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Identify error in terminal&lt;/li&gt;
&lt;li&gt;Ask assistant to analyze stack trace&lt;/li&gt;
&lt;li&gt;Get root cause explanation&lt;/li&gt;
&lt;li&gt;Receive fix suggestion with code example&lt;/li&gt;
&lt;li&gt;Apply fix directly in editor&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  🤝 Contributing
&lt;/h2&gt;

&lt;p&gt;This is an open-source hobby project built by enthusiasts, for enthusiasts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ways to contribute:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fix bugs&lt;/strong&gt; - See &lt;a href="https://github.com/AppsGanin/ai-free-vscode/issues" rel="noopener noreferrer"&gt;open issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add new models&lt;/strong&gt; - Implement additional AI providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve docs&lt;/strong&gt; - Clarify setup instructions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhance UX&lt;/strong&gt; - Better error messages, UI improvements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write tests&lt;/strong&gt; - Increase coverage for edge cases&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Getting started:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AppsGanin/ai-free-vscode
&lt;span class="nb"&gt;cd &lt;/span&gt;ai-free-vscode
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# Edit code, press F5 to test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contributions welcome! PRs are always appreciated.&lt;/p&gt;




&lt;h2&gt;
  
  
  📝 Legal Disclaimer
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;This extension is unofficial and not affiliated with any AI provider.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use at your own risk&lt;/strong&gt; - Automating web sessions may violate ToS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No guarantees&lt;/strong&gt; - May stop working if providers change APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No liability&lt;/strong&gt; - Authors not responsible for consequences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always review Terms of Service before use.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;&lt;strong&gt;AI Free VSCode&lt;/strong&gt; demonstrates that you don't need expensive API keys or multiple subscriptions to access premium AI capabilities. By leveraging browser automation and existing free tiers, we've created a solution that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💰 &lt;strong&gt;Costs nothing&lt;/strong&gt; - literally $0 monthly subscription&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Works instantly&lt;/strong&gt; - one-time sign-in, perpetual access&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Respects privacy&lt;/strong&gt; - all data stays local&lt;/li&gt;
&lt;li&gt;🛠️ &lt;strong&gt;Integrates seamlessly&lt;/strong&gt; - native VS Code experience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you're a student learning to code, a indie developer building your startup, or just someone who wants powerful AI tools without breaking the bank - this extension removes financial barriers and puts cutting-edge technology in your hands.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Ready to try it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/AppsGanin/ai-free-vscode/releases" rel="noopener noreferrer"&gt;Download the extension&lt;/a&gt;&lt;br&gt;&lt;br&gt;
⭐ Star the repo if it helps your workflow&lt;br&gt;&lt;br&gt;
📣 Share with fellow developers&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let's democratize AI access together!&lt;/strong&gt; 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
