Focus Safari

approvedunreviewed

by YOKOYAMA Taku

Animals gather in a side panel while you stay focused on your notes. The longer you focus, the more show up. A paid activation code unlocks rarer animals.

โ†“ 104 downloadsUpdated 2mo ago

Focus Safari ๐Ÿฆ (source repository)

Private. This is the source repository for Focus Safari. Users install the plugin from the public distribution repository, seekseep/focus-safari, which holds only manifest.json, versions.json, styles.css and the releases. See Releasing.

A small, playful side panel: the longer you stay focused on your notes, the more animals wander into your safari.

  • Free: six animals (ant, mouse, rabbit, fox, deer, bear) arrive as your cumulative focus time grows.
  • Pro: ten rarer animals โ€” lion, elephant, parrot, giraffe, zebra, panda, crocodile, sloth, rhino, flamingo.

๐Ÿ’ณ Paid features

The Pro animals require a paid activation code. Everything else โ€” the timer, the panel, the six free animals โ€” is fully functional without paying.

๐Ÿ”Œ Network use

None. Activation codes are Ed25519-signed and verified entirely offline against a public key embedded in the plugin. Nothing is sent anywhere, and no telemetry is collected.

How it works

While the app window has focus, one second of focus time is added per second. Large jumps (for example after the machine wakes from sleep) are clamped to two seconds per tick. When your cumulative focus time crosses an animal's threshold, that animal arrives and you get a notice.

AnimalFocus requiredTier
๐Ÿœ Ant30sFree
๐Ÿ Mouse2mFree
๐Ÿ‡ Rabbit5mFree
๐Ÿฆ Lion10mPro
๐ŸฆŠ Fox15mFree
๐Ÿ˜ Elephant20mPro
๐ŸฆŒ Deer30mFree
๐Ÿป Bear1hFree
๐Ÿผ Panda1.5hPro
๐Ÿฆฉ Flamingo3hPro

The full roster lives in src/animals.ts.

Usage

Open the panel from the paw-print ribbon icon, or run the Open safari command. Focus time keeps accumulating whether or not the panel is open.

Settings let you paste an activation code, remove it again, or reset the safari (clears focus time and sends every animal home).

Manual installation

  1. Download main.js, manifest.json and styles.css from the latest release.
  2. Copy them into <Vault>/.obsidian/plugins/focus-safari/.
  3. Reload, then enable Focus Safari under Community plugins.

Development

npm install
npm run dev      # watch build
npm run build    # production build -> main.js

Animal artwork

Animals are drawn with Twemoji rather than the platform emoji font, so the safari looks the same on every OS. The SVGs are inlined into the bundle (src/twemoji-assets.ts) โ€” nothing is fetched at runtime. After changing the roster in src/animals.ts, regenerate them:

node tools/fetch-twemoji.mjs

Activation codes

Key generation and code issuance live in a separate repository, focus-safari-licensing โ€” this repo only verifies codes (src/license.ts) against the public key committed in src/public-key.ts.

  • The signing private key is never in this repository and never in CI. It lives outside any repo at the path LICENSE_PRIVATE_KEY_PATH points to. Losing it makes future issuance impossible; replacing it invalidates every code already delivered.
  • src/public-key.ts must stay in step with the signing key. npm run key:check derives the public key from LICENSE_PRIVATE_KEY_PATH (default ~/.config/focus-safari/private.key) and fails on drift. It skips silently where no key is present, so CI is unaffected.
  • Never regenerate the keypair to make a check pass โ€” that invalidates every code already delivered.

Two checks guard the contract with the issuer. Run both after touching src/license.ts:

npm run key:check          # embedded public key still matches the signing key
npm run verify:roundtrip   # sign codes like the issuer does, verify with the real verifier

verify:roundtrip bundles src/license.ts itself, so it tests the shipped logic rather than a copy of it, and asserts the negative cases too (wrong product, expired, tampered, malformed).

Product id

Codes carry product: "focus-safari". Builds up to 1.0.4 expected the old id "obsidian-safari" and will reject anything issued now โ€” activation requires 1.0.5 or later. Say so in the release notes.

To test activation end to end, run a Stripe test purchase from the licensing repo and paste the resulting code into Settings โ†’ Focus Safari โ†’ Activation code.

Releasing

Never edit package.json, manifest.json and versions.json by hand โ€” that is how 1.0.3 and 1.0.4 shipped a manifest still declaring 1.0.2. Use npm version, which runs tools/version-sync.mjs and keeps all three in step:

