Files
magpie/internal
ylornandylorn 1aef0f3f2d proc: the login-shell probe asks nushell through /bin/sh, and a value the shell printed instead of expanding counts as unset (#738: with nushell as the login shell, shellEnv's printf '…' "$PATH" "$CLAUDE_CONFIG_DIR" … came back as the text "$PATH", "$CLAUDE_CONFIG_DIR", … — a double-quoted string is literal in nushell — and setUnset exported every one of agentenv.Vars with its own name as the value; Claude sign-in then failed with "mkdir $CLAUDE_CONFIG_DIR: read-only file system" (putClaudeLogin's MkdirAll of that relative name under the GUI's cwd /), and the account lapsed minutes later as "Claude Code is no longer signed in", its sign-in looked for under that folder; Codex, Pi, omp and the rest were pointed at "$CODEX_HOME", "$PI_CODING_AGENT_DIR", "$OMP_PROFILE" the same way, in the GUI alone, which is the one caller of UserPath) — shellProbe builds the command: for a shell named nu it is ^/bin/sh -c '<the same printf>' (nushell has no POSIX quoting to ask with and its PATH is a list, while a child of it gets the environment as any child does, PATH joined back into one string, and sh expands the references; the outer string in nushell's single quotes where a backslash is a backslash, the format in sh's double quotes where \0 reaches printf), any other shell keeps the printf it had; and parseShellEnv takes a value that is the variable's own reference ("$CODEX_HOME", or "$PATH" for PATH) for unset, so a login shell with other quoting than POSIX leaves a variable alone rather than setting it to its name. TestShellEnvNushell runs the probe through a nushell on the machine (skipped where there is none) and checks the command's shape anyway; TestParseShellEnvUnexpanded feeds the unexpanded answer. Verified against nushell 0.116.0: PATH comes back, CODEX_HOME from the environment reaches sh, unset variables are "". (#739)
Co-authored-by: ylorn <ylorn@icloud.com>
2026-10-04 11:13:50 +08:00
..
gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
2026-10-03 00:18:16 +08:00
agent: with no Node.js on the machine, Install another agent's npm commands install Node first, in the same line (#727, Sun1090 on v0.1.801: "没写curl的命令… curl xxx && npm xxxx", confirmed as option 1 — Pi, Copilot CLI, Crush and the rest had only npm install -g …, which a new machine without Node can't run) — installCommands now takes whether npm is here (nodeHere: exec.LookPath("npm"), then the folders proc.UserBinDirs knows — nvm's, fnm's, mise's, volta's, Homebrew's, the ~/.npmrc prefix — and on Windows also the registry's PATH (proc.LoginPath) and %ProgramFiles%\nodejs; npm.cmd there), and where it isn't, npmInstall gives on a Mac or Linux export NVM_DIR="$HOME/.nvm" && mkdir -p "$NVM_DIR" && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash && . "$NVM_DIR/nvm.sh" && nvm install --lts && npm install -g <pkg> (nodejs.org's nvm route; NVM_DIR is set because the installer goes by $XDG_CONFIG_HOME/nvm when that is set, and nvm.sh is read in the same shell since the profile line only reaches new terminals; nvm's Node is the user's, so no sudo), and on Windows, for PowerShell, winget install -e --id OpenJS.NodeJS.LTS; $env:Path = <Machine + User Path from the registry>; npm.cmd install -g <pkg> (";" as Windows PowerShell 5.1 has no "&&", PATH re-read since the MSI only adds Node to new terminals, npm.cmd since npm.ps1 is refused under the default execution policy). Each such command carries node (nvm, nvm-mac or winget); the row labels it "Node.js + npm" and the note says the npm commands install Node with nvm/winget first — on a Mac that nvm's installer stops without Xcode's Command Line Tools (run xcode-select --install), on Windows to run them in PowerShell and open a new terminal if npm still isn't found. With Node here everything is as before. magpie still only shows commands to copy (the list never had a one-click install; the page's Update all/update pill run only for CLIs already installed, unchanged). The via column is 84px so "Node.js + npm" stays on one line. TestInstallCommandsWithoutNode (Pi on darwin/linux/windows, Claude's installer untouched, npm alone with Node), TestNpmIn and TestInstallWithoutNodeParses (bash/zsh/sh -n read the line) are new; agent-update-all.test.cjs gains "no Node.js here" (label, note with/without Xcode, the whole line copied, no sideways scroll, winget note in Chinese) and passes in WebKit (8 tests). go vet ./internal/..., builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but for TestCursorSignInJoinsAWrappedLink, which timed out under load and passes alone. Nothing was installed: the commands were only parsed, not run, and not tried on a machine without Node.
2026-10-04 10:13:55 +08:00
portable: a portable magpie on Windows keeps WebView2's profile in data\webview2 and makes no Start-menu shortcut, and an installed one makes the shortcut once and never puts back one the user deleted (#508, suixinex: 便携模式时还是会创建 C:\Users\<username>\AppData\Roaming\magpie.exe\ 文件夹; 开始菜单的快捷方式删除后再打开magpie会重新添加) — the AppData\Roaming\magpie.exe folder is WebView2's user data folder: Wails v3 passes WindowsOptions.WebviewUserDataPath to go-webview2 as its DataPath, and left empty it is %APPDATA%\<exe name>, which portable mode (appdir) never set. The app's Windows options (windowsOptions) now give it appdir.WebView(): data\webview2 when portable, "" (WebView2's default, so the pages' storage and cookies there are kept) when installed. The shortcut came back because shortcut.Ensure ran at every start of the desktop app and wrote Magpie.lnk whenever it was missing or stale, with nothing recording that it had been made. Ensure now does nothing when portable (no shortcut, no App Paths entry); installed, it makes the shortcut on the first start and records that in start-menu-shortcut in magpie's folder (with the exe), keeps one that is still there pointed at the exe (moved exe, update), and makes none once the file is recorded and the shortcut is gone. A user who deleted it under an older magpie isn't given it back on updating either: a missing shortcut with no record but an App Paths\magpie.exe entry already there (every older magpie wrote it at each start) counts as deleted and is recorded as such. The Start menu, the shortcut writer and App Paths are behind function variables, so the decision is tested with fakes on any OS. Checked for other leaks: autostart only refreshes an entry the user turned on (Refresh never enables), the magpie:// link registration is needed for import links and stays, as appdir's doc says, as do temp folders; the update stages beside the exe, caches already follow appdir. Not changed: the Mac's WKWebView and Linux's WebKitGTK keep their own data in the profile (Wails has no option for it), which #508 didn't report. Nothing ran on Windows (the test box isn't reachable): no real WebView2 was started with the data path and no real shortcut was made or deleted. TestEnsureOnce, TestEnsureAfterOlderMagpie, TestEnsurePortable (fake Start menu in a temp folder), TestWebView and TestWindowsOptionsWebViewData fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet ./..., GOOS=windows go test -c for shortcut, gui and appdir, go test -tags nogui -count=1 ./... under a temp HOME pass (TestUsageKeepsTimeToFirstToken and TestUserPath failed once on timing, pass rerun alone; untouched).
2026-10-02 14:39:49 +08:00
gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
2026-10-03 00:18:16 +08:00
accounts: on Linux an account put first is read first at once, and on Windows a file being replaced isn't read as none (goal: 插件订阅 provider 跟内置的所有逻辑都要一模一样, run on Windows and Linux) — the provider tests run on the Windows test box and in WSL found two bugs of magpie's own files. Linux stamps a file's time from a clock a tick (a few milliseconds) coarse, and filememo took same time and size for the same file: two accounts swapped (Devin, Grok, WorkBuddy, ZCode, Claude…) read back in the old order, so magpie kept using the account the user had just moved down, and a moved-back plugin's accounts read as before. A file written within the last two seconds is now also compared by its bytes. On Windows a file being renamed over refuses to open for a moment ("Access is denied"), and one held open refuses to be renamed over: plugin-auth.json, plugins.json and the plugin providers read as nothing (a usage fetch right after a move showed no cards), logins.json failed to save, and host.js, reading plugin-auth.json as {}, would have written the other accounts away on the next sign-in. New internal/steady waits such a read or rename out (half a second at most, Windows only); filememo, edit.WriteAtomic, writePrivate, Kiro's writes and the plugin store go through it, host.js retries its read and rename, and the plugin store writes its files by a rename. Tests: USERPROFILE beside HOME in the test homes, the fake devin (a shell script) skipped on Windows, plugin-auth.json's mode bits unchecked there. TestReadSameSizeSameTick fails on Linux and TestRenameOverAFileHeldOpen/TestReadAFileHeldOpen on Windows without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; on Linux (WSL) the provider and plugin packages pass whole (13 failed before), on Windows the plugin package and the move/plugin/Grok/Devin/usage tests pass 10 times over (TestMoveToPlugin failed one run in a few before); 15 built-in tests that assume Unix (mode bits, Keychain, shell-script CLIs) still fail there, as before.
2026-10-01 13:27:12 +08:00
provider: a subscription account can be capped at a share of its usage windows, and routing treats it as used up there (Discord: hold a new Claude account at 70% of its five hours and its week to keep its ban risk down, and let magpie go on to the other accounts) — an account's cap (1–99, keyed by the provider id and the account in lower case, AccountCaps in providers.json beside AccountModels and AccountProxies) holds it once any window that applies to the model is at or past the cap and hasn't reset (Allowance.CapHeld; Aside windows and windows kept for another model are left out). One share for every window rather than one per window: the windows an account has differ by vendor (five hours and week for Claude and Codex, a month or a day for others, per-model weeks), the request was one figure for both, and a lower share for one window would be set by lowering the cap. The gateway's perKeyBarred moves a capped account to the candidates left out (barredOf marks it Capped with its use and when it is back, so the routing trace says "held at its 70% usage cap"), so groups, fallback, Ordered and Spread routing and the Codex/Claude Code switch list (NextLogin: a capped account is moved off, not returned to, and isn't a spare) all go on to the next account; a pinned capped account and a draw over only capped accounts answer 429. When every account that serves the model is capped, the client is turned away as when all are used up — 429 rate_limit_error with Retry-After at the soonest reset — and told the cap was reached, which accounts, at what use and cap, and how to lift it. A cap never spends a Codex reset: weekUsedUp, usedUp, BackAt and spendExpiringNow still go by ChatGPT's own 100%, so an account held at 80% with auto reset on is held and nothing is spent; a lone Codex account with a cap goes through serve() instead of being relayed as it came (codexAccounts), so the cap holds it. The Providers page shows a cap pill on each account's row (on hover until set) that opens the app's own menu — No cap, 50/60/70/80/90%, Other… (typed inline, focused without scrolling) — posting provider/accountcap; each window's bar gets a tick at the cap (from the right when the bar shows what is left), and an account held by its cap says so and until when. zh strings in i18n.js. CLI: magpie provider account-cap <id> [account [percent|off]] shows or sets caps. Tests: TestCapHeld, TestCapReached, TestParseCap; TestCapNeverSpendsACodexReset (80% windows, 70% cap: not used up, no reset due, routing held); TestCodexSwitchedAtCap (the active account at 75% with a 70% cap is switched off to the one at 20%, not back while capped, back once lifted; 100 and an unknown account refused); gateway TestAccountCapMovesOn (first account at 75% capped at 70%: the second is tried and the trace says 75/70; lifted: the first), TestAccountCapEveryAccount (429 rate_limit_error naming both caps, nothing sent, logged 429) and TestAccountCapSpendsNoCodexReset (one account at an 80% week, auto reset on: 429, nothing tried or spent; lifted: 200, nothing spent) — the three gateway tests fail with the cap check taken out; TestAccountCapCmd; account-cap.test.cjs (webkit, en and zh: pill, menu, Other…, No cap, cap ticks, held note, no <select>, no border stripes, no scroll on click). account-models, click-scroll, keep-login, codex-auto-reset, account-return, routing-blocked, routing-account-order, routing-quota-left, routing-auto-reset, routing-manual, quota-pools, quota-families and usage-accounts GUI tests pass in webkit. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real accounts; a cap acts on usage as last read, so a burst between reads can run a little past it.
2026-10-04 10:18:50 +08:00
imagemcp: an image that takes longer than the agent gives a tool call is answered "still being made" before the agent gives up, and is saved all the same (#575: 图片生成的mcp经常遇到超时问题 — "Tool execution timed out after 30000ms" in one agent, "Error: Request timed out" in another; the agent then looked for the file, found none, and asked for the image again, and again timed out) — generate_image held its tools/call open until the gateway's image came back, and an image model (gpt-image at quality high, 1024x1536, through a subscription) often takes a minute or more, while agents give an MCP tool call as little as 30 s, the MCP SDK's default is 60 s, and only Codex and Goose have a timeout magpie writes for them (tool_timeout_sec, timeout). So the call was cut short, the generation went on unseen, and the agent's retry started another one that was cut short too. Now each generate_image and generate_video call runs as a job that goes on to its end whatever becomes of the call, and the call waits for it at most answerWithin (20 s, under the shortest of those limits): what is ready by then is answered as before (errors too); what is not is answered, not as an error, that it is still being made, where it will be saved, not to ask for it again, and an id for the new tool generation_result, which waits on the job the same 20 s at a time and answers its paths once it is saved. A call given a progressToken is sent notifications/progress every 5 s while it waits, for clients that wait longer on progress, and magpie mcp image waits for jobs still running when the agent closes it (up to ToolTimeout) so what is being made is saved. TestSlowImageAnswersBeforeTheAgentGivesUp (a gateway taking 700 ms against a 150 ms answerWithin: the call answers in time with an id and progress notifications, generation_result then answers the saved file, the gateway was asked once, an unknown id is a tool error) fails without the change (the call took 703 ms and answered only when the image was saved). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside); go test -race ./internal/imagemcp passes.
2026-10-02 22:55:07 +08:00
library: Discover finds an MCP server by its address — a repository, page, endpoint or package pasted in (lc on Discord: https://github.com/crystaldba/postgres-mcp in Library › MCP › 发现 said 没有与 … 匹配的结果) — MarketServers matched featured servers on their words and asked the MCP Registry with the search as typed, and the registry's search is a part of a server's name (checked: "postgres mcp" lists 0, "crystaldba" 0), so a URL found nothing anywhere. A search that reads as an address (address: a scheme, git@, www., host/path, owner/repo or @scope/pkg; words with a space, or a bare word, are searched as before) is normalised (normAddr: lower case, no scheme, user, www., query, fragment, trailing slash or .git; npmjs.com/package/x, pypi.org/project/x, hub.docker.com/r/x and docker.io/x are the package, an image's tag and a package's @version dropped) and matched (addrScore: equal, under it, or a file under one's repository; owner/repo on any host; never a bare host or an owner's page standing for every repository) against each server's addresses: a featured one's page, endpoint and package; a registry one's name, websiteUrl, repository.url, remotes' URLs and packages' identifiers. The registry is asked for the names the address suggests (addrTerms: the repository's or package's name, then its owner's; a site's name for an endpoint, context7 for mcp.context7.com/mcp), and what it lists at the address comes first and alone, marked match; when nothing is, what it lists for the first name is shown, as that search would. Searched against the live registry: github.com/Eszetael/postgres-mcp-hardened.git, ghcr.io/eszetael/postgres-mcp-hardened:0.1.10, github.com/upstash/context7, https://mcp.context7.com/mcp (featured Context7), npmjs.com/package/@playwright/mcp (featured Playwright), github.com/modelcontextprotocol/servers (the reference servers) and an endpoint of ai.getvda's each find theirs. crystaldba/postgres-mcp itself isn't in the registry (no name, repository or package of it in the 124 servers a "postgres" search pages through), so it shows the 12 others called postgres-mcp, and above them "None of these is at {q} — they're found by its name." (以下都不在 {q},是按它的名字找到的。; "Nothing listed is at {q}." 没有列出位于 {q} 的服务器。 when none) with "+ Add it yourself…" (+ 自己添加…): the market's answer carries custom (CustomAt) when the search is an address nothing found is at, and the button opens the existing new-server form filled in — named for the repository (postgres), an endpoint's URL as HTTP, npx -y / uvx for an npm or PyPI page. TestAddress, TestAddrScore and TestMarketFindsByAddress (a fake registry that searches names: by repository found through its owner's name, featured by endpoint, repository, package and a repository folder, registry by endpoint, by name with one to add by hand, words as before) and market-address.test.cjs (webkit, en and zh: the note over the name matches, the form named postgres with no command, an endpoint's URL in it, words that match nothing as before; fails on the old library.js) pass, as does market-have. go vet ./internal/..., builds (-tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./internal/library/... ./internal/gui/... under a temp HOME pass. Not tried in the app.
2026-10-04 07:54:08 +08:00
gui, mcp: Sign in in an MCP server's editor signs in to the server the form shows, saving it first, and a server that needs no sign-in is said to work as it is (lc on Discord: exa mcp 无法OAuth使用,x-api-key + key也无法使用 agent使用的是omp) — Exa signs in only at mcp.exa.ai/mcp?login (401 with resource_metadata, auth.exa.ai, S256, dynamic registration); plain /mcp answers without one. Sign in sent only the name, so the backend probed the saved URL: a URL just changed to ?login, unsaved, got "this server answers without a sign-in: there is nothing to sign in to". Now Sign in saves the form first when its name, transport, URL or headers aren't the saved server's, leaves the dialog open, and signs in to it; a server being added, or one switched to HTTP, has Sign in too, and a later Save is of the server as saved. mcpauth.ErrNoSignIn says the server already works without signing in, so the agents can use it as it is, a key goes in Headers and a server that signs in at another address (Exa's ?login) needs that URL; the GUI shows it in Chinese. The gateway's /mcp/<name> relay already kept the signed-in URL's query (?login reaches the server) and passed the Library's headers (x-api-key) on, dropping only the gateway's own key: TestMCPProxyKeepsQueryAndHeaders holds it (the fake now records the query). omp reads magpie's mcp.json entry as written (type/url/headers, discovery/builtin.ts) and sends headers on every request (mcp/transports/http.ts): with omp 18.4.9's own client under a temp HOME, mcp.exa.ai/mcp with x-api-key listed Exa's tools and ?login without a sign-in got the 401, so the encoder is unchanged. mcp-signin.test.cjs (webkit, en and zh): an unchanged Exa says it works as it is, with nothing saved; its URL changed to ?login, Sign in saves it then signs in, the dialog stays and no click moves the page; old code started the sign-in without saving. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes on rerun; library and mcp GUI tests pass in webkit (chromium didn't launch here). Not tried: a real Exa OAuth sign-in in magpie, or omp through magpie's relay with a real account.
2026-10-04 01:52:46 +08:00
update: install.sh and magpie update take --proxy and --mirror, and magpie update mirror keeps one for the app's updates too (akic404 on Discord: 安装和update如果没有梯子也是麻烦事,能否在install.sh和magpie update后加子选项?--proxy等可自定义或默认ghproxy) — install.sh had no options and fetched with curl alone, and magpie update took none; without a way out to GitHub both failed. Now both take --proxy <url> (http, https, socks5, socks5h; install.sh also MAGPIE_PROXY=) for the feed and the download, and --mirror <prefix> (or MAGPIE_MIRROR=), a GitHub download mirror the release file is fetched from as <prefix>https://github.com/…; only github.com / githubusercontent.com URLs are put through it. No mirror is used unless one is given: magpie names none and has none by default. The feed with each file's SHA-256 is always fetched from usemagpie.ai, never through the mirror, so a file the mirror changed is refused and deleted ("… from the mirror X does not match its checksum from https://usemagpie.ai; not installed"); macOS still checks the app's signing team. magpie update mirror <prefix>|off saves Settings' updateMirror (http(s) only), which the app's own background update follows too; magpie update --mirror off skips it once; the Settings page's save keeps it. The app's update already followed Settings → Proxy (#294); magpie update --proxy overrides it for one run. The environment's ALL_PROXY / all_proxy is now honoured when HTTPS_PROXY / HTTP_PROXY are unset (Go's ProxyFromEnvironment ignores it), with NO_PROXY as curl reads it. magpie update takes --flag value or --flag=value, and an unknown word is an error instead of an update. The help, README and the docs' FAQ (en/zh/ja) say how. install.test.mjs's proxy, mirror and tampered-mirror tests, TestAllProxyFromTheEnvironment, TestUpdateThroughAMirror, TestUpdateThroughAProxyGiven and TestUpdateFlags fail on the old code and pass; TestSettingsSaveKeepsWhatItDoesNotSend covers updateMirror. A dry run of install.sh as Linux amd64 into a temp HOME installed the real 0.1.773 with its SHA-256 checked, refused a local mirror serving a wrong file, and with --proxy to a closed port failed reaching the site through it. go vet, builds (darwin, GOOS=linux, GOOS=windows), sh -n, node --test site/install.test.mjs and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeIdentityServedWhileAsked, a timing test that passes alone. Not tried through a real proxy server or a real public mirror; no install.ps1 exists for Windows, and the app has no field for the mirror.
2026-10-04 00:19:47 +08:00
site, omarchy: the site in Omarchy's style follows a theme set in Omarchy without a reload (owner: 现在系统换完主题后网页需要手动刷新一下才能切换到对应主题) — a browser that hasn't let usemagpie.ai reach this computer (Chromium's local network access, which nothing on the page asked for) left the page on Omarchy's default for light or dark, chosen from prefers-color-scheme once at load, and a page that had been let through could lose magpie for 30 s when one answer was slow. Now: while not let through, the page repaints as prefers-color-scheme turns (omarchy-theme-set turns GNOME's color-scheme, so Chromium's), and a "Follow my Omarchy theme" / 跟随 Omarchy 主题 button beside the chip asks the browser in one click; a permission turned to granted (the prompt, site settings) starts following at once, denied stops it; once magpie has answered, one failed or slow answer (5 s, was 3 s) keeps the page following and it asks again in 3 s (two in a row mark it absent, still asked every 3 s), and two permissions turning together poll once. The gateway's /v1/magpie/omarchy no longer asks omarchy-font-current and hyprctl again on every theme change, only when the font's or Hyprland's own files change, since a theme set reloads Hyprland and hyprctl asked meanwhile can stall (2 s timeouts each); TestCurrent checks a theme switch keeps the font without asking and fonts.conf changing asks again (fails on the old code). Tried in the Omarchy VM's Chromium with the new omarchy.js/css: not let through, the page went tokyo-night ↔ catppuccin-latte with the desktop; the button raised Chromium's "Access other apps and services on this device" prompt and the page went live on vantablack once granted, no reload; with access allowed, catppuccin-latte, tokyo-night, rose-pine and vantablack came through within 1.5–3 s each, no failed request. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. The prompt's Allow itself wasn't pressed (granted through the profile and CDP instead).
2026-10-04 04:17:42 +08:00
proc: the login-shell probe asks nushell through /bin/sh, and a value the shell printed instead of expanding counts as unset (#738: with nushell as the login shell, shellEnv's printf '…' "$PATH" "$CLAUDE_CONFIG_DIR" … came back as the text "$PATH", "$CLAUDE_CONFIG_DIR", … — a double-quoted string is literal in nushell — and setUnset exported every one of agentenv.Vars with its own name as the value; Claude sign-in then failed with "mkdir $CLAUDE_CONFIG_DIR: read-only file system" (putClaudeLogin's MkdirAll of that relative name under the GUI's cwd /), and the account lapsed minutes later as "Claude Code is no longer signed in", its sign-in looked for under that folder; Codex, Pi, omp and the rest were pointed at "$CODEX_HOME", "$PI_CODING_AGENT_DIR", "$OMP_PROFILE" the same way, in the GUI alone, which is the one caller of UserPath) — shellProbe builds the command: for a shell named nu it is ^/bin/sh -c '<the same printf>' (nushell has no POSIX quoting to ask with and its PATH is a list, while a child of it gets the environment as any child does, PATH joined back into one string, and sh expands the references; the outer string in nushell's single quotes where a backslash is a backslash, the format in sh's double quotes where \0 reaches printf), any other shell keeps the printf it had; and parseShellEnv takes a value that is the variable's own reference ("$CODEX_HOME", or "$PATH" for PATH) for unset, so a login shell with other quoting than POSIX leaves a variable alone rather than setting it to its name. TestShellEnvNushell runs the probe through a nushell on the machine (skipped where there is none) and checks the command's shape anyway; TestParseShellEnvUnexpanded feeds the unexpanded answer. Verified against nushell 0.116.0: PATH comes back, CODEX_HOME from the environment reaches sh, unset variables are "". (#739)
2026-10-04 11:13:50 +08:00
test: one list of the agent folders a sandbox clears, so no sandbox misses an agent (yetone, on #522: A shared list of env vars for the other sandboxes would be a welcome follow-up) — every sandbox kept its own list of the variables that point at an agent's folder, and each had drifted: the gateway's cleared 5 of the 32 magpie reads, the provider package's 2, the tui's 10, the library's 20. A sandbox clearing 2 meets whatever Grok, Kimi, Hermes, MiniMax, Qoder or ZCode the developer has installed, which is how #522 came about — Qoder's two were in the library's list and in none of the four that failed. internal/agentenv is now the one list, and the 21 sandboxes that fake an environment with no agent in it clear it, each keeping beside it the variables that are not an agent's: APPDATA and LOCALAPPDATA, which a sandbox sets to a folder of its own rather than clearing, VISUAL and EDITOR. A name in the list is one magpie reads, and TestVarsAreRead walks the sources to say so, which is how two names a sandbox had been clearing turned out to be read by nothing at all — GEMINI_CLI_HOME and OPENCODE_CONFIG in internal/backup's, while OPENCODE_CONFIG_DIR, the one internal/agent reads (agents.go:545), was not in it. Where a sandbox set an agent's folder to a fixture of its own, the clearing now comes first, since a list of 32 wipes what a list of 5 left standing. Not converted: the omp tests' four variables, which are the ones omp's own lookup consults (a Pi fork) rather than every agent's, and the warm-up's Claude Code traffic flags, which are not folders — CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC stays out of the list for that reason too, its one test setting it on purpose. On Windows 11 the whole suite answers as it did before the change: 31 packages pass, and the same 15 tests in internal/library, internal/gateway and internal/agent fail for what they failed for before (symlinks this machine has no privilege for, and TestSuffixModesReachAgentFiles). go vet ./..., go build -tags nogui (windows, GOOS=linux, GOOS=darwin), and go vet -tags nogui for linux and darwin over every package touched pass. (#543)
2026-10-02 17:22:16 +08:00
provider: a subscription account can be capped at a share of its usage windows, and routing treats it as used up there (Discord: hold a new Claude account at 70% of its five hours and its week to keep its ban risk down, and let magpie go on to the other accounts) — an account's cap (1–99, keyed by the provider id and the account in lower case, AccountCaps in providers.json beside AccountModels and AccountProxies) holds it once any window that applies to the model is at or past the cap and hasn't reset (Allowance.CapHeld; Aside windows and windows kept for another model are left out). One share for every window rather than one per window: the windows an account has differ by vendor (five hours and week for Claude and Codex, a month or a day for others, per-model weeks), the request was one figure for both, and a lower share for one window would be set by lowering the cap. The gateway's perKeyBarred moves a capped account to the candidates left out (barredOf marks it Capped with its use and when it is back, so the routing trace says "held at its 70% usage cap"), so groups, fallback, Ordered and Spread routing and the Codex/Claude Code switch list (NextLogin: a capped account is moved off, not returned to, and isn't a spare) all go on to the next account; a pinned capped account and a draw over only capped accounts answer 429. When every account that serves the model is capped, the client is turned away as when all are used up — 429 rate_limit_error with Retry-After at the soonest reset — and told the cap was reached, which accounts, at what use and cap, and how to lift it. A cap never spends a Codex reset: weekUsedUp, usedUp, BackAt and spendExpiringNow still go by ChatGPT's own 100%, so an account held at 80% with auto reset on is held and nothing is spent; a lone Codex account with a cap goes through serve() instead of being relayed as it came (codexAccounts), so the cap holds it. The Providers page shows a cap pill on each account's row (on hover until set) that opens the app's own menu — No cap, 50/60/70/80/90%, Other… (typed inline, focused without scrolling) — posting provider/accountcap; each window's bar gets a tick at the cap (from the right when the bar shows what is left), and an account held by its cap says so and until when. zh strings in i18n.js. CLI: magpie provider account-cap <id> [account [percent|off]] shows or sets caps. Tests: TestCapHeld, TestCapReached, TestParseCap; TestCapNeverSpendsACodexReset (80% windows, 70% cap: not used up, no reset due, routing held); TestCodexSwitchedAtCap (the active account at 75% with a 70% cap is switched off to the one at 20%, not back while capped, back once lifted; 100 and an unknown account refused); gateway TestAccountCapMovesOn (first account at 75% capped at 70%: the second is tried and the trace says 75/70; lifted: the first), TestAccountCapEveryAccount (429 rate_limit_error naming both caps, nothing sent, logged 429) and TestAccountCapSpendsNoCodexReset (one account at an 80% week, auto reset on: 429, nothing tried or spent; lifted: 200, nothing spent) — the three gateway tests fail with the cap check taken out; TestAccountCapCmd; account-cap.test.cjs (webkit, en and zh: pill, menu, Other…, No cap, cap ticks, held note, no <select>, no border stripes, no scroll on click). account-models, click-scroll, keep-login, codex-auto-reset, account-return, routing-blocked, routing-account-order, routing-quota-left, routing-auto-reset, routing-manual, quota-pools, quota-families and usage-accounts GUI tests pass in webkit. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real accounts; a cap acts on usage as last read, so a burst between reads can run a little past it.
2026-10-04 10:18:50 +08:00
providers: each model's credit rate shows by it in the provider's model list and an agent's model picker, Qoder's 0.5× and WorkBuddy's 0.03×, a discount's price before it struck through (01huadalang on Discord: qcoder 或者 workbuddy 能不能实时抓取或者按一段时间抓取能展示列表模型的积分消耗倍率,这样不用打开软件查看,能选择到便宜的模型) — magpie read each model's price only to tell free from not: Qoder's price_factor (06584e15) and WorkBuddy's "credits" ("x0.03", wbFreeCredits), so the lists said FREE or nothing, and picking a cheap model meant opening Qoder or WorkBuddy. catalog.Model now keeps the price, Rate (the credits a request costs, as a multiple; 0 is not listed, or Free) and RateWas (the price before a discount running now), and carries both through provider.Entry, agent.Option and the providers page's models. Qoder (and Qoder CN): Rate is the price_factor; RateWas an active promotion's before_promotion_price_factor, else an original_price_factor above the price (Qwen3.8-Flash: free, 0.1× struck through), and a promotion's 0 that isn't free is its price before times its discount_factor. WorkBuddy and WorkBuddy AI (the CodeBuddy plan; both builds read the same /v3/config): Rate is the credits (wbCredits; x0.00 stays FREE, an unreadable or infinite rate is none). A plugin's model gives it as it gives free: rate and rateWas on the model its provider.models hook lists, a number (0.5) or as the vendor writes it ("x0.03", "0.5×"), which host.js reads (rateOf) into plugin.Model.Rate/RateWas and pluginCatalog keeps; the moved Qoder and WorkBuddy users get it once their plugins send it. The badge is small and grey (the context badge's colours, no uppercase), beside FREE where that shows, its title saying what a request costs (每次请求消耗 0.5× 积分;折扣前 / 限时免费,原价). Rates are as of the provider's last list: a built-in's is fetched at start-up when it has none, on Refresh and when its key changes, a plugin's when its plugins are asked again (start-up, a sign-in, a plugin change); nothing asks on a timer. Kiro, Cursor, ZCode and Devin list no per-model rate magpie reads, so they show none. TestParseModelsRate (qoder) and TestWBCreditsRate (provider) are new and don't build without the change; TestFakePlugin now checks a plugin's rate given as a number and as "x0.03" (0 for both with host.js not reading them). model-rate.test.cjs (WebKit, en and zh: the chips' badges and titles, struck-through prices, grey not green, no stripe, a click picks and scrolls nothing, the picker's badges) fails without the change and passes with it; click-scroll, models-free-only, model-pick and favorite-star pass in WebKit. No request was made to Qoder, WorkBuddy or any vendor. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME, go test ./internal/gui pass.
2026-10-02 22:48:45 +08:00
gateway: a request archive, off unless turned on, keeps each call's headers and bodies in the user's own S3 bucket and a recent call reads its own back (Jorben on Discord: 请求存档) — the Gateway page's recent calls only ever held the last bodies in memory, the first 256 KB of each, with no headers, gone at a restart. A switch over Recent calls ("Request archive", POST /api/settings/archive, settings' requestArchive) turns it on, refused with a reason while sync isn't set up with an s3:// bucket; with it on, each call the gateway serves gets an id (UTC HHMMSS-<16 hex>, said back in X-Magpie-Archive-Id) and, once it is recorded, its method, path and query, request headers and body and response status, headers and body go as one JSON file to <prefix>/magpie/archive/<YYYY-MM-DD>/<id>.json in the bucket sync keeps its backup in, signed with the same client (davsync.Bucket, no new SDK). Before anything leaves, Authorization, x-api-key, Cookie, Set-Cookie, any header or JSON field or query parameter named for a key, token, secret or password, and anything that looks like a key or token anywhere in the text (the redaction rules' patterns, in a stream's data lines too) become [REDACTED] — gone, no placeholder kept; the archive is plain JSON, readable in the bucket, not sealed with the passphrase. Uploads go one at a time on their own goroutine from a queue of 16 calls, each body capped at the 256 KB the call already keeps and marked truncated; a full queue drops the call with a log line, a failed upload is logged and shown under the switch until the next one goes, and no request waits on any of it. A recent call the archive kept shows its date and id under its bodies, and "Fetch from archive" (GET /api/archive?date=&id=, only a date and an id of that shape) shows the request's and response's headers and bodies as the bucket has them, or says it isn't there yet. The hook is in record() and the Call literal, so PR #239's request log lines are not touched; the archived call carries its usage, and with #239 its vendor request id. Only calls through serve() are archived, not Codex's backend, image or video calls. TestArchiveOn, TestArchiveOff, TestArchiveName, TestRequestArchiveToS3 (a real gateway request through the signature-checking fake S3), TestScrub and request-archive.test.cjs fail without the change; TestSettingsSaveKeepsWhatItDoesNotSend now sets requestArchive. A sandbox magpie web with a fake vendor and a fake S3 server uploaded nothing while off; on, it uploaded a signed archive with none of eight planted secrets in it, read it back through /api/archive and in the page, and with the S3 server down served the call in 0.1 s and said why the upload failed. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (Claude account tests and TestClaudeRunEndsWhenTheClientLeaves pass alone); request-archive, click-scroll, old-webkit, sse-body, gateway-fold pass.
2026-10-01 17:13:34 +08:00
provider: a Codex reset about to run out is spent half an hour before it does, not three hours, and at once only when the account is held up past then (#718, thedavidweng: account A, held up for its five hours until 17:09 with 32% of its week used, spent a reset expiring that afternoon at 15:20 — within resetExpiryLead (3h) and its windows used — losing the 68% of its week it could have used between 17:09 and the reset running out). Spending a reset starts the windows again and loses what they have left at that moment, while the fresh windows are the same whenever it is spent, so check now spends it as late as is safe: resetExpiryLead is 30 minutes; an account holding a reset that runs out is looked at every 30 minutes (resetExpiryWatch), never past the start of the reset's last hour, and every 5 minutes within it (resetExpiryClose, nextResetLook), a failed read (by the reset last read, kept per account) or spend there tried again in 5, not 30; the loop goes over the accounts every 5 minutes (each read only when due), and check compares wall-clock times (Round(0)), so a Mac asleep past the lead spends it on the first pass awake. It is spent at once when the account is held up now (usedUp: a window that stops it at 100%) and won't be free again (BackAt; not known counts as not) before the reset would be spent: waiting can use nothing more, and spending it frees the account — any held reset that runs out, however far off. Windows not used still spend nothing; the week-used-up AutoUseCodexReset path is as before. expiringResetSpent takes whether the account is stopped and when it is back, and routing's Allowance.Restarts passes the same from its limits (heldUp), so Weekly pace's and Smart's deadline (dueRestart) is when check spends it. The GUI's tooltips (en, zh: 到期前约半小时, 若账号正被限流、要到那之后才解封,则立即用掉), quota auto-reset's help and settings' comment say so. TestResetSpentAsLateAsSafe walks the report's afternoon in 5-minute passes (reset running out 18:15: five hours free at 17:09 or 17:40 → spent 17:45; free at 17:50, after it runs out, not saying when, or the week used up past it → spent 15:20; not held up → 17:45; nothing used → never; at most 20 reads), TestResetExpiryLastHour (look cadence, a failed read and spend retried in 5 minutes, spent after sleeping past the lead) and TestRestartsWhenHeldUp fail on the old code; the existing tables move from 3h to the new times. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once under load, passes alone and in the package rerun); codex-auto-reset and routing-reset-deadline pass in webkit. Not tried with the reporter's accounts. The spend reads the account again under autoReset's lock (stillExpiring, spendExpiringNow) and spends nothing if the week's used-up spend has started its windows again since the look, so a held-up account isn't given two resets; TestExpiringSpendReadAgain.
2026-10-04 07:03:16 +08:00
portable: a portable magpie on Windows keeps WebView2's profile in data\webview2 and makes no Start-menu shortcut, and an installed one makes the shortcut once and never puts back one the user deleted (#508, suixinex: 便携模式时还是会创建 C:\Users\<username>\AppData\Roaming\magpie.exe\ 文件夹; 开始菜单的快捷方式删除后再打开magpie会重新添加) — the AppData\Roaming\magpie.exe folder is WebView2's user data folder: Wails v3 passes WindowsOptions.WebviewUserDataPath to go-webview2 as its DataPath, and left empty it is %APPDATA%\<exe name>, which portable mode (appdir) never set. The app's Windows options (windowsOptions) now give it appdir.WebView(): data\webview2 when portable, "" (WebView2's default, so the pages' storage and cookies there are kept) when installed. The shortcut came back because shortcut.Ensure ran at every start of the desktop app and wrote Magpie.lnk whenever it was missing or stale, with nothing recording that it had been made. Ensure now does nothing when portable (no shortcut, no App Paths entry); installed, it makes the shortcut on the first start and records that in start-menu-shortcut in magpie's folder (with the exe), keeps one that is still there pointed at the exe (moved exe, update), and makes none once the file is recorded and the shortcut is gone. A user who deleted it under an older magpie isn't given it back on updating either: a missing shortcut with no record but an App Paths\magpie.exe entry already there (every older magpie wrote it at each start) counts as deleted and is recorded as such. The Start menu, the shortcut writer and App Paths are behind function variables, so the decision is tested with fakes on any OS. Checked for other leaks: autostart only refreshes an entry the user turned on (Refresh never enables), the magpie:// link registration is needed for import links and stays, as appdir's doc says, as do temp folders; the update stages beside the exe, caches already follow appdir. Not changed: the Mac's WKWebView and Linux's WebKitGTK keep their own data in the profile (Wails has no option for it), which #508 didn't report. Nothing ran on Windows (the test box isn't reachable): no real WebView2 was started with the data path and no real shortcut was made or deleted. TestEnsureOnce, TestEnsureAfterOlderMagpie, TestEnsurePortable (fake Start menu in a temp folder), TestWebView and TestWindowsOptionsWebViewData fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet ./..., GOOS=windows go test -c for shortcut, gui and appdir, go test -tags nogui -count=1 ./... under a temp HOME pass (TestUsageKeepsTimeToFirstToken and TestUserPath failed once on timing, pass rerun alone; untouched).
2026-10-02 14:39:49 +08:00
plugins: a 「国内镜像」 switch on the Plugins page downloads the list, plugins and Bun from mirrors in China first (Discord, akic404: GitHub/npm are slow from the mainland; asked for ghproxy) — Plugins › Mirrors in China, off by default, kept in settings (chinaMirror). On, source asks a copy of the very file before its official address: npm's registry (packages, versions, search) at registry.npmmirror.com, Bun's release zips at registry.npmmirror.com/-/binary/bun, and the community registry.json at cdn.jsdelivr.net/gh/magpie-community/plugins@<branch>; bun add gets BUN_CONFIG_REGISTRY=npmmirror unless the environment names a registry. No proxy: gh-proxy (MAGPIE_MIRRORS' GitHub fallback) handles nothing the switch sends, since a proxy could hand back other code. A mirror asked first that can't answer (404/403/5xx, e.g. a version it hasn't synced) falls through to the official address; npm's weekly downloads stay api.npmjs.org's (npmmirror counts its own). Checked by hand: npmmirror's bun-v1.4.2 darwin-aarch64 zip has the SHA-256 pinned in bun.go, jsDelivr's registry.json is byte-identical to raw.githubusercontent's, and bun 1.4.2 add through npmmirror locks the same sha512 integrity npmjs gives. TestChinaFirst, TestChinaComesFirst, TestChinaMirrorAnswers, TestChinaMirrorsMissIsAskedOfficially, TestDoFaithful, TestBunInstallsFromChinaMirror and TestPluginsMirrorSwitch cover it. A live run with the switch on fetched the list from jsDelivr, npm info from npmmirror and Bun from npmmirror (checksum passed); a sandbox magpie web showed the switch in the Plugins head, saved it, kept it after a reload, and a click didn't move the page. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried from inside mainland China.
2026-10-03 15:14:45 +08:00
accounts: on Linux an account put first is read first at once, and on Windows a file being replaced isn't read as none (goal: 插件订阅 provider 跟内置的所有逻辑都要一模一样, run on Windows and Linux) — the provider tests run on the Windows test box and in WSL found two bugs of magpie's own files. Linux stamps a file's time from a clock a tick (a few milliseconds) coarse, and filememo took same time and size for the same file: two accounts swapped (Devin, Grok, WorkBuddy, ZCode, Claude…) read back in the old order, so magpie kept using the account the user had just moved down, and a moved-back plugin's accounts read as before. A file written within the last two seconds is now also compared by its bytes. On Windows a file being renamed over refuses to open for a moment ("Access is denied"), and one held open refuses to be renamed over: plugin-auth.json, plugins.json and the plugin providers read as nothing (a usage fetch right after a move showed no cards), logins.json failed to save, and host.js, reading plugin-auth.json as {}, would have written the other accounts away on the next sign-in. New internal/steady waits such a read or rename out (half a second at most, Windows only); filememo, edit.WriteAtomic, writePrivate, Kiro's writes and the plugin store go through it, host.js retries its read and rename, and the plugin store writes its files by a rename. Tests: USERPROFILE beside HOME in the test homes, the fake devin (a shell script) skipped on Windows, plugin-auth.json's mode bits unchecked there. TestReadSameSizeSameTick fails on Linux and TestRenameOverAFileHeldOpen/TestReadAFileHeldOpen on Windows without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; on Linux (WSL) the provider and plugin packages pass whole (13 failed before), on Windows the plugin package and the move/plugin/Grok/Devin/usage tests pass 10 times over (TestMoveToPlugin failed one run in a few before); 15 built-in tests that assume Unix (mode bits, Keychain, shell-script CLIs) still fail there, as before.
2026-10-01 13:27:12 +08:00
provider: Trae CN's daily check-in (每日签到) is pressed once a Beijing day for each Trae CN account, as WorkBuddy's is (#694, Hu9956: TRAE cn也有每天签到送100积分活动,这个会考虑支持吗) — Trae CN is served only by its community plugin (trae-cn, no built-in, no mover), so magpie asks Trae through the plugin's fetch, which from trae 0.1.4 (magpie-community/plugins, branch trae-checkin) sends api.trae.cn's /trae/api/ pages as the account (Cloud-IDE-JWT renewed when near its end, its device id): POST /trae/api/v2/ug/checkin_credits/status with {}, and /claim with {} only while enable is set and checked_in isn't. These are the endpoints autumnsentiment/Trae2api-cn (the relay the plugin's protocol came from) and qilimixingkong/trae-checkin both call; an unauthenticated POST to /status answers 200 {code:1001, enable:false, checked_in:false} and a made-up sibling path 404s. Answers: not enabled → inactive, checked_in → done with its credits, a claim → claimed +credits(+extra_credits), 9095 (this device is in today) → not eligible for the day (no other device id is sent to get round it, as qilimixingkong's does), 1001 or the plugin's X-Magpie-Sign-In: expired → failed, sign in again; 9074 and anything else → failed and tried again; a plugin before 0.1.4 (400, not JSON) → failed, update it. The day loop and its file are WorkBuddy's, made generic: checkin.go's checkiner (accounts as checkinAcct{key, do}) holds checkinNow, which wbCheckiner now wraps; keepCheckedIn and wbCheckinLoop.tick take a checkinRunner and a log label; withCheckins goes through markCheckins over checkinCards. Trae's is kept in trae-checkin.json by trae-cn|uid; settings.TraeCheckin (traeCheckin) is its own switch; the gateway runs KeepTraeCheckedIn beside KeepWorkBuddyCheckedIn; WithCheckins marks Trae CN cards too, with SubscriptionQuota.CheckinBy and WorkBuddyCheckin.By "trae" (never kept), so the Usage card's row uses POST /api/settings/trae-checkin and /api/usage/trae-checkin and its first Trae card holds Trae's own Auto check-in and Check in now; Settings' Usage part gains a Trae CN tab (traeList) shown while an account is signed in or the switch is on; magpie accounts checkin and the TUI's c check Trae CN accounts in too (the footer says daily check-in). Tests: trae_checkin_test.go (once a day: status then claim, nothing sent again that day, done the next; inactive, 9095 held for the day, 9074 retried, expired, an old plugin; Trae cards marked, another plugin's not), TestTUIChecksTraeIn, usage-checkin.test.cjs's Trae CN card (its own switch and press, WorkBuddy's untouched) and settings-groups.test.cjs's Trae CN tab; the WorkBuddy check-in tests pass unchanged. go vet ./internal/..., builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass; the GUI tests pass on WebKit (Playwright's Chromium here fails to start, icudtl.dat missing, for the old tests too). Not tried with a real Trae CN account: none is signed in to the owner's magpie (no trae-cn in plugin-auth.json), so the authenticated status and claim answers' fields are as both open-source clients read them, not seen here.
2026-10-04 03:54:23 +08:00
update: install.sh and magpie update take --proxy and --mirror, and magpie update mirror keeps one for the app's updates too (akic404 on Discord: 安装和update如果没有梯子也是麻烦事,能否在install.sh和magpie update后加子选项?--proxy等可自定义或默认ghproxy) — install.sh had no options and fetched with curl alone, and magpie update took none; without a way out to GitHub both failed. Now both take --proxy <url> (http, https, socks5, socks5h; install.sh also MAGPIE_PROXY=) for the feed and the download, and --mirror <prefix> (or MAGPIE_MIRROR=), a GitHub download mirror the release file is fetched from as <prefix>https://github.com/…; only github.com / githubusercontent.com URLs are put through it. No mirror is used unless one is given: magpie names none and has none by default. The feed with each file's SHA-256 is always fetched from usemagpie.ai, never through the mirror, so a file the mirror changed is refused and deleted ("… from the mirror X does not match its checksum from https://usemagpie.ai; not installed"); macOS still checks the app's signing team. magpie update mirror <prefix>|off saves Settings' updateMirror (http(s) only), which the app's own background update follows too; magpie update --mirror off skips it once; the Settings page's save keeps it. The app's update already followed Settings → Proxy (#294); magpie update --proxy overrides it for one run. The environment's ALL_PROXY / all_proxy is now honoured when HTTPS_PROXY / HTTP_PROXY are unset (Go's ProxyFromEnvironment ignores it), with NO_PROXY as curl reads it. magpie update takes --flag value or --flag=value, and an unknown word is an error instead of an update. The help, README and the docs' FAQ (en/zh/ja) say how. install.test.mjs's proxy, mirror and tampered-mirror tests, TestAllProxyFromTheEnvironment, TestUpdateThroughAMirror, TestUpdateThroughAProxyGiven and TestUpdateFlags fail on the old code and pass; TestSettingsSaveKeepsWhatItDoesNotSend covers updateMirror. A dry run of install.sh as Linux amd64 into a temp HOME installed the real 0.1.773 with its SHA-256 checked, refused a local mirror serving a wrong file, and with --proxy to a closed port failed reaching the site through it. go vet, builds (darwin, GOOS=linux, GOOS=windows), sh -n, node --test site/install.test.mjs and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeIdentityServedWhileAsked, a timing test that passes alone. Not tried through a real proxy server or a real public mirror; no install.ps1 exists for Windows, and the app has no field for the mirror.
2026-10-04 00:19:47 +08:00
docs: every built-in subscription a plugin serves once moved says so in its code, and AGENTS.md says where a fix for it goes (user: 对内置的订阅 provider 在代码层面和文档层面加上注释,防止又有别的 agent 在改内置订阅 provider 的代码而不知道生效的其实是对应的插件代码) — a moved Cursor, Devin, Grok, Command Code plan, Kiro, Zed, Qoder, Factory, Xiaomi MiMo, ZCode or WorkBuddy (provider.Moved; since v0.1.642 also the default for a new sign-in, provider.Adopt) is signed in, listed, requested and metered by its @magpie-community plugin, and none of the built-in's code in internal/provider/<name>*.go, internal/gateway/<name>.go, internal/zed or internal/qoder serves it; a fix made there alone never reaches those users. Each of those files now opens with a PLUGIN-SERVED notice naming the subscription's id, its plugin package, where the plugin lives (magpie-community/plugins, packages/<name>) and the mover whose min to raise so moved users get the fixed plugin (keepMovedCurrent); Qoder's adds that Qoder CN isn't moved and still runs there. AGENTS.md (CLAUDE.md imports it) lists the moved built-ins with their packages and movers, how to tell whether a user's is moved (move.state "plugin", onPlugins), the order a fix goes in (the plugin, publish, raise min, the built-in only for those not moved), the magpie code that does run for a moved one (internal/plugin, plugins.go, plugin_usage.go, pluginsignin.go, migrate*.go, app.js's subOf/pluginSubs/startPluginSignIn), and the parity tests. movers in migrate.go and SUBS in app.js point there too. TestMovedBuiltinsSayTheirPlugin fails for a mover whose code carries no PLUGIN-SERVED notice naming its id and package (it failed while a wrapped package name was split). Comments and docs only, no behaviour changed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut failed once on their timing under load, pass alone; untouched).
2026-10-02 19:37:02 +08:00