Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/magpie.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Magpie offline checks
on:
pull_request:
paths:
- 'magpie.*'
- 'menu.*'
- 'test-magpie.*'
- '.github/workflows/magpie.yml'
workflow_dispatch:
permissions:
contents: read
jobs:
contracts:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- name: Bash installer and menu contracts
if: runner.os != 'Windows'
run: python3 test-magpie.py
- name: PowerShell contracts
shell: pwsh
run: ./test-magpie.ps1
- name: Windows PowerShell 5.1 contracts
if: runner.os == 'Windows'
shell: powershell
run: .\test-magpie.ps1
57 changes: 57 additions & 0 deletions MAGPIE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Magpie

Install or update [yetone/magpie](https://github.com/yetone/magpie).
The desktop menu entry is **24**; the Termux entry is **7**. Existing entries keep their numbers.

```sh
bash magpie.sh
bash magpie.sh update
MAGPIE_CLI=1 bash magpie.sh # Terminal build on Linux
MAGPIE_BIN_DIR="$HOME/bin" bash magpie.sh
```

```powershell
.\magpie.ps1
.\magpie.ps1 update
.\magpie.ps1 -Cli
.\magpie.ps1 -BinDir "$HOME\bin"
```

Both files work independently. Use `help` for local usage without a download.

## Platforms

| Platform | Installation |
| --- | --- |
| macOS Intel / Apple silicon | Official installer: signed app in `/Applications` or `~/Applications`, with a command link. |
| Linux x64 / ARM64 | Official installer: desktop build when WebKitGTK 4.1 is available, otherwise terminal build. `MAGPIE_CLI=1` requests the terminal build. |
| Termux | Official installer: Android-native terminal build in `$PREFIX/bin`. The inspected release provides ARM64; other architectures stop if no matching asset exists. No Linux binary or proot fallback. |
| Windows x64 / ARM64 | Exact matching executable from the latest stable GitHub release, installed as `magpie.exe` in `~/.local/bin`. `-Cli` or a nonempty `MAGPIE_CLI` selects the terminal build. |

On Unix, upstream selects the package, checks its hash and reports missing platform dependencies. No Node.js, Go, Homebrew or system package manager is required by this wrapper. Linux GUI and Windows GUI dependencies still follow upstream requirements. This does not promise compatibility with every Linux distribution or old OS release.

`MAGPIE_BIN_DIR` changes the command directory on both entry points. Windows also accepts `-BinDir`. The wrapper does not change persistent PATH settings. Run the printed full path or add that directory to PATH yourself.

The Unix entry finishes downloading `https://usemagpie.ai/install.sh` before it executes the response and forwards arguments unchanged. Upstream options such as `--proxy` and `--mirror` remain upstream options, not an LMM proxy framework. Environment proxy settings must also allow the initial installer download. The script requires Bash and curl; the upstream installer handles its other dependencies.

Windows uses the exact asset name, release tag, repository URL and GitHub's SHA-256 digest. Missing, ambiguous or invalid metadata stops installation. A failed download or hash check does not replace an existing executable. A successful update stages the new binary on the same volume and uses an atomic file replacement. Close Magpie before an update if Windows reports that the executable is in use; the installer does not kill running applications.

Installation does not launch Magpie, sign in, save API keys or overwrite provider settings. Configure providers in Magpie after installation. Magpie's own account discovery and privacy settings apply when you run it; see its [README](https://github.com/yetone/magpie#quick-start).

## Menu downloads

Both menus prefer adjacent local scripts. Without them, all tools use one immutable `lmm-scripts` commit, including the latest merged Codewhale fix at the time this change was made. `LMM_SCRIPTS_REV` remains an explicit override for testing another revision. The pin applies to the wrapper scripts; upstream software installers still select their current releases.

## Checks

```sh
python3 test-magpie.py
```

```powershell
.\test-magpie.ps1
```

The tests use temporary directories and fake downloads. They cover argument and environment forwarding, interrupted downloads, exit status, menu numbering, local and downloaded menus, Windows architecture selection, SHA-256 validation and preservation of an old installation. They do not install or launch the real application. CI runs these checks on Linux, macOS and Windows, including Windows PowerShell 5.1. A passing fixture test is not a real-device GUI test.

Upstream inspected: the [official Unix installer](https://github.com/yetone/magpie/blob/main/site/public/install.sh) and [v0.1.1215 assets](https://github.com/yetone/magpie/releases/tag/v0.1.1215), published on 2026-10-12. Release selection is dynamic, not pinned to that software version.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ irm https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/main/menu.ps1
| `opencode` | 官方 npm 最新 OpenCode + 独立 LMM OAuth 插件 | `opencode auth login --provider lmm`,然后 `opencode` |
| `codewhale` | 官方 npm 安装 + 固定版本 LMM OAuth 适配器 | `bash codewhale.sh run` / `.\codewhale.ps1 run` |

## Magpie

菜单第 **24** 项(Termux 第 **7** 项)安装或更新 [Magpie](https://github.com/yetone/magpie),原有编号不变。

- Linux / macOS / Termux:`bash magpie.sh`,复用官方安装器;Linux 自动选择桌面或终端版。
- Windows x64 / ARM64:`.\magpie.ps1`,安装官方最新正式发行包并校验 SHA-256;`-Cli` 选择终端版。
- 重复运行同一脚本即可更新。不自动启动、登录、写入 API Key 或更改供应商配置。

单独下载的菜单统一使用同一个固定脚本提交,不再混用旧版本和 `main`;`LMM_SCRIPTS_REV` 仍可显式覆盖。
安装目录、上游选项、平台条件和测试说明见 [MAGPIE.md](MAGPIE.md)。

## 更多 AI 工具

新增入口在菜单第 10–23 项,原来的 1–9 项不变。CLI 子菜单可选安装/更新,或配置/登录;配置操作要求工具已经安装并能从 PATH 找到。单个脚本默认执行 `install`,`help` 查看用法。新增脚本完全独立,下载一个文件即可运行,不需要下载共用实现。
Expand Down
56 changes: 56 additions & 0 deletions magpie.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
param(
[ValidateSet('install', 'update', 'help')][string]$Action = 'install',
[switch]$Cli,
[string]$BinDir = $env:MAGPIE_BIN_DIR
)
$ErrorActionPreference = 'Stop'
if ($args.Count) { throw 'Usage: .\magpie.ps1 [install|update|help] [-Cli] [-BinDir PATH]' }
if ($Action -eq 'help') {
Write-Host 'Usage: .\magpie.ps1 [install|update|help] [-Cli] [-BinDir PATH]'
Write-Host 'Windows x64/ARM64. Installs the official release without starting it or changing your configuration.'
return
}
if ($env:OS -ne 'Windows_NT') { throw 'Use magpie.sh on Linux, macOS or Termux.' }
# The native architecture takes priority over an emulated PowerShell process.
$machine = $env:PROCESSOR_ARCHITEW6432
if (-not $machine) { $machine = $env:PROCESSOR_ARCHITECTURE }
$arch = switch ($machine) {
'AMD64' { 'amd64' }
'ARM64' { 'arm64' }
default { throw "Unsupported Windows architecture: $machine" }
}
$flavor = if ($Cli -or $env:MAGPIE_CLI) { '-cli' } else { '' }
$name = "magpie$flavor-windows-$arch.exe"
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$release = Invoke-RestMethod -UseBasicParsing 'https://api.github.com/repos/yetone/magpie/releases/latest'
if ($release.draft -or $release.prerelease -or -not $release.tag_name) { throw 'Expected an official stable Magpie release.' }
$assets = @($release.assets | Where-Object { $_.name -ceq $name })
if ($assets.Count -ne 1) { throw "The latest release has no unique $name asset." }
$asset = $assets[0]
$url = 'https://github.com/yetone/magpie/releases/download/' + [uri]::EscapeDataString($release.tag_name) + '/' + $name
if ($asset.browser_download_url -cne $url) { throw 'Unexpected Magpie download URL.' }
if ($asset.digest -cnotmatch '^sha256:[0-9a-f]{64}$') { throw 'The release has no valid SHA-256 digest; nothing was installed.' }
$expected = $asset.digest.Substring(7)
if (-not $BinDir) { $BinDir = Join-Path $HOME '.local\bin' }
$BinDir = [IO.Path]::GetFullPath($BinDir)
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
# Stage on the destination volume so replacing an existing binary is atomic.
$work = Join-Path $BinDir ('.magpie-install-' + [guid]::NewGuid())
try {
New-Item -ItemType Directory -Path $work | Out-Null
$download = Join-Path $work $name
Invoke-WebRequest -UseBasicParsing $url -OutFile $download
$actual = (Get-FileHash -LiteralPath $download -Algorithm SHA256).Hash
if ($actual -ine $expected) { throw 'Magpie SHA-256 mismatch; the existing installation was not changed.' }
$target = Join-Path $BinDir 'magpie.exe'
if ([IO.File]::Exists($target)) {
# PowerShell converts plain $null to an empty string for this .NET parameter.
[IO.File]::Replace($download, $target, [System.Management.Automation.Language.NullString]::Value)
} else {
[IO.File]::Move($download, $target)
}
Write-Host "Installed magpie $($release.tag_name): $target"
Write-Host 'Open it to configure providers. Add the install directory to PATH to use magpie from a terminal.'
} finally {
Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue
}
16 changes: 16 additions & 0 deletions magpie.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
case "${1:-install}" in
install|update) if [[ $# -gt 0 ]]; then shift; fi;;
help|-h|--help)
printf '%s\n' 'Usage: bash magpie.sh [install|update] [official installer options]' \
'Linux, macOS and Termux: use the official installer.' \
'MAGPIE_CLI=1 selects the terminal build on Linux.' \
'MAGPIE_BIN_DIR changes the command directory. No login or configuration is performed.'
exit 0;;
-*) ;; # Let the official installer validate its own options.
*) echo 'Usage: bash magpie.sh [install|update] [official installer options]' >&2; exit 2;;
esac
# Finish the download before executing anything; never execute a partial response.
script=$(curl -fsSL https://usemagpie.ai/install.sh)
sh -c "$script" -- "$@"
5 changes: 2 additions & 3 deletions menu.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
$ErrorActionPreference = 'Stop'
if ($args.Count) { throw 'Usage: .\menu.ps1' }
$tools = @('pi','dsh','lmm','codex','claude-code','cc-switch','clash-verge-rev','codewhale','opencode','cursor-cli','grok-build','gemini','qwen-code','kimi','codebuddy','hermes','openclaw','astrbot','aider','cursor','cherry-studio','ollama','uv')
$tools = @('pi','dsh','lmm','codex','claude-code','cc-switch','clash-verge-rev','codewhale','opencode','cursor-cli','grok-build','gemini','qwen-code','kimi','codebuddy','hermes','openclaw','astrbot','aider','cursor','cherry-studio','ollama','uv','magpie')
$work = Join-Path ([IO.Path]::GetTempPath()) ("lmm-menu-" + [guid]::NewGuid())
try {
New-Item -ItemType Directory $work | Out-Null
Expand Down Expand Up @@ -34,8 +34,7 @@ try {
if ($PSScriptRoot -and (Test-Path (Join-Path $PSScriptRoot $name))) {
$path = Join-Path $PSScriptRoot $name
} else {
$revision = '873bb8de2d9af00fb2a5fbb592b3da094e665271'
if ($tool -notin @('pi','dsh','lmm','codex','claude-code','cc-switch','clash-verge-rev','codewhale','opencode')) { $revision = 'main' }
$revision = '86391c6e604bdbbf1231770529c47d508eca7d45'
if ($env:LMM_SCRIPTS_REV) { $revision = $env:LMM_SCRIPTS_REV }
Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/$revision/$name" -OutFile $path
}
Expand Down
9 changes: 4 additions & 5 deletions menu.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
[[ $# = 0 ]] || { echo 'Usage: bash menu.sh' >&2; exit 2; }
tools=(pi dsh lmm codex claude-code cc-switch clash-verge-rev codewhale opencode cursor-cli grok-build gemini qwen-code kimi codebuddy hermes openclaw astrbot aider cursor cherry-studio ollama uv)
if [[ -n ${TERMUX_VERSION:-} || ${PREFIX:-} == */com.termux/files/usr ]]; then tools=(pi dsh lmm codex claude-code codewhale); fi
tools=(pi dsh lmm codex claude-code cc-switch clash-verge-rev codewhale opencode cursor-cli grok-build gemini qwen-code kimi codebuddy hermes openclaw astrbot aider cursor cherry-studio ollama uv magpie)
if [[ -n ${TERMUX_VERSION:-} || ${PREFIX:-} == */com.termux/files/usr ]]; then tools=(pi dsh lmm codex claude-code codewhale magpie); fi
dir=$(dirname -- "${BASH_SOURCE[0]:-}")
while true; do
printf '\nInstall / update\n'
Expand Down Expand Up @@ -36,9 +36,8 @@ while true; do
if [[ -n ${BASH_SOURCE[0]:-} && -f $dir/$tool.sh ]]; then
bash "$dir/$tool.sh" "$@" </dev/tty || printf 'Installation failed.\n' >&2
else
revision=873bb8de2d9af00fb2a5fbb592b3da094e665271
case "$tool" in pi|dsh|lmm|codex|claude-code|cc-switch|clash-verge-rev|codewhale|opencode) ;; *) revision=main;; esac
script=$(curl -fsSL "https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/${LMM_SCRIPTS_REV:-$revision}/$tool.sh") || continue
revision=86391c6e604bdbbf1231770529c47d508eca7d45
script=$(curl -fsSL "https://raw.githubusercontent.com/TokenNotIncluded/lmm-scripts/${LMM_SCRIPTS_REV:-$revision}/$tool.sh") || { printf 'Could not download %s.\n' "$tool" >&2; continue; }
bash -c "$script" -- "$@" </dev/tty || printf 'Installation failed.\n' >&2
fi
done
2 changes: 1 addition & 1 deletion test-ai-tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def test_menu_entries_have_both_platforms_and_preserve_original_numbers(self):
result=subprocess.run([shutil.which('bash'),'-c',code],env=self.env,text=True,capture_output=True)
entries=result.stdout.splitlines()
self.assertEqual(entries[:9],['pi','dsh','lmm','codex','claude-code','cc-switch','clash-verge-rev','codewhale','opencode'])
self.assertEqual(set(entries[9:]),set(ALL))
self.assertEqual(set(entries[9:]),set(ALL) | {'magpie'})
for name in ALL:
self.assertTrue((ROOT/(name+'.sh')).is_file()); self.assertTrue((ROOT/(name+'.ps1')).is_file())
self.assertIn("'"+name+"'",(ROOT/'menu.ps1').read_text())
Expand Down
Loading
Loading