{"id":51428128,"url":"https://github.com/yuanyuanzijin/tunlite","last_synced_at":"2026-07-05T02:30:26.720Z","repository":{"id":365272556,"uuid":"1271346257","full_name":"yuanyuanzijin/tunlite","owner":"yuanyuanzijin","description":"Lightweight, Agent-native SSH tunnel manager that keeps your tunnels alive — auto-reconnect, autostart, zero dependencies. A modern autossh replacement. 轻量、面向 Agent 的 SSH 隧道管理器，让隧道持续在线：自动重连、开机自启、零依赖，现代版 autossh。","archived":false,"fork":false,"pushed_at":"2026-06-16T16:44:28.000Z","size":237,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-16T17:22:39.016Z","etag":null,"topics":["autossh","cli","cross-platform","devops","nodejs","post-forward","reconnect","socks5","ssh","tunnel"],"latest_commit_sha":null,"homepage":" https://yuanyuanzijin.github.io/tunlite/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yuanyuanzijin.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-16T15:14:08.000Z","updated_at":"2026-06-16T16:41:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yuanyuanzijin/tunlite","commit_stats":null,"previous_names":["yuanyuanzijin/tunlite"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/yuanyuanzijin/tunlite","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuanyuanzijin%2Ftunlite","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuanyuanzijin%2Ftunlite/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuanyuanzijin%2Ftunlite/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuanyuanzijin%2Ftunlite/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yuanyuanzijin","download_url":"https://codeload.github.com/yuanyuanzijin/tunlite/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuanyuanzijin%2Ftunlite/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35141966,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["autossh","cli","cross-platform","devops","nodejs","post-forward","reconnect","socks5","ssh","tunnel"],"created_at":"2026-07-05T02:30:26.147Z","updated_at":"2026-07-05T02:30:26.714Z","avatar_url":"https://github.com/yuanyuanzijin.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tunlite\n\n\u003e **SSH tunnels for you and your Agent — kept alive.** Type the `-L`/`-R`/`-D` yourself, or\n\u003e just tell an AI Agent — tunlite builds the tunnel and keeps it connected.\n\n**English** · [简体中文](README.zh-CN.md)\n\n[![CI](https://github.com/yuanyuanzijin/tunlite/actions/workflows/ci.yml/badge.svg)](https://github.com/yuanyuanzijin/tunlite/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/tunlite)](https://www.npmjs.com/package/tunlite)\n[![downloads](https://img.shields.io/npm/dm/tunlite)](https://www.npmjs.com/package/tunlite)\n[![node](https://img.shields.io/badge/node-%E2%89%A518-brightgreen)](https://nodejs.org)\n[![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)\n\nKeeping SSH tunnels alive is a chore: pros juggle `autossh`, a `systemd` unit per tunnel,\nand a pile of `-L`/`-R`/`-D` flags — and reconnect by hand when one drops; newcomers don't\neven know where to start. **tunlite** folds it into one command: type it yourself, or just\ntell an **AI Agent** in plain words — either way it builds the tunnel, keeps it alive, and\nreconnects on its own. Pure Node.js, **zero dependencies**, wrapping the `ssh` you already\ntrust.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/yuanyuanzijin/tunlite/master/docs/demo.gif\" alt=\"define a tunnel, the daemon brings it up, check status, tail logs\" width=\"760\"\u003e\u003c/p\u003e\n\n\u003e 📖 **Full documentation → [tunlite.dev](https://tunlite.dev/)**\n\n- **Agent-native** — `--json` + stable exit codes on every command, plus a bundled Agent skill: an AI Agent sets up, brings up, and troubleshoots tunnels end-to-end.\n- **Zero third-party dependencies** — pure Node.js standard library; all it needs on the box is **Node ≥ 18** and the system `ssh` it wraps.\n- **Auto-reconnect** — exponential backoff + jitter, keepalive, port health probes.\n- **Start at login** — launchd (macOS) / systemd user service (Linux) / Task Scheduler (Windows — beta).\n- **Passwordless setup** — connects directly if keys already work; installs your key only if needed.\n- **Three forward types** — local `-L`, remote `-R`, dynamic SOCKS `-D`.\n\n## For Agents\n\nAn **AI Agent** is a first-class user. Ask one in plain language and it drives `tunlite`\nthrough the same `--json` surface you would — branching on exit codes, not scraping prose:\n\n```text\nyou   ▸ \"Forward the Postgres on app01 to my laptop.\"\nAgent ▸ tunlite add pg --to deploy@app01 -L 5432:localhost:5432 --json   → {\"ok\":true,…}\nAgent ▸ tunlite enable pg --json                                         → exit 4 · needs-auth\nAgent ▸ tunlite setup-key deploy@app01                                   → key installed\nAgent ▸ tunlite enable pg --json                                         → {\"state\":\"connected\"} · exit 0\nAgent ▸ \"Done — localhost:5432 reaches app01's Postgres; the daemon keeps it alive.\"\n```\n\nThe bundled [`skill/ssh-tunnel`](skill/ssh-tunnel/SKILL.md) (installed by `tunlite install\nskill`) tells an Agent exactly how: `--json`, branching on exit codes, and handling\n`needs-auth`.\n\n`tunlite monitor` gives you a live, top-style dashboard — every tunnel's state at a\nglance, with the daemon auto-reconnecting a dropped one in front of you:\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/yuanyuanzijin/tunlite/master/docs/monitor.gif\" alt=\"tunlite monitor — live dashboard with auto-reconnect and per-tunnel detail\" width=\"760\"\u003e\u003c/p\u003e\n\n## Why tunlite?\n\nIf you keep a few SSH tunnels running — a reverse tunnel to a homelab box, a SOCKS\nproxy through a bastion, a port-forward to a staging database — you've probably wired\nup `autossh` plus a `systemd`/`launchd` unit for each, and memorized which\n`-L`/`-R`/`-D` flag goes where. tunlite folds all of that into one declarative CLI on\ntop of the `ssh` you already trust: named tunnels a daemon keeps alive and the OS\nrestarts at boot — no new server, no account, no protocol. And because every command\nis `--json` with stable exit codes, an Agent drives the exact same surface you do.\n\n| | tunlite | autossh | plain `ssh -L/-R/-D` | sshuttle | frp · bore · chisel | ngrok |\n|---|:---:|:---:|:---:|:---:|:---:|:---:|\n| Agent-friendly (`--json`, stable exit codes) | ✅ | ❌ | ❌ | ❌ | ❌ | partial |\n| Wraps your system `ssh` (keys, jump hosts, `ssh_config`) | ✅ | ✅ | ✅ | partial | ❌ own protocol | ❌ own service |\n| Named, declarative tunnels | ✅ | ❌ | ❌ | ❌ | ✅ config | ✅ |\n| Auto-reconnect (backoff, keepalive, health) | ✅ | basic | ❌ | ❌ | ✅ | ✅ |\n| Start at login (launchd/systemd/Task Scheduler) | ✅ | DIY | DIY | DIY | DIY | ✅ |\n| Local **+** remote **+** dynamic SOCKS | ✅ | ✅ | ✅ | transparent proxy | varies | varies |\n| Zero deps · no server to run · self-hosted | ✅ | needs autossh | ✅ | needs python | needs a server | hosted/paid |\n\n## Install\n\nPrerequisite: **Node ≥ 18** and the system `ssh`, both on your PATH.\n\n```bash\n# Recommended — fetch + anchor (no global npm needed)\nnpx tunlite install\n\n# Or a curl one-liner (just curl/wget + tar + node)\ncurl -fsSL https://raw.githubusercontent.com/yuanyuanzijin/tunlite/master/bootstrap.sh | sh\n\n# Windows (PowerShell) — beta\nirm https://raw.githubusercontent.com/yuanyuanzijin/tunlite/master/bootstrap.ps1 | iex\n```\n\n`tunlite install` copies the runtime to a fixed directory and writes a launcher that\n**pins node's absolute path** (so switching nvm/fnm versions won't break it), then asks\nwhether to register login autostart, install the Agent skill, and enable shell\ncompletion. Pass `-y` to say yes to all without prompting (for scripts/CI); with no\n`-y` and no terminal it just anchors. To set up one piece on its own, use\n`tunlite install service` / `install skill` / `install completion`. It also writes a\nshort `tun` alias when that name is free. **Windows (autostart, launcher, PATH) is\nbeta** — macOS/Linux are the CI-tested platforms.\n\n## Quick start\n\n```bash\n# ssh-native forward flags (repeatable — one tunnel can carry several):\ntunlite add web   --to me@host -L 8080:localhost:80   # reach the server's :80 at localhost:8080\ntunlite add rev   --to me@host -R 9000:localhost:3000 # expose local 3000 as server:9000\ntunlite add socks --to me@host -D 1080                # SOCKS5 proxy (local 1080)\n\ntunlite status             # aligned table: NAME STATE HOST TYPE ROUTE PID UP RESTARTS\ntunlite logs web -f        # follow logs\ntunlite doctor             # health check: why a tunnel won't connect\n```\n\n\u003e **Upgrading from 0.9.x?** 0.10.0 leans into native `ssh`, so a couple of commands take a\n\u003e new shape — while your existing tunnel config keeps working, untouched.\n\u003e - **Forwards now speak ssh's own flags** — `add web --to me@host -L 8080:localhost:80 -D 1080`\n\u003e   (repeatable; `set \u003cname\u003e` edits them in place). The earlier `add local/remote/dynamic`\n\u003e   form gives way to this.\n\u003e - **Switching a tunnel on and off is now `enable` / `disable`** (it was `up`/`down`), each\n\u003e   naming what it acts on — a name, `--tag \u003clabel\u003e`, or `all`. A retired or mistyped verb\n\u003e   gently points you to the right one (`tunlite up` → \"did you mean `enable`?\").\n\u003e\n\u003e Run `tunlite update` (or `npx tunlite@latest install`), then `tunlite --version` to confirm.\n\nWhen the target isn't passwordless yet, running `tunlite enable \u003cname\u003e` in a terminal prompts for\nthe password once and installs your key. Or do it explicitly: `tunlite check user@server`\n(exit 0 = already passwordless) / `tunlite setup-key user@server`.\n\n**Autostart (optional):** `tunlite install service` registers the daemon to start at\nlogin (and restart on crash). It also starts everything right away, so it *replaces* `enable`\nwhen you want tunnels up persistently — you don't need both.\n\n## Update\n\n```sh\ntunlite update              # upgrade to the latest (restarts the daemon; tunnels blip ~1s)\ntunlite update v0.9.0       # install / roll back to a specific tag\ntunlite update --check      # compare current vs latest only; change nothing\n```\n\n`update` upgrades to the **latest release tag** — it fetches that tag's tarball from GitHub\nand re-anchors in place (**no npm, no git**), so `npx` installs the first copy and `update`\nkeeps it current at a real published version. It only self-updates an anchored install: from\na git checkout it points you to `git pull`, and from an `npm i -g` install to\n`npm i -g tunlite@latest` (so that channel's version stays authoritative).\n\n## Commands\n\n```\nadd \u003cname\u003e -L/-R/-D …      define a tunnel        set / rm / rename     edit / delete / rename\nlist [--tag T]             list tunnels           run --to … -L/-R/-D …   daemon-less foreground tunnel\nenable / disable / restart control (name|--tag|all)\nstatus / logs / monitor    inspect (table · follow · live dashboard)\ndoctor                     why a tunnel won't connect\ncheck / setup-key          probe / install passwordless access\nwebhook …                  drop alerts to a webhook (generic · WeCom)\nexport / import            back up / merge tunnels\ninstall [service|skill|completion] / uninstall      anchor runtime · autostart · Agent skill · Tab-completion\nupdate                     self-update from GitHub\n```\n\nRun `tunlite help` or any command with `--help` for full flags, or see the\n[documentation](https://tunlite.dev/) for jump hosts (`--jump`),\ntags (`--tag`), the webhook channels/events, and shell completion.\n\n**Forwarding model:** forwards use the standard ssh flags, and they're repeatable — one\ntunnel can carry several:\n- `-L [bind:]PORT:HOST:HOSTPORT` — **local forward**: reach a remote service on your machine.\n- `-R [bind:]PORT:HOST:HOSTPORT` — **remote forward**: expose a local service on the server.\n- `-D [bind:]PORT` — **dynamic**: a local SOCKS5 proxy.\n\nThe optional `bind:` prefix is the listen address — default loopback; use `0.0.0.0` to\nexpose the listener to your LAN. Bracket IPv6 addresses (`[::1]`). The SSH port goes on the\ntarget (`--to user@host:2222`, default 22). Editing a tunnel's forwards is `set \u003cname\u003e`:\npassing any `-L/-R/-D` **replaces the whole forward set** (`set` is the sole forward editor).\n\n**Exit codes** (add `--json` to any command): `0` ok · `2` usage · `3` not found ·\n`4` needs key · `5` can't reach daemon · `1` other.\n\n## How it works\n\nThree roles, each with one job:\n\n| Role | What it is | Job |\n|---|---|---|\n| **CLI** (`tunlite …`) | the commands you type | Edit `config.json`, talk to the daemon, run one-shot ssh. Exits when done. |\n| **daemon** (`tunlite daemon run`) | a background process | Keeps tunnels connected, reconnects on drop, serves status/logs. |\n| **service** (`install service`) | a launchd/systemd/Task Scheduler entry | Keeps the **daemon** alive — starts it at boot, restarts on crash. |\n\n`config.json` is the single source of truth. The OS service keeps the daemon alive, the\ndaemon keeps every tunnel alive. Day to day you only need `add` → `enable` → `status`/`logs`,\nplus `install service` once for autostart.\n\n## Daemon-less: `run`\n\nFor containers and `systemd` entrypoints where a background daemon doesn't fit, `run`\nsupervises one tunnel in the **foreground** (auto-reconnect, keepalive) and stays attached\nuntil you stop it — no daemon, no `config.json` entry:\n\n```sh\ntunlite run --to me@host -L 8080:localhost:80\ntunlite run --to me@host -R 9000:localhost:3000 --name rev --json --exit-on-failure\n```\n\n`--name` labels the tunnel for status lines (defaults to the target host). `--json` emits NDJSON state lines on\nstdout (one JSON object per state change). `--exit-on-failure` exits non-zero instead of\nretrying — `needs-auth` → `4`, `blocked`/`failed` → `1` — so a supervisor restarts it.\n\n## Versioning \u0026 license\n\nSemVer (`vMAJOR.MINOR.PATCH`); release notes in [`CHANGELOG.md`](https://github.com/yuanyuanzijin/tunlite/blob/master/CHANGELOG.md).\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyuanyuanzijin%2Ftunlite","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyuanyuanzijin%2Ftunlite","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyuanyuanzijin%2Ftunlite/lists"}