Docs›Under the hood
Development
Building and changing Hydor. Back to the README.
npm ci
npm run build:server && npm start # the server, on :8790
npm run dev:web # the web app with hot reload on :5173, proxying /api and /ws to :8790
npm run typecheck
npm run build builds both into dist/. The code:
server/: the HTTP and WebSocket API, the session manager (sessions.ts), the agent adapters (agents/), the transcript parsers (history/), the runner (runner/) and the indexer (indexer/).web/: the React app (Vite, Tailwind), one folder per area inweb/src/features/.shared/: types and helpers used by both.
The app's capabilities (search, the browser, memory, the guide…) each live in server/<name>/ and
web/src/features/<name>/. Each capability's tests are a script beside it (*-test.ts), built and run with a
scratch data folder:
OUTDIR=dist/dev node scripts/build-server.mjs server/guide/guide-test.ts && DATA_DIR=/tmp/hydor-test-guide node dist/dev/guide-test.js
Built-in skills are folders in skills/ (a SKILL.md each, with category: for its group on the Skills page);
server/skillpack/tests.ts checks them. Built-in plugins are folders in plugins/, in the Agent Plugins 1.0
format (plugin.json, skills/, mcp.json) with the app's own extension, com.naget.agent, for what they need.
server/plugins/ reads them, sets them up in their own data folders and gives every agent their skills and tool
servers (stdio servers start through dist/server/pluginlaunch.js, in the plugin's folder). Its tests check every
built-in plugin; with HYDOR_TEST_REAL_SETUP=1 they install the plugins' packages for real and talk to their
servers:
OUTDIR=dist/dev node scripts/build-server.mjs server/plugins/plugins-test.ts && HYDOR_TEST_REAL_SETUP=1 DATA_DIR=/tmp/hydor-test-plugins node dist/dev/plugins-test.js
The desktop apps#
desktop/ wraps the server in an Electron app (desktop/main.cjs) for macOS, Windows and Linux: it starts the server
with the Node.js inside it and a message channel (server/desktop.ts), shows it in a window, keeps it in the menu bar
(the notification area, the system tray), and installs the updates the server downloads (server/updates.ts, release
by release from hydor-mac.json, hydor-windows.json or hydor-linux.json on the newest GitHub release). How a copy
was installed decides how it updates (HYDOR_DESKTOP_FORMAT): a Mac app (app) and an AppImage swap themselves for the
new version and go back to the one before when it does not start; Windows (nsis) runs the new installer without its
window (/S --updated --force-run); the .deb installs with pkexec dpkg -i, so a version downloaded by itself waits
for Restart to Update.
On Windows and from an AppImage the app first copies its server, Node.js and tmux to runtime/<version>-<build> in
its local data folder (%LOCALAPPDATA%\Hydor, ~/.local/share/Hydor) and runs them from there: agents outlive the
app, and Windows cannot replace a file a program has open (its installer stops every program running from the app's
folder), while an AppImage's files go when it closes. Copies nothing runs from any more are removed a minute after
the server starts.
The channel goes both ways. The server tells the app what to show and do: ready (its address), update (a new
version downloaded and waiting, for Restart to Update in the menus; with notice, the words of the notification
the app shows a minute before it installs) and install. It asks the app what only the app can find out or do
(ask, answered with reply), and hears what you picked in the app's menus (update-now) and, on Windows, quit
(there is no SIGTERM a program can catch there). The server looks for a release a minute after it starts and every
4 hours; with Update automatically on it downloads a new one at once, counts down a minute in every page
(update.ready.installIn) and then sends install; Later (POST /api/update/later) puts it off for 4 hours.
server/desktop-test.ts plays the app over the real channel, for each kind of copy; HYDOR_UPDATE_NOTICE_MS shortens
the minute when trying it.
The server on Windows (server/platform.ts holds what differs): runners listen on a named pipe with a random name and
answer only a client that sends the key in their sock file; commands run in Git for Windows' bash; npm's .cmd
launchers start through Node.js and the script they name; programs stop with their whole tree (taskkill /T);
terminals are shells in ConPTY that the server keeps, with what they printed, since there is no tmux; links to folders
are junctions. server/platform-test.ts checks all of it on the system it runs on, and the desktop workflow runs it on
each.
Computer use (server/computer/, the Mac app only) works through that channel too. The tools run macOS's own programs
as children of the server, so macOS checks the app's permissions: screencapture for the main display, sips to scale
it to 1280 pixels and make a JPEG, open -a for apps, and osascript -l JavaScript with one driver script
(driver.ts) that posts CGEvents through the ObjC bridge: clicks with their click count, drags, scroll wheels, key
combinations by their US-keyboard codes, and text as Unicode on key events (a pasted fallback puts the clipboard back).
plan.ts checks actions and maps the screenshot's pixels to points (Retina, scaled resolutions); access.ts keeps the
setting, the per-session card, Stop and the one session at the Mac. The server asks the app for macOS's permissions
(ask permissions, and request to show macOS's prompt and its System Settings pane), tells it while an agent
acts (computer: the menu bar's title and Stop, with ⌘⇧Esc registered meanwhile) and hears computer-stop.
server/computer/computer-test.ts runs everything but macOS itself, the driver with stand-ins for the bridge.
Releases are built by .github/workflows/desktop.yml on GitHub's own computers when a tag v<version> (the version in
package.json) is pushed: the Mac app for Apple silicon and Intel, the Windows installer for x64, and the Linux
AppImage and .deb for x64 and ARM (on Ubuntu 22.04, so they need its C library, 2.34, or newer). Each build is
checked before it is kept: its server starts from inside it, the app itself opens and starts its server (the AppImage
from its own copy; on Windows also once installed with its installer, then uninstalled). It publishes them on the
Releases page with the update feeds and the version's notes from CHANGELOG.md, and copies under names without the
version for the README's links. Running it by hand (Actions → Desktop apps) builds without publishing. With the
signing secrets the workflow lists, the Mac app is signed with a Developer ID and notarized; without them it is signed
ad hoc. The Windows installer is not signed.
By hand, on the kind of computer the app is for (on Windows in Git's bash; on Linux with libncurses-dev for tmux):
npm ci && npm run build # at the top of the repository
desktop/scripts/stage.sh # the server with its production packages, Node.js (and tmux), in desktop/build/
cd desktop && npm ci && scripts/dist.sh # desktop/dist/: the .dmg and .zip, the installer, or the AppImage and .deb
node scripts/smoke.mjs dist/mac-arm64/Hydor.app # its server starts (dist/mac/, dist/win-unpacked, dist/linux-unpacked)
node scripts/launch-test.mjs dist/linux-unpacked/hydor # the app opens and starts its server
npm start # or: the app around this repository's own build, to work on main.cjs
To try the updater without a release, point the server at a feed of your own with HYDOR_UPDATE_FEED (in
~/.config/hydor.env), and, in development, the app at a stand-in bundle with HYDOR_DESKTOP_BUNDLE.
The Docker image#
Dockerfile makes the image: the app as a git checkout (so it can update itself in place), with Claude Code and
Codex. .github/workflows/docker.yml builds it on GitHub's runners for linux/amd64 and linux/arm64 (ARM under
emulation) when a tag v<version> is pushed, beside the desktop apps' release, and pushes algoprog/hydor:<version> and
algoprog/hydor:latest to Docker Hub, signed in with the repository secret DOCKERHUB_TOKEN. Running it by hand
(Actions → Docker image) builds both without pushing. To build one here: docker build -t hydor .
The website#
hydor.app is the website in website/: plain HTML, CSS and JavaScript, dark like the app's
dark theme, with screenshots of the app and frames of its tour film. Its docs (/docs) are made when it is built
from the same Markdown the app and GitHub show (the guide in docs/guide, the documents the README lists, and
CHANGELOG.md), with the app's own guide parser and Help's search, so the docs are written in one place only.
node website/build.mjs --serve 4321 builds it into dist/website and shows it at http://localhost:4321;
--deploy publishes it on Cloudflare Pages (project hydor) with CLOUDFLARE_API_TOKEN and
CLOUDFLARE_ACCOUNT_ID set. It names the latest release and shows the docs as they were when it was built, so
publish it again after a release or a change to the docs. website/README.md has the rest.
Keeping the docs current#
What users can see and do is described in several places, which change with the feature in the same commit:
the user guide in docs/guide (the app shows it under Help, the search box finds its sections, and
agents read it to answer questions and to change things in the app), this folder's documents and the README,
the search box's catalog (shared/find-catalog.ts, then npm run find:vectors), translations
(npm run i18n:extract), and the release notes. CLAUDE.md says how; server/guide/guide-test.ts
fails when the guide no longer matches the app (a link that points nowhere, a Settings section or an API route
it does not describe, a picture that is missing). The guide's pictures (docs/guide/images) come from a demo copy
of the app with made-up data: scripts/guide-shots takes them again when a page they show changes.
Change this page on GitHub · the app's Help shows the same text