SKILL.md
ZergScholar — Local ↔ Web Bridge
Sync papers between an Obsidian vault and the zergscholar web app via a Bearer- token-authenticated REST surface. The vault stays the source of truth for notes / drafts; zergscholar holds collaborator state and canonical paper metadata. Linkage is tracked via a zergscholar_id field on each linked note's frontmatter.
First-Time Setup (~2 minutes)
- Sign in to your zergscholar deployment (default
https://zergscholar.fly.dev) and go to Settings → API Tokens.
- Mint a token with
writescope for the workspace you want to
sync against (the personal library is the typical default). Copy the zsk_... value — it's shown once.
- Run setup:
``bash python3 ~/.claude/skills/zergscholar/zergscholar_skill.py setup ``
The walkthrough prompts for base URL, the token you just minted, the default workspace, and the Obsidian vault path. Writes ~/.claude/skills/zergscholar/config.json.
The default vault path is /Users/idanbeck/Library/Mobile Documents/iCloud~md~obsidian/Documents/idanbeck to match the layout documented in that vault's AGENTS.md (Reading/ Research/, Reading/pdfs/, Writing/, Research/).
Multi-Account Convention
config.json mirrors notion-skill:
{
"default_account": "personal",
"vault_path": "/Users/.../idanbeck",
"accounts": {
"personal": { "base_url": "...", "token": "zsk_...", "default_organization_id": "uuid" },
"epoch": { "base_url": "...", "token": "zsk_...", "default_organization_id": "uuid" }
}
}
Every command takes -a ACCOUNT to override the default. Mint one token per account (workspace) you want to bridge.
Commands
All commands print JSON to stdout and exit with non-zero on error. Run with python3 ~/.claude/skills/zergscholar/zergscholar_skill.py <subcommand>.
whoami [-a ACCOUNT]
Token info + accessible workspaces. Useful as a connectivity check.
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py whoami
list-papers [-a] [--workspace ID] [--linked|--unlinked]
Paper list for a workspace, joined with the vault. Each row tells you whether a local note exists and where.
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py list-papers
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py list-papers --unlinked
Output rows include: id, title, year, doi, arxivid, status, linkedlocal, local_path.
push-paper FILE.md [-a] [--workspace ID] [--force] [--pdf PATH]
Push a vault note to zergscholar. Reads the note's frontmatter (year, arxiv / arxiv_id, doi, authors if present), parses the H1 as the title, and uses the ## Abstract or ## Technical Synopsis section as the abstract. If a PDF named after the title sits in Reading/pdfs/, it's uploaded automatically; pass --pdf PATH to override the discovery (useful when the H1 expands what the filename abbreviated — e.g. "Large Language Model" vs "LLM").
The skill marks the note as linked (writes zergscholarid and zergscholarpushedat to frontmatter) only when there's nothing left to retry. If a local PDF was found but the upload failed, the note stays un-linked so the next push re-enters the dedup path and retries the upload instead of short-circuiting on alreadylinked.
The response includes a linked: bool field that reflects this: true means the frontmatter was updated; false means metadata was created server-side but the note wasn't linked (typically due to a PDF upload failure that should be retried).
Re-running an already-linked push skips with already_linked unless you pass --force. With the post-2026-05 skill, --force never creates a duplicate row: when there's an arXiv/DOI dedup hit, the skill re-uses the existing paper and re-runs enrichment + PDF upload against it. If you previously had a "linked but no PDF on server" state, just push-paper --force and the PDF gets attached without spawning a new paper id.
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py push-paper \
"$VAULT/Reading/Research/A Comprehensive Survey on Graph Neural Networks.md"
# Explicit PDF path when the filename diverges from the H1:
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py push-paper \
"$VAULT/Reading/Research/<note>.md" \
--pdf "$VAULT/Reading/pdfs/<abbreviated-filename>.pdf"
pull-paper IDORTITLE [-a] [-o PATH] [--no-knowledge]
Fetch a paper from zergscholar and write a vault note. Accepts either the paper's UUID or a title (fuzzy match within the default workspace). Writes to Reading/Research/<title>.md matching the existing template (frontmatter + H1 + standard H2 sections).
Pulls knowledge entries by default — workspace-side AI summaries / key findings / methodology / insights produced via the in-app research assistant or by collaborators are woven into the matching H2 sections. Pass --no-knowledge to skip (faster).
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py pull-paper \
"Attention Is All You Need"
status [-a]
Vault scan + remote count summary. Reports total notes, linked vs unlinked, plus the remote paper count for the default workspace.
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py status
setup
Interactive — see "First-Time Setup" above.
Workflows
"Publish this paper to my workspace"
# I just finished reading and noting this one — push it
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py push-paper \
"$VAULT/Reading/Research/<title>.md"
# → paper appears in /app/library; PDF auto-attached if found
"Bring this paper from the team library into my vault to read"
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py pull-paper \
"<title or uuid>" -a epoch
# → new note in Reading/Research/, ready for "My Thoughts" + notes
"What hasn't been pushed yet?"
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py list-papers --unlinked
# → JSON list of remote papers without local notes; conversely,
# walk Reading/Research/ for notes without zergscholar_id
Output Format
All commands emit JSON to stdout. push-paper response shape:
{
"ok": true,
"paper_id": "uuid",
"title": "...",
"workspace_id": "uuid",
"pdf_uploaded": true,
"pdf_path": "/Users/.../Reading/pdfs/<filename>.pdf",
"pdf_result": {
"paperId": "uuid",
"filename": "<filename>.pdf",
"bytes": 716869,
"has_pdf": true,
"extractedTextChars": 0,
"pageCount": null
},
"linked": true,
"local_path": "/Users/.../<note>.md"
}
Field notes:
linked— whetherzergscholar_idwas written to the note's frontmatter.falsemeans metadata was created/found server-side but a retry-worthy step failed (typically PDF upload). Re-runningpush-paperwill pick up where it left off via arXiv-id dedup.pdfresult.haspdf— server-confirmed boolean that the paper now has a PDF attached.pdf_result.extractedTextChars/pageCount— populated asynchronously by the server-side text-extraction pass.0/nullon a fresh upload means extraction is queued; re-fetch the paper later or trigger explicitly withextract-text.
Errors are also JSON:
{
"error": "api_error",
"message": "Token requires 'write' token scope; your token has [read].",
"status": 403
}
Exit code 0 for success, 1 for general errors, 2 for setup / configuration problems.
Frontmatter Conventions
The skill reads these fields when pushing a paper:
| Field | Use |
|---|---|
# H1 |
paper title (required) |
year |
publication year |
arxiv / arxiv_id |
arXiv ID |
doi |
DOI |
authors |
comma-separated list, ["A", "B"] style accepted |
## Abstract or ## Technical Synopsis |
paper abstract |
The skill writes these fields back after a successful push:
| Field | Meaning |
|---|---|
zergscholar_id |
UUID of the remote paper |
zergscholarpushedat |
ISO timestamp of last push |
Security Notes
config.jsoncontains a token that's equivalent to a workspace-
scoped session. Treat it like a credential — keep it on disk, don't commit it.
- Tokens are workspace-scoped: a
writetoken for workspace A can't
read or write workspace B. Mint separate tokens per account in config.
- Revoke / regenerate tokens at
<base_url>/app/settings→ API
Tokens at any time. The skill will get a clear 401 and you can re-run setup.
push-knowledge FILE.md [-a] [--paper-id ID]
Extract H2 sections of an already-linked paper note and POST them as typed knowledge entries. Mapping:
| Heading | entry_type |
|---|---|
| Technical Synopsis / Summary / Abstract | summary |
| Key Findings / Findings | key_finding |
| Methodology / Methods | methodology |
| My Thoughts / Thoughts / Insights | insight |
| Connections / Related Work | connection |
| Definitions / Glossary | definition |
Skips empty / placeholder sections (To be filled during review). Client-side dedup against existing entries on the paper — re-running won't double-post.
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py push-knowledge \
"$VAULT/Reading/Research/<title>.md"
push-draft FILE.md [-a] [--force]
Push a Writing/ note as a zergscholar documents row. Reads the H1 as title, body becomes the doc content, format from frontmatter (defaults to markdown; latex / typst honored). If the note already has zergscholar_id, update-in-place; pass --force to create a brand-new doc instead.
pull-draft DOC_ID [-a] [-o PATH]
Fetch a remote document into Writing/<title>.md with frontmatter linking and format preserved.
pull-all [-a] [--workspace ID] [--force]
Bulk-pull every paper from the workspace into Reading/Research/. Skips already-linked papers unless --force. Useful for bootstrapping a fresh vault from an existing workspace.
push-folder DIR [-a] [--workspace ID] [--dry-run] [--force]
Recursively walk a vault folder, push every .md paper note that isn't already linked. --dry-run reports the candidate set without touching the server. Idempotent — re-runs pick up only new notes. DOI/arXiv pre-flight on every push so duplicates don't sneak in.
# What would happen?
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py push-folder \
"$VAULT/Reading/Research" --dry-run
# Do it
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py push-folder \
"$VAULT/Reading/Research"
push-annotation FILE.md "..." [--page N] [--highlight TEXT]
Post an annotation against a linked paper. The note's zergscholar_id provides the paper id; pass --paper-id to override.
Dedup behaviour (push-paper)
push-paper finds an existing paper to attach to in priority order:
- arXiv-id / DOI lookup via
/api/external/find-paperwhen the
note's frontmatter has arxiv: or doi:. This is the primary path and catches most workspace-side duplicates.
- Existing
zergscholar_idon the note — used as a recovery
fallback when a previous push linked the note but the dedup key isn't (or no longer is) on file with the server. The skill calls get_paper(id) and re-attaches if the paper still exists.
If both probes return nothing, a new paper row is created.
--force does NOT create duplicate rows. It only skips the already_linked short-circuit so the post-dedup work (enrichment + PDF upload) re-runs against the existing row. This is the recovery tool for any "linked, but the upload didn't finish" state.
Bulk maintenance commands
These walk the workspace server-side and reconcile gaps. All take --workspace ID (override default), --limit N (stop after N papers), and --dry-run (report candidates without writing).
enrich-all [-a]
Walk every vault note with zergscholar_id and re-push tags, body content, and authors to the existing remote paper. Useful for backfilling fields that an earlier push-paper version didn't carry (early bulk-pushes only sent title + year + arxiv id).
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py enrich-all
enrich-metadata [-a] [--workspace ID] [--limit N] [--dry-run]
For every paper in the workspace that has an arxiv_id but is missing authors or abstract, fetch from the arXiv ATOM API (batched 100 per request, 3s between batches) and call enrich-paper server-side. Polite to arXiv; safe to re-run.
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py enrich-metadata --dry-run
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py enrich-metadata --limit 50
backfill-pdfs [-a] [--workspace ID] [--limit N] [--dry-run]
For every paper that has an arxivid but no PDF attached, fetch https://arxiv.org/pdf/<arxivid>.pdf and upload it via the existing upload-pdf endpoint. Polite at 3.5s between fetches per arXiv's unauthenticated-bulk guidance. The natural way to recover from "metadata pushed but PDF never made it" state across many papers without manually re-pushing each note.
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py backfill-pdfs --dry-run
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py backfill-pdfs --limit 20
extract-text [-a] [--workspace ID] [--limit N] [--dry-run]
For every paper with a PDF and no extracted text, hit the server's per-paper extract-text endpoint. The server runs the extraction asynchronously after upload-pdf, but if it stalls or you've backfilled PDFs in bulk, this kicks the queue.
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py extract-text --dry-run
python3 ~/.claude/skills/zergscholar/zergscholar_skill.py extract-text --limit 100
pull-drafts [-a] [--workspace ID] [--force]
Bulk-pull every document from the workspace into Writing/. Mirrors pull-all for documents instead of papers. Skips already-linked drafts unless --force.