Raven: "check for updates" only updated skills already in the library; when
dontbesilent2025/dbskill or mattpocock/skills adds a skill, magpie never said.
CheckSkills now asks GitHub's git trees API once per repo@ref (ETag-cached, a
304 is free) and only when a skill folder under the installed skills' common
folder (newBase) is neither in the library nor already offered does findNew
fetch the tarball for names and descriptions. library.json's seenSkills keeps,
per repo@ref, the paths the install picker showed and the ones set aside, so
an offered skill is offered once. The Skills page shows them under "Also in
their repositories" with Add (to the agents that have the repo's other skills)
and Ignore, plus Add all / Ignore all (/api/library/skills/{add-new,ignore-new}).
Verified: TestCheckSkillsFindsNewOnes (fake repo: nothing offered after
install, no tarball on a second check; two added skills offered, a nested one
and a .wip one not; Add installs with the repo's agents; Ignore keeps it out
on the next check via 304; remove clears) fails without the change (no
NewSkills / findNew) and passes -race -count=10; TestNewBase. A real run in a
sandbox HOME against GitHub: tdd from mattpocock/skills/skills/engineering
offered its 19 siblings; dbs from dbskill offered nothing (the picker showed
all), 33 with seenSkills wiped, and Add installed dbs-action. GUI:
skill-new-in-repo.test.cjs (Chromium+WebKit, 860/440px, en+zh) fails on
main's library.js and passes; gui-ja, gui-de, click-scroll, library-items,
every-skill, pick-skills, import/remove-all-skills, skill-groups,
skill-repo-groups, github-token, library-all pass. -race on library, gui,
tui; gofmt, vet, darwin/linux/windows builds.
The page already sends one request for a group (agents-some), but its one
change ran a sync that, for every skill given as a copy (every WSL agent,
Windows without the right to link, agents set to copies), hashed the copy
and the library's skill twice, in every agent, on every change, and the
page's read hashed the copies once more. Over a WSL distro's share that is
every file of every skill read through 9P each click.
A copy's mark now keeps the hash of what was copied (markCopy). A copy
whose mark says the library skill's current hash, with nothing in it newer
than the mark, is current without its files being read (copyIsFresh);
takeEdits' walk of a copy is not done a second time in the same sync, and
each library skill is hashed once per change (libHash). Copies with an old
mark, or edited since, are hashed as before. Claude Desktop's copies and
project copies use the same check.
Measured in a sandbox (55 skills x 31 files, copies for 10 agents, a
5-skill group switched on for an 11th): the change went from 1.54s to
0.31s and the page's read from 0.66s to 0.25s on a local SSD; the copies
not touched are no longer read at all, which is the cost over WSL.
TestSyncDoesNotReadUnchangedCopies fails without the change ("codex's
copy of pdf was read and made again"): an unreadable file in an unchanged
copy is left alone now. TestCopyFreshByMark checks a changed library skill
is still copied again, an old mark without a hash still works, and an edit
in a copy is still taken. go test -race ./internal/library ./internal/gui
pass, the new tests pass -race -count=10; vet, gofmt, darwin/linux/windows
builds pass. Not run on Windows or WSL.
The servers page had only By agent and the order, each with its own "In the library" heading, so with both the heading showed twice. Turning every server on or off, or removing them all, took one chip or one row at a time. The page now has one heading (lib-serverhead, which wraps when narrow) holding the order, Select, Turn all on, Turn all off, By agent and Remove all, as the skills page does. The skills page's helpers now serve both pages rather than being copied. picking names the page ("skills" or "servers"). pickingFor, pickRow, pickBar and groupChips(kind) are shared, and so are everyButtons, confirmEveryOff and everyOf, which replace everySkillButtons and everySkill. removeEveryButton, confirmRemoveEvery and removeEvery replace removeEverySkillButton. The skills page says and does what it did before.
- Turn all on and Turn all off post every agent shown to servers/agents-all (EveryServerAgents, now serversAgents(nil, …)). A hidden agent is left alone. Turn all on is greyed once every server is on for every agent that can reach it. Turn all off asks first, in the page.
- Select puts a box on each row. A row's click ticks it rather than opening the server. The bar's chips post the servers picked to the new servers/agents-some (SomeServersAgents), which gives an agent only the ones it can reach (mcpFile.supports) and leaves unnamed servers and agents as they are. An agent that can reach none of the picks has its chip greyed, with the reason.
- Remove all asks first. It says which agents lose the servers, the projects they are taken from, the sign-ins forgotten, and that the agents' own servers stay. It then posts every name to the new servers/remove-all (RemoveServers). This is one change that takes each server out as RemoveServer does: out of the library, its projects and its icon (removeServer, now shared by both), then out of every agent through syncMCP, which takes only what Applied says magpie wrote, and then forgets its sign-in. A name the library hasn't is listed in Unremoved, and the rest go all the same.
- The pick bar wraps, so its chips and Done fit at 440px in ja and de.
TestRemoveServers checks a server the user put in Claude Code's file and Codex's own "docs" of a library server's name: both stay, while fs, docs and neon leave the library, both agents and the project's .mcp.json, and the project's own "team" stays. neon's sign-in is gone, "nope" is reported, and an empty list or one with only "nope" errors. TestSomeServersAgents checks that Codex gets fs but not the SSE web, that git stays untouched, that a call naming no servers or no agents errors, and that a call naming an unknown server writes nothing. Both pass with -race -count=10, alongside TestEveryServerAgents and TestRemoveServer*.
library-servers-bulk.test.cjs passes on Chromium and WebKit in en, zh, ja and de at 440px. It checks one heading with every button, Turn all on's post without the hidden Copilot CLI, Turn all off's Cancel posting nothing and Confirm keeping Copilot CLI's, the pick bar's codex chip posting agents-some for the two picked and its greyed state for web alone, the All box, Done, and Remove all's lines, Cancel and post. It also checks for no sideways scroll and that each control clicked stays where it was on the screen. The fake check answers every server, since an unchecked one is checked again at each drawing. It fails without the change and passes 6 runs beside pick-skills, every-skill and gui-ja at --test-concurrency=4. by-agent, pick-skills, every-skill, remove-all-skills, group-*, library-*, mcp-signin, project-servers, skill-*, i18n-keys, gui-ja and gui-de pass. skill-how's WebKit case and click-scroll's "Live and a day" fail the same on the unchanged origin/main when run beside each other, and pass alone. go test -tags nogui ./internal/..., go vet, gofmt -l, and the linux (nogui) and windows builds pass. Not tried in the real app. docs/subsystems/library.md says it.
On Windows, RTK installed by winget was 0.50.0 and GitHub had 0.51.0, which winget didn't yet. Library → RTK showed "v0.51.0 可升级" with Upgrade; Upgrade ran winget upgrade, which rightly said "winget's RTK is 0.50.0 so far…", and the card went back to "可升级" with Upgrade. The card compared rtk's version only with GitHub's latest release, never with what the package manager that installed it has, and the upgrade's answer was forgotten as soon as the card was read again.
RTKView.CheckLatest now also asks, when the latest release is newer than this rtk and it was installed by winget or Homebrew, what that package manager has: winget show --id rtk-ai.rtk --exact (the version is the first one after the line naming the package, as winget's labels are in the system's language), and brew info --json=v2 rtk without auto-update, as the Agents page's CLI updates read Homebrew. An upgrade through either records the version it left as the newest that package manager has. The answer is kept six hours (a failure fifteen minutes) and is asked again after that. When it has nothing newer than this rtk, the view says waiting (winget or Homebrew) and waitingHas. The card then shows "v{v} is out · waiting for {via}" (zh "v{v} 已发布 · 等待 {via}", ja, de), with a tooltip saying what it has, and no Upgrade button. Once it has the release, Upgrade is back. A package manager that can't be asked, or doesn't say, leaves Upgrade offered as before. GET /api/library/rtk runs the whole check within its 3s budget. magpie library rtk prints the same. Cargo (built from rtk's repository) and rtk's own script (its GitHub releases) can't lag, so they are unchanged. The Agents page's CLI updates already compare with the installing channel's own latest (Homebrew's or npm's), so they don't have this bug. The Japanese "v{v} is out" is shortened to "v{v} 公開済み", because with Upgrade beside it the card head overflowed at 440px.
TestRTKWaitsForWinget (a fake winget whose show prints Chinese labels: unaskable leaves Upgrade; asked, it waits; the reporter's upgrade waits with the "so far" note, and so does the card read again; winget given 0.51.0 and asked again, Upgrade is back and works) and the extended TestRTKUpgradeBrew (Homebrew's 0.49.0 after brew upgrade waits, on the card read again too) fail without the change (waiting <nil>) and pass. TestRTKChannelVersion covers winget show in English, German and Chinese with spinner frames, no package, and brew info's JSON. -race -count=10 on them. library-rtk-upgrade.test.cjs's new test (Upgrade finds winget behind: the waiting tag, no button, the note; a reload still waits; winget has it: Upgrade back; card head and page fit at 440px) fails without the library.js change and passes on Chromium and WebKit in en, zh, ja and de. i18n-keys, gui-ja, gui-de and the library-rtk-* tests pass on both engines. go test -tags nogui ./internal/..., go vet, gofmt -l, darwin, linux (nogui) and windows builds pass. Not tried with a real winget or on Windows: winget show's output is taken from its documented format, and the version line is assumed to follow the package line in every language.
With skills given as copies, a sync made a copy that differed from the library's again from the library's, so an agent's edit was reverted when magpie restarted. A sync now takes edits first (takeEdits): a copy with anything newer than magpie's mark was edited since it was made, and the newest such edit is mirrored into the library (the library's old version kept with the backups), then given to every other agent. The library's own change wins when it is newer, and an edit that loses is set aside with the backups, never lost. TestSkillCopyEditReachesTheOthers fails without the change; TestSkillHow now expects an edit made before a switch to links to reach the library.
White Immortal asked for the skills of github.com/mattpocock/skills to be one group, the ones installed before too. Skills installed from GitHub in magpie were already grouped by repository, but one installed another way sat under On this computer. Each skill on the Library page now carries the repository it came from (repo). A skill installed from GitHub, or by CC Switch from there, says so as before. For the others it is traced:
- npx skills add (the skills CLI) writes ~/.agents/.skill-lock.json, with each skill's repository by name. A skill in ~/.agents/skills, or moved into the library from there, gets its repository from it.
- A skill linked from a git clone gets the remote "origin" of the clone it is in (up to four folders up). The URL can be https, ssh or scp-style, and a user or token in it is never kept. A checkout that is the home folder, or an app folder in it such as ~/.claude kept in git, is the user's dotfiles, and its skills are not grouped by it.
- When a skill is brought in from an agent's folder, its repository is traced first and kept as From, since a copy leaves its .git behind.
- Typing a repository in Install (or opening it from the market) notes it as From on the skills the library has already whose SKILL.md is that repository's very own. Installing from github.com/mattpocock/skills again then groups the ones installed before. A skill of the same name whose file differs is someone else's, and stays where it was.
From and a traced repository only group. Such a skill is still not updated from GitHub. A repository on another git host (gitlab.com/…) is a group of its own, and its icon opens it at that host. A repository group with skills that only were traced there goes by name, not by a folder in it. TestSkillRepoTraced, TestRemoteRepo and TestInstallAgainNotesFrom are new, along with skill-repo-groups.test.cjs (Chromium and WebKit, English and Chinese). The GUI test fails against the old library.js. go vet, go build (GOOS=linux and windows, nogui) and go test -tags nogui ./... under a temp HOME pass. TestProvidersAnswerWhileListsComeIn and TestUserPath failed once under load and pass on their own. The library's GUI tests pass on WebKit. On Chromium, import-all-skills, library-own-folder and shared-skills fail the same way at HEAD without this change.
Both folders can hash to an empty string after read failures. Treating
those hashes as equal makes relink delete the agent's copy without a
backup. Require a nonempty matching hash so an unreadable copy takes the
existing backup path.
Cover switching from copies to links with unreadable files in both
folders and edited notes in the agent's copy.
* library: a skill copied into an agent where it can't be linked is copied again when the library's changes
Where magpie can't make a link (Windows without Developer Mode, where
os.Symlink needs a privilege), link() gives the agent a copy of the
library's skill instead, marked as magpie's. syncSkills then took any
entry of its own as done, so the copy stayed as it was first made: an
update of a skill from GitHub (UpdateSkill, Update all), or a change to
the skill's files in the library's folder, never reached Claude Code,
Codex or any other agent on such a machine, while a link shows the new
files at once. The project skills already keep their copies up with the
library's (Project.place compares hashDir and copies again).
syncSkills now does the same for an agent's copy: when hashDir of the
copy differs from the library's skill, a new copy is made beside it and
put in its place. A link, or a library skill that can't be read, is left
as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* library: move the old copy of a skill aside before putting the new one in
copyIn removed the agent's old copy in place and then renamed the new
one in. When a file in the old copy can't be removed (held open by an
antivirus, an editor or the agent on Windows), the removal fails
part-way and leaves a copy with no SKILL.md and no marker; magpie then
no longer knows it for its own and every later sync refuses the skill.
Rename an old copy aside to .<name>.magpie-old, rename the new copy into
place, and remove the old one best effort. If the new copy can't go in,
the old one is put back. A link is still replaced in one step. Leftovers
are removed on the next sync and when the skill is taken away.
* library: test the swap for a WSL agent's copy too
A t.Copy target (an agent in WSL) gets its copy made again through the
same move-aside swap: with a file in the old copy that can't be removed,
the agent still gets the whole new copy that magpie knows for its own,
and the leftover is cleared by a later sync. Fails with an unlink-first
swap.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#825 (cjyrainbow): Settings → Web search's 代搜供应商 offered only DeepSeek though they have many providers. Only a provider that searches the web by itself can search for another model — Claude, Codex and Grok accounts; the APIs of Anthropic, OpenAI, DeepSeek, xAI, Zhipu and OpenRouter; Gemini on a Google sign-in; a Kimi Code plan — so the list is right, but the row never said so. It now names the providers on that are left out (searchLeftOut; past six, the first five and how many more) and says that for their models the search APIs below search. zh, ja, de strings. searcher-pick.test.cjs (WebKit, en and zh) checks the line and the cut-short list; it fails on the old code.
#824 (babyjiang7): Alma reads personal skills from ~/.config/alma/skills (its skills service, in Alma.app's bundle: personalSkillsPath, beside ~/.claude/skills, ~/.codex/skills and ~/.agents/skills), so the library now gives it skills there, Claude Code's, Codex's and the shared folder's counted as its too. It still has no user-wide instructions or MCP file of the kind magpie writes, and says so. TestAgentsSkillsFolders (a skill given to Alma lands in ~/.config/alma/skills) and TestTakesRefusesAlma fail on the old code.
go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut flaked, pass alone).
* Add German translation and harden paths against unset env vars
Add a German locale to the GUI, tray menu, usage alerts and website home page, and refuse to start when the home folder is unset or relative so nothing is written under the working folder; the pi plugin no longer adopts the user's PI_CODING_AGENT_DIR, and an inherited APPIMAGE no longer selects portable mode. Scripts refuse to run from the wrong folder or empty a shared out folder, and a new internal/testenv sandboxes every test package from the user's real files, which also fixes the Claude picker and Termux installer tests on Windows.
* Makefile: know magpie's folder by its go.mod, so paths with spaces work
The check compared $(CURDIR) with $(dir $(abspath …)), which make splits on spaces, so a checkout under 'My Projects' or 'John Doe' refused every target. It now reads go.mod relative to the working folder and wants magpie's module there: make -C and a symlinked Makefile work, make -f from another folder still stops before any recipe runs.
* i18n: German for the strings main added since, with a parity test
75 strings main added (group order, patterns, slow-start wait, usage caps, RTK savings …) get their German, and two that main dropped go. gui-de.test.cjs checks, as gui-ja does, that de has every zh string with its placeholders and tags, and that the window reads in German when picked or on a German system.
* appdir: ignore relative folder variables when reading, keep them for children
CheckEnv no longer unsets anything. magpie reads the XDG folders, APPDATA/LOCALAPPDATA and every agent's folder variable through appdir.Getenv/LookupEnv, which pass over a relative value; the environment stays as it is, so the agents magpie starts get the variable as the user set it. That covers what proc.UserPath lends from the login shell too, which is set after main's check and reached the desktop app unchecked. On Windows a path rooted on the current drive (\Users\x, Git Bash's /c/x) counts as usable, and magpie version and help answer without a home, before run makes any folder.
* Sandbox the tests main added since
internal/wslrun gets testenv's TestMain, and four tests main added that set HOME alone set their home with testenv.SetHome, so on Windows they don't read the runner's profile through USERPROFILE.
* agentenv: PI_CONFIG_DIR and OPENCODE_DB take relative names on purpose
omp puts $PI_CONFIG_DIR under the home and OpenCode a relative $OPENCODE_DB in its data folder, so neither depends on the working folder: appdir no longer passes them over.
* i18n: German for the Library's multi-select and System One's decision API
The 19 strings main added since (#791's skill multi-select, the decision API snippets) get their German; the one main replaced (four APIs → five APIs) goes.
* appdir: tidy the import into magpie's group; say what Go makes of /c/x
The appdir import joins the group of magpie's own imports where a file has one. rooted's comment now says that Go takes a Git Bash path like /c/x as \c\x on the current drive, not C:\x.
* agent: AtomCode is wired like its own CodingPlan sign-in — one provider account, one model table per catalog model
AtomCode (atomgit.com/atomgit_atomcode/atomcode, AtomGit's open Rust terminal agent) keeps its settings in ~/.atomcode/config.toml ($ATOMCODE_HOME). Its account schema, what its own CodingPlan sign-in writes, is one [provider_accounts."magpie"] table at the gateway's /v1 (protocol openai, gateway.TokenFor("atomcode") as the key, so a request is its agent's even when the UA is overridden) and one [models."magpie/<provider>/<model>"] table per catalog model: the model string goes to the gateway as it is to resolve, with the model's context window (AtomCode compacts as it nears it; a 128000 one is told when the model's isn't known, and an output above the told window is cut to it), its output limit, vision and reasoning_effort_levels; reasoning_effort rides on the table once the user picks one. Picking a model points default_provider and default_model at it — default_model wins when both are set, as atomcode reads it — and stashes the defaults the user had; stepping out puts them back. The picker offers the user's own model tables and the catalog via magpie; the Library gives AtomCode its ATOMCODE.md, its mcp.json (omp's shape) and skills/; its UA is atomcode/<version>.
Found against a real atomcode 5.2.1: the documented legacy [providers.*] tables work, but its own sign-in writes the account schema, so magpie writes that; reasoning_effort, max_tokens and supports_vision were verified to reach the request from the model table, and default_model to win over default_provider. Verified end to end on a real install: atomcode --provider "magpie/…" answered through the gateway, and the Agents page shows the tile, its picker and the Usage attribution. TestAtomcodeWiring, TestAtomcodeRestoresOwnDefaults and TestAtomcodeSync fail on the old code; TestDriftUnwiredEveryAgent and TestUnsetAgentsReadEmpty cover it from All(). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./internal/agent ./internal/library ./internal/agentenv pass; the plugin-host and timing failures of the full ./... run fail on main without this change too.
* tweak
* agent: AtomCode's config is matched by its own spellings, and Sync keeps the effort — AtomCode rewrites its config through serde on every /model and /cd, and that writes the account header bare ([provider_accounts.magpie]) where magpie wrote it quoted, so after the first rewrite Check read the wiring as gone, Sync and every pick appended a second account table (edited TOML is invalid: table magpie already exists) and clearing left the unquoted table and its api_key behind. The account is now matched by exact name in its three spellings (quoted, bare, literal — magpie-2 stays out) and the model tables by their decoded keys (kimiModelKey, replacing atomcodeModelName), through a new SetTOMLTablesMatching that takes exact names beside prefixes; SetTOMLTables delegates with none, unchanged. Sync carries each magpie model table's reasoning_effort over when rewriting — it ran at every gateway start and catalog change and dropped both an effort picked in magpie and one picked with AtomCode's /effort, and picking another model cleared every table's. The effort Set validates against the levels AtomCode knows (low, medium, high, xhigh, max — its clamp_effort_to_levels drops none, minimal and ultra), a model without levels offers none now, and the low/medium/high fallback is gone with it. supports_vision is written explicitly false, where AtomCode otherwise guesses from the model name. The user's legacy [providers.*] entries join the picker, a stashed default_model naming one comes back on Disconnect, and picking one sets default_provider and takes default_model out, as the legacy shape reads. site docs (en, zh, ja) list AtomCode. TestAtomcodeSync rewrites the config into the bare and single-quoted spellings before Sync and again before clearing and fails on the old code (drift reported, effort dropped, unquoted table left behind); TestAtomcodeLegacyProvidersArePickableAndRestored and TestAtomcodeEffortLevelsMatchAgentSupport are new. Reported by yetone on #736. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./internal/agent ./internal/edit pass.
* agent: TestAtomcodeWiring picks an effort through the field too — the write side of the validation the last commit added (a level AtomCode knows lands as reasoning_effort on the model table, one it does not know is refused) had no coverage left: the test only read back what AtomCode's own /effort had written. provider.SetModelEfforts gives fake/m1 levels to offer.
* agent: Disconnect puts AtomCode's default_provider back only where AtomCode still has it — the restore checked the stash's value was not magpie's but not that the table it names was still there, so a table the user had deleted in between came back as a dangling default (the account schema's default_provider names a model table, the legacy shape a [providers.*] one; own answers for both). The default_model branch checked already. TestAtomcodeRestoresOwnDefaults deletes the stashed provider's model table before clearing and fails on the old code.
* agent: AtomCode joins the CLI updates — npm @atomgit.com/atomcode, the brew cask atomcode, and the curl script's ~/.local/bin/atomcode updating itself with atomcode upgrade, so the Agents page shows its version and a newer one as the other agents' do (#736). TestHowInstalled covers the npm prefix, the cask and the self-updating installer.
* agent: AtomCode's self-updater waits for its install paths to be confirmed — the branch recognized a binary under ~/.local/bin as `atomcode upgrade`, on a docs page, while the README shows another path and the command has not been driven outside the help text; the reviewer asked for it removed until the standalone command and the script's real default are settled. npm and the brew cask stay, with their tests; a comment marks what is pending.
---------
Co-authored-by: googya <118465+googya@users.noreply.github.com>
"Add from folder" pointed at a skill already in ~/.config/magpie/library/skills emptied every file of it: InstallSkills' symlink failed (it was there) and its fallback copied the folder onto itself, O_TRUNC first. A folder from elsewhere by a name the library's folder already had was copied into it the same way. Such a folder is now refused ("…is in the library's folder already: bring it in from the skills found there"), anything by that name in the library's folder is never written into, the copy fallback runs only when the symlink failed for another reason, and copyDir refuses a destination that is its source or inside it.
A skill put in the library's folder by hand, not in library.json, was shown nowhere: foundSkills now scans the library's folder too, first, and such a one gets FoundSkill.Library (magpie's links to it in the agents count as the agents having it). Bringing it in lists it where it is: nothing moved or linked, Source nil for a folder of its own, {folder, its real path} for a link. The found row says "in the library's folder, not listed" with its path, and Bring in that it is listed where it is (en, zh).
Codex, Gemini CLI and the others that read ~/.agents/skills themselves got a ~/.codex/skills/X link to a library skill whose real folder is in ~/.agents/skills, and saw it twice. A user's own entry there (not magpie's link) that is the very folder the library's is now counts as the skill being in the shared folder: no link of their own is made, magpie's made before are taken away, and a byte copy of their own is kept aside with the backups as before. Nothing in ~/.agents/skills is moved or removed. SkillView.Always names the agents that read that folder, and their chips are lit, disabled, and say "Codex reads ~/.agents/skills itself, where this skill is kept — it has it whatever is ticked here" (en, zh); a click on another chip or All neither sends nor darkens them.
A skill's kind and "linked from" are now what its library entry is, not what was written when it came in: a linked folder moved in by hand is the library's own, shown without "linked from", and its Remove says the folder goes to the backups, as removeSkill does (which now also counts a junction as a link, as the page does).
TestAddFromLibraryFolderKeepsIt (the skill's folder and the library's skills folder as the source, another folder by that name, copyDir onto and into itself: bytes unchanged), TestFoundInLibraryFolder, TestSharedSkillNotLinkedTwice and TestSkillKindIsWhatTheFolderIs are new and fail without the change (the first with SKILL.md and run.sh emptied); TestSharedAgentsSkills now asserts Codex and Gemini CLI get no second link (Pi keeps its) and are Always. library-own-folder.test.cjs (WebKit, en and zh) is new and fails without the change but for its Remove case; library-all, library-items, library-loading, library-row-open-scroll, library-toggle-status, click-scroll, import-all-skills, own-skill, every-skill, remove-all-skills, by-agent and market-have pass in WebKit. 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.
* fix(proc): end what a CLI probe started, on its timeout and when magpie exits
The probes that ask an agent's CLI something (claude auth status,
cursor-agent about/models/status, the --version checks) ran through
proc.CommandContext, which on its timeout kills only the command itself.
A CLI that is a script (a version manager's wrapper, a shim) left what it
had started running, adopted by init; and a probe still asking when magpie
exited (a one-shot command, an update, a restart) was left whole, with
nothing to end it. With Omarchy's mise wrappers stuck in their exec loop
(omacom/omarchy#7234) that was five processes holding about half a core
each for up to six hours.
proc.ProbeContext gives such a probe a session of its own on Unix, as
StartTree does, and its context ending kills the whole group. Probes still
running are listed, and proc.EndProbes kills them when magpie exits (after
run in main) or puts a new version in its place (update.Reexec). On
Windows a probe is killed alone, as before, a job being joined only once
the command has started.
TestProbeTimeoutEndsWhatItStarted and TestEndProbes fail without the
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(proc): end probes on a terminal's signals and on a Mac's quit
Review of the first change: a probe leading its own session no longer
gets the Ctrl+C, hang-up or SIGTERM sent to magpie's terminal or service,
and magpie ending on the signal never reached EndProbes, so the probe was
left to init where before it died with magpie. main now catches SIGINT,
SIGTERM and SIGHUP, ends the probes and exits as the signal would have
(128+n). A signal magpie was started ignoring (nohup, a background job)
stays ignored. The account sign-in and plugin commands, which end
themselves on Ctrl+C, go through interruptContext and are left to do so.
On a Mac the desktop app's quit ([NSApp terminate:], also the restart to
update) exits without returning to main, so OnShutdown ends the probes.
TestProbeTimeoutEndsWhatItStarted ended its probe on a 500ms timeout,
which on a Mac could come before the script had written its child's pid
(6 of 10 runs failed in review). It now ends the context once that pid is
read, the same Cancel path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: let the app and the TUI quit on a signal their own way
Review of the signal handling: endProbesOnSignal exited ahead of Wails
and bubbletea, which quit on SIGINT and SIGTERM themselves. `magpie tray`
exited 143 without OnShutdown (no downloaded update installed, and
sessions.Saved skipped in main), and `magpie tui` left the terminal on its
alternate screen with the cursor hidden. The app's and the TUI's entry
points now hand the signals back (ownSignals): the app's OnShutdown ends
the probes, and the TUI comes back to main, which ends them too. A
one-shot command keeps the handler.
TestOwnSignalsHandsThemBack fails without the change (magpie exited 143
before the command could quit its own way); TestSignalExits keeps the
one-shot behavior.
childPid waited 3s for a script just written to start its child, which a
busy Mac can exceed (2 of 3 full runs failed in review). It waits 30s now
and, should the probe end first, says the probe's error rather than "the
script started nothing".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: keep SIGHUP for the app and the TUI, and die of the signal
ownSignals handed SIGHUP back along with SIGINT and SIGTERM, but neither
Wails nor bubbletea handles it: the terminal closing under the TUI or an
app started from a shell ended magpie on the spot, and its probes, in a
session of their own, out of the hang-up's reach, kept running (before
this PR they were in the terminal's group and ended with it). SIGHUP now
stays with endProbesOnSignal. A tray under a pty whose terminal closes
left 2 processes before, none now.
The handler asked whether a command was ending itself on Ctrl+C only
after EndProbes, which can take up to a second; a sign-in done in the
meantime got magpie ended in the middle of its own cleanup. It is asked
first.
It exited 128+n, which a shell takes for a child that handled the signal:
a loop running magpie went on to the next run on Ctrl+C. It now dies of
the signal (handling reset, the signal sent again); Windows, which can't
send one, still exits with the code.
agentProbe and agentCommand share withProxy; the Windows probeGroup
comment no longer says EndProbes ends what a shim started.
TestOwnSignalsKeepsHangUp and TestInterruptWhileEndingProbes fail without
the change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: hand signals to the TUI and the app only once they listen
Review: runTUI handed SIGINT and SIGTERM back before tui.Run, whose
newModel asks agents' CLIs (claude auth status, up to 5s) before
bubbletea listens for signals. Ctrl+C, SIGTERM or SIGINT while it loaded
ended magpie by Go's default and left the probe and its child running.
tui.Run now takes ready, called once the model is made, and runTUI passes
ownSignals.
The app had the same window: runGUI handed them back before gui.Run,
which starts the backend (probes included) before Wails' Run starts
listening. ownSignals is now gui.Started, called on ApplicationStarted,
which Wails emits after its signal handler is set up. A tray sent SIGTERM
0.05s after starting, a probe under way, left 2 processes before and
none now; one sent it once started still quits through OnShutdown
(exit 0).
TestTUIKeepsSignalsWhileLoading fails without the change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With 30+ skills, turning them all on or off was a row's All at a time.
"Turn all on" and "Turn all off" beside "In the library" give every skill
to every agent shown that can take skills, or take every one from them, in
one write (POST library/skills/agents-all, EverySkillAgents); an agent not
shown keeps what it has, as with a row's All. Off asks first in the page;
each is greyed when there's nothing for it to do. TestEverySkillAgents and
every-skill.test.cjs fail without the change.
A skill npx skills put in ~/.agents/skills and taken into the library has a
folder source or none, and the market's added mark matched only a GitHub
source, so skills.sh kept offering it; adding it was then turned away by
name. The mark now also matches a library skill by the market's skill id or
name, as adding does. TestMarketSkillHadByName fails without the change.
* gateway: draw through a Grok subscription (Grok Imagine)
A Grok subscription (SuperGrok, X Premium+) could not draw: Drawers() returned
nothing for it and draw() refused with "Grok (SuperGrok) can't draw: magpie
draws only through an OpenAI-compatible API, and Grok (SuperGrok) has none".
Grok Build's own image_gen / image_edit tools call the backend's Imagine API
at <cli-chat-proxy.grok.com/v1>/images/generations and /images/edits with the
CLI's sign-in, which magpie already uses for chat.
- drawsGrok / grokDrawers: a Grok account draws with grok-imagine-image and
grok-imagine-image-quality, like a ChatGPT account does with GPT Image
- the Imagine API takes an aspect ratio, not a size or quality: size
WIDTHxHEIGHT is matched to the nearest ratio it accepts (aspectAmong,
aspectOf is unchanged); the image is asked for as base64
- an edit is JSON with the images as data URLs
- a plan's images cost nothing extra, so AutoDrawer ranks Grok with ChatGPT
(ChatGPT still wins a tie on release date)
- provider.grokBase is exported as GrokBase, like CodexBase, for the test
Tested against a real SuperGrok account through the gateway: generation,
16:9, the quality model and an edit. Video is not covered: the Grok CLI's
video tools need zero data retention off or a storage bucket.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* gateway: film through a Grok subscription, and a generate_video tool for agents
A Grok account that draws (c6578bc) can make videos too: Grok Imagine's
video endpoints sit behind the same sign-in.
The gateway speaks OpenAI's Videos API, so its SDK works as it is:
POST /v1/videos, GET /v1/videos/{id}, GET /v1/videos/{id}/content. A
request is taken on at once and polled; the id carries the provider and
the vendor's request id, so nothing is kept in the gateway and a restart
loses no video. Status is mapped (pending at 0% -> queued, pending ->
in_progress, done -> completed, failed/expired -> failed, with why).
seconds (1-15, the vendor's limit, which it says when exceeded), size
(-> aspect ratio and 480p/720p/1080p by the shorter side), input_reference
for image-to-video (URL, data URL, {image_url}, or a multipart file) and
reference_images are taken; a model that isn't a Grok video model is
refused by name. /content answers 409 until the video is ready.
`magpie mcp image` lists a second tool, generate_video: it starts the
video, waits (up to 10 minutes), saves the mp4 into the project
(generated-videos/ unless path says where, never over a file) and says
where, so an agent can animate an image it just drew by passing its path.
Checked against a real SuperGrok account: text-to-video, image-to-video
(input_reference), reference_images, both video models, the OpenAI
Python SDK (videos.create/retrieve/download_content), the MCP tool end to
end with two videos at once, and the error paths. Unit tests use a fake
Grok upstream.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* gateway: videos follow the Settings' off switch, and an id can't change the vendor's path
Review of the video change:
- Image generation "off" turns off the video a request that names no model
gets (videomaker, beside drawer), so generate_video says it is off too.
- A video id is put in the vendor's URL with the account's token: its parts
must be [A-Za-z0-9_-]+ (parseVideoID) and the vendor id is path-escaped, so
video_grok.x%3Fa=1.1 and %2F answer 404 and ask the vendor nothing.
- The id carries a short mark of the account that started it; after a switch
the video says it was started with another account instead of the vendor's
not-found.
- generate_video waits up to ten minutes, and Codex gives a tool a minute:
magpie-image's Codex entry gets tool_timeout_sec (Goose's timeout) of
eleven minutes, the user's own value kept.
- grok-imagine-video stops at 720p (the vendor turns 1080p away for it; 1.5
makes 1080p), so a 1080p size is asked at 720p there.
- A model may be named without its provider (grok-imagine-video).
- "seconds": 6.0 is 6; 6.5 is still refused.
- completed_at is left out: the vendor doesn't say when a video finished,
and a time made up at each poll changed each time.
Checked against a real SuperGrok account: bare model name, seconds 2.0,
1080p on the base model asked at 720p (the mp4 is 1280x720), 1080p accepted
on 1.5, both malformed ids, no completed_at.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* gateway: TestVideoModelByItsBareName asks its two models in a fixed order
It checked the body of the second request the upstream got, but asked the
two models by ranging over a map, so it failed about one run in seven
(Ubuntu CI). Ask them from a slice.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* library: an installed magpie-image gets the longer tool timeout on the next sync
magpie-image already ships, so many users have it in Codex and Goose as it was
written before: no tool_timeout_sec in Codex, and the timeout: 300 every Goose
server was given. syncMCP skipped an entry whose command, args and env were
unchanged, so an upgrade left Codex at its one-minute default, and Goose kept
300 as if the user had chosen it.
- mcpFile.behind says an entry lacks the timeout magpie gives the server now:
Codex's tool_timeout_sec is missing, or Goose's timeout is still exactly the
300 magpie wrote. syncMCP rewrites such an entry; merged() doesn't keep that
300 as the user's. A value the user chose (any other number) is left alone.
- Test: an entry as main wrote it is given 660 in both, one the user set stays,
and a second sync changes nothing. It fails without the change ("changed []").
Also, from the review:
- seconds is validated once in readFilming: a decimal number that is whole and
at least 1 (hex, inf, nan, underscores, negatives and 0 are refused), and the
object echoes 6 for 6.0.
- imagemcp: Run's doc comment is above Run again.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>