Tim Helgeson59 downloadsMirror a GitLab package or GitHub release artifact into a vault folder on a schedule. Read-only, no git required, works on desktop and mobile.
Pull a generated artifact from a remote registry — a GitLab generic package or a GitHub release asset — unzip it, and mirror its contents into a vault folder, on a schedule or on demand.
Read-only. No git, no clone, no push, no write path back to the source. The plugin cannot modify anything outside the folder you point it at.
For the git-based sibling, see Halyard Sync, which syncs a whole vault with a git repository and deliberately excludes this use case.
This plugin was developed with substantial AI assistance (Claude).
| GitLab Generic Package Registry | GitHub Releases | |
|---|---|---|
| Endpoint | GET /api/v4/projects/:id/packages/generic/:pkg/:version/:file |
Release metadata, then the matching asset |
| Version pin | An exact version or a registry alias (e.g. latest) |
An exact tag or latest |
| Auth | PRIVATE-TOKEN header, project-scoped PAT (read_api) |
Authorization: Bearer, fine-grained PAT (Contents: Read) |
CI-artifact sources (GitLab CI job artifacts, GitHub Actions artifacts) and plain authenticated URLs are designed for but not built — see DESIGN.md.
Community plugin directory — not yet listed; submission is pending.
BRAT (beta) — install BRAT,
then "Add beta plugin" with timrs2998/halyard-fetch.
Manual — download manifest.json, main.js and styles.css from the
latest release into
<vault>/.obsidian/plugins/halyard-fetch/, then enable it in
Settings → Community plugins.
Add source — via the ribbon icon, command palette, or settings tab — opens a wizard:
For distribution, choose one model per source:
The choice is retained even when Halyard Sync is not installed. Shared mode is available once a compatible Halyard Sync is installed; an older Halyard Sync must be updated before shared mode can be enabled safely.
Editing a source reopens the same wizard, pre-filled, and revalidates on save.
Each source refreshes on its own interval, optionally whenever Obsidian regains focus, and once at startup to catch up after being fully quit.
Mobile has no background timer. There, "every N minutes" means "the next time you open the app after N minutes have passed since the last success" — a staleness threshold, not a timer. The first mobile launch asks whether scheduled refreshes should be restricted to Wi-Fi; "Refresh now" always bypasses that.
A freshness check runs before every download, so an unchanged source costs one small request rather than a full re-download.
A failing source shows a Notice, a badge in settings, and an entry in the log viewer (ribbon, status bar, or the "Open log" command).
Repeated identical failures don't re-notify — only transitions do, meaning the first failure after a success or a different error than last time. There's no automatic backoff: a failed source retries on its normal schedule, and "Refresh now" is always available once you've fixed whatever broke.
app.secretStorage (OS keychain on desktop) when available. Where it
isn't, they fall back to this plugin's data.json in plaintext, and settings shows
a warning saying so.data.json remains a managed
exclusion in every mode because it can contain fallback tokens. If a destination
was already tracked, changing its mode does not rewrite Git history; review any
existing tracked snapshot manually.Existing sources created before distribution choices were added keep their old
exclusion until you explicitly review them. Settings shows a migration prompt for
each source. Dismissing keeps the old behavior and warning. Choosing shared mode
removes only the exact old destination pattern; broader patterns such as
Sources/ remain blockers and are reported.
npm install
npm run dev # esbuild watch
npm run build # tsc --noEmit + production esbuild
npm run lint # eslint
npm test # vitest
npm run test:e2e # real Obsidian, driven headlessly via WebdriverIO
Pure logic — id generation, content-root detection, mirror-diff computation,
scheduling, notice dedup, retry and rate-limit handling — is unit-tested against real
zip fixtures built with fflate, not mocked extraction. UI glue is verified by
type-checking and building.
npm run test:e2e uses
wdio-obsidian-service,
which downloads a real Obsidian build (cached in .obsidian-cache/, gitignored) and
drives it against the fixture vault in e2e/vaults/simple — once as desktop
Obsidian, once under emulated-mobile UI. The first run is slower. Widen the version
matrix with OBSIDIAN_VERSIONS (e.g. "earliest/earliest latest/latest"). GitHub
Actions runs this coverage on pushes, pull requests, and release tags on Ubuntu
with Xvfb and herbstluftwm; it does not test a physical iOS device.
Architecture and rationale live in DESIGN.md; contributor conventions in AGENTS.md.
npm version x.y.z — bumps package.json, manifest.json and versions.json
together, commits, and tags x.y.zgit push origin main --tagsmanifest.json, and attaches
manifest.json, main.js and styles.css to the releaseNo v prefix on the tag — Obsidian requires it to equal manifest.json's version
exactly. .npmrc's tag-version-prefix="" is what keeps npm version from adding one.
esbuild.config.mjs, version-bump.mjs,
versions.json).MIT — see LICENSE. Third-party notices: THIRD-PARTY-NOTICES.md.