npm version patch          # or minor / major
git push origin main --follow-tags

The tag must be bare semver โ€” Obsidian forbids a v prefix, and tools/check-release-version.mjs rejects one. npm tags as v1.2.3 by default, so .npmrc sets an empty tag-version-prefix. Keep that file; without it every npm version produces a tag the release cannot accept.

1.0.3 and 1.0.4 are missing from versions.json on purpose: both shipped a manifest still declaring 1.0.2, so neither ever existed as a version Obsidian could install. Do not backfill them.

Pushing the tag runs .github/workflows/release.yml, which:

  1. verifies that the tag, package.json, manifest.json and versions.json all agree (tools/check-release-version.mjs) and fails the release if they do not;
  2. refuses to continue if a release for that tag already exists in the public repo;
  3. builds main.js;
  4. signs main.js, manifest.json and styles.css with cosign (see below);
  5. commits manifest.json, styles.css and versions.json to the public repo's main, refusing to publish any file outside those three;
  6. tags the public repo and creates the release, attaching the three files plus their *.sigstore.json bundles.

Nothing from src/ or tools/ is ever copied to the public repo. main.js is build output and is a release asset only โ€” it is committed nowhere. manifest.json and versions.json do get committed because Obsidian reads them from the public repo's default branch.

PUBLIC_REPO_TOKEN

A fine-grained personal access token scoped to seekseep/focus-safari only, with Contents: Read and write. Stored as an Actions secret on this repository. It expires โ€” renew it before the expiry date or releases will start failing at the checkout step.

Signing and verification

actions/attest-build-provenance cannot be used here: GitHub's attestation store rejects user-owned private repositories (Feature not available for user-owned private repositories), and create-storage-record: false does not bypass it. The workflow signs with cosign attest-blob --new-bundle-format instead, which writes an in-toto SLSA provenance statement to the Sigstore public-good instance and emits a bundle file per artifact.

Because the bundle travels with the release, anyone can verify offline โ€” no cosign, no authentication, and no access to this repository:

gh attestation verify main.js \
  --bundle main.js.sigstore.json \
  --repo seekseep/focus-safari-source

Verification fails if the artifact is modified or if a different --repo is claimed.

Note what this does and does not prove. The source is private, so provenance cannot make the build reproducible or auditable by a third party. What it establishes is that a given main.js came out of this repository's release workflow at a given tag and has not been altered since โ€” tamper-evidence, not transparency.

License

Proprietary โ€” see LICENSE. Versions up to 1.0.4 were released under MIT.

The bundled animal graphics come from Twemoji, ยฉ Twitter, Inc and other contributors, licensed under CC-BY 4.0.


ๆ—ฅๆœฌ่ชž

ใƒŽใƒผใƒˆใซ้›†ไธญใ—ใฆใ„ใ‚‹ๆ™‚้–“ใŒๆบœใพใ‚‹ใปใฉใ€ใ‚ตใ‚คใƒ‰ใƒ‘ใƒใƒซใฎใ€Œใ‚ตใƒ•ใ‚กใƒชใ€ใซๅ‹•็‰ฉใŒ้›†ใพใฃใฆใใ‚‹ใƒ—ใƒฉใ‚ฐใ‚คใƒณใงใ™ใ€‚็„กๆ–™ใง6็จฎ้กžใ€Pro๏ผˆๆœ‰ๆ–™ใฎใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ™ใƒผใ‚ทใƒงใƒณใ‚ณใƒผใƒ‰ใŒๅฟ…่ฆ๏ผ‰ใงใ•ใ‚‰ใซ10็จฎ้กžใฎใƒฌใ‚ขๅ‹•็‰ฉใŒ็™ปๅ ดใ—ใพใ™ใ€‚

ใ‚ณใƒผใƒ‰ใฎๆคœ่จผใฏ Ed25519 ็ฝฒๅใซใ‚ˆใ‚‹ใ‚ชใƒ•ใƒฉใ‚คใƒณๆคœ่จผใงใ€ใƒใƒƒใƒˆใƒฏใƒผใ‚ฏ้€šไฟกใƒปใƒ†ใƒฌใƒกใƒˆใƒชใฏไธ€ๅˆ‡ใ‚ใ‚Šใพใ›ใ‚“ใ€‚้–พๅ€คใฎ่ชฟๆ•ดใฏ src/animals.ts ใ‚’ๆ›ธใๆ›ใˆใ‚‹ใ ใ‘ใงใ™ใ€‚

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.