feat(install): auto-detect agents, route MCP auth to QR mode for remote installs (#18)

## Summary

Two end-to-end improvements to `install.sh` / `install.ps1`, mirrored
across both:

### 1. Agent auto-detection

Probe well-known config dirs and binaries for ~18 of
[vercel-labs/skills](https://github.com/vercel-labs/skills)' 45
supported agents (`claude-code`, `cursor`, `codex`, `gemini-cli`,
`opencode`, `openclaw`, `qwen-code`, `iflow-cli`, `windsurf`, `warp`,
`amp`, `crush`, `goose`, `droid`, `kode`, `kilo`, `kimi-cli`,
`kiro-cli`).

When detection finds anything, the `skills add` step gets `-a id1,id2,…`
instead of dumping the user into the multi-select. New flags:

- `--list-agents` — preview what we'd auto-select (and exit)
- `--all-agents` — skip our detection, let `skills` CLI scan everything
- `--only` — manual override (unchanged)

### 2. Local-vs-remote MCP auth routing

The current `--auth-login` always auto-opens a browser. On SSH sessions,
Docker containers, and OpenClaw remote channels (where the user is on a
phone), that silently launches a browser they can't see — leaving them
stuck on "Waiting for authorization…".

Detection (any signal fires ⇒ remote):

| Signal | Notes |
|---|---|
| `$HOME/.openclaw` exists | OpenClaw runtime — most reliable single
signal |
| `$SSH_CONNECTION` / `$SSH_TTY` | Generic SSH session |
| Linux without `$DISPLAY` / `$WAYLAND_DISPLAY` | Headless |

When remote, pass `--no-browser` to the MCP CLI, which prints URL + ANSI
QR for the user to scan with a phone.

New flags:

- `--remote` / `--local` — force either mode
- `--force-mcp` — re-auth even if AgentKey is already configured

Also adds an idempotency short-circuit: if any known MCP config already
has an `agentkey` block with a valid-looking API key, skip the auth step
entirely (`--force-mcp` to override).

### 3. Docs

`README.md` + `docs/README_zh.md` updated to document the new flags and
add a dedicated **"Installing over SSH / Docker / OpenClaw"** section.

## Companion PRs (server side)

The MCP-server changes that this installer routes to ship in a chain of
three PRs against `chainbase-labs/AgentKey-Server`:

| PR | Status | What it adds |
|---|---|---|
| [#2](https://github.com/chainbase-labs/AgentKey-Server/pull/2) |
merged | `--no-browser` / `--qr` / `--no-qr` flags + `qrcode-terminal`
dep (v0.3.5 source bump) |
| [#3](https://github.com/chainbase-labs/AgentKey-Server/pull/3) |
merged | Bump to 0.3.6 — npm `0.3.5` had been published from a pre-merge
commit and shipped stale `dist/` |
| [#4](https://github.com/chainbase-labs/AgentKey-Server/pull/4) | open
| Fix QR not rendering in `--no-browser` (CJS interop bug —
`qrcode-terminal`'s `generate` lives on `.default` under NodeNext); bump
to 0.3.7 |

**This installer needs `@agentkey/mcp@0.3.7`** to be on npm for the QR
flow to actually work. Sequence:

1. Merge #4 ✅
2. Maintainer runs `cd mcp-server && npm publish` (no npm-publish CI
workflow) ✅
3. Merge this PR — `npx -y @agentkey/mcp` will then pull 0.3.7+ and the
`--remote` path renders the QR

This installer change is **forward-compatible**: it can land anytime —
older `@agentkey/mcp` versions silently ignore the unknown
`--no-browser` flag, so worst case a remote user gets the old
browser-opens-on-the-wrong-host UX until the new mcp publishes.

## Test plan

Verified end-to-end on macOS against a local build of
`@agentkey/mcp@0.3.7` (server PR #4):

- [x] Bash syntax check passes (`bash -n install.sh`)
- [x] `install.sh --help` renders all 5 new flag rows
- [x] `install.sh --list-agents` correctly prints detected agents on
test host (10 agents incl. `openclaw`)
- [x] `install.sh --remote --local` exits with code 1 (mutex)
- [x] `install.sh --skip-skill` short-circuits with "AgentKey is already
configured…" when configs have agentkey
- [x] `install.sh --remote --skip-skill --force-mcp` end-to-end: prints
`Detected remote install context — printing QR + URL instead of opening
a browser here`, then `reason: $HOME/.openclaw exists (OpenClaw
runtime)`, launches mcp CLI, renders 16-row ANSI QR, polls — no `open`
process spawned
- [x] All 4 mcp 0.3.7 flag combinations behave correctly (`--no-browser`
/ `--no-browser --no-qr` / `--qr` / no-flags back-compat)
- [ ] `install.ps1` not lint-checked locally (no pwsh on test host);
logic is a strict mirror of bash — needs Windows smoke before merge
- [ ] Inside a real OpenClaw container — confirm `~/.openclaw` triggers
as expected

## Why now

Users running the installer via OpenClaw / Claude Code remote channels
(on a phone) currently see a black-hole UX: the browser opens on the
wrong machine and the install process appears to hang. The detection
here removes the manual `--remote` flag for the common case.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
不白
2026-04-27 21:55:57 +08:00
committed by GitHub
parent 400cf97578
commit 29176d1aae
4 changed files with 466 additions and 39 deletions
+39 -3
View File
@@ -218,21 +218,32 @@ Still stuck? See the "Where do I get help" item below.
<details>
<summary><b>Advanced install options (CI / specific agents / manual two-step)</b></summary>
The installer auto-detects which AI agents you have on this machine (by probing well-known config dirs and binaries from the [vercel-labs/skills supported-agents list](https://github.com/vercel-labs/skills)) and pre-selects them — no multi-select prompt. Override with the flags below.
**Installer flags:**
```bash
# Non-interactive (CI / unattended): install to every detected agent, no prompts
curl -fsSL https://agentkey.app/install.sh | bash -s -- --yes
# Only install the skill for specific agents
# See which agents the installer would auto-select on this host (and exit)
curl -fsSL https://agentkey.app/install.sh | bash -s -- --list-agents
# Only install the skill for specific agents (overrides auto-detection)
curl -fsSL https://agentkey.app/install.sh | bash -s -- --only claude-code,cursor
# Skip our agent detection; let `skills` CLI install for every agent it finds
curl -fsSL https://agentkey.app/install.sh | bash -s -- --all-agents
# Only the skill, or only the MCP auth
curl -fsSL https://agentkey.app/install.sh | bash -s -- --skip-mcp
curl -fsSL https://agentkey.app/install.sh | bash -s -- --skip-skill
# Re-authenticate even if AgentKey is already configured locally
curl -fsSL https://agentkey.app/install.sh | bash -s -- --force-mcp
```
PowerShell equivalents: `-Yes`, `-Only`, `-SkipMcp`, `-SkipSkill`.
PowerShell equivalents: `-Yes`, `-ListAgents`, `-Only`, `-AllAgents`, `-SkipMcp`, `-SkipSkill`, `-ForceMcp`.
**Manual two-step install** (if you'd rather run the two underlying commands yourself, or the one-line installer can't reach your machine):
@@ -244,7 +255,32 @@ npx skills add chainbase-labs/agentkey
npx -y @agentkey/mcp --auth-login
```
Over SSH or any shell where a browser can't open, use `npx -y @agentkey/mcp --setup` — an interactive wizard asks for the key and lets you pick which MCP clients to write to.
</details>
<details>
<summary><b>Installing over SSH, inside Docker, or via OpenClaw / Claude Code remote channels</b></summary>
When the installer runs on a machine you can't see (an SSH server, a Docker container, an OpenClaw runtime triggered from your phone), the default `--auth-login` would silently spawn a browser on the *remote* host — invisible to you.
The installer detects this automatically and switches to a **scan-from-phone** flow: it prints the auth URL plus a terminal QR code, and skips the browser auto-open. Detection signals (any one fires):
- `~/.openclaw/` exists (OpenClaw runtime)
- `$SSH_CONNECTION` / `$SSH_TTY` set
- Linux without `$DISPLAY` / `$WAYLAND_DISPLAY`
Force the mode either way:
```bash
# Force remote mode (URL + QR, no browser)
curl -fsSL https://agentkey.app/install.sh | bash -s -- --remote
# Force local mode (auto-open browser, ignore heuristics)
curl -fsSL https://agentkey.app/install.sh | bash -s -- --local
```
PowerShell: `-Remote` / `-Local`.
If you'd rather skip the URL/QR flow entirely and type a key manually, `npx -y @agentkey/mcp --setup` opens an interactive wizard that asks for the key and lets you pick which MCP clients to write to.
</details>
+39 -3
View File
@@ -218,21 +218,32 @@ npx skills remove chainbase-labs/agentkey
<details>
<summary><b>进阶安装(CI / 指定 Agent / 手动两步)</b></summary>
安装器会自动探测本机已安装的 AI Agent(依据 [vercel-labs/skills 支持列表](https://github.com/vercel-labs/skills) 比对配置目录和命令行工具),自动选中它们 —— 不再弹多选框。需要覆盖时用下面的旗标:
**安装器参数:**
```bash
# 非交互模式(CI / 无人值守):安装到所有检测到的 Agent,不询问
curl -fsSL https://agentkey.app/install.sh | bash -s -- --yes
# 只安装到指定的 Agent
# 看一下安装器在本机会自动选中哪些 Agent(看完即退出)
curl -fsSL https://agentkey.app/install.sh | bash -s -- --list-agents
# 只安装到指定的 Agent(覆盖自动检测结果)
curl -fsSL https://agentkey.app/install.sh | bash -s -- --only claude-code,cursor
# 跳过我们的检测,让 skills CLI 自己识别全部 Agent
curl -fsSL https://agentkey.app/install.sh | bash -s -- --all-agents
# 只装 Skill 或只做 MCP 授权
curl -fsSL https://agentkey.app/install.sh | bash -s -- --skip-mcp
curl -fsSL https://agentkey.app/install.sh | bash -s -- --skip-skill
# 即使本机已经配置过 AgentKey 也强制重新走一次授权
curl -fsSL https://agentkey.app/install.sh | bash -s -- --force-mcp
```
PowerShell 对应参数:`-Yes`、`-Only`、`-SkipMcp`、`-SkipSkill`。
PowerShell 对应参数:`-Yes`、`-ListAgents`、`-Only`、`-AllAgents`、`-SkipMcp`、`-SkipSkill`、`-ForceMcp`。
**手动两步安装**(想自己跑两条底层命令,或一键脚本在你的环境里跑不起来):
@@ -244,7 +255,32 @@ npx skills add chainbase-labs/agentkey
npx -y @agentkey/mcp --auth-login
```
在 SSH 远程或无法弹浏览器的终端里,用 `npx -y @agentkey/mcp --setup` —— 交互式向导,问你要 Key 并让你勾选要写入的 MCP 客户端。
</details>
<details>
<summary><b>在 SSH / Docker / OpenClaw 远程通道里安装</b></summary>
如果安装命令是在你看不到屏幕的机器上跑(远程 SSH 服务器、Docker 容器、由手机触发的 OpenClaw 运行时),默认 `--auth-login` 会在远端"成功"打开一个你看不见的浏览器,然后你只能盯着 "Waiting for authorization..." 卡死。
安装器会自动识别这种场景,切换到"扫码授权"流程:终端里直接打印授权 URL 加二维码,不再尝试本地开浏览器。**触发条件**(任一命中即判定为远程):
- `~/.openclaw/` 目录存在(OpenClaw 运行时)
- `$SSH_CONNECTION` / `$SSH_TTY` 已设置
- Linux 且无 `$DISPLAY` / `$WAYLAND_DISPLAY`
需要强制其中一种模式:
```bash
# 强制远程模式(URL + 二维码,不开浏览器)
curl -fsSL https://agentkey.app/install.sh | bash -s -- --remote
# 强制本地模式(自动开浏览器,无视启发式判断)
curl -fsSL https://agentkey.app/install.sh | bash -s -- --local
```
PowerShell:`-Remote` / `-Local`。
如果完全不想走 URL/二维码流程、想自己手动粘 Key,可以用 `npx -y @agentkey/mcp --setup` —— 交互式向导,问你要 Key 并让你勾选要写入的 MCP 客户端。
</details>
+168 -17
View File
@@ -9,9 +9,11 @@
& ([scriptblock]::Create((irm https://agentkey.app/install.ps1))) -Only "claude-code,cursor"
Behavior mirrors install.sh: checks Node >= 18 (installs via winget/scoop/choco),
runs `npx skills add` (auto-detects agents), then `npx @agentkey/mcp --auth-login`
to open a browser for device auth. MCP config is written automatically for
Claude Code / Claude Desktop / Cursor.
auto-detects which AI agents are installed and runs `npx skills add` for them,
then `npx @agentkey/mcp --auth-login` for device auth. The auth step opens a
local browser by default; under SSH / Docker / OpenClaw it switches to a
QR + URL flow that the user scans on a phone (`--no-browser` server-side flag).
MCP config is written automatically for Claude Code / Claude Desktop / Cursor.
#>
[CmdletBinding()]
@@ -19,6 +21,11 @@ param(
[switch]$Yes,
[switch]$Interactive,
[string]$Only,
[switch]$AllAgents,
[switch]$ListAgents,
[switch]$Remote,
[switch]$Local,
[switch]$ForceMcp,
[switch]$SkipSkill,
[switch]$SkipMcp,
[switch]$Help
@@ -29,6 +36,31 @@ $SkillRepo = 'chainbase-labs/agentkey'
$McpPackage = '@agentkey/mcp'
$NodeMinMajor = 18
# ── Agent markers (mirror of install.sh) ──────────────────────────────────
# Subset of vercel-labs/skills' 45 supported agent IDs that have reliable
# Windows-side markers. Sync source:
# https://github.com/vercel-labs/skills (Supported Agents table).
$AgentMarkers = @(
@{ Id = 'claude-code'; Markers = @("path:$env:USERPROFILE\.claude.json", 'cmd:claude', "path:$env:APPDATA\Claude") }
@{ Id = 'cursor'; Markers = @("path:$env:USERPROFILE\.cursor", 'cmd:cursor', "path:$env:LOCALAPPDATA\Programs\cursor") }
@{ Id = 'codex'; Markers = @("path:$env:USERPROFILE\.codex", 'cmd:codex') }
@{ Id = 'gemini-cli'; Markers = @("path:$env:USERPROFILE\.gemini", 'cmd:gemini') }
@{ Id = 'opencode'; Markers = @("path:$env:USERPROFILE\.opencode", 'cmd:opencode') }
@{ Id = 'openclaw'; Markers = @("path:$env:USERPROFILE\.openclaw") }
@{ Id = 'qwen-code'; Markers = @("path:$env:USERPROFILE\.qwen", 'cmd:qwen') }
@{ Id = 'iflow-cli'; Markers = @("path:$env:USERPROFILE\.iflow", 'cmd:iflow') }
@{ Id = 'windsurf'; Markers = @("path:$env:USERPROFILE\.windsurf", 'cmd:windsurf') }
@{ Id = 'warp'; Markers = @("path:$env:USERPROFILE\.warp") }
@{ Id = 'amp'; Markers = @('cmd:amp') }
@{ Id = 'crush'; Markers = @('cmd:crush') }
@{ Id = 'goose'; Markers = @('cmd:goose') }
@{ Id = 'droid'; Markers = @('cmd:droid') }
@{ Id = 'kode'; Markers = @('cmd:kode') }
@{ Id = 'kilo'; Markers = @('cmd:kilo') }
@{ Id = 'kimi-cli'; Markers = @("path:$env:USERPROFILE\.kimi", 'cmd:kimi') }
@{ Id = 'kiro-cli'; Markers = @("path:$env:USERPROFILE\.kiro", 'cmd:kiro') }
)
# ── UI helpers ────────────────────────────────────────────────────────────
function Write-Banner {
Write-Host ''
@@ -52,6 +84,58 @@ function Write-Muted($text) { Write-Host " $text" -ForegroundColor DarkGray }
function Die ($text) { Write-Err $text; exit 1 }
# ── Helpers: agent + remote detection ─────────────────────────────────────
function Test-AgentMarker {
param([string]$Marker)
if ($Marker.StartsWith('cmd:')) {
return [bool](Get-Command $Marker.Substring(4) -ErrorAction SilentlyContinue)
}
if ($Marker.StartsWith('path:')) {
return Test-Path -LiteralPath $Marker.Substring(5)
}
return $false
}
function Get-DetectedAgents {
$hits = New-Object System.Collections.Generic.List[string]
foreach ($entry in $AgentMarkers) {
foreach ($m in $entry.Markers) {
if (Test-AgentMarker $m) { $hits.Add($entry.Id) | Out-Null; break }
}
}
return @($hits | Sort-Object -Unique)
}
# Detect "remote install" — context where opening a browser on this host
# is futile (SSH, Docker, OpenClaw remote channels). Mirrors the bash
# script's logic.
function Test-RemoteInstall {
if ($script:Local) { return $false }
if ($script:Remote) { return $true }
if (Test-Path -LiteralPath "$env:USERPROFILE\.openclaw") { return $true }
if ($env:SSH_CONNECTION -or $env:SSH_TTY) { return $true }
return $false
}
# Cheap "is AgentKey already configured?" check across known MCP config files.
function Test-AlreadyAuthed {
$configs = @(
"$env:USERPROFILE\.claude.json",
"$env:USERPROFILE\.cursor\mcp.json",
"$env:APPDATA\Claude\claude_desktop_config.json"
)
foreach ($cfg in $configs) {
if (-not (Test-Path -LiteralPath $cfg)) { continue }
$content = Get-Content -Raw -LiteralPath $cfg -ErrorAction SilentlyContinue
if (-not $content) { continue }
if ($content -match '"agentkey"' -and $content -match '"AGENTKEY_API_KEY"\s*:\s*"ak_[A-Za-z0-9_-]+"') {
return $true
}
}
return $false
}
# ── Help ──────────────────────────────────────────────────────────────────
if ($Help) {
@'
@@ -65,13 +149,38 @@ Parameters:
-Yes Non-interactive: install skill to every detected agent, no prompts
-Interactive Force interactive mode (fails if console input is redirected)
-Only <a,b,c> Only install skill for these agents (e.g. "claude-code,cursor")
-AllAgents Skip auto-detection; let 'skills' CLI install for every detected agent
-ListAgents Print the agents we'd auto-select on this machine and exit
-Remote Force remote-install mode: print URL + QR for the auth step,
do NOT auto-open a local browser. Use this when running over
SSH, in WinRM, in a container, or via OpenClaw / Claude Code
remote channels.
-Local Force local mode (auto-open browser) and bypass remote heuristics
-ForceMcp Re-run MCP auth even if AgentKey is already configured
-SkipSkill Skip the skill install step (only run MCP auth)
-SkipMcp Skip the MCP auth step (only install the skill)
-Help Show this help
Behavior:
The installer auto-detects which AI agents are on this machine and
pre-selects them for skill installation. Remote-install mode is auto-
detected from %USERPROFILE%\.openclaw and SSH env vars; override with
-Remote / -Local.
'@
exit 0
}
if ($Remote -and $Local) {
Die '-Remote and -Local are mutually exclusive.'
}
if ($ListAgents) {
$detected = Get-DetectedAgents
if ($detected.Count -gt 0) { $detected -join "`n" | Write-Output }
else { Write-Host 'no agents detected on this host' -ForegroundColor Yellow }
exit 0
}
Write-Banner
# ── 1. Preflight ──────────────────────────────────────────────────────────
@@ -151,15 +260,38 @@ if (-not (Get-Command npx -ErrorAction SilentlyContinue)) {
# ── 2. Install the AgentKey skill ─────────────────────────────────────────
if (-not $SkipSkill) {
Write-Step '2. Install the AgentKey skill'
Write-Info "The 'skills' CLI will auto-detect every supported agent on this machine."
# Resolve target agent list:
# 1. -Only wins (manual override)
# 2. else -AllAgents ⇒ no -a (let skills CLI auto-detect everything)
# 3. else our auto-detection ⇒ -a <detected list>
# 4. else (nothing detected) ⇒ no -a (fall back to skills CLI default)
$targets = @()
if ($Only) {
$targets = $Only -split ',' | Where-Object { $_ -ne '' }
Write-Info "Targeting agents from -Only: $($targets -join ', ')"
} elseif ($AllAgents) {
Write-Info "Installing for every agent the 'skills' CLI detects (-AllAgents)"
} else {
$targets = Get-DetectedAgents
if ($targets.Count -gt 0) {
Write-Ok "Detected agents on this host: $($targets -join ', ')"
Write-Muted '(override with -Only <ids>, or use -AllAgents)'
} else {
Write-Info "No agents auto-detected — letting 'skills' CLI scan."
}
}
$skillsArgs = @('-y', 'skills', 'add', $SkillRepo, '-g')
if ($Only) {
$agentList = $Only -split ',' | Where-Object { $_ -ne '' }
if ($targets.Count -gt 0) {
$skillsArgs += '-a'
$skillsArgs += $agentList
$skillsArgs += $targets
}
# Always pass -y in noninteractive mode AND when we already resolved
# an explicit target list — there's nothing left to ask the user.
if ($Mode -eq 'noninteractive' -or $targets.Count -gt 0) {
$skillsArgs += '-y'
}
if ($Mode -eq 'noninteractive') { $skillsArgs += '-y' }
& npx @skillsArgs
if ($LASTEXITCODE -ne 0) { Die "Failed to install skill via 'skills' CLI" }
@@ -170,22 +302,41 @@ if (-not $SkipSkill) {
}
# ── 3. MCP authentication ────────────────────────────────────────────────
if (-not $SkipMcp) {
Write-Step '3. Register the MCP server (browser login)'
Write-Info 'Opening your browser for AgentKey device authentication ...'
Write-Muted 'When auth finishes, the MCP server is written into Claude Code / Claude Desktop / Cursor configs.'
if ($SkipMcp) {
Write-Step '3. Register the MCP server'
Write-Muted 'Skipped (-SkipMcp)'
} elseif ((Test-AlreadyAuthed) -and -not $ForceMcp) {
Write-Step '3. Register the MCP server'
Write-Ok 'AgentKey is already configured in an MCP client config — skipping auth.'
Write-Muted 'Re-run with -ForceMcp to authenticate again.'
} else {
$isRemote = Test-RemoteInstall
$authArgs = @('--auth-login')
if ($isRemote) {
Write-Step '3. Register the MCP server (remote auth: scan QR with phone)'
Write-Info 'Detected remote install context — printing QR + URL instead of opening a browser here.'
if (Test-Path -LiteralPath "$env:USERPROFILE\.openclaw") {
Write-Muted ' reason: %USERPROFILE%\.openclaw exists (OpenClaw runtime)'
} elseif ($env:SSH_CONNECTION -or $env:SSH_TTY) {
Write-Muted ' reason: SSH session detected'
}
Write-Muted 'Override with -Local if you want a browser opened on this machine instead.'
$authArgs += '--no-browser'
} else {
Write-Step '3. Register the MCP server (browser login)'
Write-Info 'Opening your browser for AgentKey device authentication ...'
Write-Muted 'When auth finishes, the MCP server is written into Claude Code / Claude Desktop / Cursor configs.'
}
Write-Host ''
& npx -y $McpPackage --auth-login
& npx -y $McpPackage @authArgs
if ($LASTEXITCODE -ne 0) {
Write-Err 'MCP auth failed.'
Write-Muted "Retry manually: npx -y $McpPackage --auth-login"
Write-Muted "Retry manually: npx -y $McpPackage $($authArgs -join ' ')"
exit 1
}
Write-Ok 'MCP server registered'
} else {
Write-Step '3. Register the MCP server'
Write-Muted 'Skipped (-SkipMcp)'
}
# ── 4. Summary ───────────────────────────────────────────────────────────
+220 -16
View File
@@ -5,6 +5,7 @@
# curl -fsSL https://agentkey.app/install.sh | bash -s -- --yes
# curl -fsSL https://agentkey.app/install.sh | bash -s -- --interactive
# curl -fsSL https://agentkey.app/install.sh | bash -s -- --only claude-code,cursor
# curl -fsSL https://agentkey.app/install.sh | bash -s -- --remote
# curl -fsSL https://agentkey.app/install.sh | bash -s -- --skip-mcp
#
# The whole procedural body is wrapped in `main()` so that under `curl | bash`
@@ -20,6 +21,37 @@ SKILL_REPO="chainbase-labs/agentkey"
MCP_PACKAGE="@agentkey/mcp"
NODE_MIN_MAJOR=18
# ── Agent markers ─────────────────────────────────────────────────────────
# Subset of vercel-labs/skills' 45 supported agent IDs that have reliable
# on-disk markers (config dirs / binaries on PATH). Agents we can't probe
# cleanly (mostly VS Code extensions like cline/continue/roo) just don't get
# pre-detected — the user can pass --all-agents or --only to include them.
# Sync source: https://github.com/vercel-labs/skills (Supported Agents table).
#
# Format: <agent-id>|<marker>[,<marker>...]
# marker types: cmd:foo — `command -v foo`
# path:/abs/or/~path — file or dir exists (~ expands to $HOME)
AGENT_MARKERS=(
"claude-code|path:~/.claude.json,cmd:claude,path:~/Library/Application Support/Claude,path:~/.config/Claude"
"cursor|path:~/.cursor,cmd:cursor"
"codex|path:~/.codex,cmd:codex"
"gemini-cli|path:~/.gemini,cmd:gemini"
"opencode|path:~/.opencode,cmd:opencode"
"openclaw|path:~/.openclaw"
"qwen-code|path:~/.qwen,cmd:qwen"
"iflow-cli|path:~/.iflow,cmd:iflow"
"windsurf|path:~/.windsurf,cmd:windsurf"
"warp|path:~/.warp,path:~/Library/Application Support/dev.warp.Warp-Stable"
"amp|cmd:amp"
"crush|cmd:crush"
"goose|cmd:goose"
"droid|cmd:droid"
"kode|cmd:kode"
"kilo|cmd:kilo"
"kimi-cli|path:~/.kimi,cmd:kimi"
"kiro-cli|path:~/.kiro,cmd:kiro"
)
# ── Colors (only if stdout is a TTY) ─────────────────────────────────────
# Use $'...' so variables hold real ESC bytes — otherwise heredoc output prints
# the literal string "\033[1m" instead of applying the SGR code.
@@ -70,15 +102,112 @@ Options:
--yes, -y Non-interactive: install skill to every detected agent, no prompts
--interactive Force interactive mode (fails if no TTY/terminal is reachable)
--only <a,b,c> Only install skill for these agents (comma-separated, e.g. claude-code,cursor)
--all-agents Skip auto-detection; let 'skills' CLI install for every detected agent
--list-agents Print the agents we'd auto-select on this machine and exit
--remote Force remote-install mode: print URL + QR for the auth step,
do NOT auto-open a local browser. Use this when running over
SSH, in Docker, or via OpenClaw / Claude Code remote channels.
--local Force local mode (auto-open browser) and bypass remote heuristics
--force-mcp Re-run MCP auth even if AgentKey is already configured
--skip-skill Skip the skill install step (only run MCP auth)
--skip-mcp Skip the MCP auth step (only install the skill)
-h, --help Show this help
Default: interactive when a terminal is reachable (even under 'curl | bash'),
otherwise falls back to --yes.
Behavior:
Interactive mode is the default when a terminal is reachable; otherwise it
falls back to --yes. The installer auto-detects which AI agents are on this
machine and pre-selects them for skill installation. Remote-install mode is
auto-detected from \$HOME/.openclaw, SSH env vars, and missing \$DISPLAY;
override with --remote / --local.
EOF
}
# ── Helpers: agent + remote detection ─────────────────────────────────────
# Expand a leading "~" to \$HOME (no glob expansion, no eval).
_expand_path() {
local p="$1"
case "$p" in
"~"|"~/"*) printf '%s\n' "$HOME${p#"~"}" ;;
*) printf '%s\n' "$p" ;;
esac
}
# Probe a single marker: cmd:NAME (binary on PATH) or path:PATH (file/dir).
_probe_marker() {
local m="$1"
case "$m" in
cmd:*) command -v "${m#cmd:}" >/dev/null 2>&1 ;;
path:*) [ -e "$(_expand_path "${m#path:}")" ] ;;
*) return 1 ;;
esac
}
# Print detected agent IDs as a comma-separated list (empty if none).
detect_agents() {
local entry id markers marker hits=()
for entry in "${AGENT_MARKERS[@]}"; do
id="${entry%%|*}"
markers="${entry#*|}"
# Any marker hit ⇒ agent detected.
IFS=',' read -ra marker_list <<<"$markers"
for marker in "${marker_list[@]}"; do
if _probe_marker "$marker"; then
hits+=("$id")
break
fi
done
done
if [ ${#hits[@]} -gt 0 ]; then
printf '%s\n' "${hits[@]}" | sort -u | paste -sd, -
fi
}
# Detect "remote install" — a context where auto-opening a browser on this
# host is futile because the user isn't sitting in front of it. Mutates
# nothing; returns 0 (true) for remote, 1 (false) for local.
detect_remote() {
[ "$FORCE_LOCAL" = true ] && return 1
[ "$FORCE_REMOTE" = true ] && return 0
# OpenClaw runtime — the project owner confirms ~/.openclaw exists in
# any host where OpenClaw is installed/active. Most reliable single
# signal because it doesn't depend on env-var inheritance through the
# docker → channel → shell chain.
[ -d "$HOME/.openclaw" ] && return 0
# Generic SSH session.
[ -n "${SSH_CONNECTION:-}" ] && return 0
[ -n "${SSH_TTY:-}" ] && return 0
# Headless Linux (no GUI session).
if [ "$(uname -s)" = "Linux" ] \
&& [ -z "${DISPLAY:-}" ] \
&& [ -z "${WAYLAND_DISPLAY:-}" ]; then
return 0
fi
return 1
}
# Cheap "is AgentKey already configured?" check across known MCP config files.
# Greps for an agentkey block + a non-empty AGENTKEY_API_KEY of the expected
# shape. Returns 0 if at least one config has both.
already_authed() {
local cfg
for cfg in "$HOME/.claude.json" \
"$HOME/.cursor/mcp.json" \
"$HOME/Library/Application Support/Claude/claude_desktop_config.json" \
"$HOME/.config/Claude/claude_desktop_config.json"; do
[ -f "$cfg" ] || continue
if grep -q '"agentkey"' "$cfg" 2>/dev/null \
&& grep -qE '"AGENTKEY_API_KEY"[^"]*"ak_[A-Za-z0-9_-]+"' "$cfg" 2>/dev/null; then
return 0
fi
done
return 1
}
install_node() {
local platform="$1"
ui_info "Installing Node.js v$NODE_MIN_MAJOR+ ..."
@@ -118,6 +247,15 @@ main() {
local SKIP_MCP=false
local SKIP_SKILL=false
local PRINT_HELP=false
local LIST_AGENTS=false
local ALL_AGENTS=false
local FORCE_MCP=false
# FORCE_REMOTE / FORCE_LOCAL are read by detect_remote(). Declared as
# plain (non-`local`) so the helpers see them — they're dynamic-scope
# accessible either way in bash, but explicit assignment here keeps
# `set -u` happy.
FORCE_REMOTE=false
FORCE_LOCAL=false
while [ $# -gt 0 ]; do
case "$1" in
@@ -125,6 +263,11 @@ main() {
--interactive) MODE=interactive; shift ;;
--only) ONLY_AGENTS="${2:-}"; shift 2 ;;
--only=*) ONLY_AGENTS="${1#*=}"; shift ;;
--all-agents) ALL_AGENTS=true; shift ;;
--list-agents) LIST_AGENTS=true; shift ;;
--remote) FORCE_REMOTE=true; shift ;;
--local) FORCE_LOCAL=true; shift ;;
--force-mcp) FORCE_MCP=true; shift ;;
--skip-skill) SKIP_SKILL=true; shift ;;
--skip-mcp) SKIP_MCP=true; shift ;;
-h|--help) PRINT_HELP=true; shift ;;
@@ -133,6 +276,20 @@ main() {
done
if $PRINT_HELP; then print_help; exit 0; fi
if $FORCE_REMOTE && $FORCE_LOCAL; then
die "--remote and --local are mutually exclusive"
fi
if $LIST_AGENTS; then
local detected
detected="$(detect_agents)"
if [ -n "$detected" ]; then
printf '%s\n' "$detected" | tr ',' '\n'
else
printf 'no agents detected on this host\n' >&2
fi
exit 0
fi
ui_banner
@@ -207,15 +364,39 @@ main() {
# ── 2. Install the AgentKey skill ─────────────────────────────────────
if ! $SKIP_SKILL; then
ui_step "2. Install the AgentKey skill"
ui_info "The 'skills' CLI will auto-detect every supported agent on this machine."
# Resolve target agent list:
# 1. --only wins (manual override)
# 2. else --all-agents ⇒ no -a (let skills CLI auto-detect everything)
# 3. else our auto-detection ⇒ -a <detected list>
# 4. else (nothing detected) ⇒ no -a (fall back to skills CLI default)
local TARGETS=""
if [ -n "$ONLY_AGENTS" ]; then
TARGETS="$ONLY_AGENTS"
ui_info "Targeting agents from --only: $TARGETS"
elif $ALL_AGENTS; then
ui_info "Installing for every agent the 'skills' CLI detects (--all-agents)"
else
TARGETS="$(detect_agents)"
if [ -n "$TARGETS" ]; then
ui_ok "Detected agents on this host: $TARGETS"
ui_muted "(override with --only <ids>, or use --all-agents)"
else
ui_info "No agents auto-detected — letting 'skills' CLI scan."
fi
fi
local SKILLS_ARGS=(-y skills add "$SKILL_REPO" -g)
if [ -n "$ONLY_AGENTS" ]; then
# shellcheck disable=SC2206
local AGENT_LIST=(${ONLY_AGENTS//,/ })
if [ -n "$TARGETS" ]; then
# `skills` CLI accepts -a as either repeated or comma-separated.
# We pass each ID individually for maximum compatibility.
local AGENT_LIST=()
IFS=',' read -ra AGENT_LIST <<<"$TARGETS"
SKILLS_ARGS+=(-a "${AGENT_LIST[@]}")
fi
if [ "$MODE" = noninteractive ]; then
# Always pass -y in noninteractive mode AND when we already resolved
# an explicit target list — there's nothing left to ask the user.
if [ "$MODE" = noninteractive ] || [ -n "$TARGETS" ]; then
SKILLS_ARGS+=(-y)
fi
@@ -237,21 +418,44 @@ main() {
fi
# ── 3. MCP authentication ────────────────────────────────────────────
if ! $SKIP_MCP; then
ui_step "3. Register the MCP server (browser login)"
ui_info "Opening your browser for AgentKey device authentication ..."
ui_muted "When auth finishes, the MCP server is written into Claude Code / Claude Desktop / Cursor configs."
if $SKIP_MCP; then
ui_step "3. Register the MCP server"
ui_muted "Skipped (--skip-mcp)"
elif already_authed && ! $FORCE_MCP; then
ui_step "3. Register the MCP server"
ui_ok "AgentKey is already configured in an MCP client config — skipping auth."
ui_muted "Re-run with --force-mcp to authenticate again."
else
# Decide local-vs-remote and route the MCP CLI flags accordingly.
local IS_REMOTE=false
if detect_remote; then IS_REMOTE=true; fi
local AUTH_ARGS=(--auth-login)
if $IS_REMOTE; then
ui_step "3. Register the MCP server (remote auth: scan QR with phone)"
ui_info "Detected remote install context — printing QR + URL instead of opening a browser here."
if [ -d "$HOME/.openclaw" ]; then
ui_muted " reason: \$HOME/.openclaw exists (OpenClaw runtime)"
elif [ -n "${SSH_CONNECTION:-}" ] || [ -n "${SSH_TTY:-}" ]; then
ui_muted " reason: SSH session detected"
elif [ "$(uname -s)" = "Linux" ]; then
ui_muted " reason: Linux without \$DISPLAY / \$WAYLAND_DISPLAY"
fi
ui_muted "Override with --local if you want a browser opened on this machine instead."
AUTH_ARGS+=(--no-browser)
else
ui_step "3. Register the MCP server (browser login)"
ui_info "Opening your browser for AgentKey device authentication ..."
ui_muted "When auth finishes, the MCP server is written into Claude Code / Claude Desktop / Cursor configs."
fi
echo
if ! npx -y "$MCP_PACKAGE" --auth-login; then
if ! npx -y "$MCP_PACKAGE" "${AUTH_ARGS[@]}"; then
ui_error "MCP auth failed."
ui_muted "Retry manually: npx -y $MCP_PACKAGE --auth-login"
ui_muted "Retry manually: npx -y $MCP_PACKAGE ${AUTH_ARGS[*]}"
exit 1
fi
ui_ok "MCP server registered"
else
ui_step "3. Register the MCP server"
ui_muted "Skipped (--skip-mcp)"
fi
# ── 4. Summary ───────────────────────────────────────────────────────