{
  "generated_at": "2026-04-26T17:17:21.372Z",
  "total": 33,
  "ecosystems": {
    "zig": {
      "label": "Zig",
      "color": "#f7a41d",
      "install_template": "zig fetch --save git+https://github.com/{repo}.git",
      "registry_url_template": "https://zigistry.dev/package/{repo}",
      "icon": "zig"
    },
    "rust": {
      "label": "Rust",
      "color": "#dea584",
      "install_template": "cargo add {crate_name}",
      "registry_url_template": "https://crates.io/crates/{crate_name}",
      "icon": "rust"
    },
    "npm": {
      "label": "NPM",
      "color": "#cb3837",
      "install_template": "npm install {npm_name}",
      "registry_url_template": "https://www.npmjs.com/package/{npm_name}",
      "icon": "npm"
    },
    "multi": {
      "label": "Multi",
      "color": "#6366f1",
      "icon": "layers"
    }
  },
  "categories": [
    {
      "id": "cryptography",
      "label": "Cryptography & Security",
      "description": "Cryptographic primitives, keychain access, FIDO2/WebAuthn"
    },
    {
      "id": "systems",
      "label": "Systems & Platform",
      "description": "Desktop notifications, window management, OS integration"
    },
    {
      "id": "scheduling",
      "label": "Scheduling & Payments",
      "description": "Appointment booking, payment integration, calendar sync"
    },
    {
      "id": "auth",
      "label": "Authentication",
      "description": "PostgreSQL/Redis auth adapters, OAuth multiplexing"
    },
    {
      "id": "ai-tools",
      "label": "AI & Developer Tools",
      "description": "MCP servers, transcript extraction, TUI dashboards"
    },
    {
      "id": "integrations",
      "label": "Integrations",
      "description": "GSuite, Linear, third-party API bridges"
    },
    {
      "id": "devex",
      "label": "Developer Experience",
      "description": "Documentation tools, accessibility, i18n, type utilities"
    },
    {
      "id": "uncategorized",
      "label": "Other",
      "description": "Uncategorized packages"
    }
  ],
  "packages": [
    {
      "slug": "jesssullivan-tinyland-cleanup",
      "name": "tinyland-cleanup",
      "repo": "jesssullivan/tinyland-cleanup",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "systems",
      "description": "Cross-platform disk cleanup daemon with graduated thresholds — Go, Nix, systemd/launchd",
      "featured": false,
      "tags": [
        "disk",
        "cleanup",
        "daemon",
        "go"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [
        "cleanup",
        "daemon",
        "disk-management",
        "go",
        "launchd",
        "nix",
        "systemd"
      ],
      "languages": [
        {
          "name": "Go",
          "color": "#00ADD8",
          "bytes": 445694
        },
        {
          "name": "Starlark",
          "color": "#76d275",
          "bytes": 4645
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 1636
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 588
        }
      ],
      "primary_language": "Go",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">tinyland-cleanup</h1><a id=\"user-content-tinyland-cleanup\" class=\"anchor\" aria-label=\"Permalink: tinyland-cleanup\" href=\"#tinyland-cleanup\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><code>tinyland-cleanup</code> is a conservative disk-pressure cleanup daemon for developer\nmachines and CI hosts. It focuses on build-system and developer-tool caches\nwhere unmanaged disk pressure can break local work, remote runners, or\nhermetic build flows.</p>\n<p dir=\"auto\">The current production target is Darwin developer machines plus Linux/Rocky\nbuilder and runner machines.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Safety Model</h2><a id=\"user-content-safety-model\" class=\"anchor\" aria-label=\"Permalink: Safety Model\" href=\"#safety-model\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>Dry-run behavior must stay useful enough for operator review.</li>\n<li>Cleanup policy should explain what it plans to remove and why.</li>\n<li>Host free-space accounting should be measured before and after cleanup.</li>\n<li>Real cleanup should stop once the configured host free-space target is met.</li>\n<li>Daemon-triggered non-critical cleanup should honor cooldown state.</li>\n<li>Privileged actions, offline compaction, and service disruption must remain\nexplicit policy choices.</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Build And Test</h2><a id=\"user-content-build-and-test\" class=\"anchor\" aria-label=\"Permalink: Build And Test\" href=\"#build-and-test\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Fast local validation:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"env GOCACHE=/tmp/tinyland-cleanup-gocache GOFLAGS=-mod=vendor go test ./...\nenv GOCACHE=/tmp/tinyland-cleanup-gocache GOFLAGS=-mod=vendor go vet ./...\nenv GOCACHE=/tmp/tinyland-cleanup-gocache GOFLAGS=-mod=vendor go build ./...\"><pre>env GOCACHE=/tmp/tinyland-cleanup-gocache GOFLAGS=-mod=vendor go <span class=\"pl-c1\">test</span> ./...\nenv GOCACHE=/tmp/tinyland-cleanup-gocache GOFLAGS=-mod=vendor go vet ./...\nenv GOCACHE=/tmp/tinyland-cleanup-gocache GOFLAGS=-mod=vendor go build ./...</pre></div>\n<p dir=\"auto\">Nix package build:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix build .#default --no-link --print-build-logs\"><pre>nix build .<span class=\"pl-c\"><span class=\"pl-c\">#</span>default --no-link --print-build-logs</span></pre></div>\n<p dir=\"auto\">Bazel build/test graph:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix shell nixpkgs#bazelisk --command bazelisk --output_user_root=/tmp/tinyland-cleanup-bazel test //...\"><pre>nix shell nixpkgs#bazelisk --command bazelisk --output_user_root=/tmp/tinyland-cleanup-bazel <span class=\"pl-c1\">test</span> //...</pre></div>\n<p dir=\"auto\">Shared-cache Bazel runners can use:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"BAZEL_REMOTE_CACHE=grpc://bazel-cache.nix-cache.svc.cluster.local:9092 \\\n  scripts/bazel-cache-backed.sh test //...\"><pre>BAZEL_REMOTE_CACHE=grpc://bazel-cache.nix-cache.svc.cluster.local:9092 \\\n  scripts/bazel-cache-backed.sh <span class=\"pl-c1\">test</span> //...</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Operator Review</h2><a id=\"user-content-operator-review\" class=\"anchor\" aria-label=\"Permalink: Operator Review\" href=\"#operator-review\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Review the cleanup plan before mutating a high-pressure machine:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"tinyland-cleanup --once --dry-run --level critical --output text\"><pre>tinyland-cleanup --once --dry-run --level critical --output text</pre></div>\n<p dir=\"auto\">Use JSON when another tool needs the stable report schema:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"tinyland-cleanup --once --dry-run --level critical --output json\"><pre>tinyland-cleanup --once --dry-run --level critical --output json</pre></div>\n<p dir=\"auto\">List available plugin names before constraining an evidence run:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"tinyland-cleanup --list-plugins\"><pre>tinyland-cleanup --list-plugins</pre></div>\n<p dir=\"auto\">Constrain review to specific plugins before scanning broad cache surfaces:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"tinyland-cleanup --once --dry-run --level critical --plugins bazel,nix --output text\"><pre>tinyland-cleanup --once --dry-run --level critical --plugins bazel,nix --output text</pre></div>\n<p dir=\"auto\">For a one-off run, override the configured maximum used-space target without\nediting the config file:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"tinyland-cleanup --once --dry-run --level critical --target-used-percent 82\"><pre>tinyland-cleanup --once --dry-run --level critical --target-used-percent 82</pre></div>\n<p dir=\"auto\">See <a href=\"docs/operator-workflow.md\">docs/operator-workflow.md</a> for the current\ndry-run, candidate policy tier, and host free-space accounting workflow.</p>\n<p dir=\"auto\">Podman on macOS needs extra care because <code>applehv</code> raw sparse images do not\nshrink from guest <code>fstrim</code> alone. See\n<a href=\"docs/podman-darwin-compaction.md\">docs/podman-darwin-compaction.md</a> before\nenabling offline compaction.</p>\n<p dir=\"auto\">Darwin developer cache review is documented in\n<a href=\"docs/darwin-dev-caches.md\">docs/darwin-dev-caches.md</a>.</p>\n<p dir=\"auto\">Nix store and generation cleanup policy is documented in\n<a href=\"docs/nix-cleanup-policy.md\">docs/nix-cleanup-policy.md</a>.</p>\n<p dir=\"auto\">Bazel cache and output-base review is documented in\n<a href=\"docs/bazel-cache-policy.md\">docs/bazel-cache-policy.md</a>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Distribution Status</h2><a id=\"user-content-distribution-status\" class=\"anchor\" aria-label=\"Permalink: Distribution Status\" href=\"#distribution-status\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Current package authority is the Nix flake package <code>.#tinyland-cleanup</code>.\nRelease archives are produced from GitHub tags. No public tag has been cut yet.\nLinux RPM packaging is documented in\n<a href=\"docs/rpm-packaging.md\">docs/rpm-packaging.md</a>; the RPM installs a systemd unit\nbut leaves enable/start as an explicit operator action.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Roadmap</h2><a id=\"user-content-roadmap\" class=\"anchor\" aria-label=\"Permalink: Roadmap\" href=\"#roadmap\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Open productionization work is tracked in GitHub issues:</p>\n<ul dir=\"auto\">\n<li><code>#2</code>: durable disk-pressure policy overhaul</li>\n<li><code>#3</code>: Bazel cache tiering, budgets, and active-use detection</li>\n<li><code>#4</code>: Nix cleanup policy for generations, roots, and daemon contention</li>\n<li><code>#5</code>: Darwin IDE and developer-tool cache budgets</li>\n<li><code>#6</code>: Podman offline compaction for Darwin <code>applehv</code></li>\n<li><code>#9</code>: GloriousFlywheel shared-cache runner proof</li>\n</ul>\n<p dir=\"auto\">Recently completed productionization work:</p>\n<ul dir=\"auto\">\n<li><code>#7</code>: dry-run, telemetry, and host free-space accounting</li>\n</ul>\n<p dir=\"auto\">See <a href=\"docs/productionization-plan-2026-04-25.md\">docs/productionization-plan-2026-04-25.md</a>\nfor the current productionization plan.</p>\n<p dir=\"auto\">Current validation notes are tracked in\n<a href=\"docs/validation-status-2026-04-26.md\">docs/validation-status-2026-04-26.md</a>.</p>\n</article></div>",
      "readme_excerpt": "tinyland-cleanup is a conservative disk-pressure cleanup daemon for developer\nmachines and CI hosts. It focuses on build-system and developer-tool caches\nwhere unmanaged disk pressure can break local work, remote runners, or\nhermetic build flows.\nThe current production target is Darwin developer machines plus Linux/Rocky\nbuilder and runner machines.\n- Dry-run behavior must stay useful enough for operator review.\n- Cleanup policy should explain what it plans to remove and why.\n- Host free-space...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/tinyland-cleanup",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/tinyland-cleanup/releases",
      "og_image_url": "https://opengraph.githubassets.com/758127e5e9ac0f380a78d50b72873f187b985dda1eeb71cfede831c6d38e57e8/Jesssullivan/tinyland-cleanup",
      "license": "MIT",
      "pushed_at": "2026-04-26T17:14:49Z",
      "enriched_at": "2026-04-26T17:17:11.521Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-cmux",
      "name": "cmux",
      "repo": "jesssullivan/cmux",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "systems",
      "description": "de-attestation development work for cmux terminal; lets get this built for common linux DEs ^w^",
      "featured": true,
      "tags": [
        "terminal",
        "multiplexer",
        "ghostty",
        "ai"
      ],
      "version": "lab-v0.75.0",
      "release_date": "2026-04-06T04:04:29Z",
      "releases": [
        {
          "tag": "lab-v0.75.0",
          "date": "2026-04-06T04:04:29Z",
          "body": "Linux release packages for lab-v0.75.0"
        },
        {
          "tag": "lab-v0.74.0",
          "date": "2026-04-05T20:33:29Z",
          "body": "Linux release packages for lab-v0.74.0"
        },
        {
          "tag": "lab-v0.73.1",
          "date": "2026-04-04T01:03:15Z",
          "body": "Linux release packages for lab-v0.73.1"
        },
        {
          "tag": "lab-v0.73.0-test3",
          "date": "2026-04-03T13:41:18Z",
          "body": "Linux release packages for lab-v0.73.0-test3"
        },
        {
          "tag": "lab-v0.73.0",
          "date": "2026-03-29T14:10:03Z",
          "body": "Signed and notarized fork release with FIDO2/WebAuthn enterprise auth support.\n\n## Assets\n- **cmux-lab-macos.dmg** — signed macOS .app (trans-themed LAB branding, Developer ID)\n- **cmuxd-remote-{darwin,linux}-{arm64,amd64}** — cross-platform remote daemon binaries\n\n## Nix\n```bash\nnix run github:Jesssullivan/cmux/${TAG}\n```"
        },
        {
          "tag": "lab-v0.72.0",
          "date": "2026-03-27T18:42:01Z",
          "body": "Signed and notarized fork release with FIDO2/WebAuthn enterprise auth support.\n\n## Assets\n- **cmux-lab-macos.dmg** — signed macOS .app (trans-themed LAB branding, Developer ID)\n\n## Nix\n```bash\nnix run github:Jesssullivan/cmux/${TAG}\n```"
        },
        {
          "tag": "lab-v0.71.0",
          "date": "2026-03-27T17:16:46Z",
          "body": "Signed and notarized fork release with FIDO2/WebAuthn enterprise auth support.\n\n## Assets\n- **cmux-lab-macos.dmg** — signed macOS .app (trans-themed LAB branding, Developer ID)\n\n## Nix\n```bash\nnix run github:Jesssullivan/cmux/${TAG}\n```"
        },
        {
          "tag": "lab-v0.70.0",
          "date": "2026-03-27T14:31:40Z",
          "body": "Signed and notarized fork release with FIDO2/WebAuthn enterprise auth support.\n\n## Assets\n- **cmux-lab-macos.dmg** — signed macOS .app (trans-themed LAB branding, Developer ID)\n\n## Nix\n```bash\nnix run github:Jesssullivan/cmux/${TAG}\n```"
        },
        {
          "tag": "lab-v0.69.0",
          "date": "2026-03-27T13:09:14Z",
          "body": "Signed and notarized fork release with FIDO2/WebAuthn enterprise auth support.\n\n## Assets\n- **cmux-lab-macos.dmg** — signed macOS .app (trans-themed LAB branding, Developer ID)\n\n## Nix\n```bash\nnix run github:Jesssullivan/cmux/${TAG}\n```"
        },
        {
          "tag": "lab-v0.68.0",
          "date": "2026-03-27T05:21:27Z",
          "body": "Signed and notarized fork release with FIDO2/WebAuthn enterprise auth support.\n\n## Assets\n- **cmux-lab-macos.dmg** — signed macOS .app (trans-themed LAB branding, Developer ID)\n\n## Nix\n```bash\nnix run github:Jesssullivan/cmux/${TAG}\n```"
        }
      ],
      "stars": 2,
      "topics": [
        "attestation-compatibility",
        "ffi",
        "multiarch",
        "zig",
        "no-apple"
      ],
      "languages": [
        {
          "name": "Swift",
          "color": "#F05138",
          "bytes": 9132655
        },
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 1809390
        },
        {
          "name": "Zig",
          "color": "#ec915c",
          "bytes": 332730
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 302748
        },
        {
          "name": "TypeScript",
          "color": "#3178c6",
          "bytes": 283693
        }
      ],
      "primary_language": "Swift",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 align=\"center\" class=\"heading-element\" dir=\"auto\">cmux</h1><a id=\"user-content-cmux\" class=\"anchor\" aria-label=\"Permalink: cmux\" href=\"#cmux\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p align=\"center\" dir=\"auto\">A Ghostty-based macOS terminal with vertical tabs and notifications for AI coding agents</p>\n<p align=\"center\" dir=\"auto\">\n  <a href=\"https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg\">\n    <img src=\"./docs/assets/macos-badge.png\" alt=\"Download cmux for macOS\" width=\"180\" style=\"max-width: 100%;\">\n  </a>\n</p>\n<p align=\"center\" dir=\"auto\">\n  English | <a href=\"README.ja.md\">日本語</a> | <a href=\"README.vi.md\">Tiếng Việt</a> | <a href=\"README.zh-CN.md\">简体中文</a> | <a href=\"README.zh-TW.md\">繁體中文</a> | <a href=\"README.ko.md\">한국어</a> | <a href=\"README.de.md\">Deutsch</a> | <a href=\"README.es.md\">Español</a> | <a href=\"README.fr.md\">Français</a> | <a href=\"README.it.md\">Italiano</a> | <a href=\"README.da.md\">Dansk</a> | <a href=\"README.pl.md\">Polski</a> | <a href=\"README.ru.md\">Русский</a> | <a href=\"README.bs.md\">Bosanski</a> | <a href=\"README.ar.md\">العربية</a> | <a href=\"README.no.md\">Norsk</a> | <a href=\"README.pt-BR.md\">Português (Brasil)</a> | <a href=\"README.th.md\">ไทย</a> | <a href=\"README.tr.md\">Türkçe</a> | <a href=\"README.km.md\">ភាសាខ្មែរ</a> | <a href=\"README.uk.md\">Українська</a>\n</p>\n<p align=\"center\" dir=\"auto\">\n  <a href=\"https://x.com/manaflowai\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/9ba62b02bd55c93053ef0b0fda144fc8d9c13e3f14d2052490c7adba6d5c4103/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f406d616e61666c6f772d3535353f6c6f676f3d78\" alt=\"X / Twitter\" data-canonical-src=\"https://img.shields.io/badge/@manaflow-555?logo=x\" style=\"max-width: 100%;\"></a>\n  <a href=\"https://discord.gg/xsgFEVrWCZ\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/8d5c1c186058f674d9613aa69f226c68138844d4e340ff8eb6e184d6642bf28c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f446973636f72642d3535353f6c6f676f3d646973636f7264\" alt=\"Discord\" data-canonical-src=\"https://img.shields.io/badge/Discord-555?logo=discord\" style=\"max-width: 100%;\"></a>\n  <a href=\"https://github.com/manaflow-ai/cmux\"><img src=\"https://camo.githubusercontent.com/2aff096a5df0de24e22652222654f51272dc80bdbe85e7e0a422909666c7cc1c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f6d616e61666c6f772d61692f636d75783f7374796c653d666c6174266c6f676f3d676974687562266c6162656c3d737461727326636f6c6f723d346337316632\" alt=\"GitHub stars\" data-canonical-src=\"https://img.shields.io/github/stars/manaflow-ai/cmux?style=flat&amp;logo=github&amp;label=stars&amp;color=4c71f2\" style=\"max-width: 100%;\"></a>\n</p>\n<p align=\"center\" dir=\"auto\">\n  <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"./docs/assets/main-first-image.png\"><img src=\"./docs/assets/main-first-image.png\" alt=\"cmux screenshot\" width=\"900\" style=\"max-width: 100%;\"></a>\n</p>\n<p align=\"center\" dir=\"auto\">\n  <a href=\"https://www.youtube.com/watch?v=i-WxO5YUTOs\" rel=\"nofollow\">▶ Demo video</a> · <a href=\"https://cmux.com/blog/zen-of-cmux\" rel=\"nofollow\">The Zen of cmux</a>\n</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Features</h2><a id=\"user-content-features\" class=\"anchor\" aria-label=\"Permalink: Features\" href=\"#features\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<tbody><tr>\n<td width=\"40%\" valign=\"middle\">\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Notification rings</h3><a id=\"user-content-notification-rings\" class=\"anchor\" aria-label=\"Permalink: Notification rings\" href=\"#notification-rings\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\nPanes get a blue ring and tabs light up when coding agents need your attention\n</td>\n<td width=\"60%\">\n<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"./docs/assets/notification-rings.png\"><img src=\"./docs/assets/notification-rings.png\" alt=\"Notification rings\" width=\"100%\" style=\"max-width: 100%;\"></a>\n</td>\n</tr>\n<tr>\n<td width=\"40%\" valign=\"middle\">\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Notification panel</h3><a id=\"user-content-notification-panel\" class=\"anchor\" aria-label=\"Permalink: Notification panel\" href=\"#notification-panel\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\nSee all pending notifications in one place, jump to the most recent unread\n</td>\n<td width=\"60%\">\n<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"./docs/assets/sidebar-notification-badge.png\"><img src=\"./docs/assets/sidebar-notification-badge.png\" alt=\"Sidebar notification badge\" width=\"100%\" style=\"max-width: 100%;\"></a>\n</td>\n</tr>\n<tr>\n<td width=\"40%\" valign=\"middle\">\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">In-app browser</h3><a id=\"user-content-in-app-browser\" class=\"anchor\" aria-label=\"Permalink: In-app browser\" href=\"#in-app-browser\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\nSplit a browser alongside your terminal with a scriptable API ported from <a href=\"https://github.com/vercel-labs/agent-browser\">agent-browser</a>\n</td>\n<td width=\"60%\">\n<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"./docs/assets/built-in-browser.png\"><img src=\"./docs/assets/built-in-browser.png\" alt=\"Built-in browser\" width=\"100%\" style=\"max-width: 100%;\"></a>\n</td>\n</tr>\n<tr>\n<td width=\"40%\" valign=\"middle\">\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Vertical + horizontal tabs</h3><a id=\"user-content-vertical--horizontal-tabs\" class=\"anchor\" aria-label=\"Permalink: Vertical + horizontal tabs\" href=\"#vertical--horizontal-tabs\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\nSidebar shows git branch, linked PR status/number, working directory, listening ports, and latest notification text. Split horizontally and vertically.\n</td>\n<td width=\"60%\">\n<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"./docs/assets/vertical-horizontal-tabs-and-splits.png\"><img src=\"./docs/assets/vertical-horizontal-tabs-and-splits.png\" alt=\"Vertical tabs and split panes\" width=\"100%\" style=\"max-width: 100%;\"></a>\n</td>\n</tr>\n<tr>\n<td width=\"40%\" valign=\"middle\">\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">SSH</h3><a id=\"user-content-ssh\" class=\"anchor\" aria-label=\"Permalink: SSH\" href=\"#ssh\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<code>cmux ssh user@remote</code> creates a workspace for a remote machine. Browser panes route through the remote network so localhost just works. Drag an image into a remote session to upload via scp.\n</td>\n<td width=\"60%\">\n<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"./docs/assets/ssh.png\"><img src=\"./docs/assets/ssh.png\" alt=\"cmux SSH\" width=\"100%\" style=\"max-width: 100%;\"></a>\n</td>\n</tr>\n<tr>\n<td width=\"40%\" valign=\"middle\">\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Claude Code Teams</h3><a id=\"user-content-claude-code-teams\" class=\"anchor\" aria-label=\"Permalink: Claude Code Teams\" href=\"#claude-code-teams\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<code>cmux claude-teams</code> runs Claude Code's teammate mode with one command. Teammates spawn as native splits with sidebar metadata and notifications. No tmux required.\n</td>\n<td width=\"60%\">\n<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"./docs/assets/claude-code-teams.png\"><img src=\"./docs/assets/claude-code-teams.png\" alt=\"Claude Code Teams\" width=\"100%\" style=\"max-width: 100%;\"></a>\n</td>\n</tr>\n</tbody></table></markdown-accessiblity-table>\n<ul dir=\"auto\">\n<li><strong>Browser import</strong> — Import cookies, history, and sessions from Chrome, Firefox, Arc, and 20+ browsers so browser panes start authenticated</li>\n<li><strong>Custom commands</strong> — Define project-specific actions in <a href=\"https://cmux.com/docs/custom-commands\" rel=\"nofollow\"><code>cmux.json</code></a> that launch from the command palette</li>\n<li><strong>Scriptable</strong> — CLI and socket API to create workspaces, split panes, send keystrokes, and automate the browser</li>\n<li><strong>Native macOS app</strong> — Built with Swift and AppKit, not Electron. Fast startup, low memory.</li>\n<li><strong>Ghostty compatible</strong> — Reads your existing <code>~/.config/ghostty/config</code> for themes, fonts, and colors</li>\n<li><strong>GPU-accelerated</strong> — Powered by libghostty for smooth rendering</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Install</h2><a id=\"user-content-install\" class=\"anchor\" aria-label=\"Permalink: Install\" href=\"#install\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">DMG (recommended)</h3><a id=\"user-content-dmg-recommended\" class=\"anchor\" aria-label=\"Permalink: DMG (recommended)\" href=\"#dmg-recommended\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<a href=\"https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg\">\n  <img src=\"./docs/assets/macos-badge.png\" alt=\"Download cmux for macOS\" width=\"180\" style=\"max-width: 100%;\">\n</a>\n<p dir=\"auto\">Open the <code>.dmg</code> and drag cmux to your Applications folder. cmux auto-updates via Sparkle, so you only need to download once.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Homebrew</h3><a id=\"user-content-homebrew\" class=\"anchor\" aria-label=\"Permalink: Homebrew\" href=\"#homebrew\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"brew tap manaflow-ai/cmux\nbrew install --cask cmux\"><pre>brew tap manaflow-ai/cmux\nbrew install --cask cmux</pre></div>\n<p dir=\"auto\">To update later:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"brew upgrade --cask cmux\"><pre>brew upgrade --cask cmux</pre></div>\n<p dir=\"auto\">On first launch, macOS may ask you to confirm opening an app from an identified developer. Click <strong>Open</strong> to proceed.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Why cmux?</h2><a id=\"user-content-why-cmux\" class=\"anchor\" aria-label=\"Permalink: Why cmux?\" href=\"#why-cmux\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">I run a lot of Claude Code and Codex sessions in parallel. I was using Ghostty with a bunch of split panes, and relying on native macOS notifications to know when an agent needed me. But Claude Code's notification body is always just \"Claude is waiting for your input\" with no context, and with enough tabs open I couldn't even read the titles anymore.</p>\n<p dir=\"auto\">I tried a few coding orchestrators but most of them were Electron/Tauri apps and the performance bugged me. I also just prefer the terminal since GUI orchestrators lock you into their workflow. So I built cmux as a native macOS app in Swift/AppKit. It uses libghostty for terminal rendering and reads your existing Ghostty config for themes, fonts, and colors.</p>\n<p dir=\"auto\">The main additions are the sidebar and notification system. The sidebar has vertical tabs that show git branch, linked PR status/number, working directory, listening ports, and the latest notification text for each workspace. The notification system picks up terminal sequences (OSC 9/99/777) and has a CLI (<code>cmux notify</code>) you can wire into agent hooks for Claude Code, OpenCode, etc. When an agent is waiting, its pane gets a blue ring and the tab lights up in the sidebar, so I can tell which one needs me across splits and tabs. Cmd+Shift+U jumps to the most recent unread.</p>\n<p dir=\"auto\">The in-app browser has a scriptable API ported from <a href=\"https://github.com/vercel-labs/agent-browser\">agent-browser</a>. Agents can snapshot the accessibility tree, get element refs, click, fill forms, and evaluate JS. You can split a browser pane next to your terminal and have Claude Code interact with your dev server directly.</p>\n<p dir=\"auto\">Everything is scriptable through the CLI and socket API — create workspaces/tabs, split panes, send keystrokes, open URLs in the browser.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">The Zen of cmux</h2><a id=\"user-content-the-zen-of-cmux\" class=\"anchor\" aria-label=\"Permalink: The Zen of cmux\" href=\"#the-zen-of-cmux\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">cmux is not prescriptive about how developers hold their tools. It's a terminal and browser with a CLI, and the rest is up to you.</p>\n<p dir=\"auto\">cmux is a primitive, not a solution. It gives you a terminal, a browser, notifications, workspaces, splits, tabs, and a CLI to control all of it. cmux doesn't force you into an opinionated way to use coding agents. What you build with the primitives is yours.</p>\n<p dir=\"auto\">The best developers have always built their own tools. Nobody has figured out the best way to work with agents yet, and the teams building closed products definitely haven't either. The developers closest to their own codebases will figure it out first.</p>\n<p dir=\"auto\">Give a million developers composable primitives and they'll collectively find the most efficient workflows faster than any product team could design top-down.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Documentation</h2><a id=\"user-content-documentation\" class=\"anchor\" aria-label=\"Permalink: Documentation\" href=\"#documentation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">For more info on how to configure cmux, <a href=\"https://cmux.com/docs/getting-started?utm_source=readme\" rel=\"nofollow\">head over to our docs</a>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Keyboard Shortcuts</h2><a id=\"user-content-keyboard-shortcuts\" class=\"anchor\" aria-label=\"Permalink: Keyboard Shortcuts\" href=\"#keyboard-shortcuts\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Workspaces</h3><a id=\"user-content-workspaces\" class=\"anchor\" aria-label=\"Permalink: Workspaces\" href=\"#workspaces\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Shortcut</th>\n<th>Action</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>⌘ N</td>\n<td>New workspace</td>\n</tr>\n<tr>\n<td>⌘ 1–8</td>\n<td>Jump to workspace 1–8</td>\n</tr>\n<tr>\n<td>⌘ 9</td>\n<td>Jump to last workspace</td>\n</tr>\n<tr>\n<td>⌃ ⌘ ]</td>\n<td>Next workspace</td>\n</tr>\n<tr>\n<td>⌃ ⌘ [</td>\n<td>Previous workspace</td>\n</tr>\n<tr>\n<td>⌘ ⇧ W</td>\n<td>Close workspace</td>\n</tr>\n<tr>\n<td>⌘ ⇧ R</td>\n<td>Rename workspace</td>\n</tr>\n<tr>\n<td>⌘ B</td>\n<td>Toggle sidebar</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Surfaces</h3><a id=\"user-content-surfaces\" class=\"anchor\" aria-label=\"Permalink: Surfaces\" href=\"#surfaces\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Shortcut</th>\n<th>Action</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>⌘ T</td>\n<td>New surface</td>\n</tr>\n<tr>\n<td>⌘ ⇧ ]</td>\n<td>Next surface</td>\n</tr>\n<tr>\n<td>⌘ ⇧ [</td>\n<td>Previous surface</td>\n</tr>\n<tr>\n<td>⌃ Tab</td>\n<td>Next surface</td>\n</tr>\n<tr>\n<td>⌃ ⇧ Tab</td>\n<td>Previous surface</td>\n</tr>\n<tr>\n<td>⌃ 1–8</td>\n<td>Jump to surface 1–8</td>\n</tr>\n<tr>\n<td>⌃ 9</td>\n<td>Jump to last surface</td>\n</tr>\n<tr>\n<td>⌘ W</td>\n<td>Close surface</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Split Panes</h3><a id=\"user-content-split-panes\" class=\"anchor\" aria-label=\"Permalink: Split Panes\" href=\"#split-panes\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Shortcut</th>\n<th>Action</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>⌘ D</td>\n<td>Split right</td>\n</tr>\n<tr>\n<td>⌘ ⇧ D</td>\n<td>Split down</td>\n</tr>\n<tr>\n<td>⌥ ⌘ ← → ↑ ↓</td>\n<td>Focus pane directionally</td>\n</tr>\n<tr>\n<td>⌘ ⇧ H</td>\n<td>Flash focused panel</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Browser</h3><a id=\"user-content-browser\" class=\"anchor\" aria-label=\"Permalink: Browser\" href=\"#browser\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Browser developer-tool shortcuts follow Safari defaults and are customizable in <code>Settings → Keyboard Shortcuts</code>.</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Shortcut</th>\n<th>Action</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>⌘ ⇧ L</td>\n<td>Open browser in split</td>\n</tr>\n<tr>\n<td>⌘ L</td>\n<td>Focus address bar</td>\n</tr>\n<tr>\n<td>⌘ [</td>\n<td>Back</td>\n</tr>\n<tr>\n<td>⌘ ]</td>\n<td>Forward</td>\n</tr>\n<tr>\n<td>⌘ R</td>\n<td>Reload page</td>\n</tr>\n<tr>\n<td>⌥ ⌘ I</td>\n<td>Toggle Developer Tools (Safari default)</td>\n</tr>\n<tr>\n<td>⌥ ⌘ C</td>\n<td>Show JavaScript Console (Safari default)</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Notifications</h3><a id=\"user-content-notifications\" class=\"anchor\" aria-label=\"Permalink: Notifications\" href=\"#notifications\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Shortcut</th>\n<th>Action</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>⌘ I</td>\n<td>Show notifications panel</td>\n</tr>\n<tr>\n<td>⌘ ⇧ U</td>\n<td>Jump to latest unread</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Find</h3><a id=\"user-content-find\" class=\"anchor\" aria-label=\"Permalink: Find\" href=\"#find\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Shortcut</th>\n<th>Action</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>⌘ F</td>\n<td>Find</td>\n</tr>\n<tr>\n<td>⌘ G / ⌘ ⇧ G</td>\n<td>Find next / previous</td>\n</tr>\n<tr>\n<td>⌘ ⇧ F</td>\n<td>Hide find bar</td>\n</tr>\n<tr>\n<td>⌘ E</td>\n<td>Use selection for find</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Terminal</h3><a id=\"user-content-terminal\" class=\"anchor\" aria-label=\"Permalink: Terminal\" href=\"#terminal\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Shortcut</th>\n<th>Action</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>⌘ K</td>\n<td>Clear scrollback</td>\n</tr>\n<tr>\n<td>⌘ C</td>\n<td>Copy (with selection)</td>\n</tr>\n<tr>\n<td>⌘ V</td>\n<td>Paste</td>\n</tr>\n<tr>\n<td>⌘ + / ⌘ -</td>\n<td>Increase / decrease font size</td>\n</tr>\n<tr>\n<td>⌘ 0</td>\n<td>Reset font size</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Window</h3><a id=\"user-content-window\" class=\"anchor\" aria-label=\"Permalink: Window\" href=\"#window\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Shortcut</th>\n<th>Action</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>⌘ ⇧ N</td>\n<td>New window</td>\n</tr>\n<tr>\n<td>⌘ ⇧ O</td>\n<td>Reopen previous session</td>\n</tr>\n<tr>\n<td>⌘ ,</td>\n<td>Settings</td>\n</tr>\n<tr>\n<td>⌘ ⇧ ,</td>\n<td>Reload configuration</td>\n</tr>\n<tr>\n<td>⌘ Q</td>\n<td>Quit</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Nightly Builds</h2><a id=\"user-content-nightly-builds\" class=\"anchor\" aria-label=\"Permalink: Nightly Builds\" href=\"#nightly-builds\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><a href=\"https://github.com/manaflow-ai/cmux/releases/download/nightly/cmux-nightly-macos.dmg\">Download cmux NIGHTLY</a></p>\n<p dir=\"auto\">cmux NIGHTLY is a separate app with its own bundle ID, so it runs alongside the stable version. Built automatically from the latest <code>main</code> commit and auto-updates via its own Sparkle feed.</p>\n<p dir=\"auto\">Report nightly bugs on <a href=\"https://github.com/manaflow-ai/cmux/issues\">GitHub Issues</a> or in <a href=\"https://discord.gg/xsgFEVrWCZ\" rel=\"nofollow\">#nightly-bugs on Discord</a>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Session restore</h2><a id=\"user-content-session-restore\" class=\"anchor\" aria-label=\"Permalink: Session restore\" href=\"#session-restore\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Quitting cmux saves the current session. On relaunch, cmux restores:</p>\n<ul dir=\"auto\">\n<li>Window/workspace/pane layout</li>\n<li>Working directories</li>\n<li>Terminal scrollback (best effort)</li>\n<li>Browser URL and navigation history</li>\n<li>Saved Claude Code and Codex sessions, when cmux has a resume token for the panel</li>\n</ul>\n<p dir=\"auto\">If you need to reapply the last saved snapshot manually, use:</p>\n<ul dir=\"auto\">\n<li><code>File &gt; Reopen Previous Session</code></li>\n<li><code>⌘ ⇧ O</code></li>\n<li><code>cmux restore-session</code></li>\n</ul>\n<p dir=\"auto\">cmux does <strong>not</strong> restore arbitrary live terminal process state. tmux, vim, shells, and other tools without a cmux resume flow still reopen as normal terminals rather than resuming in-process state.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Star History</h2><a id=\"user-content-star-history\" class=\"anchor\" aria-label=\"Permalink: Star History\" href=\"#star-history\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<a href=\"https://star-history.com/#manaflow-ai/cmux&amp;Date\" rel=\"nofollow\">\n <themed-picture data-catalyst-inline=\"true\"><picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://camo.githubusercontent.com/043fb47135952a1d08bbbb44ca44e83bb38c2304ee08f0cda5a786b6a8f4b67b/68747470733a2f2f6170692e737461722d686973746f72792e636f6d2f7376673f7265706f733d6d616e61666c6f772d61692f636d757826747970653d44617465267468656d653d6461726b\" data-canonical-src=\"https://api.star-history.com/svg?repos=manaflow-ai/cmux&amp;type=Date&amp;theme=dark\">\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://camo.githubusercontent.com/707042505918cab1d347c7bc1896b19f7cdf202e11ca2c736b494eaad950c73e/68747470733a2f2f6170692e737461722d686973746f72792e636f6d2f7376673f7265706f733d6d616e61666c6f772d61692f636d757826747970653d44617465\" data-canonical-src=\"https://api.star-history.com/svg?repos=manaflow-ai/cmux&amp;type=Date\">\n   <img alt=\"Star History Chart\" src=\"https://camo.githubusercontent.com/707042505918cab1d347c7bc1896b19f7cdf202e11ca2c736b494eaad950c73e/68747470733a2f2f6170692e737461722d686973746f72792e636f6d2f7376673f7265706f733d6d616e61666c6f772d61692f636d757826747970653d44617465\" width=\"600\" data-canonical-src=\"https://api.star-history.com/svg?repos=manaflow-ai/cmux&amp;type=Date\">\n </picture></themed-picture>\n</a>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Contributing</h2><a id=\"user-content-contributing\" class=\"anchor\" aria-label=\"Permalink: Contributing\" href=\"#contributing\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Ways to get involved:</p>\n<ul dir=\"auto\">\n<li>Follow us on X for updates <a href=\"https://x.com/manaflowai\" rel=\"nofollow\">@manaflowai</a>, <a href=\"https://x.com/lawrencecchen\" rel=\"nofollow\">@lawrencecchen</a>, and <a href=\"https://x.com/austinywang\" rel=\"nofollow\">@austinywang</a></li>\n<li>Join the conversation on <a href=\"https://discord.gg/xsgFEVrWCZ\" rel=\"nofollow\">Discord</a></li>\n<li>Create and participate in <a href=\"https://github.com/manaflow-ai/cmux/issues\">GitHub issues</a> and <a href=\"https://github.com/manaflow-ai/cmux/discussions\">discussions</a></li>\n<li>Let us know what you're building with cmux</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Community</h2><a id=\"user-content-community\" class=\"anchor\" aria-label=\"Permalink: Community\" href=\"#community\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><a href=\"https://discord.gg/xsgFEVrWCZ\" rel=\"nofollow\">Discord</a></li>\n<li><a href=\"https://github.com/manaflow-ai/cmux\">GitHub</a></li>\n<li><a href=\"https://twitter.com/manaflowai\" rel=\"nofollow\">X / Twitter</a></li>\n<li><a href=\"https://www.youtube.com/channel/UCAa89_j-TWkrXfk9A3CbASw\" rel=\"nofollow\">YouTube</a></li>\n<li><a href=\"https://www.linkedin.com/company/manaflow-ai/\" rel=\"nofollow\">LinkedIn</a></li>\n<li><a href=\"https://www.reddit.com/r/cmux/\" rel=\"nofollow\">Reddit</a></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Founder's Edition</h2><a id=\"user-content-founders-edition\" class=\"anchor\" aria-label=\"Permalink: Founder's Edition\" href=\"#founders-edition\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">cmux is free, open source, and always will be. If you'd like to support development and get early access to what's coming next:</p>\n<p dir=\"auto\"><strong><a href=\"https://buy.stripe.com/3cI00j2Ld0it5OU33r5EY0q\" rel=\"nofollow\">Get Founder's Edition</a></strong></p>\n<ul dir=\"auto\">\n<li><strong>Prioritized feature requests/bug fixes</strong></li>\n<li><strong>Early access: cmux AI that gives you context on every workspace, tab and panel</strong></li>\n<li><strong>Early access: iOS app with terminals synced between desktop and phone</strong></li>\n<li><strong>Early access: Cloud VMs</strong></li>\n<li><strong>Early access: Voice mode</strong></li>\n<li><strong>My personal iMessage/WhatsApp</strong></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">cmux is open source under <a href=\"LICENSE\">GPL-3.0-or-later</a>.</p>\n<p dir=\"auto\">If your organization cannot comply with GPL, a commercial license is available. Contact <a href=\"mailto:founders@manaflow.com\">founders@manaflow.com</a> for details.</p>\n</article></div>",
      "readme_excerpt": "<h1 align=\"center\">cmux</h1>\n<p align=\"center\">A Ghostty-based macOS terminal with vertical tabs and notifications for AI coding agents</p>\n<p align=\"center\">\n  <a href=\"https://github.com/manaflow-ai/cmux/releases/latest/download/cmux-macos.dmg\">\n    <img src=\"./docs/assets/macos-badge.png\" alt=\"Download cmux for macOS\" width=\"180\" />\n  </a>\n</p>\n<p align=\"center\">\n  English | <a href=\"README.ja.md\">日本語</a> | <a href=\"README.vi.md\">Tiếng Việt</a> | <a href=\"README.zh-CN.md\">简体中文</a> | <a...",
      "install_commands": {
        "brew": "brew install --cask cmux"
      },
      "repo_url": "https://github.com/jesssullivan/cmux",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/cmux/releases",
      "og_image_url": "https://opengraph.githubassets.com/632121431fa4c8793c9cacbc08303d818b2c321ddf3be8b48b024172cdc38f1b/Jesssullivan/cmux",
      "license": "NOASSERTION",
      "pushed_at": "2026-04-26T17:04:55Z",
      "enriched_at": "2026-04-26T17:17:11.870Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-xoxdwm",
      "name": "XoxdWM",
      "repo": "jesssullivan/XoxdWM",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "systems",
      "description": "Eye-gesture VR & BCI XWayland Emacs Window Manager for transhumans and cyborgs",
      "featured": false,
      "tags": [
        "wayland",
        "compositor",
        "vr",
        "research"
      ],
      "version": "0.5.3",
      "release_date": "2026-04-13T22:55:11Z",
      "releases": [
        {
          "tag": "0.5.3",
          "date": "2026-04-13T22:55:11Z",
          "body": "## What's Changed\n* Fix tagged RPM version/source handling by @Jesssullivan in https://github.com/Jesssullivan/XoxdWM/pull/24\n\n\n**Full Changelog**: https://github.com/Jesssullivan/XoxdWM/compare/v0.5.2...v0.5.3"
        },
        {
          "tag": "0.5.1",
          "date": "2026-04-13T19:51:03Z",
          "body": "## What's Changed\n* Document current XoxdWM status and tighten CI surfaces by @Jesssullivan in https://github.com/Jesssullivan/XoxdWM/pull/9\n* Scope self-hosted CI to available runner infra by @Jesssullivan in https://github.com/Jesssullivan/XoxdWM/pull/14\n* Document current honey and yoga XR reality by @Jesssullivan in https://github.com/Jesssullivan/XoxdWM/pull/15\n* Guard Rocky RPM release artifacts by @Jesssullivan in https://github.com/Jesssullivan/XoxdWM/pull/16\n* Prepare v0.5.1 release metadata by @Jesssullivan in https://github.com/Jesssullivan/XoxdWM/pull/17\n* Allow manual dispatch of multi-arch workflow by @Jesssullivan in https://github.com/Jesssullivan/XoxdWM/pull/21\n\n\n**Full Changelog**: https://github.com/Jesssullivan/XoxdWM/compare/v0.5.0...v0.5.1"
        },
        {
          "tag": "0.5.0",
          "date": "2026-03-03T19:07:50Z",
          "body": "## What's Changed\n* feat(nix): Rocky 10 first-class deployment (v0.5.0) by @Jesssullivan in https://github.com/Jesssullivan/XoxdWM/pull/7\n\n\n**Full Changelog**: https://github.com/Jesssullivan/XoxdWM/compare/v0.4.1...v0.5.0"
        }
      ],
      "stars": 0,
      "topics": [
        "dont-take-this-too-seriously",
        "but-actually-this-is-for-real",
        "getting-rusty-with-it"
      ],
      "languages": [
        {
          "name": "Emacs Lisp",
          "color": "#c065db",
          "bytes": 1869383
        },
        {
          "name": "Rust",
          "color": "#dea584",
          "bytes": 1138222
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 135751
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 76166
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 60325
        }
      ],
      "primary_language": "Emacs Lisp",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">XoxdWM</h1><a id=\"user-content-xoxdwm\" class=\"anchor\" aria-label=\"Permalink: XoxdWM\" href=\"#xoxdwm\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">XoxdWM is an experimental Wayland compositor plus Emacs window-management layer with VR, eye-tracking, hand-tracking, and BCI research surfaces.</p>\n<p dir=\"auto\">This repository is the canonical public home for the compositor, packaging, releases, and status tracking. It is not a claim that every documented subsystem is proven on lab hardware today.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Current State</h2><a id=\"user-content-current-state\" class=\"anchor\" aria-label=\"Permalink: Current State\" href=\"#current-state\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">As of 2026-04-26:</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Area</th>\n<th>Status</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Release artifacts</td>\n<td>Smoke</td>\n<td><code>v0.5.1</code> publishes RPM and DEB artifacts. The Rocky base compositor RPM is public and host-installable, and branch-scoped <code>0.5.4-1.el10</code> RPMs from Actions run <code>24768509226</code> are now installed and revalidated on <code>yoga</code>.</td>\n</tr>\n<tr>\n<td>Headless compositor path</td>\n<td>Smoke</td>\n<td>Build and test surfaces exist, but not re-validated in this pass.</td>\n</tr>\n<tr>\n<td>Rocky 10 package install</td>\n<td>Smoke</td>\n<td><code>yoga</code> now has refreshed installed <code>0.5.4-1.el10</code> <code>exwm-vr-*</code> RPMs from the current branch, the real installed units pass a named-host bounded proof, and a controlled SDDM autologin run reached a real <code>EXWM-VR</code> Wayland user session on <code>seat0</code>. The packaged <code>SuccessExitStatus=15</code> stop-path fix is now on-host; the remaining follow-on is repeatability and operator polish, not package repair.</td>\n</tr>\n<tr>\n<td><code>honey</code> compositor/substrate path</td>\n<td>Smoke</td>\n<td><code>honey</code> now has installed branch-scoped <code>exwm-vr-0.5.4-1.el10</code> packages, a bounded named-host <code>exwm-vr.target</code> startup, and a direct-mode lease proof from the installed <code>/usr/bin/ewwm-compositor</code> after reinstalling the branch RPM artifact from run <code>24776900393</code>.</td>\n</tr>\n<tr>\n<td><code>honey</code> VR session</td>\n<td>Smoke</td>\n<td>On <code>2026-04-22</code>, the installed <code>ewwm-compositor</code> on <code>honey</code> initialized <code>wp_drm_lease_v1</code>, reserved <code>DP-2</code> via <code>~/.config/exwm-vr/compositor.env</code>, and granted a real DRM lease to Monado. A repo-owned <code>exwm-vr-monado.service</code> then proved three runtime surfaces on the host: the older local <code>/usr/local/bin/monado-service</code> lane, a staged <code>monado-beyond</code> companion RPM tree from run <code>24804821792</code>, and an installed <code>monado-beyond</code> host package from run <code>24807084915</code> using <code>/usr/bin/monado-service</code> with no <code>MONADO_SERVICE_BIN</code> override. The repo now has <code>just honey-openxr-status</code>, <code>just honey-openxr-smoke</code>, <code>just honey-openxr-clean-cycle</code>, and <code>just honey-openxr-fresh-boot-check</code> wrappers for the OpenXR client lane, and the <code>exwm-vr-openxr-smoke-client</code> RPM from run <code>24938791255</code> is installed on <code>honey</code>. On <code>2026-04-25</code> EDT, three bounded packaged-client smoke passes and three clean stop/start cycles selected <code>/usr/libexec/exwm-vr/hello_xr -g Vulkan</code>, reached Monado / Bigscreen Beyond, and created <code>3561x3561</code> eye swapchains. This is clean-cycle repeatability evidence, not yet proof of fresh-boot, in-goggles first-frame, or long-running operator stability; see <a href=\"docs/honey-substrate-proof-2026-04-22.md\">Honey Substrate Proof</a> and the <a href=\"docs/honey-fresh-boot-runbook-2026-04-26.md\">Honey Fresh-Boot Runbook</a>.</td>\n</tr>\n<tr>\n<td><code>yoga</code> desktop/dev target</td>\n<td>Smoke</td>\n<td><code>yoga</code> now has an installed <code>0.5.4</code> session proof with explicit <code>drm</code> backend and dedicated Emacs bootstrap, plus a one-time SDDM greeter-path proof via <code>sddm-autologin</code> on <code>seat0</code>. The remaining work is repeatability and session polish, not basic launch viability or packaged stop-path repair.</td>\n</tr>\n<tr>\n<td>Eye tracking / hand tracking / BCI</td>\n<td>Design</td>\n<td>Documented and partially implemented, but not currently claimed as proven on named lab hosts.</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Start Here</h2><a id=\"user-content-start-here\" class=\"anchor\" aria-label=\"Permalink: Start Here\" href=\"#start-here\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><a href=\"docs/support-matrix.md\">Support Matrix</a></li>\n<li><a href=\"docs/remote-build-authority.md\">Remote Build Authority</a></li>\n<li><a href=\"docs/remote-proof-lanes.md\">Remote Proof Lanes</a></li>\n<li><a href=\"docs/hygiene-minisprint-2026-04-25.md\">Hygiene Mini-Sprint</a></li>\n<li><a href=\"docs/reality-check-2026-04-22.md\">Reality Check</a></li>\n<li><a href=\"docs/reality-driven-development-arc-2026-q2.md\">Reality-Driven Development Arc</a></li>\n<li><a href=\"docs/grounded-milestone-plan-2026-q2.md\">Grounded Milestone Plan</a></li>\n<li><a href=\"docs/yoga-session-proof-2026-04-22.md\">Yoga Session Proof</a></li>\n<li><a href=\"docs/honey-substrate-proof-2026-04-22.md\">Honey Substrate Proof</a></li>\n<li><a href=\"docs/honey-fresh-boot-runbook-2026-04-26.md\">Honey Fresh-Boot Runbook</a></li>\n<li><a href=\"docs/status.md\">Status</a></li>\n<li><a href=\"docs/roadmap-2026-q2.md\">Q2 2026 Roadmap</a></li>\n<li><a href=\"docs/installation-quickstart.md\">Installation Quickstart</a></li>\n<li><a href=\"docs/vr-guide.md\">VR Guide</a></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Scope</h2><a id=\"user-content-scope\" class=\"anchor\" aria-label=\"Permalink: Scope\" href=\"#scope\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The repo contains four different kinds of work:</p>\n<ul dir=\"auto\">\n<li>compositor and Emacs WM code</li>\n<li>packaging for Rocky/Nix/systemd/SELinux</li>\n<li>hardware and upstream patch research</li>\n<li>aspirational feature inventory</li>\n</ul>\n<p dir=\"auto\">Use the reality check, status, and support matrix together as the current truth surface.\nSubsystem docs and feature inventories can still be more aspirational than present support.\n<code>neo</code> and other Darwin machines are control-plane surfaces only. Authoritative\nbuild and runtime validation belongs on Rocky / Linux remote lanes and named-host\nproof; see <a href=\"docs/remote-build-authority.md\">Remote Build Authority</a> and\n<a href=\"docs/remote-proof-lanes.md\">Remote Proof Lanes</a>.</p>\n<p dir=\"auto\">Shared self-hosted CI now runs from this canonical repo. Honey-backed hardware lanes remain explicit opt-in via <code>USE_VR_HARDWARE</code> rather than implicit fork-only behavior.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Related repositories</h2><a id=\"user-content-related-repositories\" class=\"anchor\" aria-label=\"Permalink: Related repositories\" href=\"#related-repositories\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Platform-specific work for the Dell Precision 7810 (<code>honey</code> host) lives in\n<a href=\"https://github.com/Jesssullivan/Dell-7810\"><code>Jesssullivan/Dell-7810</code></a>:</p>\n<ul dir=\"auto\">\n<li>Hardware design, reset behavior, power paths, BIOS/SMI characterization:\n<a href=\"https://github.com/Jesssullivan/Dell-7810/tree/main/docs/platform\"><code>Dell-7810/docs/platform/</code></a></li>\n<li><code>honey</code> reset matrix and power characterization:\n<a href=\"https://github.com/Jesssullivan/Dell-7810/tree/main/docs/research\"><code>Dell-7810/docs/research/</code></a></li>\n<li>Ownership boundary between repos:\n<a href=\"https://github.com/Jesssullivan/Dell-7810/blob/main/docs/platform/xoxdwm-boundary-audit.md\"><code>Dell-7810/docs/platform/xoxdwm-boundary-audit.md</code></a></li>\n<li>Chapel/NUMA host characterization and property-based testing:\n<a href=\"https://github.com/Jesssullivan/Dell-7810/tree/main/analysis\"><code>Dell-7810/analysis/</code></a></li>\n</ul>\n<p dir=\"auto\">XoxdWM proves that software works on a prepared host. Dell-7810 proves the\nhost is prepared.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Releases</h2><a id=\"user-content-releases\" class=\"anchor\" aria-label=\"Permalink: Releases\" href=\"#releases\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Latest public release:</p>\n<ul dir=\"auto\">\n<li><a href=\"https://github.com/Jesssullivan/XoxdWM/releases/tag/v0.5.0\"><code>v0.5.0</code></a></li>\n<li><a href=\"https://github.com/Jesssullivan/XoxdWM/releases/tag/v0.5.1\"><code>v0.5.1</code></a></li>\n</ul>\n<p dir=\"auto\">Current public install artifacts:</p>\n<ul dir=\"auto\">\n<li><code>exwm-vr-compositor-*.x86_64.rpm</code></li>\n<li><code>ewwm-compositor_*_amd64.deb</code></li>\n</ul>\n<p dir=\"auto\">These artifacts currently package the compositor path. They do not, by themselves, establish a proven full VR deployment on <code>honey</code>, and they do not yet mean <code>yoga</code> has a polished or repeatedly exercised local login/session lane. Right now <code>yoga</code> has the installed session entry, refreshed packaged user units, an active SDDM greeter, and a one-time <code>sddm-autologin</code> Wayland user session proof on <code>seat0</code>. SELinux hardening, Monado integration, and the BrainFlow BCI virtualenv remain separate follow-on package or host-integration paths instead of blocking the base Rocky compositor release.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Near-Term Goal</h2><a id=\"user-content-near-term-goal\" class=\"anchor\" aria-label=\"Permalink: Near-Term Goal\" href=\"#near-term-goal\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The next 12 weeks are aimed at one honest, ordered MVP:</p>\n<ul dir=\"auto\">\n<li><code>yoga</code>: reproducible Rocky 10 desktop/dev install with a real local session path</li>\n<li><code>honey</code> substrate: stable XR bridge path with kernel, connector, runtime, and client-tool truth</li>\n<li><code>honey</code> smoke: convert the one-shot direct-mode proof into a repeatable installed lane</li>\n</ul>\n<p dir=\"auto\">Everything else remains secondary until those three named outcomes are green.</p>\n</article></div>",
      "readme_excerpt": "XoxdWM is an experimental Wayland compositor plus Emacs window-management layer with VR, eye-tracking, hand-tracking, and BCI research surfaces.\nThis repository is the canonical public home for the compositor, packaging, releases, and status tracking. It is not a claim that every documented subsystem is proven on lab hardware today.\nAs of 2026-04-26:\n| Area | Status | Notes |\n| --- | --- | --- |\n| Release artifacts | Smoke | v0.5.1 publishes RPM and DEB artifacts. The Rocky base compositor RPM...",
      "install_commands": {
        "nix": "nix run github:Jesssullivan/XoxdWM"
      },
      "repo_url": "https://github.com/jesssullivan/XoxdWM",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/XoxdWM/releases",
      "og_image_url": "https://opengraph.githubassets.com/a02c6a64ce530fce3865f4ff5a4991920a9b0c1ed3717830c086b9dc5e1a2ff1/Jesssullivan/XoxdWM",
      "license": "GPL-3.0",
      "pushed_at": "2026-04-26T05:02:22Z",
      "enriched_at": "2026-04-26T17:17:12.159Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-scheduling-bridge",
      "name": "scheduling-bridge",
      "repo": "jesssullivan/scheduling-bridge",
      "org": "jesssullivan",
      "ecosystem": "npm",
      "category": "scheduling",
      "description": "Why pay for an API to rebuild a closed source wizard when you *are* a wizard?  Playwright-based Modal + container native middleware server for migrating off of Acuity scheduling, on mass with zero downtime.",
      "featured": false,
      "tags": [
        "scheduling",
        "acuity",
        "playwright",
        "bridge"
      ],
      "version": "0.4.3",
      "release_date": "2026-04-25T07:16:14Z",
      "releases": [
        {
          "tag": "0.4.3",
          "date": "2026-04-25T07:16:14Z",
          "body": "Release `@tummycrypt/scheduling-bridge@0.4.3`.\n\n- Adds RemoteAdapter custom header support for authenticated bridge calls.\n- Fixes cross-repo publish credential forwarding for npm and GitHub Packages.\n- Publishes the Bazel-built package artifact from the shared runner lane."
        },
        {
          "tag": "0.3.1",
          "date": "2026-04-12T23:25:47Z",
          "body": "## Changes\n- deploy truth fixes for Modal and Docker entrypoints\n- shared Acuity service catalog for static JSON, BUSINESS extraction, and scraper fallback\n- handler and local wizard adapter now resolve services through the shared catalog\n- package version bump for release\n\n## Validation\n- CI green on PR #16 before merge"
        },
        {
          "tag": "0.1.0",
          "date": "2026-03-24T20:46:00Z",
          "body": "## @tummycrypt/acuity-middleware v0.1.0\n\nPlaywright-based middleware server for migrating off Acuity Scheduling with zero downtime.\n\n### What's included\n\n- **16-method `SchedulingAdapter` interface** — services, availability, reservations, bookings, clients\n- **Effect TS wizard automation** — 7 step programs driving the Acuity booking wizard via headless Playwright\n- **CSS selector registry** — 30+ fallback chains for resilience against Emotion CSS hash instability\n- **Modal Labs deployment** — containerized Chromium with esbuild bundling, warm pools, serialized requests\n- **Standalone HTTP server** — Bearer auth, 8 endpoints replacing blocked Acuity REST API\n- **Remote adapter** — HTTP proxy client for offloading browser automation to Modal/Fly.io\n- **Dual effect system** — Effect TS for browser lifecycle, fp-ts `TaskEither` at adapter boundary\n\n### Install\n\n```bash\nnpm install @tummycrypt/acuity-middleware\n```\n\n### npm\n\nhttps://www.npmjs.com/package/@tummycrypt/acuity-middleware"
        }
      ],
      "stars": 0,
      "topics": [
        "brute-force",
        "effect-ts",
        "functional-design",
        "modal-labs",
        "no-api-no-problem",
        "work-harder-not-smarter",
        "playwrite-in-production",
        "scheduling-lifecycle",
        "wip"
      ],
      "languages": [
        {
          "name": "TypeScript",
          "color": "#3178c6",
          "bytes": 350757
        },
        {
          "name": "JavaScript",
          "color": "#f1e05a",
          "bytes": 27850
        },
        {
          "name": "Starlark",
          "color": "#76d275",
          "bytes": 9634
        },
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 3158
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 1724
        }
      ],
      "primary_language": "TypeScript",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">scheduling-bridge</h1><a id=\"user-content-scheduling-bridge\" class=\"anchor\" aria-label=\"Permalink: scheduling-bridge\" href=\"#scheduling-bridge\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n\n<p dir=\"auto\">Backend-agnostic scheduling adapter hub. Currently bridges Acuity Scheduling via Playwright browser automation, with architecture designed to support additional scheduling backends.</p>\n<blockquote>\n<p dir=\"auto\">Formerly <code>acuity-middleware</code>. Historical GitHub URLs may redirect, but the\ncanonical repo is <code>Jesssullivan/scheduling-bridge</code>.</p>\n</blockquote>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Architecture</h2><a id=\"user-content-architecture\" class=\"anchor\" aria-label=\"Permalink: Architecture\" href=\"#architecture\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">An HTTP server wrapping Playwright wizard flows that automate the Acuity booking UI. The bridge uses Effect TS for resource lifecycle management (browser/page acquisition and release).</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"HTTP Request\n  -&gt; server/handler.ts (route matching, auth, JSON serialization)\n    -&gt; acuity-service-catalog.ts (static env catalog -&gt; BUSINESS -&gt; scraper fallback)\n    -&gt; steps/ (Effect TS programs for each wizard stage)\n      -&gt; browser-service.ts (Playwright lifecycle via Effect Layer)\n        -&gt; selectors.ts (CSS selector registry with fallback chains)\"><pre class=\"notranslate\"><code>HTTP Request\n  -&gt; server/handler.ts (route matching, auth, JSON serialization)\n    -&gt; acuity-service-catalog.ts (static env catalog -&gt; BUSINESS -&gt; scraper fallback)\n    -&gt; steps/ (Effect TS programs for each wizard stage)\n      -&gt; browser-service.ts (Playwright lifecycle via Effect Layer)\n        -&gt; selectors.ts (CSS selector registry with fallback chains)\n</code></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Key Components</h3><a id=\"user-content-key-components\" class=\"anchor\" aria-label=\"Permalink: Key Components\" href=\"#key-components\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>server/handler.ts</strong> -- Standalone Node.js HTTP server with Bearer token auth</li>\n<li><strong>acuity-service-catalog.ts</strong> -- Shared service source order and cache for static config, BUSINESS extraction, and scraper fallback</li>\n<li><strong>browser-service.ts</strong> -- Effect TS Layers for a warm shared browser process plus request-scoped page sessions</li>\n<li><strong>acuity-wizard.ts</strong> -- Full <code>SchedulingAdapter</code> implementation (local Playwright or remote HTTP proxy)</li>\n<li><strong>remote-adapter.ts</strong> -- HTTP client adapter for proxying to a remote middleware instance</li>\n<li><strong>selectors.ts</strong> -- Single source of truth for all Acuity DOM selectors</li>\n<li><strong>steps/</strong> -- Individual wizard step programs plus BUSINESS extraction helpers</li>\n<li><strong>acuity-scraper.ts</strong> -- Deprecated read fallback for services, dates, and time slots</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Endpoints</h2><a id=\"user-content-endpoints\" class=\"anchor\" aria-label=\"Permalink: Endpoints\" href=\"#endpoints\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Method</th>\n<th>Path</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>GET</td>\n<td><code>/health</code></td>\n<td>Health check (no auth required)</td>\n</tr>\n<tr>\n<td>GET</td>\n<td><code>/services</code></td>\n<td>List appointment types via <code>SERVICES_JSON</code> -&gt; BUSINESS -&gt; scraper fallback</td>\n</tr>\n<tr>\n<td>GET</td>\n<td><code>/services/:id</code></td>\n<td>Get a specific service</td>\n</tr>\n<tr>\n<td>POST</td>\n<td><code>/availability/dates</code></td>\n<td>Available dates for a service</td>\n</tr>\n<tr>\n<td>POST</td>\n<td><code>/availability/slots</code></td>\n<td>Time slots for a specific date</td>\n</tr>\n<tr>\n<td>POST</td>\n<td><code>/availability/check</code></td>\n<td>Check if a slot is available</td>\n</tr>\n<tr>\n<td>POST</td>\n<td><code>/booking/create</code></td>\n<td>Create a booking (standard)</td>\n</tr>\n<tr>\n<td>POST</td>\n<td><code>/booking/create-with-payment</code></td>\n<td>Create booking with payment bypass (coupon)</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Health Contract</h3><a id=\"user-content-health-contract\" class=\"anchor\" aria-label=\"Permalink: Health Contract\" href=\"#health-contract\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><code>GET /health</code> is the stable downstream runtime-truth surface.</p>\n<p dir=\"auto\">In addition to basic runtime data, it now publishes:</p>\n<ul dir=\"auto\">\n<li>release tuple:\n<ul dir=\"auto\">\n<li><code>releaseSha</code></li>\n<li><code>releaseRef</code></li>\n<li><code>releaseVersion</code></li>\n<li><code>releaseBuiltAt</code></li>\n<li>nested <code>release.{ sha, ref, version, builtAt, modalEnvironment }</code></li>\n</ul>\n</li>\n<li>protocol tuple:\n<ul dir=\"auto\">\n<li><code>protocolVersion</code></li>\n<li>nested <code>protocol.version</code></li>\n<li><code>protocol.flowOwner = \"scheduling-bridge\"</code></li>\n<li><code>protocol.backend = \"acuity\"</code></li>\n<li><code>protocol.transport = \"http-json\"</code></li>\n<li><code>protocol.endpoints</code></li>\n<li><code>protocol.capabilities</code></li>\n</ul>\n</li>\n</ul>\n<p dir=\"auto\">Downstream apps should use this tuple to assert which bridge release and protocol\nsurface they are talking to during beta validation and rollout claims.</p>\n<p dir=\"auto\">This tuple is the supported runtime truth surface for adopters. Downstream apps\nshould not infer bridge ownership from package metadata, branch names, or Modal\ndashboard state when <code>/health</code> is available.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Environment Variables</h2><a id=\"user-content-environment-variables\" class=\"anchor\" aria-label=\"Permalink: Environment Variables\" href=\"#environment-variables\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Variable</th>\n<th>Required</th>\n<th>Default</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>PORT</code></td>\n<td>No</td>\n<td><code>3001</code></td>\n<td>Server port</td>\n</tr>\n<tr>\n<td><code>ACUITY_BASE_URL</code></td>\n<td>No</td>\n<td><code>https://MassageIthaca.as.me</code></td>\n<td>Acuity scheduling page URL</td>\n</tr>\n<tr>\n<td><code>AUTH_TOKEN</code></td>\n<td>Recommended</td>\n<td>--</td>\n<td>Bearer token for all endpoints (except /health)</td>\n</tr>\n<tr>\n<td><code>ACUITY_BYPASS_COUPON</code></td>\n<td>For payment bypass</td>\n<td>--</td>\n<td>100% gift certificate code</td>\n</tr>\n<tr>\n<td><code>PLAYWRIGHT_HEADLESS</code></td>\n<td>No</td>\n<td><code>true</code></td>\n<td>Run browser headless</td>\n</tr>\n<tr>\n<td><code>PLAYWRIGHT_TIMEOUT</code></td>\n<td>No</td>\n<td><code>30000</code></td>\n<td>Page operation timeout (ms)</td>\n</tr>\n<tr>\n<td><code>CHROMIUM_EXECUTABLE_PATH</code></td>\n<td>No</td>\n<td>--</td>\n<td>Custom Chromium path (for Lambda/serverless)</td>\n</tr>\n<tr>\n<td><code>CHROMIUM_LAUNCH_ARGS</code></td>\n<td>No</td>\n<td>--</td>\n<td>Comma-separated Chromium args</td>\n</tr>\n<tr>\n<td><code>SERVICES_JSON</code></td>\n<td>No</td>\n<td>--</td>\n<td>Optional static service catalog to bypass live Acuity reads</td>\n</tr>\n<tr>\n<td><code>ACUITY_SERVICE_CACHE_TTL_MS</code></td>\n<td>No</td>\n<td><code>300000</code></td>\n<td>TTL for cached live service catalogs before BUSINESS/scraper refresh</td>\n</tr>\n<tr>\n<td><code>SCHEDULING_BRIDGE_SLOT_PROFILE_THRESHOLD_MS</code></td>\n<td>No</td>\n<td><code>1500</code></td>\n<td>Threshold in ms for logging long-tail slot-read profile events</td>\n</tr>\n<tr>\n<td><code>SCHEDULING_BRIDGE_PROFILE_SLOT_READS</code></td>\n<td>No</td>\n<td><code>false</code></td>\n<td>Force logging of slot-read profile events even when under threshold</td>\n</tr>\n<tr>\n<td><code>MIDDLEWARE_RELEASE_SHA</code></td>\n<td>No</td>\n<td>--</td>\n<td>Release commit SHA exposed via <code>/health</code></td>\n</tr>\n<tr>\n<td><code>MIDDLEWARE_RELEASE_REF</code></td>\n<td>No</td>\n<td>--</td>\n<td>Release ref/tag exposed via <code>/health</code></td>\n</tr>\n<tr>\n<td><code>MIDDLEWARE_RELEASE_VERSION</code></td>\n<td>No</td>\n<td>--</td>\n<td>Release version exposed via <code>/health</code></td>\n</tr>\n<tr>\n<td><code>MIDDLEWARE_RELEASE_BUILT_AT</code></td>\n<td>No</td>\n<td>--</td>\n<td>Build timestamp exposed via <code>/health</code></td>\n</tr>\n<tr>\n<td><code>MIDDLEWARE_BUILD_TIMESTAMP</code></td>\n<td>No</td>\n<td>--</td>\n<td>Legacy fallback build timestamp for <code>/health</code></td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Observability</h3><a id=\"user-content-observability\" class=\"anchor\" aria-label=\"Permalink: Observability\" href=\"#observability\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The bridge emits NDJSON logs to stdout/stderr for runtime analysis.</p>\n<ul dir=\"auto\">\n<li><code>/health</code> remains the authoritative runtime-truth surface for downstream apps</li>\n<li>request handlers emit request-scoped structured events, including <code>requestId</code></li>\n<li>long-tail slot reads emit <code>slot_read_profile</code> events with phase timings</li>\n<li><code>SCHEDULING_BRIDGE_PROFILE_SLOT_READS=1</code> forces profile emission for all slot reads</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Deployment</h2><a id=\"user-content-deployment\" class=\"anchor\" aria-label=\"Permalink: Deployment\" href=\"#deployment\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Standalone Node.js</h3><a id=\"user-content-standalone-nodejs\" class=\"anchor\" aria-label=\"Permalink: Standalone Node.js\" href=\"#standalone-nodejs\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm install\npnpm dev           # Development with tsx against src/server/handler.ts\n# or\npnpm build &amp;&amp; pnpm start  # Materialize Bazel-derived dist/ and start it\"><pre>pnpm install\npnpm dev           <span class=\"pl-c\"><span class=\"pl-c\">#</span> Development with tsx against src/server/handler.ts</span>\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> or</span>\npnpm build <span class=\"pl-k\">&amp;&amp;</span> pnpm start  <span class=\"pl-c\"><span class=\"pl-c\">#</span> Materialize Bazel-derived dist/ and start it</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Docker</h3><a id=\"user-content-docker\" class=\"anchor\" aria-label=\"Permalink: Docker\" href=\"#docker\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"docker build -t scheduling-bridge .\ndocker run -p 3001:3001 \\\n  -e AUTH_TOKEN=your-secret-token \\\n  -e ACUITY_BASE_URL=https://YourBusiness.as.me \\\n  -e ACUITY_BYPASS_COUPON=your-coupon-code \\\n  scheduling-bridge\"><pre>docker build -t scheduling-bridge <span class=\"pl-c1\">.</span>\ndocker run -p 3001:3001 \\\n  -e AUTH_TOKEN=your-secret-token \\\n  -e ACUITY_BASE_URL=https://YourBusiness.as.me \\\n  -e ACUITY_BYPASS_COUPON=your-coupon-code \\\n  scheduling-bridge</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Modal Labs</h3><a id=\"user-content-modal-labs\" class=\"anchor\" aria-label=\"Permalink: Modal Labs\" href=\"#modal-labs\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Set secrets in Modal dashboard first:\n#   AUTH_TOKEN, ACUITY_BASE_URL, ACUITY_BYPASS_COUPON\n# The Modal workflow materializes the Bazel-derived pkg/ before deploy.\nmodal deploy modal-app.py\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Set secrets in Modal dashboard first:</span>\n<span class=\"pl-c\"><span class=\"pl-c\">#</span>   AUTH_TOKEN, ACUITY_BASE_URL, ACUITY_BYPASS_COUPON</span>\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> The Modal workflow materializes the Bazel-derived pkg/ before deploy.</span>\nmodal deploy modal-app.py</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">Supported deployment path</h4><a id=\"user-content-supported-deployment-path\" class=\"anchor\" aria-label=\"Permalink: Supported deployment path\" href=\"#supported-deployment-path\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The supported deployment path for the live Acuity bridge is:</p>\n<ol dir=\"auto\">\n<li>merge to <code>main</code></li>\n<li>let <code>.github/workflows/deploy-modal.yml</code> deploy <code>modal-app.py</code></li>\n<li>inject <code>MIDDLEWARE_RELEASE_SHA</code>, <code>MIDDLEWARE_RELEASE_REF</code>,\n<code>MIDDLEWARE_RELEASE_VERSION</code>, and <code>MIDDLEWARE_RELEASE_BUILT_AT</code></li>\n<li>verify the resulting bridge tuple via <code>GET /health</code></li>\n</ol>\n<p dir=\"auto\">Operationally, this means:</p>\n<ul dir=\"auto\">\n<li>Modal deployment is part of release truth, not a side channel</li>\n<li>the live bridge should be identified by the <code>/health</code> release + protocol tuple</li>\n<li>downstream apps should validate the tuple they expect before making rollout claims</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Nix</h3><a id=\"user-content-nix\" class=\"anchor\" aria-label=\"Permalink: Nix\" href=\"#nix\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix develop   # Enter dev shell with Node.js + Playwright\npnpm install\npnpm dev\"><pre>nix develop   <span class=\"pl-c\"><span class=\"pl-c\">#</span> Enter dev shell with Node.js + Playwright</span>\npnpm install\npnpm dev</pre></div>\n\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Release Authority</h2><a id=\"user-content-release-authority\" class=\"anchor\" aria-label=\"Permalink: Release Authority\" href=\"#release-authority\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Current release authority:</p>\n<ul dir=\"auto\">\n<li>canonical repo: <code>Jesssullivan/scheduling-bridge</code></li>\n<li>npm package: <code>@tummycrypt/scheduling-bridge</code></li>\n<li>GitHub Packages mirror: <code>@jesssullivan/scheduling-bridge</code></li>\n</ul>\n<p dir=\"auto\">The current publish + deploy shape is:</p>\n<ol dir=\"auto\">\n<li>release metadata declared once</li>\n<li>Bazel validates/builds the publishable artifact</li>\n<li>CI dry-runs the extracted Bazel package surface before release</li>\n<li>GitHub Actions publishes that extracted artifact</li>\n<li>GitHub Actions deploys the Modal runtime from <code>main</code></li>\n<li>downstream apps consume the published package and verify the live runtime\ntuple via <code>/health</code></li>\n</ol>\n<p dir=\"auto\">This repo is the sole owner of Acuity automation concerns. App repos and shared\npackages may consume the bridge and assert its runtime tuple, but they should\nnot duplicate bridge runtime ownership or release truth logic.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Runner Authority</h2><a id=\"user-content-runner-authority\" class=\"anchor\" aria-label=\"Permalink: Runner Authority\" href=\"#runner-authority\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Package CI and publish currently use the shared <code>js-bazel-package</code> workflow with\n<code>runner_mode: shared</code> and <code>publish_mode: same_runner</code>.</p>\n<p dir=\"auto\">The concrete shared-runner labels come from repository Actions variables and\nmust be proven by green workflow runs before they are treated as operational\ntruth. Keep private runner topology and apply details out of this public repo.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Development</h2><a id=\"user-content-development\" class=\"anchor\" aria-label=\"Permalink: Development\" href=\"#development\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm install      # Install dependencies\npnpm dev          # Start dev server with tsx\npnpm typecheck    # Run Bazel typecheck target\npnpm build        # Materialize local pkg/ and dist/ from bazel-bin/pkg\npnpm test         # Run Bazel test target\npnpm docs:generate\"><pre>pnpm install      <span class=\"pl-c\"><span class=\"pl-c\">#</span> Install dependencies</span>\npnpm dev          <span class=\"pl-c\"><span class=\"pl-c\">#</span> Start dev server with tsx</span>\npnpm typecheck    <span class=\"pl-c\"><span class=\"pl-c\">#</span> Run Bazel typecheck target</span>\npnpm build        <span class=\"pl-c\"><span class=\"pl-c\">#</span> Materialize local pkg/ and dist/ from bazel-bin/pkg</span>\npnpm <span class=\"pl-c1\">test</span>         <span class=\"pl-c\"><span class=\"pl-c\">#</span> Run Bazel test target</span>\npnpm docs:generate</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">MIT</p>\n</article></div>",
      "readme_excerpt": "<!-- markdownlint-disable MD013 MD040 MD060 -->\nBackend-agnostic scheduling adapter hub. Currently bridges Acuity Scheduling via Playwright browser automation, with architecture designed to support additional scheduling backends.\n> Formerly acuity-middleware. Historical GitHub URLs may redirect, but the\n> canonical repo is Jesssullivan/scheduling-bridge.\nAn HTTP server wrapping Playwright wizard flows that automate the Acuity booking UI. The bridge uses Effect TS for resource lifecycle...",
      "install_commands": {
        "npm": "npm install @tummycrypt/scheduling-bridge"
      },
      "repo_url": "https://github.com/jesssullivan/scheduling-bridge",
      "website_url": null,
      "docs_url": null,
      "registry_url": "https://www.npmjs.com/package/@tummycrypt/scheduling-bridge",
      "releases_url": "https://github.com/jesssullivan/scheduling-bridge/releases",
      "og_image_url": "https://opengraph.githubassets.com/3d508e58b48c4c1a934e77ec05d12dec5f4946557142afedca82b17160f6c40d/Jesssullivan/scheduling-bridge",
      "license": "MIT",
      "pushed_at": "2026-04-26T03:01:19Z",
      "enriched_at": "2026-04-26T17:17:12.499Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-scheduling-kit",
      "name": "scheduling-kit",
      "repo": "jesssullivan/scheduling-kit",
      "org": "jesssullivan",
      "ecosystem": "npm",
      "category": "scheduling",
      "description": "Backend-agnostic scheduling system with Acuity, CalCom, and homegrown adapters",
      "featured": true,
      "tags": [
        "scheduling",
        "svelte",
        "payments",
        "effect-ts"
      ],
      "version": "0.6.1",
      "release_date": "2026-04-11T22:38:29Z",
      "releases": [
        {
          "tag": "0.6.1",
          "date": "2026-04-11T22:38:29Z",
          "body": "Status helpers: getStripeSetupSteps, getPayPalSetupSteps, getOverallProgress + 17 tests."
        },
        {
          "tag": "0.6.0",
          "date": "2026-04-11T17:02:51Z",
          "body": "New `@tummycrypt/scheduling-kit/onboarding` subpackage.\n\n## What's new\n- `CredentialStore` + `EncryptionProvider` interfaces for app-provided storage\n- Stripe Connect OAuth: `buildStripeAuthorizeUrl()`, `exchangeStripeCode()`\n- Stripe account status: `getStripeAccountStatus()`\n- Stripe key validation: `validateStripeKeys()`\n- Stripe webhook CRUD: `createStripeWebhook()`, `deleteStripeWebhooks()`\n- PayPal credential validation: `validatePayPalCredentials()`\n- PayPal webhook creation: `createPayPalWebhook()`\n- Bazel `//src/onboarding` target\n- `./onboarding` package.json export\n\n## Pattern\nLibrary defines interfaces + pure helpers. Application implements CredentialStore with its DB.\nSame DI pattern as HomegrownAdapter's getDb callback."
        },
        {
          "tag": "0.5.2",
          "date": "2026-04-11T01:34:51Z",
          "body": "PayPal return URLs for proper popup flow. Fixes extra 2FA verification loops during Venmo checkout."
        },
        {
          "tag": "0.5.1",
          "date": "2026-04-08T14:20:35Z",
          "body": "Add payeeEmail option to VenmoAdapterConfig. Routes payments to practitioner's PayPal account via purchase_units.payee.email_address."
        },
        {
          "tag": "0.5.0",
          "date": "2026-04-07T21:05:02Z",
          "body": "## Breaking Changes\n\n- Removed `AcuityScraper` and `createScraperAdapter()` — scraper functionality now lives in `@tummycrypt/scheduling-bridge`\n- Removed middleware code (11,215 lines) — now in `@tummycrypt/scheduling-bridge`\n\n## Added\n\n- Bazel 8 configuration (MODULE.bazel, BUILD.bazel with subpackage targets)\n- `.bazelrc` with CI/debug/release configs\n- `.npmrc` with `hoist=false` for rules_js compatibility\n\n## Migration\n\nReplace `@tummycrypt/scheduling-kit` scraper imports with `@tummycrypt/scheduling-bridge`:\n\n```diff\n- import { createScraperAdapter } from '@tummycrypt/scheduling-kit/adapters'\n+ import { createScraperAdapter } from '@tummycrypt/scheduling-bridge'\n```"
        },
        {
          "tag": "0.4.0",
          "date": "2026-04-06T00:20:10Z",
          "body": "BREAKING: Remove middleware code (11,215 lines). Middleware belongs in @tummycrypt/acuity-middleware. scheduling-kit is the homegrown backend, not the Acuity adapter."
        },
        {
          "tag": "0.3.1",
          "date": "2026-04-05T17:33:03Z",
          "body": "Fix dark mode text contrast: 63 single-value color classes replaced with Skeleton light-dark pairings across 13 component files."
        },
        {
          "tag": "0.3.0",
          "date": "2026-03-30T03:18:42Z",
          "body": "Middleware subsystem, Effect migration, test gate, npm provenance."
        },
        {
          "tag": "0.2.1",
          "date": "2026-03-29T03:17:09Z",
          "body": "## What's Changed\n- **Fix 5 runtime crashes** from leftover fp-ts calling conventions that survived the Effect 3.x migration\n- **Fix parse error** in homegrown adapter `createBookingWithPaymentRef` (extra closing paren)\n- **Fix broken `getAvailableMethods()`** in PaymentRegistry — was always returning empty array\n- **Fix `CheckoutDrawer`** — all 5 data-loading functions now use `Effect.runPromise` instead of fp-ts thunks\n- **Remove dead re-export** of non-existent `fp-ts.js` test helper\n- **Fix `E.Either` type references** in deprecated acuity-scraper\n\n## Upgrading from 0.2.0\nNo API changes — this is a bugfix release. All consumers should upgrade:\n```bash\npnpm add @tummycrypt/scheduling-kit@0.2.1\n```"
        },
        {
          "tag": "0.1.1",
          "date": "2026-03-24T20:57:31Z",
          "body": "## @tummycrypt/scheduling-kit v0.1.1\n\nBackend-agnostic scheduling system with Acuity, CalCom, and homegrown PostgreSQL adapters.\n\n### Install\n\n```bash\nnpm install @tummycrypt/scheduling-kit\n```\n\n### npm\n\nhttps://www.npmjs.com/package/@tummycrypt/scheduling-kit\n\n### Changes in v0.1.1\n\n- Updated repository URL after transfer to Jesssullivan/scheduling-kit"
        }
      ],
      "stars": 0,
      "topics": [
        "acuity-scheduling",
        "calcom",
        "functional-design",
        "postgresql",
        "property-based-testing",
        "scheduling-system",
        "sveltekit",
        "zod-validation",
        "no-api-no-problem",
        "work-harder-not-smarter"
      ],
      "languages": [
        {
          "name": "TypeScript",
          "color": "#3178c6",
          "bytes": 652061
        },
        {
          "name": "Svelte",
          "color": "#ff3e00",
          "bytes": 121992
        },
        {
          "name": "JavaScript",
          "color": "#f1e05a",
          "bytes": 14612
        },
        {
          "name": "Starlark",
          "color": "#76d275",
          "bytes": 11719
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 2794
        }
      ],
      "primary_language": "TypeScript",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">@tummycrypt/scheduling-kit</h1><a id=\"user-content-tummycryptscheduling-kit\" class=\"anchor\" aria-label=\"Permalink: @tummycrypt/scheduling-kit\" href=\"#tummycryptscheduling-kit\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Backend-agnostic scheduling library with Svelte 5 components, pluggable\nscheduling adapters, alternative payment support, and Effect-powered workflow\ncomposition.</p>\n<p dir=\"auto\">This repo keeps two intentionally different build surfaces:</p>\n<ul dir=\"auto\">\n<li><code>pnpm</code> is the local package-manager and script interface</li>\n<li>Bazel defines and builds the publishable package artifact used by CI</li>\n</ul>\n<p dir=\"auto\">The recommended local bootstrap path is the repo flake plus <code>direnv</code>, which\nmakes <code>pnpm</code>, <code>bazel</code> through Bazelisk, and the docs toolchain available from a\nclean shell.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Features</h2><a id=\"user-content-features\" class=\"anchor\" aria-label=\"Permalink: Features\" href=\"#features\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>Multiple scheduling backends</strong> -- Acuity REST API, Cal.com, or\nbring-your-own PostgreSQL (HomegrownAdapter)</li>\n<li><strong>Svelte 5 components</strong> -- ServicePicker, DateTimePicker, ClientForm,\nCheckoutDrawer, and more</li>\n<li><strong>Payment adapters</strong> -- Stripe, Venmo/PayPal SDK, cash, Zelle, check</li>\n<li><strong>Availability engine</strong> -- Pure-function slot generation, DST-safe via <code>Intl.DateTimeFormat</code></li>\n<li><strong>Reconciliation</strong> -- Alt-payment matching and webhook handling</li>\n<li><strong>Test infrastructure</strong> -- Cassette-based API recording/playback, MSW\nmocking, property-based tests</li>\n<li><strong>Functional core</strong> -- Effect-powered scheduling flows and typed error handling</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Installation</h2><a id=\"user-content-installation\" class=\"anchor\" aria-label=\"Permalink: Installation\" href=\"#installation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm add @tummycrypt/scheduling-kit\"><pre>pnpm add @tummycrypt/scheduling-kit</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Development Environment</h2><a id=\"user-content-development-environment\" class=\"anchor\" aria-label=\"Permalink: Development Environment\" href=\"#development-environment\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"direnv allow\npnpm install\"><pre>direnv allow\npnpm install</pre></div>\n<p dir=\"auto\">If <code>bazel</code> is missing in your current shell, enter the flake environment with\n<code>nix develop</code> or let <code>direnv</code> load <code>.envrc</code>. The dev shell provides a <code>bazel</code>\nwrapper backed by Bazelisk and pinned by <code>.bazelversion</code>.</p>\n<p dir=\"auto\">Peer dependencies (install those you need):</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Required\npnpm add svelte\n\n# Optional -- for UI components\npnpm add @skeletonlabs/skeleton @skeletonlabs/skeleton-svelte\n\n# Optional -- for E2E tests\npnpm add -D playwright-core\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Required</span>\npnpm add svelte\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Optional -- for UI components</span>\npnpm add @skeletonlabs/skeleton @skeletonlabs/skeleton-svelte\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Optional -- for E2E tests</span>\npnpm add -D playwright-core</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Release Hygiene</h2><a id=\"user-content-release-hygiene\" class=\"anchor\" aria-label=\"Permalink: Release Hygiene\" href=\"#release-hygiene\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm check:release-metadata\npnpm check:package\nbazel build //:pkg\nnpm pack --dry-run ./bazel-bin/pkg\"><pre>pnpm check:release-metadata\npnpm check:package\nbazel build //:pkg\nnpm pack --dry-run ./bazel-bin/pkg</pre></div>\n<p dir=\"auto\">Those checks keep <code>package.json</code>, <code>MODULE.bazel</code>, and <code>BUILD.bazel</code> aligned,\nthen validate the Bazel-built package artifact before anything gets near a\nregistry.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Documentation</h2><a id=\"user-content-documentation\" class=\"anchor\" aria-label=\"Permalink: Documentation\" href=\"#documentation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm docs:generate      # Regenerate derived Markdown and llms surfaces\npnpm docs:check         # Validate generated docs and MkDocs config\npnpm docs:serve         # Local docs preview at http://127.0.0.1:8000\nnix build .#docs        # Build the static docs site as a derivation\nnix flake check         # Evaluate flake outputs and run lightweight checks\"><pre>pnpm docs:generate      <span class=\"pl-c\"><span class=\"pl-c\">#</span> Regenerate derived Markdown and llms surfaces</span>\npnpm docs:check         <span class=\"pl-c\"><span class=\"pl-c\">#</span> Validate generated docs and MkDocs config</span>\npnpm docs:serve         <span class=\"pl-c\"><span class=\"pl-c\">#</span> Local docs preview at http://127.0.0.1:8000</span>\nnix build .<span class=\"pl-c\"><span class=\"pl-c\">#</span>docs        # Build the static docs site as a derivation</span>\nnix flake check         <span class=\"pl-c\"><span class=\"pl-c\">#</span> Evaluate flake outputs and run lightweight checks</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Release Authority</h2><a id=\"user-content-release-authority\" class=\"anchor\" aria-label=\"Permalink: Release Authority\" href=\"#release-authority\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Current reality:</p>\n<ul dir=\"auto\">\n<li>the functional release line is <code>Jesssullivan/scheduling-kit</code></li>\n<li><code>tinyland-inc/scheduling-kit</code> is now a downstream mirror and validation\nsurface, not a second publish authority</li>\n</ul>\n<p dir=\"auto\">Treat <code>Jesssullivan/main</code> as the release authority for package publication and\nmetadata changes. Do not assume both <code>main</code> branches are equivalent.</p>\n<p dir=\"auto\">Longer term, the intended publish shape is:</p>\n<ol dir=\"auto\">\n<li>release metadata declared once</li>\n<li>Bazel defines and builds the publishable artifact</li>\n<li>GitHub Actions publishes that artifact to npm</li>\n<li>downstream apps consume the published package only</li>\n</ol>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Runner Authority</h2><a id=\"user-content-runner-authority\" class=\"anchor\" aria-label=\"Permalink: Runner Authority\" href=\"#runner-authority\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Package CI and publish currently use the shared <code>js-bazel-package</code> workflow with\n<code>runner_mode: shared</code> and labels from <code>PRIMARY_LINUX_RUNNER_LABELS_JSON</code>.</p>\n<p dir=\"auto\">Treat that runner contract as pending proof until the repo Actions runner API\nand green workflow runs confirm the lane. Keep private runner topology and\napply details out of this public repo.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick Start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick Start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { Effect } from 'effect';\nimport {\n  createSchedulingKit,\n  createHomegrownAdapter,\n  createStripeAdapter,\n  createVenmoAdapter,\n} from '@tummycrypt/scheduling-kit';\n\n// Create a scheduling adapter\nconst scheduler = createHomegrownAdapter({\n  db: drizzleInstance,\n  timezone: 'America/New_York',\n});\n\n// Create payment adapters\nconst stripe = createStripeAdapter({\n  type: 'stripe',\n  secretKey: process.env.STRIPE_SECRET_KEY!,\n  publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!,\n});\n\nconst venmo = createVenmoAdapter({\n  type: 'venmo',\n  clientId: process.env.PAYPAL_CLIENT_ID!,\n  clientSecret: process.env.PAYPAL_CLIENT_SECRET!,\n  environment: 'sandbox',\n});\n\n// Compose into a scheduling kit\nconst kit = createSchedulingKit(scheduler, [stripe, venmo]);\n\n// Complete a booking\nconst result = await Effect.runPromise(\n  kit.completeBooking(request, 'stripe')\n);\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-v\">Effect</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'effect'</span><span class=\"pl-kos\">;</span>\n<span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span>\n  <span class=\"pl-s1\">createSchedulingKit</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">createHomegrownAdapter</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">createStripeAdapter</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">createVenmoAdapter</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/scheduling-kit'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-c\">// Create a scheduling adapter</span>\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">scheduler</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createHomegrownAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">db</span>: <span class=\"pl-s1\">drizzleInstance</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">timezone</span>: <span class=\"pl-s\">'America/New_York'</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-c\">// Create payment adapters</span>\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">stripe</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createStripeAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">type</span>: <span class=\"pl-s\">'stripe'</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">secretKey</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">STRIPE_SECRET_KEY</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">publishableKey</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">STRIPE_PUBLISHABLE_KEY</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">venmo</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createVenmoAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">type</span>: <span class=\"pl-s\">'venmo'</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">clientId</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">PAYPAL_CLIENT_ID</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">clientSecret</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">PAYPAL_CLIENT_SECRET</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">environment</span>: <span class=\"pl-s\">'sandbox'</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-c\">// Compose into a scheduling kit</span>\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">kit</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createSchedulingKit</span><span class=\"pl-kos\">(</span><span class=\"pl-s1\">scheduler</span><span class=\"pl-kos\">,</span> <span class=\"pl-kos\">[</span><span class=\"pl-s1\">stripe</span><span class=\"pl-kos\">,</span> <span class=\"pl-s1\">venmo</span><span class=\"pl-kos\">]</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-c\">// Complete a booking</span>\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">result</span> <span class=\"pl-c1\">=</span> <span class=\"pl-k\">await</span> <span class=\"pl-v\">Effect</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">runPromise</span><span class=\"pl-kos\">(</span>\n  <span class=\"pl-s1\">kit</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">completeBooking</span><span class=\"pl-kos\">(</span><span class=\"pl-s1\">request</span><span class=\"pl-kos\">,</span> <span class=\"pl-s\">'stripe'</span><span class=\"pl-kos\">)</span>\n<span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Ownership Boundary</h2><a id=\"user-content-ownership-boundary\" class=\"anchor\" aria-label=\"Permalink: Ownership Boundary\" href=\"#ownership-boundary\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">This package owns reusable scheduling contracts, payment adapters, checkout UI,\nand Acuity REST plus iframe handoff primitives.</p>\n<p dir=\"auto\">It does <strong>not</strong> own:</p>\n<ul dir=\"auto\">\n<li>browser automation</li>\n<li>remote Acuity scraping</li>\n<li>Modal deployment/runtime control</li>\n<li>site-specific booking orchestration</li>\n</ul>\n<p dir=\"auto\">If your Acuity flow needs browser automation or remote bridge-backed booking\nsemantics, that ownership belongs to <code>@tummycrypt/scheduling-bridge</code> plus the\nadopter app that wires the handoff.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Adapters</h2><a id=\"user-content-adapters\" class=\"anchor\" aria-label=\"Permalink: Adapters\" href=\"#adapters\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">HomegrownAdapter</h3><a id=\"user-content-homegrownadapter\" class=\"anchor\" aria-label=\"Permalink: HomegrownAdapter\" href=\"#homegrownadapter\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Direct PostgreSQL adapter using Drizzle ORM. Replaces third-party scheduling\nAPIs entirely.</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { createHomegrownAdapter } from '@tummycrypt/scheduling-kit/adapters';\n\nconst adapter = createHomegrownAdapter({\n  db: drizzleInstance,\n  timezone: 'America/New_York',\n});\n\n// 16 methods: getServices, getAvailability, getSlots, book, cancel,\n// reschedule, ...\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">createHomegrownAdapter</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/scheduling-kit/adapters'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">adapter</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createHomegrownAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">db</span>: <span class=\"pl-s1\">drizzleInstance</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">timezone</span>: <span class=\"pl-s\">'America/New_York'</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-c\">// 16 methods: getServices, getAvailability, getSlots, book, cancel,</span>\n<span class=\"pl-c\">// reschedule, ...</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">AcuityAdapter</h3><a id=\"user-content-acuityadapter\" class=\"anchor\" aria-label=\"Permalink: AcuityAdapter\" href=\"#acuityadapter\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">API-based adapter for Acuity Scheduling (requires Powerhouse plan).\nFor browser automation and no-API migration flows, use\n<code>@tummycrypt/scheduling-bridge</code>.</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { createAcuityAdapter } from '@tummycrypt/scheduling-kit/adapters';\n\nconst config = {\n  type: 'acuity' as const,\n  userId: process.env.ACUITY_USER_ID!,\n  apiKey: process.env['ACUITY_API_KEY']!,  // from Acuity Integrations page\n};\nconst adapter = createAcuityAdapter(config);\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">createAcuityAdapter</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/scheduling-kit/adapters'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">config</span> <span class=\"pl-c1\">=</span> <span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">type</span>: <span class=\"pl-s\">'acuity'</span> <span class=\"pl-k\">as</span> <span class=\"pl-k\">const</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">userId</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">ACUITY_USER_ID</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">apiKey</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">[</span><span class=\"pl-s\">'ACUITY_API_KEY'</span><span class=\"pl-kos\">]</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>  <span class=\"pl-c\">// from Acuity Integrations page</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">;</span>\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">adapter</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createAcuityAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-s1\">config</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">CalComAdapter</h3><a id=\"user-content-calcomadapter\" class=\"anchor\" aria-label=\"Permalink: CalComAdapter\" href=\"#calcomadapter\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Stub adapter for future Cal.com integration.</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { createCalComAdapter } from '@tummycrypt/scheduling-kit/adapters';\n\nconst adapter = createCalComAdapter({\n  type: 'calcom',\n  apiKey: process.env['CALCOM_API_KEY']!,\n  baseUrl: 'https://api.cal.com/v1',\n});\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">createCalComAdapter</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/scheduling-kit/adapters'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">adapter</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createCalComAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">type</span>: <span class=\"pl-s\">'calcom'</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">apiKey</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">[</span><span class=\"pl-s\">'CALCOM_API_KEY'</span><span class=\"pl-kos\">]</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">baseUrl</span>: <span class=\"pl-s\">'https://api.cal.com/v1'</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Availability Engine</h2><a id=\"user-content-availability-engine\" class=\"anchor\" aria-label=\"Permalink: Availability Engine\" href=\"#availability-engine\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Pure functions for slot generation. DST-safe, timezone-aware, fully tested.</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import {\n  getAvailableSlots,\n  isSlotAvailable,\n  getDatesWithAvailability,\n  getEffectiveHours,\n} from '@tummycrypt/scheduling-kit/adapters';\n\nconst slots = getAvailableSlots({\n  date: '2026-03-22',\n  timezone: 'America/New_York',\n  hours: [{ dayOfWeek: 6, startTime: '11:00', endTime: '16:00' }],\n  overrides: [],\n  occupied: [],\n  slotDuration: 60,\n  bufferMinutes: 15,\n});\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span>\n  <span class=\"pl-s1\">getAvailableSlots</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">isSlotAvailable</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">getDatesWithAvailability</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">getEffectiveHours</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/scheduling-kit/adapters'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">slots</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">getAvailableSlots</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">date</span>: <span class=\"pl-s\">'2026-03-22'</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">timezone</span>: <span class=\"pl-s\">'America/New_York'</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">hours</span>: <span class=\"pl-kos\">[</span><span class=\"pl-kos\">{</span> <span class=\"pl-c1\">dayOfWeek</span>: <span class=\"pl-c1\">6</span><span class=\"pl-kos\">,</span> <span class=\"pl-c1\">startTime</span>: <span class=\"pl-s\">'11:00'</span><span class=\"pl-kos\">,</span> <span class=\"pl-c1\">endTime</span>: <span class=\"pl-s\">'16:00'</span> <span class=\"pl-kos\">}</span><span class=\"pl-kos\">]</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">overrides</span>: <span class=\"pl-kos\">[</span><span class=\"pl-kos\">]</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">occupied</span>: <span class=\"pl-kos\">[</span><span class=\"pl-kos\">]</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">slotDuration</span>: <span class=\"pl-c1\">60</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">bufferMinutes</span>: <span class=\"pl-c1\">15</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Components</h2><a id=\"user-content-components\" class=\"anchor\" aria-label=\"Permalink: Components\" href=\"#components\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Svelte 5 components using runes syntax. Optional Skeleton 4 integration for styling.</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>ServicePicker</code></td>\n<td>Service/appointment type selector</td>\n</tr>\n<tr>\n<td><code>DateTimePicker</code></td>\n<td>Calendar date + time slot picker</td>\n</tr>\n<tr>\n<td><code>ClientForm</code></td>\n<td>Client info form with Zod validation</td>\n</tr>\n<tr>\n<td><code>PaymentSelector</code></td>\n<td>Payment method chooser</td>\n</tr>\n<tr>\n<td><code>ProviderPicker</code></td>\n<td>Practitioner/provider selector</td>\n</tr>\n<tr>\n<td><code>BookingConfirmation</code></td>\n<td>Post-booking confirmation display</td>\n</tr>\n<tr>\n<td><code>CheckoutDrawer</code></td>\n<td>Full checkout flow in a slide-out drawer</td>\n</tr>\n<tr>\n<td><code>HybridCheckoutDrawer</code></td>\n<td>Checkout UI for adopter-provided Acuity handoff</td>\n</tr>\n<tr>\n<td><code>VenmoButton</code></td>\n<td>Venmo/PayPal payment button</td>\n</tr>\n<tr>\n<td><code>VenmoCheckout</code></td>\n<td>Full Venmo checkout flow</td>\n</tr>\n<tr>\n<td><code>StripeCheckout</code></td>\n<td>Stripe Elements checkout</td>\n</tr>\n<tr>\n<td><code>AcuityEmbedHandoff</code></td>\n<td>Prefilled Acuity iframe handoff with postMessage</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"highlight highlight-source-svelte notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"&lt;script lang=&quot;ts&quot;&gt;\n  import {\n    ServicePicker,\n    DateTimePicker,\n    ClientForm,\n  } from '@tummycrypt/scheduling-kit/components';\n&lt;/script&gt;\n\n&lt;ServicePicker services={data.services} onselect={handleSelect} /&gt;\n&lt;DateTimePicker slots={availableSlots} onselect={handleTimeSelect} /&gt;\n&lt;ClientForm onsubmit={handleSubmit} /&gt;\"><pre>&lt;<span class=\"pl-ent\">script</span> <span class=\"pl-e\">lang</span>=<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>ts<span class=\"pl-pds\">\"</span></span>&gt;<span class=\"pl-s1\"></span>\n<span class=\"pl-s1\">  <span class=\"pl-k\">import</span> {</span>\n<span class=\"pl-s1\">    <span class=\"pl-smi\">ServicePicker</span>,</span>\n<span class=\"pl-s1\">    <span class=\"pl-smi\">DateTimePicker</span>,</span>\n<span class=\"pl-s1\">    <span class=\"pl-smi\">ClientForm</span>,</span>\n<span class=\"pl-s1\">  } <span class=\"pl-k\">from</span> <span class=\"pl-s\"><span class=\"pl-pds\">'</span>@tummycrypt/scheduling-kit/components<span class=\"pl-pds\">'</span></span>;</span>\n<span class=\"pl-s1\"></span>&lt;/<span class=\"pl-ent\">script</span>&gt;\n\n&lt;<span class=\"pl-ent\">ServicePicker</span> <span class=\"pl-e\">services</span>={<span class=\"pl-smi\">data</span>.<span class=\"pl-smi\">services</span>} <span class=\"pl-e\">onselect</span>={<span class=\"pl-smi\">handleSelect</span>} /&gt;\n&lt;<span class=\"pl-ent\">DateTimePicker</span> <span class=\"pl-e\">slots</span>={<span class=\"pl-smi\">availableSlots</span>} <span class=\"pl-e\">onselect</span>={<span class=\"pl-smi\">handleTimeSelect</span>} /&gt;\n&lt;<span class=\"pl-ent\">ClientForm</span> <span class=\"pl-e\">onsubmit</span>={<span class=\"pl-smi\">handleSubmit</span>} /&gt;</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Payment Adapters</h2><a id=\"user-content-payment-adapters\" class=\"anchor\" aria-label=\"Permalink: Payment Adapters\" href=\"#payment-adapters\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import {\n  createStripeAdapter,\n  createVenmoAdapter,\n  createCashAdapter,\n  createZelleAdapter,\n  createCheckAdapter,\n  createVenmoDirectAdapter,\n} from '@tummycrypt/scheduling-kit/payments';\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span>\n  <span class=\"pl-s1\">createStripeAdapter</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">createVenmoAdapter</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">createCashAdapter</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">createZelleAdapter</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">createCheckAdapter</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-s1\">createVenmoDirectAdapter</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/scheduling-kit/payments'</span><span class=\"pl-kos\">;</span></pre></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Adapter</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>createStripeAdapter</code></td>\n<td><code>stripe</code></td>\n<td>Stripe Connect with Payment Intents</td>\n</tr>\n<tr>\n<td><code>createVenmoAdapter</code></td>\n<td><code>venmo</code></td>\n<td>PayPal SDK with Venmo button</td>\n</tr>\n<tr>\n<td><code>createCashAdapter</code></td>\n<td><code>cash</code></td>\n<td>Cash/in-person manual payment</td>\n</tr>\n<tr>\n<td><code>createZelleAdapter</code></td>\n<td><code>zelle</code></td>\n<td>Zelle manual payment</td>\n</tr>\n<tr>\n<td><code>createCheckAdapter</code></td>\n<td><code>check</code></td>\n<td>Check manual payment</td>\n</tr>\n<tr>\n<td><code>createVenmoDirectAdapter</code></td>\n<td><code>venmo-direct</code></td>\n<td>Venmo deep link (no SDK)</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Reconciliation</h2><a id=\"user-content-reconciliation\" class=\"anchor\" aria-label=\"Permalink: Reconciliation\" href=\"#reconciliation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Match alt-payment transactions (Venmo, Zelle, cash) to bookings.</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import {\n  createReconciliationMatcher,\n} from '@tummycrypt/scheduling-kit/reconciliation';\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span>\n  <span class=\"pl-s1\">createReconciliationMatcher</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/scheduling-kit/reconciliation'</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Stores</h2><a id=\"user-content-stores\" class=\"anchor\" aria-label=\"Permalink: Stores\" href=\"#stores\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Svelte 5 runes-based checkout state management.</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { checkoutStore } from '@tummycrypt/scheduling-kit/stores';\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">checkoutStore</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/scheduling-kit/stores'</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Testing</h2><a id=\"user-content-testing\" class=\"anchor\" aria-label=\"Permalink: Testing\" href=\"#testing\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Unit Tests</h3><a id=\"user-content-unit-tests\" class=\"anchor\" aria-label=\"Permalink: Unit Tests\" href=\"#unit-tests\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm test:unit           # Run all unit tests\npnpm test:coverage       # With coverage report\"><pre>pnpm test:unit           <span class=\"pl-c\"><span class=\"pl-c\">#</span> Run all unit tests</span>\npnpm test:coverage       <span class=\"pl-c\"><span class=\"pl-c\">#</span> With coverage report</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Integration Tests</h3><a id=\"user-content-integration-tests\" class=\"anchor\" aria-label=\"Permalink: Integration Tests\" href=\"#integration-tests\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm test:integration    # Mocked backend integration tests\"><pre>pnpm test:integration    <span class=\"pl-c\"><span class=\"pl-c\">#</span> Mocked backend integration tests</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Component Tests</h3><a id=\"user-content-component-tests\" class=\"anchor\" aria-label=\"Permalink: Component Tests\" href=\"#component-tests\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm test:component      # jsdom-based component tests\"><pre>pnpm test:component      <span class=\"pl-c\"><span class=\"pl-c\">#</span> jsdom-based component tests</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">E2E Tests</h3><a id=\"user-content-e2e-tests\" class=\"anchor\" aria-label=\"Permalink: E2E Tests\" href=\"#e2e-tests\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm test:e2e            # Playwright browser tests (starts dev server)\"><pre>pnpm test:e2e            <span class=\"pl-c\"><span class=\"pl-c\">#</span> Playwright browser tests (starts dev server)</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Live Tests</h3><a id=\"user-content-live-tests\" class=\"anchor\" aria-label=\"Permalink: Live Tests\" href=\"#live-tests\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Copy .env.test.local.example to .env.test.local and fill in credentials\nRUN_LIVE_TESTS=true pnpm test:live\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Copy .env.test.local.example to .env.test.local and fill in credentials</span>\nRUN_LIVE_TESTS=true pnpm test:live</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Test Utilities</h3><a id=\"user-content-test-utilities\" class=\"anchor\" aria-label=\"Permalink: Test Utilities\" href=\"#test-utilities\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The <code>@tummycrypt/scheduling-kit/testing</code> export provides cassette-based API\nrecording and playback for deterministic integration tests.</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { CassetteRecorder, CassettePlayer } from '@tummycrypt/scheduling-kit/testing';\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-v\">CassetteRecorder</span><span class=\"pl-kos\">,</span> <span class=\"pl-v\">CassettePlayer</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/scheduling-kit/testing'</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Development</h2><a id=\"user-content-development\" class=\"anchor\" aria-label=\"Permalink: Development\" href=\"#development\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm install\npnpm dev              # Start dev server\npnpm build            # Build package\npnpm check            # TypeScript check\npnpm lint             # ESLint\npnpm test:all         # Run all test suites\"><pre>pnpm install\npnpm dev              <span class=\"pl-c\"><span class=\"pl-c\">#</span> Start dev server</span>\npnpm build            <span class=\"pl-c\"><span class=\"pl-c\">#</span> Build package</span>\npnpm check            <span class=\"pl-c\"><span class=\"pl-c\">#</span> TypeScript check</span>\npnpm lint             <span class=\"pl-c\"><span class=\"pl-c\">#</span> ESLint</span>\npnpm test:all         <span class=\"pl-c\"><span class=\"pl-c\">#</span> Run all test suites</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">MIT -- see <a href=\"./LICENSE\">LICENSE</a> for details.</p>\n</article></div>",
      "readme_excerpt": "Backend-agnostic scheduling library with Svelte 5 components, pluggable\nscheduling adapters, alternative payment support, and Effect-powered workflow\ncomposition.\nThis repo keeps two intentionally different build surfaces:\n- pnpm is the local package-manager and script interface\n- Bazel defines and builds the publishable package artifact used by CI\nThe recommended local bootstrap path is the repo flake plus direnv, which\nmakes pnpm, bazel through Bazelisk, and the docs toolchain available from...",
      "install_commands": {
        "npm": "npm install @tummycrypt/scheduling-kit"
      },
      "repo_url": "https://github.com/jesssullivan/scheduling-kit",
      "website_url": null,
      "docs_url": null,
      "registry_url": "https://www.npmjs.com/package/@tummycrypt/scheduling-kit",
      "releases_url": "https://github.com/jesssullivan/scheduling-kit/releases",
      "og_image_url": "https://opengraph.githubassets.com/66be7190fcfbe417c3a4a1a23fb0c2ed085b1cfb755a5bfa8c2f95a43e538807/Jesssullivan/scheduling-kit",
      "license": "MIT",
      "pushed_at": "2026-04-26T03:00:51Z",
      "enriched_at": "2026-04-26T17:17:12.838Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-darwinnicutil",
      "name": "DarwinNicUtil",
      "repo": "jesssullivan/DarwinNicUtil",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "systems",
      "description": "Extensible utility for dealing with out-of-band management / air gapped network devices, mostly for institutionalized Macs (ABR, Sophos, NIC precedence etc) ",
      "featured": false,
      "tags": [
        "network",
        "oob",
        "mac",
        "enterprise"
      ],
      "version": "2.1.2",
      "release_date": "2026-04-25T20:41:54Z",
      "releases": [
        {
          "tag": "2.1.2",
          "date": "2026-04-25T20:41:54Z",
          "body": "## What's Changed\n* Document live PyPI install by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/31\n* Polish final release surfaces by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/32\n* Improve docs install and Pages surfaces by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/33\n* Prepare v2.1.2 metadata release by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/34\n\n\n**Full Changelog**: https://github.com/Jesssullivan/DarwinNicUtil/compare/v2.1.1...v2.1.2"
        },
        {
          "tag": "2.1.1",
          "date": "2026-04-25T19:49:34Z",
          "body": "## What's Changed\n* Restrict release asset globs by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/7\n* Clarify release artifact docs by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/8\n* Define agent integration surface by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/15\n* Track distribution follow-up surfaces by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/18\n* Defer Homebrew distribution by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/19\n* [codex] stage FlakeHub publication workflow by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/20\n* [codex] advertise proven FlakeHub release by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/21\n* [codex] record Bazel packaging decision by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/22\n* [codex] stage PyPI trusted publishing by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/23\n* [codex] align standalone binary entrypoint by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/24\n* Ratchet coverage gate to 50 percent by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/25\n* Draft public project spec by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/26\n* Document ABR and Sophos boundary decisions by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/27\n* Clarify PyPI cutover runbook by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/28\n* Add package metadata links by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/29\n* Prepare v2.1.1 PyPI release by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/30\n\n\n**Full Changelog**: https://github.com/Jesssullivan/DarwinNicUtil/compare/v2.1.0...v2.1.1"
        },
        {
          "tag": "2.1.0",
          "date": "2026-04-25T15:22:13Z",
          "body": "## What's Changed\n* feat: Nix flake packaging + justfile + HM/SM modules by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/1\n* add bastion diagnostics for darwin nic by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/5\n* Prepare v2.1 release readiness by @Jesssullivan in https://github.com/Jesssullivan/DarwinNicUtil/pull/6\n\n## New Contributors\n* @Jesssullivan made their first contribution in https://github.com/Jesssullivan/DarwinNicUtil/pull/1\n\n**Full Changelog**: https://github.com/Jesssullivan/DarwinNicUtil/compare/v2.0.0...v2.1.0"
        }
      ],
      "stars": 1,
      "topics": [
        "airgapped-security",
        "compliance",
        "developer-experience",
        "nat-punchthrough",
        "nat-traversal",
        "tui",
        "enterprise-bastion"
      ],
      "languages": [
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 352236
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 18217
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 6787
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 5524
        }
      ],
      "primary_language": "Python",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">Darwin Management NIC Configurator</h1><a id=\"user-content-darwin-management-nic-configurator\" class=\"anchor\" aria-label=\"Permalink: Darwin Management NIC Configurator\" href=\"#darwin-management-nic-configurator\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Configure a USB Ethernet adapter for out-of-band management without letting it\ntake over normal Wi-Fi or tailnet connectivity.</p>\n<p dir=\"auto\"><code>darwin-nic</code> is aimed at bastion and bench workflows where a Mac needs a\ntemporary management link to network gear while keeping its primary network\npath intact.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Status</h2><a id=\"user-content-status\" class=\"anchor\" aria-label=\"Permalink: Status\" href=\"#status\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>macOS is the primary supported platform.</li>\n<li>Linux support is experimental and currently limited.</li>\n<li>Release artifacts are PyPI distributions, GitHub Release wheel/source\nfiles, Nix packages, and FlakeHub releases.</li>\n<li>The PyInstaller spec is retained for manual builds, but standalone binaries\nare not the primary release artifact yet.</li>\n<li>Public docs are built with MkDocs and published at\n<a href=\"https://transscendsurvival.org/DarwinNicUtil/\" rel=\"nofollow\">https://transscendsurvival.org/DarwinNicUtil/</a>.</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick Start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick Start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Recommended CLI install\nuv tool install darwin-mgmt-nic-configurator\ndarwin-nic status\ndarwin-nic init-config\ndarwin-nic configure --profile homelab --preserve-wifi\n\n# Run without installing, using the stable FlakeHub release\nnix run &quot;https://flakehub.com/f/Jesssullivan/DarwinNicUtil/v2.1.2&quot; -- status\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Recommended CLI install</span>\nuv tool install darwin-mgmt-nic-configurator\ndarwin-nic status\ndarwin-nic init-config\ndarwin-nic configure --profile homelab --preserve-wifi\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Run without installing, using the stable FlakeHub release</span>\nnix run <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>https://flakehub.com/f/Jesssullivan/DarwinNicUtil/v2.1.2<span class=\"pl-pds\">\"</span></span> -- status</pre></div>\n<p dir=\"auto\">For a one-off setup without a saved profile:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"darwin-nic configure \\\n  --device-ip &lt;device-ipv4&gt; \\\n  --laptop-ip &lt;usb-nic-ipv4&gt; \\\n  --mgmt-network &lt;cidr&gt; \\\n  --preserve-wifi\"><pre>darwin-nic configure \\\n  --device-ip <span class=\"pl-k\">&lt;</span>device-ipv<span class=\"pl-k\">4&gt;</span> \\\n  --laptop-ip <span class=\"pl-k\">&lt;</span>usb-nic-ipv<span class=\"pl-k\">4&gt;</span> \\\n  --mgmt-network <span class=\"pl-k\">&lt;</span>cidr<span class=\"pl-k\">&gt;</span> \\\n  --preserve-wifi</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Install</h2><a id=\"user-content-install\" class=\"anchor\" aria-label=\"Permalink: Install\" href=\"#install\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Path</th>\n<th>Use When</th>\n<th>Command</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>PyPI</td>\n<td>You want the normal CLI on your PATH</td>\n<td><code>uv tool install darwin-mgmt-nic-configurator</code></td>\n</tr>\n<tr>\n<td>FlakeHub</td>\n<td>You want a stable Nix release</td>\n<td><code>nix profile install \"https://flakehub.com/f/Jesssullivan/DarwinNicUtil/v2.1.2\"</code></td>\n</tr>\n<tr>\n<td>GitHub flake</td>\n<td>You want the current repository flake</td>\n<td><code>nix profile install github:Jesssullivan/DarwinNicUtil</code></td>\n</tr>\n<tr>\n<td>Source checkout</td>\n<td>You are developing or testing local changes</td>\n<td><code>uv sync --extra dev &amp;&amp; uv run darwin-nic status</code></td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">Wheel and source distribution files are attached to GitHub Releases and\npublished to PyPI. Standalone binary downloads are not supported yet.</p>\n<p dir=\"auto\">Home Manager and System Manager modules are available under <code>nix/modules/</code>.\nFor the release shape and productionization summary, see\n<a href=\"docs/project-spec.md\"><code>docs/project-spec.md</code></a>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Commands</h2><a id=\"user-content-commands\" class=\"anchor\" aria-label=\"Permalink: Commands\" href=\"#commands\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Command</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>darwin-nic setup</code></td>\n<td>Interactive guided setup wizard</td>\n</tr>\n<tr>\n<td><code>darwin-nic configure</code></td>\n<td>Configure a USB NIC</td>\n</tr>\n<tr>\n<td><code>darwin-nic status</code></td>\n<td>Show interfaces, routes, and bastion diagnostics</td>\n</tr>\n<tr>\n<td><code>darwin-nic dashboard</code></td>\n<td>Show network monitoring status</td>\n</tr>\n<tr>\n<td><code>darwin-nic test</code></td>\n<td>Run basic connectivity checks</td>\n</tr>\n<tr>\n<td><code>darwin-nic restore</code></td>\n<td>Restore saved network service order</td>\n</tr>\n<tr>\n<td><code>darwin-nic config</code></td>\n<td>Show resolved settings and profiles</td>\n</tr>\n<tr>\n<td><code>darwin-nic profiles</code></td>\n<td>List available profiles</td>\n</tr>\n<tr>\n<td><code>darwin-nic init-config</code></td>\n<td>Create a starter config file</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Configuration</h2><a id=\"user-content-configuration\" class=\"anchor\" aria-label=\"Permalink: Configuration\" href=\"#configuration\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Settings are loaded in this order, with later sources overriding earlier ones:</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Location</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>/etc/darwin-nic/config.toml</code></td>\n<td>System-wide defaults</td>\n</tr>\n<tr>\n<td><code>~/.config/darwin-nic/config.toml</code></td>\n<td>User defaults</td>\n</tr>\n<tr>\n<td><code>~/.darwin-nic.toml</code></td>\n<td>Legacy user config</td>\n</tr>\n<tr>\n<td><code>./.darwin-nic.toml</code></td>\n<td>Directory-local override</td>\n</tr>\n<tr>\n<td><code>./darwin-nic.toml</code></td>\n<td>Alternate directory-local override</td>\n</tr>\n<tr>\n<td><code>DARWIN_NIC_*</code></td>\n<td>Environment overrides</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">Example:</p>\n<div class=\"highlight highlight-source-toml notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"default_profile = &quot;homelab&quot;\n\n[defaults]\npreserve_wifi = true\n\n[profiles.homelab]\ndevice_ip = &quot;192.168.88.1&quot;\nlaptop_ip = &quot;192.168.88.100&quot;\nmgmt_network = &quot;192.168.88.0/24&quot;\ndevice_name = &quot;Lab Management Device&quot;\ndevice_type = &quot;network&quot;\"><pre><span class=\"pl-smi\">default_profile</span> = <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>homelab<span class=\"pl-pds\">\"</span></span>\n\n[<span class=\"pl-en\">defaults</span>]\n<span class=\"pl-smi\">preserve_wifi</span> = <span class=\"pl-c1\">true</span>\n\n[<span class=\"pl-en\">profiles</span>.<span class=\"pl-en\">homelab</span>]\n<span class=\"pl-smi\">device_ip</span> = <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>192.168.88.1<span class=\"pl-pds\">\"</span></span>\n<span class=\"pl-smi\">laptop_ip</span> = <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>192.168.88.100<span class=\"pl-pds\">\"</span></span>\n<span class=\"pl-smi\">mgmt_network</span> = <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>192.168.88.0/24<span class=\"pl-pds\">\"</span></span>\n<span class=\"pl-smi\">device_name</span> = <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>Lab Management Device<span class=\"pl-pds\">\"</span></span>\n<span class=\"pl-smi\">device_type</span> = <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>network<span class=\"pl-pds\">\"</span></span></pre></div>\n<p dir=\"auto\">See <code>examples/config.toml</code> for a fuller profile example.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Bastion Notes</h2><a id=\"user-content-bastion-notes\" class=\"anchor\" aria-label=\"Permalink: Bastion Notes\" href=\"#bastion-notes\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">For a generic <code>tailnet -&gt; bastion host -&gt; USB OOB NIC -&gt; managed network device</code> flow:</p>\n<ul dir=\"auto\">\n<li>keep <code>mgmt_network</code> aligned with the real management subnet;</li>\n<li>use <code>darwin-nic status</code> before making privileged changes;</li>\n<li>use <code>--dry-run</code> to preview interface and route changes;</li>\n<li>pre-authenticate with <code>sudo -v</code> for non-interactive wrappers;</li>\n<li>check <code>status</code> when raw or link-layer tools work but ordinary sockets fail.</li>\n</ul>\n<p dir=\"auto\">On macOS, <code>status</code> includes <code>scutil --nwi</code>, Tailscale system-extension state,\nand recent NECP socket-drop hints when available.</p>\n<p dir=\"auto\">Device-specific hostnames, credentials, OOB MAC addresses, and switch policy\nbelong in downstream operator repositories, not in this generic tool.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Safety</h2><a id=\"user-content-safety\" class=\"anchor\" aria-label=\"Permalink: Safety\" href=\"#safety\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>Protected interfaces such as Wi-Fi, loopback, and system virtual links are\nnot modified.</li>\n<li><code>--preserve-wifi</code> keeps the primary network path ahead of the USB NIC.</li>\n<li>Dry-run mode previews intended changes without applying them.</li>\n<li>The emergency restore helper is available at <code>scripts/emergency-restore.sh</code>.</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Requirements</h2><a id=\"user-content-requirements\" class=\"anchor\" aria-label=\"Permalink: Requirements\" href=\"#requirements\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>Python 3.14+ for source and uv installs.</li>\n<li>Nix for flake-based package usage.</li>\n<li>A USB-to-Ethernet adapter.</li>\n<li>macOS for the full current feature set.</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Development</h2><a id=\"user-content-development\" class=\"anchor\" aria-label=\"Permalink: Development\" href=\"#development\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"just dev\njust check\njust test\njust docs-build\nuv build\"><pre>just dev\njust check\njust <span class=\"pl-c1\">test</span>\njust docs-build\nuv build</pre></div>\n<p dir=\"auto\">Run <code>just</code> with no arguments to see all recipes.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Artifacts</h2><a id=\"user-content-artifacts\" class=\"anchor\" aria-label=\"Permalink: Artifacts\" href=\"#artifacts\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Current release artifacts are:</p>\n<ul dir=\"auto\">\n<li>PyPI distribution for <code>darwin-mgmt-nic-configurator</code>;</li>\n<li>GitHub Release wheel and source distribution files;</li>\n<li>Nix flake package outputs, including FlakeHub <code>v2.1.2</code>;</li>\n<li>MkDocs site artifacts from the docs workflow.</li>\n</ul>\n<p dir=\"auto\">GitHub Release, PyPI, FlakeHub, and docs workflows are present for tag-based\npublication. Standalone binary distribution remains a tracked release follow-up.</p>\n<p dir=\"auto\">Public artifact URLs:</p>\n<ul dir=\"auto\">\n<li>Docs: <a href=\"https://transscendsurvival.org/DarwinNicUtil/\" rel=\"nofollow\">https://transscendsurvival.org/DarwinNicUtil/</a></li>\n<li>PyPI: <a href=\"https://pypi.org/project/darwin-mgmt-nic-configurator/\" rel=\"nofollow\">https://pypi.org/project/darwin-mgmt-nic-configurator/</a></li>\n<li>Releases: <a href=\"https://github.com/Jesssullivan/DarwinNicUtil/releases\">https://github.com/Jesssullivan/DarwinNicUtil/releases</a></li>\n<li>FlakeHub: <a href=\"https://flakehub.com/f/Jesssullivan/DarwinNicUtil\" rel=\"nofollow\">https://flakehub.com/f/Jesssullivan/DarwinNicUtil</a></li>\n</ul>\n</article></div>",
      "readme_excerpt": "Configure a USB Ethernet adapter for out-of-band management without letting it\ntake over normal Wi-Fi or tailnet connectivity.\ndarwin-nic is aimed at bastion and bench workflows where a Mac needs a\ntemporary management link to network gear while keeping its primary network\npath intact.\n- macOS is the primary supported platform.\n- Linux support is experimental and currently limited.\n- Release artifacts are PyPI distributions, GitHub Release wheel/source\n  files, Nix packages, and FlakeHub...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/DarwinNicUtil",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/DarwinNicUtil/releases",
      "og_image_url": "https://opengraph.githubassets.com/c92f2ef076c3573813327495c89a91f024d86c0b33f4ffeb255b3a73f427c632/Jesssullivan/DarwinNicUtil",
      "license": "Zlib",
      "pushed_at": "2026-04-25T20:41:27Z",
      "enriched_at": "2026-04-26T17:17:13.165Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-zig-notify",
      "name": "zig-notify",
      "repo": "jesssullivan/zig-notify",
      "org": "jesssullivan",
      "ecosystem": "zig",
      "category": "systems",
      "description": "Cross-platform notifications in Zig with C FFI — macOS osascript / Linux libnotify",
      "featured": false,
      "tags": [
        "notifications",
        "desktop",
        "ffi",
        "cross-platform"
      ],
      "version": "0.1.0",
      "release_date": "2026-04-25T18:50:20Z",
      "releases": [
        {
          "tag": "0.1.0",
          "date": "2026-04-25T18:50:20Z",
          "body": "## What's Changed\n* fix(build): add Fedora/Rocky/Arch glib-2.0 include paths by @Jesssullivan in https://github.com/Jesssullivan/zig-notify/pull/1\n* Productionize: build.zig.zon, autodoc, examples, agent metadata by @Jesssullivan in https://github.com/Jesssullivan/zig-notify/pull/2\n* Add autodoc CI job and release workflow by @Jesssullivan in https://github.com/Jesssullivan/zig-notify/pull/3\n* Fix: run release workflow on macOS by @Jesssullivan in https://github.com/Jesssullivan/zig-notify/pull/4\n\n## New Contributors\n* @Jesssullivan made their first contribution in https://github.com/Jesssullivan/zig-notify/pull/1\n\n**Full Changelog**: https://github.com/Jesssullivan/zig-notify/commits/v0.1.0"
        }
      ],
      "stars": 0,
      "topics": [
        "abstraction",
        "d-bus",
        "libnotify",
        "notificationcenter",
        "c-interop",
        "ffi",
        "linux",
        "macos",
        "notifications",
        "zig"
      ],
      "languages": [
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 38397
        },
        {
          "name": "Zig",
          "color": "#ec915c",
          "bytes": 13391
        },
        {
          "name": "C",
          "color": "#555555",
          "bytes": 2164
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 1002
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 21
        }
      ],
      "primary_language": "Python",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">zig-notify</h1><a id=\"user-content-zig-notify\" class=\"anchor\" aria-label=\"Permalink: zig-notify\" href=\"#zig-notify\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Cross-platform notification abstraction in Zig with C FFI -- macOS (osascript) and Linux (libnotify).</p>\n<p dir=\"auto\"><strong>License:</strong> Zlib OR MIT</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Why</h2><a id=\"user-content-why\" class=\"anchor\" aria-label=\"Permalink: Why\" href=\"#why\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Desktop notifications have completely different APIs on macOS (UNUserNotificationCenter or osascript) and Linux (libnotify/D-Bus). This library provides a single C API that works on both platforms, with urgency level support and proper lifecycle management.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Features</h2><a id=\"user-content-features\" class=\"anchor\" aria-label=\"Permalink: Features\" href=\"#features\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>Send notifications</strong>: Title, body, and urgency level</li>\n<li><strong>Permission handling</strong>: Request notification permission (macOS), no-op on Linux</li>\n<li><strong>Lifecycle</strong>: Init/deinit for proper resource management (Linux)</li>\n<li><strong>C FFI</strong>: 4 exported functions callable from Swift, C, C++, or any language with C interop</li>\n<li><strong>macOS</strong>: osascript (AppleScript <code>display notification</code>)</li>\n<li><strong>Linux</strong>: libnotify (GLib notification API)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Installation</h2><a id=\"user-content-installation\" class=\"anchor\" aria-label=\"Permalink: Installation\" href=\"#installation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Zig Package Manager (recommended)</h3><a id=\"user-content-zig-package-manager-recommended\" class=\"anchor\" aria-label=\"Permalink: Zig Package Manager (recommended)\" href=\"#zig-package-manager-recommended\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"zig fetch --save git+https://github.com/Jesssullivan/zig-notify.git\"><pre>zig fetch --save git+https://github.com/Jesssullivan/zig-notify.git</pre></div>\n<p dir=\"auto\">Then in your <code>build.zig</code>:</p>\n<div class=\"highlight highlight-source-zig notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"const dep = b.dependency(&quot;zig-notify&quot;, .{ .target = target, .optimize = optimize });\nexe.root_module.addImport(&quot;zig-notify&quot;, dep.module(&quot;zig-notify&quot;));\"><pre><span class=\"pl-k\">const</span> <span class=\"pl-v\">dep</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">b</span>.<span class=\"pl-v\">dependency</span>(<span class=\"pl-s\">\"zig-notify\"</span>, .{ .<span class=\"pl-v\">target</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">target</span>, .<span class=\"pl-v\">optimize</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">optimize</span> });\n<span class=\"pl-v\">exe</span>.<span class=\"pl-v\">root_module</span>.<span class=\"pl-v\">addImport</span>(<span class=\"pl-s\">\"zig-notify\"</span>, <span class=\"pl-v\">dep</span>.<span class=\"pl-v\">module</span>(<span class=\"pl-s\">\"zig-notify\"</span>));</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Git Submodule (C FFI consumers)</h3><a id=\"user-content-git-submodule-c-ffi-consumers\" class=\"anchor\" aria-label=\"Permalink: Git Submodule (C FFI consumers)\" href=\"#git-submodule-c-ffi-consumers\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"git submodule add https://github.com/Jesssullivan/zig-notify.git vendor/notify\ncd vendor/notify &amp;&amp; zig build -Doptimize=ReleaseFast\"><pre>git submodule add https://github.com/Jesssullivan/zig-notify.git vendor/notify\n<span class=\"pl-c1\">cd</span> vendor/notify <span class=\"pl-k\">&amp;&amp;</span> zig build -Doptimize=ReleaseFast</pre></div>\n<p dir=\"auto\">Link (macOS): <code>-lzig-notify</code> (no frameworks; uses osascript binary).\nLink (Linux): <code>-lzig-notify -lnotify -lglib-2.0 -lgobject-2.0</code>.\nInclude: <code>#include \"zig_notify.h\"</code>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Requirements</h2><a id=\"user-content-requirements\" class=\"anchor\" aria-label=\"Permalink: Requirements\" href=\"#requirements\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>Zig 0.14.1+</li>\n<li>macOS 13+ or Linux (libnotify)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Architecture</h2><a id=\"user-content-architecture\" class=\"anchor\" aria-label=\"Permalink: Architecture\" href=\"#architecture\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<section class=\"js-render-needs-enrichment render-needs-enrichment position-relative\" data-identity=\"139255a4-a889-45ce-855e-896710ad457b\" data-host=\"https://viewscreen.githubusercontent.com\" data-src=\"https://viewscreen.githubusercontent.com/markdown/mermaid?docs_host=https%3A%2F%2Fdocs.github.com\" data-type=\"mermaid\" aria-label=\"mermaid rendered output container\">\n  <div class=\"js-render-enrichment-target\" data-json=\"{&quot;data&quot;:&quot;graph TD\\n    A[Application] --&amp;gt;|C FFI| B[ffi.zig&amp;lt;br/&amp;gt;4 exported functions]\\n    B --&amp;gt; C[notify.zig&amp;lt;br/&amp;gt;Platform dispatch]\\n    C --&amp;gt;|macOS| D[notify_macos.zig&amp;lt;br/&amp;gt;osascript]\\n    C --&amp;gt;|Linux| E[notify_linux.zig&amp;lt;br/&amp;gt;libnotify]\\n\\n    D --&amp;gt; F[osascript -e&amp;lt;br/&amp;gt;display notification]\\n    E --&amp;gt; G[notify_notification_new&amp;lt;br/&amp;gt;notify_notification_show]\\n\\n    style B fill:#48c,stroke:#269\\n    style D fill:#a6d,stroke:#84b\\n    style E fill:#6a4,stroke:#483\\n&quot;}\" data-plain=\"graph TD\n    A[Application] --&gt;|C FFI| B[ffi.zig&lt;br/&gt;4 exported functions]\n    B --&gt; C[notify.zig&lt;br/&gt;Platform dispatch]\n    C --&gt;|macOS| D[notify_macos.zig&lt;br/&gt;osascript]\n    C --&gt;|Linux| E[notify_linux.zig&lt;br/&gt;libnotify]\n\n    D --&gt; F[osascript -e&lt;br/&gt;display notification]\n    E --&gt; G[notify_notification_new&lt;br/&gt;notify_notification_show]\n\n    style B fill:#48c,stroke:#269\n    style D fill:#a6d,stroke:#84b\n    style E fill:#6a4,stroke:#483\n\" dir=\"auto\">\n    <div class=\"render-plaintext-hidden\" dir=\"auto\">\n      <pre lang=\"mermaid\" aria-label=\"Raw mermaid code\">graph TD\n    A[Application] --&gt;|C FFI| B[ffi.zig&lt;br/&gt;4 exported functions]\n    B --&gt; C[notify.zig&lt;br/&gt;Platform dispatch]\n    C --&gt;|macOS| D[notify_macos.zig&lt;br/&gt;osascript]\n    C --&gt;|Linux| E[notify_linux.zig&lt;br/&gt;libnotify]\n\n    D --&gt; F[osascript -e&lt;br/&gt;display notification]\n    E --&gt; G[notify_notification_new&lt;br/&gt;notify_notification_show]\n\n    style B fill:#48c,stroke:#269\n    style D fill:#a6d,stroke:#84b\n    style E fill:#6a4,stroke:#483\n</pre>\n    </div>\n  </div>\n  <span class=\"js-render-enrichment-loader d-flex flex-justify-center flex-items-center width-full\" style=\"min-height:100px\" role=\"presentation\">\n    <span data-view-component=\"true\">\n  <svg style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" data-view-component=\"true\" class=\"octospinner mx-auto tmp-mx-auto anim-rotate\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n    <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n</svg>    <span class=\"sr-only\">Loading</span>\n</span>\n  </span>\n</section>\n\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Build</h2><a id=\"user-content-build\" class=\"anchor\" aria-label=\"Permalink: Build\" href=\"#build\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Static library (libzig-notify.a)\nzig build -Doptimize=ReleaseFast\n\n# Run unit tests\nzig build test\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Static library (libzig-notify.a)</span>\nzig build -Doptimize=ReleaseFast\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Run unit tests</span>\nzig build <span class=\"pl-c1\">test</span></pre></div>\n<p dir=\"auto\">With Nix:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix develop       # dev shell\nnix build         # build library package\"><pre>nix develop       <span class=\"pl-c\"><span class=\"pl-c\">#</span> dev shell</span>\nnix build         <span class=\"pl-c\"><span class=\"pl-c\">#</span> build library package</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Platform Support</h2><a id=\"user-content-platform-support\" class=\"anchor\" aria-label=\"Permalink: Platform Support\" href=\"#platform-support\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Platform</th>\n<th>Backend</th>\n<th>Packages</th>\n<th>Status</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>macOS 13+ (arm64/x86_64)</td>\n<td>osascript (AppleScript)</td>\n<td>None</td>\n<td>Tested</td>\n</tr>\n<tr>\n<td>Linux (x86_64/arm64)</td>\n<td>libnotify (GLib)</td>\n<td><code>libnotify-dev</code> (apt) / <code>libnotify-devel</code> (dnf)</td>\n<td>Supported</td>\n</tr>\n<tr>\n<td>Cross-compilation</td>\n<td>--</td>\n<td>Libs linked at final build</td>\n<td>Supported</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">C FFI API Reference</h2><a id=\"user-content-c-ffi-api-reference\" class=\"anchor\" aria-label=\"Permalink: C FFI API Reference\" href=\"#c-ffi-api-reference\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Header: <a href=\"include/zig_notify.h\"><code>include/zig_notify.h</code></a></p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Types</h3><a id=\"user-content-types\" class=\"anchor\" aria-label=\"Permalink: Types\" href=\"#types\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"typedef enum {\n    ZIG_NOTIFY_URGENCY_LOW = 0,\n    ZIG_NOTIFY_URGENCY_NORMAL = 1,\n    ZIG_NOTIFY_URGENCY_CRITICAL = 2,\n} zig_notify_urgency_t;\"><pre><span class=\"pl-k\">typedef</span> <span class=\"pl-k\">enum</span> {\n    <span class=\"pl-c1\">ZIG_NOTIFY_URGENCY_LOW</span> <span class=\"pl-c1\">=</span> <span class=\"pl-c1\">0</span>,\n    <span class=\"pl-c1\">ZIG_NOTIFY_URGENCY_NORMAL</span> <span class=\"pl-c1\">=</span> <span class=\"pl-c1\">1</span>,\n    <span class=\"pl-c1\">ZIG_NOTIFY_URGENCY_CRITICAL</span> <span class=\"pl-c1\">=</span> <span class=\"pl-c1\">2</span>,\n} <span class=\"pl-smi\">zig_notify_urgency_t</span>;</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Initialize</h3><a id=\"user-content-initialize\" class=\"anchor\" aria-label=\"Permalink: Initialize\" href=\"#initialize\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Initialize the notification system.\n// Must be called before zig_notify_send on Linux (calls notify_init).\n// No-op on macOS.\n// Returns: 0 on success, -1 on failure\nint zig_notify_init(const char *app_name, size_t app_name_len);\"><pre><span class=\"pl-c\">// Initialize the notification system.</span>\n<span class=\"pl-c\">// Must be called before zig_notify_send on Linux (calls notify_init).</span>\n<span class=\"pl-c\">// No-op on macOS.</span>\n<span class=\"pl-c\">// Returns: 0 on success, -1 on failure</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-en\">zig_notify_init</span>(<span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">app_name</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">app_name_len</span>);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Send Notification</h3><a id=\"user-content-send-notification\" class=\"anchor\" aria-label=\"Permalink: Send Notification\" href=\"#send-notification\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Send a desktop notification.\n// body may be NULL (body_len = 0) for title-only notifications.\n// Returns: 0 on success, -1 on failure\n//\n// macOS: osascript &quot;display notification&quot; (urgency ignored)\n// Linux: notify_notification_new + notify_notification_show\nint zig_notify_send(\n    const char *title, size_t title_len,\n    const char *body, size_t body_len,\n    zig_notify_urgency_t urgency\n);\"><pre><span class=\"pl-c\">// Send a desktop notification.</span>\n<span class=\"pl-c\">// body may be NULL (body_len = 0) for title-only notifications.</span>\n<span class=\"pl-c\">// Returns: 0 on success, -1 on failure</span>\n<span class=\"pl-c\">//</span>\n<span class=\"pl-c\">// macOS: osascript \"display notification\" (urgency ignored)</span>\n<span class=\"pl-c\">// Linux: notify_notification_new + notify_notification_show</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-en\">zig_notify_send</span>(\n    <span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">title</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">title_len</span>,\n    <span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">body</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">body_len</span>,\n    <span class=\"pl-smi\">zig_notify_urgency_t</span> <span class=\"pl-s1\">urgency</span>\n);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Request Permission</h3><a id=\"user-content-request-permission\" class=\"anchor\" aria-label=\"Permalink: Request Permission\" href=\"#request-permission\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Request notification permission (macOS only, no-op on Linux).\n// Returns: 0 if granted, -1 if denied, -2 on error\n//\n// Currently returns 0 on all platforms (osascript does not require permission).\nint zig_notify_request_permission(void);\"><pre><span class=\"pl-c\">// Request notification permission (macOS only, no-op on Linux).</span>\n<span class=\"pl-c\">// Returns: 0 if granted, -1 if denied, -2 on error</span>\n<span class=\"pl-c\">//</span>\n<span class=\"pl-c\">// Currently returns 0 on all platforms (osascript does not require permission).</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-en\">zig_notify_request_permission</span>(<span class=\"pl-smi\">void</span>);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Cleanup</h3><a id=\"user-content-cleanup\" class=\"anchor\" aria-label=\"Permalink: Cleanup\" href=\"#cleanup\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Clean up notification resources.\n// Should be called on app exit on Linux (calls notify_uninit).\n// No-op on macOS.\nvoid zig_notify_deinit(void);\"><pre><span class=\"pl-c\">// Clean up notification resources.</span>\n<span class=\"pl-c\">// Should be called on app exit on Linux (calls notify_uninit).</span>\n<span class=\"pl-c\">// No-op on macOS.</span>\n<span class=\"pl-smi\">void</span> <span class=\"pl-en\">zig_notify_deinit</span>(<span class=\"pl-smi\">void</span>);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Zig API Reference</h2><a id=\"user-content-zig-api-reference\" class=\"anchor\" aria-label=\"Permalink: Zig API Reference\" href=\"#zig-api-reference\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">For direct Zig usage (not via C FFI):</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Module</th>\n<th>Public API</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>notify.zig</code></td>\n<td><code>send(title, body, urgency) !void</code></td>\n<td>Send a notification (platform-dispatched)</td>\n</tr>\n<tr>\n<td><code>notify.zig</code></td>\n<td><code>init(app_name) !void</code></td>\n<td>Initialize (Linux: notify_init)</td>\n</tr>\n<tr>\n<td><code>notify.zig</code></td>\n<td><code>deinit()</code></td>\n<td>Cleanup (Linux: notify_uninit)</td>\n</tr>\n<tr>\n<td><code>notify.zig</code></td>\n<td><code>Urgency</code> (enum: low, normal, critical)</td>\n<td>Notification urgency level</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Integration</h2><a id=\"user-content-integration\" class=\"anchor\" aria-label=\"Permalink: Integration\" href=\"#integration\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">As a Git Submodule</h3><a id=\"user-content-as-a-git-submodule\" class=\"anchor\" aria-label=\"Permalink: As a Git Submodule\" href=\"#as-a-git-submodule\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"git submodule add https://github.com/Jesssullivan/zig-notify.git vendor/notify\ncd vendor/notify &amp;&amp; zig build -Doptimize=ReleaseFast\"><pre>git submodule add https://github.com/Jesssullivan/zig-notify.git vendor/notify\n<span class=\"pl-c1\">cd</span> vendor/notify <span class=\"pl-k\">&amp;&amp;</span> zig build -Doptimize=ReleaseFast</pre></div>\n<p dir=\"auto\">Link (macOS): <code>-lzig-notify</code> (no frameworks needed; osascript is a system binary)</p>\n<p dir=\"auto\">Link (Linux): <code>-lzig-notify -lnotify -lglib-2.0 -lgobject-2.0</code></p>\n<p dir=\"auto\">Include: <code>#include \"zig_notify.h\"</code> (path: <code>vendor/notify/include/</code>)</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Swift via Bridging Header</h3><a id=\"user-content-swift-via-bridging-header\" class=\"anchor\" aria-label=\"Permalink: Swift via Bridging Header\" href=\"#swift-via-bridging-header\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-swift notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"#include &quot;zig_notify.h&quot;\n\n// Send a notification\nlet title = &quot;Download Complete&quot;\nlet body = &quot;file.zip has been saved&quot;\nzig_notify_send(title, title.utf8.count, body, body.utf8.count, ZIG_NOTIFY_URGENCY_NORMAL)\"><pre>#include <span class=\"pl-s\">\"</span><span class=\"pl-s\">zig_notify.h</span><span class=\"pl-s\">\"</span>\n\n// Send a notification\n<span class=\"pl-k\">let</span> <span class=\"pl-s1\">title</span> <span class=\"pl-c1\">=</span> <span class=\"pl-s\">\"</span><span class=\"pl-s\">Download Complete</span><span class=\"pl-s\">\"</span>\n<span class=\"pl-k\">let</span> <span class=\"pl-s1\">body</span> <span class=\"pl-c1\">=</span> <span class=\"pl-s\">\"</span><span class=\"pl-s\">file.zip has been saved</span><span class=\"pl-s\">\"</span>\n<span class=\"pl-en\">zig_notify_send</span><span class=\"pl-kos\">(</span>title<span class=\"pl-kos\">,</span> title<span class=\"pl-kos\">.</span>utf8<span class=\"pl-kos\">.</span>count<span class=\"pl-kos\">,</span> body<span class=\"pl-kos\">,</span> body<span class=\"pl-kos\">.</span>utf8<span class=\"pl-kos\">.</span>count<span class=\"pl-kos\">,</span> ZIG_NOTIFY_URGENCY_NORMAL<span class=\"pl-kos\">)</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Dual-licensed under <a href=\"https://opensource.org/licenses/Zlib\" rel=\"nofollow\">Zlib</a> and <a href=\"https://opensource.org/licenses/MIT\" rel=\"nofollow\">MIT</a>. Choose whichever you prefer.</p>\n</article></div>",
      "readme_excerpt": "Cross-platform notification abstraction in Zig with C FFI -- macOS (osascript) and Linux (libnotify).\nLicense: Zlib OR MIT\nDesktop notifications have completely different APIs on macOS (UNUserNotificationCenter or osascript) and Linux (libnotify/D-Bus). This library provides a single C API that works on both platforms, with urgency level support and proper lifecycle management.\n- Send notifications: Title, body, and urgency level\n- Permission handling: Request notification permission (macOS),...",
      "install_commands": {
        "zig": "zig fetch --save git+https://github.com/jesssullivan/zig-notify.git"
      },
      "repo_url": "https://github.com/jesssullivan/zig-notify",
      "website_url": "http://transscendsurvival.org/zig-notify/",
      "docs_url": "https://jesssullivan.github.io/zig-notify/",
      "registry_url": "https://zigistry.dev/package/{repo}",
      "releases_url": "https://github.com/jesssullivan/zig-notify/releases",
      "og_image_url": "https://opengraph.githubassets.com/ea19b432c1d980e16792ed94df39b3d139f2d6e4877bcd1e9a66a20fbeeaaa87/Jesssullivan/zig-notify",
      "license": "NOASSERTION",
      "pushed_at": "2026-04-25T18:49:45Z",
      "enriched_at": "2026-04-26T17:17:13.472Z",
      "gate": "pages"
    },
    {
      "slug": "jesssullivan-zig-keychain",
      "name": "zig-keychain",
      "repo": "jesssullivan/zig-keychain",
      "org": "jesssullivan",
      "ecosystem": "zig",
      "category": "cryptography",
      "description": "Cross-platform keychain abstraction in Zig with C FFI — macOS SecItem / Linux libsecret",
      "featured": false,
      "tags": [
        "keychain",
        "secrets",
        "ffi",
        "cross-platform"
      ],
      "version": "0.1.0",
      "release_date": "2026-04-25T18:50:26Z",
      "releases": [
        {
          "tag": "0.1.0",
          "date": "2026-04-25T18:50:26Z",
          "body": "## What's Changed\n* Use linkSystemLibrary for distro-agnostic include paths by @Jesssullivan in https://github.com/Jesssullivan/zig-keychain/pull/3\n* Productionize: build.zig.zon, autodoc, examples, agent metadata by @Jesssullivan in https://github.com/Jesssullivan/zig-keychain/pull/4\n* Add autodoc CI and release workflow by @Jesssullivan in https://github.com/Jesssullivan/zig-keychain/pull/5\n* Fix: run release workflow on macOS by @Jesssullivan in https://github.com/Jesssullivan/zig-keychain/pull/6\n\n## New Contributors\n* @Jesssullivan made their first contribution in https://github.com/Jesssullivan/zig-keychain/pull/3\n\n**Full Changelog**: https://github.com/Jesssullivan/zig-keychain/commits/v0.1.0"
        }
      ],
      "stars": 0,
      "topics": [
        "attestation-compatibility",
        "ffi",
        "libsecret",
        "secitem",
        "c-interop",
        "keychain",
        "secrets",
        "zig",
        "zig-package"
      ],
      "languages": [
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 37765
        },
        {
          "name": "Zig",
          "color": "#ec915c",
          "bytes": 18276
        },
        {
          "name": "C",
          "color": "#555555",
          "bytes": 7214
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 1187
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 21
        }
      ],
      "primary_language": "Python",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">zig-keychain</h1><a id=\"user-content-zig-keychain\" class=\"anchor\" aria-label=\"Permalink: zig-keychain\" href=\"#zig-keychain\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Cross-platform keychain/secrets abstraction in Zig with C FFI -- macOS Keychain (SecItem) and Linux Secret Service (libsecret).</p>\n<p dir=\"auto\"><strong>License:</strong> Zlib OR MIT</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Why</h2><a id=\"user-content-why\" class=\"anchor\" aria-label=\"Permalink: Why\" href=\"#why\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Applications need to store credentials, tokens, and other secrets securely. Platform APIs differ significantly: macOS uses Security.framework (SecItemAdd/CopyMatching/Delete), Linux uses the D-Bus Secret Service API via libsecret. This library provides a single C API that works on both platforms.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Features</h2><a id=\"user-content-features\" class=\"anchor\" aria-label=\"Permalink: Features\" href=\"#features\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>Store</strong>: Save secrets to the system keychain (upsert semantics -- overwrites existing)</li>\n<li><strong>Lookup</strong>: Retrieve secrets by service + account name</li>\n<li><strong>Delete</strong>: Remove secrets by service + account name</li>\n<li><strong>Search</strong>: Find keychain items matching an account name (declared in header, macOS implemented)</li>\n<li><strong>C FFI</strong>: 4 exported functions callable from Swift, C, C++, or any language with C interop</li>\n<li><strong>macOS</strong>: Security.framework (kSecClassGenericPassword)</li>\n<li><strong>Linux</strong>: libsecret (org.freedesktop.secrets / D-Bus Secret Service)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Installation</h2><a id=\"user-content-installation\" class=\"anchor\" aria-label=\"Permalink: Installation\" href=\"#installation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Zig Package Manager (recommended)</h3><a id=\"user-content-zig-package-manager-recommended\" class=\"anchor\" aria-label=\"Permalink: Zig Package Manager (recommended)\" href=\"#zig-package-manager-recommended\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"zig fetch --save git+https://github.com/Jesssullivan/zig-keychain.git\"><pre>zig fetch --save git+https://github.com/Jesssullivan/zig-keychain.git</pre></div>\n<p dir=\"auto\">Then in your <code>build.zig</code>:</p>\n<div class=\"highlight highlight-source-zig notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"const dep = b.dependency(&quot;zig-keychain&quot;, .{ .target = target, .optimize = optimize });\nexe.root_module.addImport(&quot;zig-keychain&quot;, dep.module(&quot;zig-keychain&quot;));\"><pre><span class=\"pl-k\">const</span> <span class=\"pl-v\">dep</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">b</span>.<span class=\"pl-v\">dependency</span>(<span class=\"pl-s\">\"zig-keychain\"</span>, .{ .<span class=\"pl-v\">target</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">target</span>, .<span class=\"pl-v\">optimize</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">optimize</span> });\n<span class=\"pl-v\">exe</span>.<span class=\"pl-v\">root_module</span>.<span class=\"pl-v\">addImport</span>(<span class=\"pl-s\">\"zig-keychain\"</span>, <span class=\"pl-v\">dep</span>.<span class=\"pl-v\">module</span>(<span class=\"pl-s\">\"zig-keychain\"</span>));</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Git Submodule (C FFI consumers)</h3><a id=\"user-content-git-submodule-c-ffi-consumers\" class=\"anchor\" aria-label=\"Permalink: Git Submodule (C FFI consumers)\" href=\"#git-submodule-c-ffi-consumers\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"git submodule add https://github.com/Jesssullivan/zig-keychain.git vendor/keychain\ncd vendor/keychain &amp;&amp; zig build -Doptimize=ReleaseFast\"><pre>git submodule add https://github.com/Jesssullivan/zig-keychain.git vendor/keychain\n<span class=\"pl-c1\">cd</span> vendor/keychain <span class=\"pl-k\">&amp;&amp;</span> zig build -Doptimize=ReleaseFast</pre></div>\n<p dir=\"auto\">Link (macOS): <code>-lzig-keychain -framework Security -framework CoreFoundation</code></p>\n<p dir=\"auto\">Link (Linux): <code>-lzig-keychain $(pkg-config --libs libsecret-1 glib-2.0)</code></p>\n<p dir=\"auto\">Include <code>#include \"zig_keychain.h\"</code>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Requirements</h2><a id=\"user-content-requirements\" class=\"anchor\" aria-label=\"Permalink: Requirements\" href=\"#requirements\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>Zig 0.14.1+</li>\n<li>macOS 13+ (Security.framework) or Linux (libsecret-1-dev)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Architecture</h2><a id=\"user-content-architecture\" class=\"anchor\" aria-label=\"Permalink: Architecture\" href=\"#architecture\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<section class=\"js-render-needs-enrichment render-needs-enrichment position-relative\" data-identity=\"fd569b1a-9123-4f66-ae09-8842f89a6908\" data-host=\"https://viewscreen.githubusercontent.com\" data-src=\"https://viewscreen.githubusercontent.com/markdown/mermaid?docs_host=https%3A%2F%2Fdocs.github.com\" data-type=\"mermaid\" aria-label=\"mermaid rendered output container\">\n  <div class=\"js-render-enrichment-target\" data-json=\"{&quot;data&quot;:&quot;graph TD\\n    A[Application] --&amp;gt;|C FFI| B[ffi.zig&amp;lt;br/&amp;gt;4 exported functions]\\n    B --&amp;gt; C[keychain.zig&amp;lt;br/&amp;gt;Platform dispatch]\\n    C --&amp;gt;|macOS| D[keychain_macos.zig&amp;lt;br/&amp;gt;Security.framework&amp;lt;br/&amp;gt;SecItem API]\\n    C --&amp;gt;|Linux| E[keychain_linux.zig&amp;lt;br/&amp;gt;libsecret&amp;lt;br/&amp;gt;D-Bus Secret Service]\\n\\n    D --&amp;gt; F[SecItemAdd&amp;lt;br/&amp;gt;SecItemCopyMatching&amp;lt;br/&amp;gt;SecItemDelete]\\n    E --&amp;gt; G[secret_password_store_binary_sync&amp;lt;br/&amp;gt;secret_password_lookup_binary_sync&amp;lt;br/&amp;gt;secret_password_clear_sync]\\n\\n    style B fill:#48c,stroke:#269\\n    style D fill:#a6d,stroke:#84b\\n    style E fill:#6a4,stroke:#483\\n&quot;}\" data-plain=\"graph TD\n    A[Application] --&gt;|C FFI| B[ffi.zig&lt;br/&gt;4 exported functions]\n    B --&gt; C[keychain.zig&lt;br/&gt;Platform dispatch]\n    C --&gt;|macOS| D[keychain_macos.zig&lt;br/&gt;Security.framework&lt;br/&gt;SecItem API]\n    C --&gt;|Linux| E[keychain_linux.zig&lt;br/&gt;libsecret&lt;br/&gt;D-Bus Secret Service]\n\n    D --&gt; F[SecItemAdd&lt;br/&gt;SecItemCopyMatching&lt;br/&gt;SecItemDelete]\n    E --&gt; G[secret_password_store_binary_sync&lt;br/&gt;secret_password_lookup_binary_sync&lt;br/&gt;secret_password_clear_sync]\n\n    style B fill:#48c,stroke:#269\n    style D fill:#a6d,stroke:#84b\n    style E fill:#6a4,stroke:#483\n\" dir=\"auto\">\n    <div class=\"render-plaintext-hidden\" dir=\"auto\">\n      <pre lang=\"mermaid\" aria-label=\"Raw mermaid code\">graph TD\n    A[Application] --&gt;|C FFI| B[ffi.zig&lt;br/&gt;4 exported functions]\n    B --&gt; C[keychain.zig&lt;br/&gt;Platform dispatch]\n    C --&gt;|macOS| D[keychain_macos.zig&lt;br/&gt;Security.framework&lt;br/&gt;SecItem API]\n    C --&gt;|Linux| E[keychain_linux.zig&lt;br/&gt;libsecret&lt;br/&gt;D-Bus Secret Service]\n\n    D --&gt; F[SecItemAdd&lt;br/&gt;SecItemCopyMatching&lt;br/&gt;SecItemDelete]\n    E --&gt; G[secret_password_store_binary_sync&lt;br/&gt;secret_password_lookup_binary_sync&lt;br/&gt;secret_password_clear_sync]\n\n    style B fill:#48c,stroke:#269\n    style D fill:#a6d,stroke:#84b\n    style E fill:#6a4,stroke:#483\n</pre>\n    </div>\n  </div>\n  <span class=\"js-render-enrichment-loader d-flex flex-justify-center flex-items-center width-full\" style=\"min-height:100px\" role=\"presentation\">\n    <span data-view-component=\"true\">\n  <svg style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" data-view-component=\"true\" class=\"octospinner mx-auto tmp-mx-auto anim-rotate\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n    <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n</svg>    <span class=\"sr-only\">Loading</span>\n</span>\n  </span>\n</section>\n\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Build</h2><a id=\"user-content-build\" class=\"anchor\" aria-label=\"Permalink: Build\" href=\"#build\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Static library (libzig-keychain.a)\nzig build -Doptimize=ReleaseFast\n\n# Run unit tests\nzig build test\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Static library (libzig-keychain.a)</span>\nzig build -Doptimize=ReleaseFast\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Run unit tests</span>\nzig build <span class=\"pl-c1\">test</span></pre></div>\n<p dir=\"auto\">With Nix:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix develop       # dev shell\nnix build         # build library package\"><pre>nix develop       <span class=\"pl-c\"><span class=\"pl-c\">#</span> dev shell</span>\nnix build         <span class=\"pl-c\"><span class=\"pl-c\">#</span> build library package</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Platform Support</h2><a id=\"user-content-platform-support\" class=\"anchor\" aria-label=\"Permalink: Platform Support\" href=\"#platform-support\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Platform</th>\n<th>Backend</th>\n<th>Packages</th>\n<th>Status</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>macOS 13+ (arm64/x86_64)</td>\n<td>Security.framework (SecItem)</td>\n<td>None</td>\n<td>Tested</td>\n</tr>\n<tr>\n<td>Linux (x86_64/arm64)</td>\n<td>libsecret (D-Bus Secret Service)</td>\n<td><code>libsecret-1-dev</code> (apt) / <code>libsecret-devel</code> (dnf)</td>\n<td>Supported</td>\n</tr>\n<tr>\n<td>Cross-compilation</td>\n<td>--</td>\n<td>Frameworks/libs linked at final build</td>\n<td>Supported</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">C FFI API Reference</h2><a id=\"user-content-c-ffi-api-reference\" class=\"anchor\" aria-label=\"Permalink: C FFI API Reference\" href=\"#c-ffi-api-reference\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Header: <a href=\"include/zig_keychain.h\"><code>include/zig_keychain.h</code></a></p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Store</h3><a id=\"user-content-store\" class=\"anchor\" aria-label=\"Permalink: Store\" href=\"#store\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Store a generic secret in the system keychain/secret store.\n// Uses upsert semantics: existing item with same service+account is replaced.\n// Returns: 0 on success, -1 on failure\n//\n// macOS: SecItemDelete + SecItemAdd (kSecClassGenericPassword)\n// Linux: secret_password_store_binary_sync\nint zig_keychain_store(\n    const char *service, size_t service_len,\n    const char *account, size_t account_len,\n    const uint8_t *data, size_t data_len\n);\"><pre><span class=\"pl-c\">// Store a generic secret in the system keychain/secret store.</span>\n<span class=\"pl-c\">// Uses upsert semantics: existing item with same service+account is replaced.</span>\n<span class=\"pl-c\">// Returns: 0 on success, -1 on failure</span>\n<span class=\"pl-c\">//</span>\n<span class=\"pl-c\">// macOS: SecItemDelete + SecItemAdd (kSecClassGenericPassword)</span>\n<span class=\"pl-c\">// Linux: secret_password_store_binary_sync</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-en\">zig_keychain_store</span>(\n    <span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">service</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">service_len</span>,\n    <span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">account</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">account_len</span>,\n    <span class=\"pl-k\">const</span> <span class=\"pl-smi\">uint8_t</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">data</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">data_len</span>\n);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Lookup</h3><a id=\"user-content-lookup\" class=\"anchor\" aria-label=\"Permalink: Lookup\" href=\"#lookup\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Look up a generic secret from the system keychain/secret store.\n// Returns: bytes written on success, -1 on not found, -2 on error\n//\n// macOS: SecItemCopyMatching (kSecReturnData)\n// Linux: secret_password_lookup_binary_sync\nint zig_keychain_lookup(\n    const char *service, size_t service_len,\n    const char *account, size_t account_len,\n    uint8_t *out, size_t out_capacity\n);\"><pre><span class=\"pl-c\">// Look up a generic secret from the system keychain/secret store.</span>\n<span class=\"pl-c\">// Returns: bytes written on success, -1 on not found, -2 on error</span>\n<span class=\"pl-c\">//</span>\n<span class=\"pl-c\">// macOS: SecItemCopyMatching (kSecReturnData)</span>\n<span class=\"pl-c\">// Linux: secret_password_lookup_binary_sync</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-en\">zig_keychain_lookup</span>(\n    <span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">service</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">service_len</span>,\n    <span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">account</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">account_len</span>,\n    <span class=\"pl-smi\">uint8_t</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">out</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">out_capacity</span>\n);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Delete</h3><a id=\"user-content-delete\" class=\"anchor\" aria-label=\"Permalink: Delete\" href=\"#delete\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Delete a generic secret from the system keychain/secret store.\n// Returns: 0 on success (including not-found), -1 on error\n//\n// macOS: SecItemDelete\n// Linux: secret_password_clear_sync\nint zig_keychain_delete(\n    const char *service, size_t service_len,\n    const char *account, size_t account_len\n);\"><pre><span class=\"pl-c\">// Delete a generic secret from the system keychain/secret store.</span>\n<span class=\"pl-c\">// Returns: 0 on success (including not-found), -1 on error</span>\n<span class=\"pl-c\">//</span>\n<span class=\"pl-c\">// macOS: SecItemDelete</span>\n<span class=\"pl-c\">// Linux: secret_password_clear_sync</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-en\">zig_keychain_delete</span>(\n    <span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">service</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">service_len</span>,\n    <span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">account</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">account_len</span>\n);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Search</h3><a id=\"user-content-search\" class=\"anchor\" aria-label=\"Permalink: Search\" href=\"#search\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Search for keychain items matching an account name.\n// Writes matching service names as null-separated strings to out.\n// Returns: number of matches found, -1 on error\n//\n// macOS: SecItemCopyMatching (kSecMatchLimitAll, kSecReturnAttributes)\n// Linux: secret_service_search_sync\nint zig_keychain_search(\n    const char *account, size_t account_len,\n    char *out, size_t out_capacity\n);\"><pre><span class=\"pl-c\">// Search for keychain items matching an account name.</span>\n<span class=\"pl-c\">// Writes matching service names as null-separated strings to out.</span>\n<span class=\"pl-c\">// Returns: number of matches found, -1 on error</span>\n<span class=\"pl-c\">//</span>\n<span class=\"pl-c\">// macOS: SecItemCopyMatching (kSecMatchLimitAll, kSecReturnAttributes)</span>\n<span class=\"pl-c\">// Linux: secret_service_search_sync</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-en\">zig_keychain_search</span>(\n    <span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">account</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">account_len</span>,\n    <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">out</span>, <span class=\"pl-smi\">size_t</span> <span class=\"pl-s1\">out_capacity</span>\n);</pre></div>\n<p dir=\"auto\"><strong>Note:</strong> <code>zig_keychain_search</code> is declared in the C header but the FFI implementation is pending.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Zig API Reference</h2><a id=\"user-content-zig-api-reference\" class=\"anchor\" aria-label=\"Permalink: Zig API Reference\" href=\"#zig-api-reference\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">For direct Zig usage (not via C FFI):</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Module</th>\n<th>Public API</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>keychain.zig</code></td>\n<td><code>store(service, account, data) !void</code></td>\n<td>Store a secret (platform-dispatched)</td>\n</tr>\n<tr>\n<td><code>keychain.zig</code></td>\n<td><code>lookup(service, account) !Result</code></td>\n<td>Lookup a secret (returns <code>.success</code>, <code>.not_found</code>, or <code>.err</code>)</td>\n</tr>\n<tr>\n<td><code>keychain.zig</code></td>\n<td><code>delete(service, account) !void</code></td>\n<td>Delete a secret (platform-dispatched)</td>\n</tr>\n<tr>\n<td><code>keychain.zig</code></td>\n<td><code>Result</code> (union: success/not_found/err)</td>\n<td>Lookup result type</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Integration</h2><a id=\"user-content-integration\" class=\"anchor\" aria-label=\"Permalink: Integration\" href=\"#integration\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">As a Git Submodule</h3><a id=\"user-content-as-a-git-submodule\" class=\"anchor\" aria-label=\"Permalink: As a Git Submodule\" href=\"#as-a-git-submodule\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"git submodule add https://github.com/Jesssullivan/zig-keychain.git vendor/keychain\ncd vendor/keychain &amp;&amp; zig build -Doptimize=ReleaseFast\"><pre>git submodule add https://github.com/Jesssullivan/zig-keychain.git vendor/keychain\n<span class=\"pl-c1\">cd</span> vendor/keychain <span class=\"pl-k\">&amp;&amp;</span> zig build -Doptimize=ReleaseFast</pre></div>\n<p dir=\"auto\">Link (macOS): <code>-lzig-keychain -framework Security -framework CoreFoundation</code></p>\n<p dir=\"auto\">Link (Linux): <code>-lzig-keychain -lsecret-1 -lglib-2.0</code></p>\n<p dir=\"auto\">Include: <code>#include \"zig_keychain.h\"</code> (path: <code>vendor/keychain/include/</code>)</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Swift via Bridging Header</h3><a id=\"user-content-swift-via-bridging-header\" class=\"anchor\" aria-label=\"Permalink: Swift via Bridging Header\" href=\"#swift-via-bridging-header\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-swift notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"#include &quot;zig_keychain.h&quot;\n\n// Store\nlet data: [UInt8] = Array(&quot;my-token&quot;.utf8)\nzig_keychain_store(&quot;MyApp&quot;, 5, &quot;user@example.com&quot;, 16, data, data.count)\n\n// Lookup\nvar buf = [UInt8](repeating: 0, count: 256)\nlet len = zig_keychain_lookup(&quot;MyApp&quot;, 5, &quot;user@example.com&quot;, 16, &amp;buf, buf.count)\nif len &gt; 0 {\n    let token = String(bytes: buf[0..&lt;Int(len)], encoding: .utf8)\n}\"><pre>#include <span class=\"pl-s\">\"</span><span class=\"pl-s\">zig_keychain.h</span><span class=\"pl-s\">\"</span>\n\n// Store\n<span class=\"pl-k\">let</span> <span class=\"pl-s1\">data</span><span class=\"pl-kos\">:</span> <span class=\"pl-kos\">[</span><span class=\"pl-smi\">UInt8</span><span class=\"pl-kos\">]</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">Array</span><span class=\"pl-kos\">(</span><span class=\"pl-s\">\"</span><span class=\"pl-s\">my-token</span><span class=\"pl-s\">\"</span><span class=\"pl-kos\">.</span>utf8<span class=\"pl-kos\">)</span>\n<span class=\"pl-en\">zig_keychain_store</span><span class=\"pl-kos\">(</span><span class=\"pl-s\">\"</span><span class=\"pl-s\">MyApp</span><span class=\"pl-s\">\"</span><span class=\"pl-kos\">,</span> <span class=\"pl-c1\">5</span><span class=\"pl-kos\">,</span> <span class=\"pl-s\">\"</span><span class=\"pl-s\">user@example.com</span><span class=\"pl-s\">\"</span><span class=\"pl-kos\">,</span> <span class=\"pl-c1\">16</span><span class=\"pl-kos\">,</span> data<span class=\"pl-kos\">,</span> data<span class=\"pl-kos\">.</span>count<span class=\"pl-kos\">)</span>\n\n// Lookup\n<span class=\"pl-k\">var</span> <span class=\"pl-s1\">buf</span> <span class=\"pl-c1\">=</span> <span class=\"pl-kos\">[</span>UInt8<span class=\"pl-kos\">]</span><span class=\"pl-kos\">(</span>repeating<span class=\"pl-kos\">:</span> <span class=\"pl-c1\">0</span><span class=\"pl-kos\">,</span> count<span class=\"pl-kos\">:</span> <span class=\"pl-c1\">256</span><span class=\"pl-kos\">)</span>\n<span class=\"pl-k\">let</span> <span class=\"pl-s1\">len</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">zig_keychain_lookup</span><span class=\"pl-kos\">(</span><span class=\"pl-s\">\"</span><span class=\"pl-s\">MyApp</span><span class=\"pl-s\">\"</span><span class=\"pl-kos\">,</span> <span class=\"pl-c1\">5</span><span class=\"pl-kos\">,</span> <span class=\"pl-s\">\"</span><span class=\"pl-s\">user@example.com</span><span class=\"pl-s\">\"</span><span class=\"pl-kos\">,</span> <span class=\"pl-c1\">16</span><span class=\"pl-kos\">,</span> <span class=\"pl-c1\">&amp;</span>buf<span class=\"pl-kos\">,</span> buf<span class=\"pl-kos\">.</span>count<span class=\"pl-kos\">)</span>\n<span class=\"pl-k\">if</span> len <span class=\"pl-c1\">&gt;</span> <span class=\"pl-c1\">0</span> <span class=\"pl-kos\">{</span>\n    <span class=\"pl-k\">let</span> <span class=\"pl-s1\">token</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">String</span><span class=\"pl-kos\">(</span>bytes<span class=\"pl-kos\">:</span> <span class=\"pl-en\">buf</span><span class=\"pl-kos\">[</span><span class=\"pl-c1\">0</span><span class=\"pl-c1\">..&lt;</span><span class=\"pl-en\">Int</span><span class=\"pl-kos\">(</span>len<span class=\"pl-kos\">)</span><span class=\"pl-kos\">]</span><span class=\"pl-kos\">,</span> encoding<span class=\"pl-kos\">:</span> <span class=\"pl-kos\">.</span>utf8<span class=\"pl-kos\">)</span>\n<span class=\"pl-kos\">}</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Dual-licensed under <a href=\"https://opensource.org/licenses/Zlib\" rel=\"nofollow\">Zlib</a> and <a href=\"https://opensource.org/licenses/MIT\" rel=\"nofollow\">MIT</a>. Choose whichever you prefer.</p>\n</article></div>",
      "readme_excerpt": "Cross-platform keychain/secrets abstraction in Zig with C FFI -- macOS Keychain (SecItem) and Linux Secret Service (libsecret).\nLicense: Zlib OR MIT\nApplications need to store credentials, tokens, and other secrets securely. Platform APIs differ significantly: macOS uses Security.framework (SecItemAdd/CopyMatching/Delete), Linux uses the D-Bus Secret Service API via libsecret. This library provides a single C API that works on both platforms.\n- Store: Save secrets to the system keychain (upsert...",
      "install_commands": {
        "zig": "zig fetch --save git+https://github.com/jesssullivan/zig-keychain.git"
      },
      "repo_url": "https://github.com/jesssullivan/zig-keychain",
      "website_url": "http://transscendsurvival.org/zig-keychain/",
      "docs_url": "https://jesssullivan.github.io/zig-keychain/",
      "registry_url": "https://zigistry.dev/package/{repo}",
      "releases_url": "https://github.com/jesssullivan/zig-keychain/releases",
      "og_image_url": "https://opengraph.githubassets.com/7e27457f9ea26b684dc975b90712262fb41869f595a8ef58c6f0525b0f31d5b1/Jesssullivan/zig-keychain",
      "license": "NOASSERTION",
      "pushed_at": "2026-04-25T18:49:34Z",
      "enriched_at": "2026-04-26T17:17:13.743Z",
      "gate": "pages"
    },
    {
      "slug": "jesssullivan-zig-ctap2",
      "name": "zig-ctap2",
      "repo": "jesssullivan/zig-ctap2",
      "org": "jesssullivan",
      "ecosystem": "zig",
      "category": "cryptography",
      "description": "Portable FIDO2/WebAuthn in Zig with C FFI — direct USB HID, no Apple entitlements needed",
      "featured": true,
      "tags": [
        "fido2",
        "ctap2",
        "webauthn",
        "usb-hid",
        "yubikey"
      ],
      "version": "0.4.0",
      "release_date": "2026-04-25T18:41:24Z",
      "releases": [
        {
          "tag": "0.4.0",
          "date": "2026-04-25T18:41:24Z",
          "body": "## What's Changed\n* Add keepalive callback FFI for dock touch indicator by @Jesssullivan in https://github.com/Jesssullivan/zig-ctap2/pull/22\n* Fix ctap2_get_assertion_with_keepalive arg mismatch by @Jesssullivan in https://github.com/Jesssullivan/zig-ctap2/pull/23\n* Fix Linux build: platform IOReturn guard + Zig 0.15 ArrayList by @Jesssullivan in https://github.com/Jesssullivan/zig-ctap2/pull/24\n* fix(security): harden hidraw enumeration and FFI bounds by @Jesssullivan in https://github.com/Jesssullivan/zig-ctap2/pull/25\n* fix: add DevicesNotAccessible to macOS error set by @Jesssullivan in https://github.com/Jesssullivan/zig-ctap2/pull/26\n* productionize: package metadata, module export, docs by @Jesssullivan in https://github.com/Jesssullivan/zig-ctap2/pull/27\n* Add autodoc CI job and release workflow by @Jesssullivan in https://github.com/Jesssullivan/zig-ctap2/pull/28\n\n\n**Full Changelog**: https://github.com/Jesssullivan/zig-ctap2/compare/v0.3.1...v0.4.0"
        }
      ],
      "stars": 0,
      "topics": [
        "ctap2",
        "ffi",
        "fido2",
        "library",
        "webauthn",
        "c-interop",
        "zig",
        "zig-package"
      ],
      "languages": [
        {
          "name": "Zig",
          "color": "#ec915c",
          "bytes": 159340
        },
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 34474
        },
        {
          "name": "C",
          "color": "#555555",
          "bytes": 10791
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 2013
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 1808
        }
      ],
      "primary_language": "Zig",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">zig-ctap2</h1><a id=\"user-content-zig-ctap2\" class=\"anchor\" aria-label=\"Permalink: zig-ctap2\" href=\"#zig-ctap2\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Portable CTAP2/FIDO2 library in Zig — direct USB HID communication with security keys (YubiKey, SoloKeys, etc.), no Apple entitlements or platform authentication frameworks needed.</p>\n<p dir=\"auto\"><strong>License:</strong> Zlib OR MIT</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Why</h2><a id=\"user-content-why\" class=\"anchor\" aria-label=\"Permalink: Why\" href=\"#why\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Apple's <code>ASAuthorizationController</code> requires a restricted entitlement + provisioning profile for WebAuthn in general-purpose browsers. This library talks directly to FIDO2 devices over USB HID via IOKit (macOS) and hidraw (Linux), bypassing platform authentication frameworks entirely.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Features</h2><a id=\"user-content-features\" class=\"anchor\" aria-label=\"Permalink: Features\" href=\"#features\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>CTAP2 protocol</strong>: makeCredential, getAssertion, getInfo, with structured response parsing</li>\n<li><strong>PIN protocol v2</strong>: ECDH P-256 key agreement, AES-256-CBC, HMAC-SHA-256 for PIN-authenticated operations</li>\n<li><strong>CTAPHID framing</strong>: 64-byte packet fragmentation/reassembly, CID management, keepalive handling</li>\n<li><strong>Minimal CBOR codec</strong>: encoder/decoder for the CTAP2 subset (integers, byte/text strings, arrays, maps, booleans)</li>\n<li><strong>Platform HID transports</strong>: macOS (IOKit), Linux (hidraw)</li>\n<li><strong>C FFI</strong>: 16 exported functions callable from Swift, C, C++, or any language with C interop</li>\n<li><strong>Error mapping</strong>: All CTAP2 status codes mapped to human-readable messages</li>\n<li><strong>Property-based tests</strong>: 1000-iteration roundtrip tests for CBOR and CTAPHID framing</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Requirements</h2><a id=\"user-content-requirements\" class=\"anchor\" aria-label=\"Permalink: Requirements\" href=\"#requirements\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>Zig 0.14.1+</li>\n<li>macOS 13+ (IOKit) or Linux (hidraw)</li>\n<li>USB security key (tested with YubiKey 5C NFC)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Installation</h2><a id=\"user-content-installation\" class=\"anchor\" aria-label=\"Permalink: Installation\" href=\"#installation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Zig Package Manager (recommended)</h3><a id=\"user-content-zig-package-manager-recommended\" class=\"anchor\" aria-label=\"Permalink: Zig Package Manager (recommended)\" href=\"#zig-package-manager-recommended\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"zig fetch --save git+https://github.com/Jesssullivan/zig-ctap2.git\"><pre>zig fetch --save git+https://github.com/Jesssullivan/zig-ctap2.git</pre></div>\n<p dir=\"auto\">Then in your <code>build.zig</code>:</p>\n<div class=\"highlight highlight-source-zig notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"const dep = b.dependency(&quot;zig-ctap2&quot;, .{ .target = target, .optimize = optimize });\nexe.root_module.addImport(&quot;zig-ctap2&quot;, dep.module(&quot;zig-ctap2&quot;));\"><pre><span class=\"pl-k\">const</span> <span class=\"pl-v\">dep</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">b</span>.<span class=\"pl-v\">dependency</span>(<span class=\"pl-s\">\"zig-ctap2\"</span>, .{ .<span class=\"pl-v\">target</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">target</span>, .<span class=\"pl-v\">optimize</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">optimize</span> });\n<span class=\"pl-v\">exe</span>.<span class=\"pl-v\">root_module</span>.<span class=\"pl-v\">addImport</span>(<span class=\"pl-s\">\"zig-ctap2\"</span>, <span class=\"pl-v\">dep</span>.<span class=\"pl-v\">module</span>(<span class=\"pl-s\">\"zig-ctap2\"</span>));</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Git Submodule (C FFI consumers)</h3><a id=\"user-content-git-submodule-c-ffi-consumers\" class=\"anchor\" aria-label=\"Permalink: Git Submodule (C FFI consumers)\" href=\"#git-submodule-c-ffi-consumers\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"git submodule add https://github.com/Jesssullivan/zig-ctap2.git vendor/ctap2\ncd vendor/ctap2 &amp;&amp; zig build -Doptimize=ReleaseFast\"><pre>git submodule add https://github.com/Jesssullivan/zig-ctap2.git vendor/ctap2\n<span class=\"pl-c1\">cd</span> vendor/ctap2 <span class=\"pl-k\">&amp;&amp;</span> zig build -Doptimize=ReleaseFast</pre></div>\n<p dir=\"auto\">Link <code>-lctap2</code> and include <code>ctap2.h</code>. At final link time, add platform frameworks:</p>\n<ul dir=\"auto\">\n<li><strong>macOS:</strong> <code>-framework IOKit -framework CoreFoundation</code></li>\n<li><strong>Linux:</strong> no extra libraries needed (uses hidraw via kernel)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Build</h2><a id=\"user-content-build\" class=\"anchor\" aria-label=\"Permalink: Build\" href=\"#build\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Static library (libctap2.a)\nzig build -Doptimize=ReleaseFast\n\n# Run unit tests\nzig build test\n\n# Run property-based tests\nzig build test-pbt\n\n# Run hardware tests (requires YubiKey connected)\nYUBIKEY_TESTS=1 zig build test-hardware\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Static library (libctap2.a)</span>\nzig build -Doptimize=ReleaseFast\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Run unit tests</span>\nzig build <span class=\"pl-c1\">test</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Run property-based tests</span>\nzig build test-pbt\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Run hardware tests (requires YubiKey connected)</span>\nYUBIKEY_TESTS=1 zig build test-hardware</pre></div>\n<p dir=\"auto\">With <a href=\"https://just.systems\" rel=\"nofollow\">just</a> (recommended):</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"just test-all     # unit + PBT tests\njust build        # ReleaseFast static library\njust info         # show library stats\njust              # list all recipes\"><pre>just test-all     <span class=\"pl-c\"><span class=\"pl-c\">#</span> unit + PBT tests</span>\njust build        <span class=\"pl-c\"><span class=\"pl-c\">#</span> ReleaseFast static library</span>\njust info         <span class=\"pl-c\"><span class=\"pl-c\">#</span> show library stats</span>\njust              <span class=\"pl-c\"><span class=\"pl-c\">#</span> list all recipes</span></pre></div>\n<p dir=\"auto\">With Nix:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix develop       # dev shell (zig, just, detect-secrets, pre-commit)\nnix build         # build library package\"><pre>nix develop       <span class=\"pl-c\"><span class=\"pl-c\">#</span> dev shell (zig, just, detect-secrets, pre-commit)</span>\nnix build         <span class=\"pl-c\"><span class=\"pl-c\">#</span> build library package</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Architecture</h2><a id=\"user-content-architecture\" class=\"anchor\" aria-label=\"Permalink: Architecture\" href=\"#architecture\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<section class=\"js-render-needs-enrichment render-needs-enrichment position-relative\" data-identity=\"8bc5722c-d5c1-4fee-b6fa-8ef0db360f94\" data-host=\"https://viewscreen.githubusercontent.com\" data-src=\"https://viewscreen.githubusercontent.com/markdown/mermaid?docs_host=https%3A%2F%2Fdocs.github.com\" data-type=\"mermaid\" aria-label=\"mermaid rendered output container\">\n  <div class=\"js-render-enrichment-target\" data-json=\"{&quot;data&quot;:&quot;graph TD\\n    A[Application / Browser] --&amp;gt;|C FFI| B[ffi.zig]\\n    B --&amp;gt; C[ctap2.zig&amp;lt;br/&amp;gt;Commands + Response Parsing]\\n    B --&amp;gt; D[pin.zig&amp;lt;br/&amp;gt;PIN Protocol v2]\\n    C --&amp;gt; E[cbor.zig&amp;lt;br/&amp;gt;CBOR Codec]\\n    D --&amp;gt; E\\n    C --&amp;gt; F[ctaphid.zig&amp;lt;br/&amp;gt;HID Framing]\\n    D --&amp;gt; F\\n    F --&amp;gt; G{Platform}\\n    G --&amp;gt;|macOS| H[hid_macos.zig&amp;lt;br/&amp;gt;IOKit HID]\\n    G --&amp;gt;|Linux| I[hid_linux.zig&amp;lt;br/&amp;gt;hidraw]\\n&quot;}\" data-plain=\"graph TD\n    A[Application / Browser] --&gt;|C FFI| B[ffi.zig]\n    B --&gt; C[ctap2.zig&lt;br/&gt;Commands + Response Parsing]\n    B --&gt; D[pin.zig&lt;br/&gt;PIN Protocol v2]\n    C --&gt; E[cbor.zig&lt;br/&gt;CBOR Codec]\n    D --&gt; E\n    C --&gt; F[ctaphid.zig&lt;br/&gt;HID Framing]\n    D --&gt; F\n    F --&gt; G{Platform}\n    G --&gt;|macOS| H[hid_macos.zig&lt;br/&gt;IOKit HID]\n    G --&gt;|Linux| I[hid_linux.zig&lt;br/&gt;hidraw]\n\" dir=\"auto\">\n    <div class=\"render-plaintext-hidden\" dir=\"auto\">\n      <pre lang=\"mermaid\" aria-label=\"Raw mermaid code\">graph TD\n    A[Application / Browser] --&gt;|C FFI| B[ffi.zig]\n    B --&gt; C[ctap2.zig&lt;br/&gt;Commands + Response Parsing]\n    B --&gt; D[pin.zig&lt;br/&gt;PIN Protocol v2]\n    C --&gt; E[cbor.zig&lt;br/&gt;CBOR Codec]\n    D --&gt; E\n    C --&gt; F[ctaphid.zig&lt;br/&gt;HID Framing]\n    D --&gt; F\n    F --&gt; G{Platform}\n    G --&gt;|macOS| H[hid_macos.zig&lt;br/&gt;IOKit HID]\n    G --&gt;|Linux| I[hid_linux.zig&lt;br/&gt;hidraw]\n</pre>\n    </div>\n  </div>\n  <span class=\"js-render-enrichment-loader d-flex flex-justify-center flex-items-center width-full\" style=\"min-height:100px\" role=\"presentation\">\n    <span data-view-component=\"true\">\n  <svg style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" data-view-component=\"true\" class=\"octospinner mx-auto tmp-mx-auto anim-rotate\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n    <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n</svg>    <span class=\"sr-only\">Loading</span>\n</span>\n  </span>\n</section>\n\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Registration Flow</h3><a id=\"user-content-registration-flow\" class=\"anchor\" aria-label=\"Permalink: Registration Flow\" href=\"#registration-flow\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<section class=\"js-render-needs-enrichment render-needs-enrichment position-relative\" data-identity=\"c92dd85c-9c6d-485f-98a0-2081ca4e2ae6\" data-host=\"https://viewscreen.githubusercontent.com\" data-src=\"https://viewscreen.githubusercontent.com/markdown/mermaid?docs_host=https%3A%2F%2Fdocs.github.com\" data-type=\"mermaid\" aria-label=\"mermaid rendered output container\">\n  <div class=\"js-render-enrichment-target\" data-json=\"{&quot;data&quot;:&quot;sequenceDiagram\\n    participant App\\n    participant FFI as ffi.zig\\n    participant CTAP as ctap2.zig\\n    participant CBOR as cbor.zig\\n    participant HID as ctaphid.zig\\n    participant Key as YubiKey\\n\\n    App-&amp;gt;&amp;gt;FFI: ctap2_make_credential_parsed()\\n    FFI-&amp;gt;&amp;gt;CTAP: encodeMakeCredential()\\n    CTAP-&amp;gt;&amp;gt;CBOR: CBOR encode request\\n    CBOR--&amp;gt;&amp;gt;CTAP: bytes\\n    CTAP-&amp;gt;&amp;gt;HID: CTAPHID_CBOR (fragmented)\\n    HID-&amp;gt;&amp;gt;Key: USB HID packets\\n    Note over Key: User touches key\\n    Key--&amp;gt;&amp;gt;HID: Response packets\\n    HID--&amp;gt;&amp;gt;CTAP: Reassembled CBOR\\n    CTAP-&amp;gt;&amp;gt;CTAP: parseMakeCredentialResponse()\\n    CTAP--&amp;gt;&amp;gt;FFI: credential_id, attestation_object\\n    FFI--&amp;gt;&amp;gt;App: Structured result\\n&quot;}\" data-plain=\"sequenceDiagram\n    participant App\n    participant FFI as ffi.zig\n    participant CTAP as ctap2.zig\n    participant CBOR as cbor.zig\n    participant HID as ctaphid.zig\n    participant Key as YubiKey\n\n    App-&gt;&gt;FFI: ctap2_make_credential_parsed()\n    FFI-&gt;&gt;CTAP: encodeMakeCredential()\n    CTAP-&gt;&gt;CBOR: CBOR encode request\n    CBOR--&gt;&gt;CTAP: bytes\n    CTAP-&gt;&gt;HID: CTAPHID_CBOR (fragmented)\n    HID-&gt;&gt;Key: USB HID packets\n    Note over Key: User touches key\n    Key--&gt;&gt;HID: Response packets\n    HID--&gt;&gt;CTAP: Reassembled CBOR\n    CTAP-&gt;&gt;CTAP: parseMakeCredentialResponse()\n    CTAP--&gt;&gt;FFI: credential_id, attestation_object\n    FFI--&gt;&gt;App: Structured result\n\" dir=\"auto\">\n    <div class=\"render-plaintext-hidden\" dir=\"auto\">\n      <pre lang=\"mermaid\" aria-label=\"Raw mermaid code\">sequenceDiagram\n    participant App\n    participant FFI as ffi.zig\n    participant CTAP as ctap2.zig\n    participant CBOR as cbor.zig\n    participant HID as ctaphid.zig\n    participant Key as YubiKey\n\n    App-&gt;&gt;FFI: ctap2_make_credential_parsed()\n    FFI-&gt;&gt;CTAP: encodeMakeCredential()\n    CTAP-&gt;&gt;CBOR: CBOR encode request\n    CBOR--&gt;&gt;CTAP: bytes\n    CTAP-&gt;&gt;HID: CTAPHID_CBOR (fragmented)\n    HID-&gt;&gt;Key: USB HID packets\n    Note over Key: User touches key\n    Key--&gt;&gt;HID: Response packets\n    HID--&gt;&gt;CTAP: Reassembled CBOR\n    CTAP-&gt;&gt;CTAP: parseMakeCredentialResponse()\n    CTAP--&gt;&gt;FFI: credential_id, attestation_object\n    FFI--&gt;&gt;App: Structured result\n</pre>\n    </div>\n  </div>\n  <span class=\"js-render-enrichment-loader d-flex flex-justify-center flex-items-center width-full\" style=\"min-height:100px\" role=\"presentation\">\n    <span data-view-component=\"true\">\n  <svg style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" data-view-component=\"true\" class=\"octospinner mx-auto tmp-mx-auto anim-rotate\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n    <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n</svg>    <span class=\"sr-only\">Loading</span>\n</span>\n  </span>\n</section>\n\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Error Handling</h3><a id=\"user-content-error-handling\" class=\"anchor\" aria-label=\"Permalink: Error Handling\" href=\"#error-handling\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<section class=\"js-render-needs-enrichment render-needs-enrichment position-relative\" data-identity=\"ad2db1fd-f7f3-4ed5-8e26-7483bbe06b18\" data-host=\"https://viewscreen.githubusercontent.com\" data-src=\"https://viewscreen.githubusercontent.com/markdown/mermaid?docs_host=https%3A%2F%2Fdocs.github.com\" data-type=\"mermaid\" aria-label=\"mermaid rendered output container\">\n  <div class=\"js-render-enrichment-target\" data-json=\"{&quot;data&quot;:&quot;graph TD\\n    E[CTAP2 Status Byte] --&amp;gt;|0x00| OK[Success]\\n    E --&amp;gt;|0x2E| NC[No Credentials&amp;lt;br/&amp;gt;for this site]\\n    E --&amp;gt;|0x27| OD[Operation Denied&amp;lt;br/&amp;gt;by user]\\n    E --&amp;gt;|0x31| IP[Incorrect PIN]\\n    E --&amp;gt;|0x32| PB[PIN Blocked]\\n    E --&amp;gt;|0x35| PNS[PIN Not Set]\\n    E --&amp;gt;|0x36| PV[PIN Policy&amp;lt;br/&amp;gt;Violation]\\n\\n    style OK fill:#2d5,stroke:#1a3\\n    style NC fill:#d52,stroke:#a31\\n    style OD fill:#d52,stroke:#a31\\n    style IP fill:#d85,stroke:#a63\\n    style PB fill:#d52,stroke:#a31\\n    style PNS fill:#d85,stroke:#a63\\n    style PV fill:#d85,stroke:#a63\\n&quot;}\" data-plain=\"graph TD\n    E[CTAP2 Status Byte] --&gt;|0x00| OK[Success]\n    E --&gt;|0x2E| NC[No Credentials&lt;br/&gt;for this site]\n    E --&gt;|0x27| OD[Operation Denied&lt;br/&gt;by user]\n    E --&gt;|0x31| IP[Incorrect PIN]\n    E --&gt;|0x32| PB[PIN Blocked]\n    E --&gt;|0x35| PNS[PIN Not Set]\n    E --&gt;|0x36| PV[PIN Policy&lt;br/&gt;Violation]\n\n    style OK fill:#2d5,stroke:#1a3\n    style NC fill:#d52,stroke:#a31\n    style OD fill:#d52,stroke:#a31\n    style IP fill:#d85,stroke:#a63\n    style PB fill:#d52,stroke:#a31\n    style PNS fill:#d85,stroke:#a63\n    style PV fill:#d85,stroke:#a63\n\" dir=\"auto\">\n    <div class=\"render-plaintext-hidden\" dir=\"auto\">\n      <pre lang=\"mermaid\" aria-label=\"Raw mermaid code\">graph TD\n    E[CTAP2 Status Byte] --&gt;|0x00| OK[Success]\n    E --&gt;|0x2E| NC[No Credentials&lt;br/&gt;for this site]\n    E --&gt;|0x27| OD[Operation Denied&lt;br/&gt;by user]\n    E --&gt;|0x31| IP[Incorrect PIN]\n    E --&gt;|0x32| PB[PIN Blocked]\n    E --&gt;|0x35| PNS[PIN Not Set]\n    E --&gt;|0x36| PV[PIN Policy&lt;br/&gt;Violation]\n\n    style OK fill:#2d5,stroke:#1a3\n    style NC fill:#d52,stroke:#a31\n    style OD fill:#d52,stroke:#a31\n    style IP fill:#d85,stroke:#a63\n    style PB fill:#d52,stroke:#a31\n    style PNS fill:#d85,stroke:#a63\n    style PV fill:#d85,stroke:#a63\n</pre>\n    </div>\n  </div>\n  <span class=\"js-render-enrichment-loader d-flex flex-justify-center flex-items-center width-full\" style=\"min-height:100px\" role=\"presentation\">\n    <span data-view-component=\"true\">\n  <svg style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" data-view-component=\"true\" class=\"octospinner mx-auto tmp-mx-auto anim-rotate\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n    <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n</svg>    <span class=\"sr-only\">Loading</span>\n</span>\n  </span>\n</section>\n\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">C API</h2><a id=\"user-content-c-api\" class=\"anchor\" aria-label=\"Permalink: C API\" href=\"#c-api\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">All functions are blocking (with timeouts) and thread-safe. See <a href=\"include/ctap2.h\"><code>include/ctap2.h</code></a> for full signatures.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Core Operations</h3><a id=\"user-content-core-operations\" class=\"anchor\" aria-label=\"Permalink: Core Operations\" href=\"#core-operations\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"#include &quot;ctap2.h&quot;\n\n// Enumerate connected FIDO2 devices\nint count = ctap2_device_count();\n\n// Register a credential (raw CBOR response)\nint bytes = ctap2_make_credential(\n    client_data_hash, rp_id, rp_name,\n    user_id, user_id_len, user_name, user_display_name,\n    alg_ids, alg_count, resident_key,\n    result_buf, result_buf_len\n);\n\n// Authenticate (raw CBOR response)\nint bytes = ctap2_get_assertion(\n    client_data_hash, rp_id,\n    allow_list_ids, allow_list_id_lens, allow_list_count,\n    result_buf, result_buf_len\n);\n\n// Get device capabilities\nint bytes = ctap2_get_info(result_buf, result_buf_len);\"><pre><span class=\"pl-k\">#include</span> <span class=\"pl-s\">\"ctap2.h\"</span>\n\n<span class=\"pl-c\">// Enumerate connected FIDO2 devices</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-s1\">count</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">ctap2_device_count</span>();\n\n<span class=\"pl-c\">// Register a credential (raw CBOR response)</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-s1\">bytes</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">ctap2_make_credential</span>(\n    <span class=\"pl-s1\">client_data_hash</span>, <span class=\"pl-s1\">rp_id</span>, <span class=\"pl-s1\">rp_name</span>,\n    <span class=\"pl-s1\">user_id</span>, <span class=\"pl-s1\">user_id_len</span>, <span class=\"pl-s1\">user_name</span>, <span class=\"pl-s1\">user_display_name</span>,\n    <span class=\"pl-s1\">alg_ids</span>, <span class=\"pl-s1\">alg_count</span>, <span class=\"pl-s1\">resident_key</span>,\n    <span class=\"pl-s1\">result_buf</span>, <span class=\"pl-s1\">result_buf_len</span>\n);\n\n<span class=\"pl-c\">// Authenticate (raw CBOR response)</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-s1\">bytes</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">ctap2_get_assertion</span>(\n    <span class=\"pl-s1\">client_data_hash</span>, <span class=\"pl-s1\">rp_id</span>,\n    <span class=\"pl-s1\">allow_list_ids</span>, <span class=\"pl-s1\">allow_list_id_lens</span>, <span class=\"pl-s1\">allow_list_count</span>,\n    <span class=\"pl-s1\">result_buf</span>, <span class=\"pl-s1\">result_buf_len</span>\n);\n\n<span class=\"pl-c\">// Get device capabilities</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-s1\">bytes</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">ctap2_get_info</span>(<span class=\"pl-s1\">result_buf</span>, <span class=\"pl-s1\">result_buf_len</span>);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Parsed Responses</h3><a id=\"user-content-parsed-responses\" class=\"anchor\" aria-label=\"Permalink: Parsed Responses\" href=\"#parsed-responses\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">These perform the CTAP2 command AND parse the CBOR, returning structured fields:</p>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Register + parse → credential_id, attestation_object\nint status = ctap2_make_credential_parsed(\n    client_data_hash, rp_id, rp_name,\n    user_id, user_id_len, user_name, user_display_name,\n    alg_ids, alg_count, resident_key,\n    out_credential_id, &amp;out_credential_id_len,\n    out_attestation_object, &amp;out_attestation_object_len\n);\n\n// Authenticate + parse → credential_id, auth_data, signature, user_handle\nint status = ctap2_get_assertion_parsed(\n    client_data_hash, rp_id,\n    allow_list_ids, allow_list_id_lens, allow_list_count,\n    out_credential_id, &amp;out_credential_id_len,\n    out_auth_data, &amp;out_auth_data_len,\n    out_signature, &amp;out_signature_len,\n    out_user_handle, &amp;out_user_handle_len\n);\"><pre><span class=\"pl-c\">// Register + parse → credential_id, attestation_object</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-s1\">status</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">ctap2_make_credential_parsed</span>(\n    <span class=\"pl-s1\">client_data_hash</span>, <span class=\"pl-s1\">rp_id</span>, <span class=\"pl-s1\">rp_name</span>,\n    <span class=\"pl-s1\">user_id</span>, <span class=\"pl-s1\">user_id_len</span>, <span class=\"pl-s1\">user_name</span>, <span class=\"pl-s1\">user_display_name</span>,\n    <span class=\"pl-s1\">alg_ids</span>, <span class=\"pl-s1\">alg_count</span>, <span class=\"pl-s1\">resident_key</span>,\n    <span class=\"pl-s1\">out_credential_id</span>, <span class=\"pl-c1\">&amp;</span><span class=\"pl-s1\">out_credential_id_len</span>,\n    <span class=\"pl-s1\">out_attestation_object</span>, <span class=\"pl-c1\">&amp;</span><span class=\"pl-s1\">out_attestation_object_len</span>\n);\n\n<span class=\"pl-c\">// Authenticate + parse → credential_id, auth_data, signature, user_handle</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-s1\">status</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">ctap2_get_assertion_parsed</span>(\n    <span class=\"pl-s1\">client_data_hash</span>, <span class=\"pl-s1\">rp_id</span>,\n    <span class=\"pl-s1\">allow_list_ids</span>, <span class=\"pl-s1\">allow_list_id_lens</span>, <span class=\"pl-s1\">allow_list_count</span>,\n    <span class=\"pl-s1\">out_credential_id</span>, <span class=\"pl-c1\">&amp;</span><span class=\"pl-s1\">out_credential_id_len</span>,\n    <span class=\"pl-s1\">out_auth_data</span>, <span class=\"pl-c1\">&amp;</span><span class=\"pl-s1\">out_auth_data_len</span>,\n    <span class=\"pl-s1\">out_signature</span>, <span class=\"pl-c1\">&amp;</span><span class=\"pl-s1\">out_signature_len</span>,\n    <span class=\"pl-s1\">out_user_handle</span>, <span class=\"pl-c1\">&amp;</span><span class=\"pl-s1\">out_user_handle_len</span>\n);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Pure Parsing (no I/O)</h3><a id=\"user-content-pure-parsing-no-io\" class=\"anchor\" aria-label=\"Permalink: Pure Parsing (no I/O)\" href=\"#pure-parsing-no-io\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Parse raw CTAP2 response bytes you already have:</p>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"ctap2_parse_make_credential_response(response, len, ...);\nctap2_parse_get_assertion_response(response, len, fallback_cred, ...);\"><pre><span class=\"pl-en\">ctap2_parse_make_credential_response</span>(<span class=\"pl-s1\">response</span>, <span class=\"pl-s1\">len</span>, ...);\n<span class=\"pl-en\">ctap2_parse_get_assertion_response</span>(<span class=\"pl-s1\">response</span>, <span class=\"pl-s1\">len</span>, <span class=\"pl-s1\">fallback_cred</span>, ...);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">PIN Protocol</h3><a id=\"user-content-pin-protocol\" class=\"anchor\" aria-label=\"Permalink: PIN Protocol\" href=\"#pin-protocol\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Check remaining PIN retries\nint retries;\nctap2_get_pin_retries(&amp;retries);\n\n// Get PIN token (ECDH + AES-256-CBC handshake)\nuint8_t pin_token[32];\nctap2_get_pin_token(&quot;123456&quot;, pin_token, 32);\n\n// PIN-authenticated registration\nctap2_make_credential_with_pin(\n    client_data_hash, rp_id, rp_name, ...,\n    pin_token, 2,  // pin_protocol = 2\n    out_credential_id, &amp;out_credential_id_len,\n    out_attestation_object, &amp;out_attestation_object_len\n);\n\n// PIN-authenticated assertion\nctap2_get_assertion_with_pin(\n    client_data_hash, rp_id, ...,\n    pin_token, 2,\n    out_credential_id, &amp;out_credential_id_len, ...\n);\"><pre><span class=\"pl-c\">// Check remaining PIN retries</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-s1\">retries</span>;\n<span class=\"pl-en\">ctap2_get_pin_retries</span>(<span class=\"pl-c1\">&amp;</span><span class=\"pl-s1\">retries</span>);\n\n<span class=\"pl-c\">// Get PIN token (ECDH + AES-256-CBC handshake)</span>\n<span class=\"pl-smi\">uint8_t</span> <span class=\"pl-s1\">pin_token</span>[<span class=\"pl-c1\">32</span>];\n<span class=\"pl-en\">ctap2_get_pin_token</span>(<span class=\"pl-s\">\"123456\"</span>, <span class=\"pl-s1\">pin_token</span>, <span class=\"pl-c1\">32</span>);\n\n<span class=\"pl-c\">// PIN-authenticated registration</span>\n<span class=\"pl-en\">ctap2_make_credential_with_pin</span>(\n    <span class=\"pl-s1\">client_data_hash</span>, <span class=\"pl-s1\">rp_id</span>, <span class=\"pl-s1\">rp_name</span>, ...,\n    <span class=\"pl-s1\">pin_token</span>, <span class=\"pl-c1\">2</span>,  <span class=\"pl-c\">// pin_protocol = 2</span>\n    <span class=\"pl-s1\">out_credential_id</span>, <span class=\"pl-c1\">&amp;</span><span class=\"pl-s1\">out_credential_id_len</span>,\n    <span class=\"pl-s1\">out_attestation_object</span>, <span class=\"pl-c1\">&amp;</span><span class=\"pl-s1\">out_attestation_object_len</span>\n);\n\n<span class=\"pl-c\">// PIN-authenticated assertion</span>\n<span class=\"pl-en\">ctap2_get_assertion_with_pin</span>(\n    <span class=\"pl-s1\">client_data_hash</span>, <span class=\"pl-s1\">rp_id</span>, ...,\n    <span class=\"pl-s1\">pin_token</span>, <span class=\"pl-c1\">2</span>,\n    <span class=\"pl-s1\">out_credential_id</span>, <span class=\"pl-c1\">&amp;</span><span class=\"pl-s1\">out_credential_id_len</span>, ...\n);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Utilities</h3><a id=\"user-content-utilities\" class=\"anchor\" aria-label=\"Permalink: Utilities\" href=\"#utilities\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-c notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Human-readable error messages\nconst char *msg = ctap2_status_message(0x35);\n// → &quot;PIN not set - configure a PIN on your security key first&quot;\n\n// Debug: last IOKit return code\nint ioret = ctap2_debug_last_ioreturn();\"><pre><span class=\"pl-c\">// Human-readable error messages</span>\n<span class=\"pl-k\">const</span> <span class=\"pl-smi\">char</span> <span class=\"pl-c1\">*</span><span class=\"pl-s1\">msg</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">ctap2_status_message</span>(<span class=\"pl-c1\">0x35</span>);\n<span class=\"pl-c\">// → \"PIN not set - configure a PIN on your security key first\"</span>\n\n<span class=\"pl-c\">// Debug: last IOKit return code</span>\n<span class=\"pl-smi\">int</span> <span class=\"pl-s1\">ioret</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">ctap2_debug_last_ioreturn</span>();</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Status Codes</h3><a id=\"user-content-status-codes\" class=\"anchor\" aria-label=\"Permalink: Status Codes\" href=\"#status-codes\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Code</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>CTAP2_OK</code> (0)</td>\n<td>Success</td>\n</tr>\n<tr>\n<td><code>CTAP2_ERR_NO_DEVICE</code> (-1)</td>\n<td>No FIDO2 device connected</td>\n</tr>\n<tr>\n<td><code>CTAP2_ERR_TIMEOUT</code> (-2)</td>\n<td>Device communication timeout</td>\n</tr>\n<tr>\n<td><code>CTAP2_ERR_PROTOCOL</code> (-3)</td>\n<td>CTAPHID protocol error</td>\n</tr>\n<tr>\n<td><code>CTAP2_ERR_BUFFER_TOO_SMALL</code> (-4)</td>\n<td>Output buffer too small</td>\n</tr>\n<tr>\n<td><code>CTAP2_ERR_OPEN_FAILED</code> (-5)</td>\n<td>Failed to open HID device</td>\n</tr>\n<tr>\n<td><code>CTAP2_ERR_WRITE_FAILED</code> (-6)</td>\n<td>USB write failed</td>\n</tr>\n<tr>\n<td><code>CTAP2_ERR_READ_FAILED</code> (-7)</td>\n<td>USB read failed</td>\n</tr>\n<tr>\n<td><code>CTAP2_ERR_CBOR</code> (-8)</td>\n<td>CBOR encoding/decoding error</td>\n</tr>\n<tr>\n<td><code>CTAP2_ERR_DEVICE</code> (-9)</td>\n<td>CTAP2 device error (check status byte)</td>\n</tr>\n<tr>\n<td><code>CTAP2_ERR_PIN</code> (-10)</td>\n<td>PIN protocol error</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Entitlements</h2><a id=\"user-content-entitlements\" class=\"anchor\" aria-label=\"Permalink: Entitlements\" href=\"#entitlements\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">On macOS with hardened runtime, add to your entitlements:</p>\n<div class=\"highlight highlight-text-xml notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"&lt;key&gt;com.apple.security.device.usb&lt;/key&gt;\n&lt;true/&gt;\"><pre>&lt;<span class=\"pl-ent\">key</span>&gt;com.apple.security.device.usb&lt;/<span class=\"pl-ent\">key</span>&gt;\n&lt;<span class=\"pl-ent\">true</span>/&gt;</pre></div>\n<p dir=\"auto\">The user must grant <strong>Input Monitoring</strong> permission in System Settings &gt; Privacy &amp; Security.</p>\n<p dir=\"auto\">No other entitlements needed — no <code>com.apple.developer.web-browser.public-key-credential</code>, no provisioning profile, no Apple Developer portal configuration.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Integration with cmux</h2><a id=\"user-content-integration-with-cmux\" class=\"anchor\" aria-label=\"Permalink: Integration with cmux\" href=\"#integration-with-cmux\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">This library powers the FIDO2/WebAuthn support in <a href=\"https://github.com/Jesssullivan/cmux\">cmux</a> (fork), integrated as a git submodule at <code>vendor/ctap2</code>. The JS bridge in WKWebView intercepts <code>navigator.credentials.create/get</code> and routes to libctap2 via Swift C FFI.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Tested Devices</h2><a id=\"user-content-tested-devices\" class=\"anchor\" aria-label=\"Permalink: Tested Devices\" href=\"#tested-devices\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>YubiKey 5C NFC (USB, firmware 5.x)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Status</h2><a id=\"user-content-status\" class=\"anchor\" aria-label=\"Permalink: Status\" href=\"#status\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul class=\"contains-task-list\">\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> makeCredential (registration)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> getAssertion (authentication)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> getInfo (device capabilities)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> CBOR response parsing (structured result types)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> CTAP2 error code mapping (human-readable messages)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> PIN protocol v2 (ECDH P-256, AES-256-CBC, HMAC-SHA-256)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> Property-based tests (CBOR + CTAPHID, 1000 iterations each)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> Hardware integration tests (YubiKey roundtrips)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Incomplete task\"> Extensions (credProtect, hmac-secret)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Incomplete task\"> NFC transport</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Dual-licensed under <a href=\"https://opensource.org/licenses/Zlib\" rel=\"nofollow\">Zlib</a> and <a href=\"https://opensource.org/licenses/MIT\" rel=\"nofollow\">MIT</a>. Choose whichever you prefer.</p>\n</article></div>",
      "readme_excerpt": "Portable CTAP2/FIDO2 library in Zig — direct USB HID communication with security keys (YubiKey, SoloKeys, etc.), no Apple entitlements or platform authentication frameworks needed.\nLicense: Zlib OR MIT\nApple's ASAuthorizationController requires a restricted entitlement + provisioning profile for WebAuthn in general-purpose browsers. This library talks directly to FIDO2 devices over USB HID via IOKit (macOS) and hidraw (Linux), bypassing platform authentication frameworks entirely.\n- CTAP2...",
      "install_commands": {
        "zig": "zig fetch --save git+https://github.com/jesssullivan/zig-ctap2.git"
      },
      "repo_url": "https://github.com/jesssullivan/zig-ctap2",
      "website_url": "http://transscendsurvival.org/zig-ctap2/",
      "docs_url": "https://jesssullivan.github.io/zig-ctap2/",
      "registry_url": "https://zigistry.dev/package/{repo}",
      "releases_url": "https://github.com/jesssullivan/zig-ctap2/releases",
      "og_image_url": "https://opengraph.githubassets.com/dc7e068042a483fe04565a726405cc25e437cd991c827a6290873c4898ccec4a/Jesssullivan/zig-ctap2",
      "license": "NOASSERTION",
      "pushed_at": "2026-04-25T18:40:26Z",
      "enriched_at": "2026-04-26T17:17:14.048Z",
      "gate": "pages"
    },
    {
      "slug": "jesssullivan-zig-crypto",
      "name": "zig-crypto",
      "repo": "jesssullivan/zig-crypto",
      "org": "jesssullivan",
      "ecosystem": "zig",
      "category": "cryptography",
      "description": "Portable cryptographic primitives in Zig with C FFI — SHA-256, AES, ECDH, Ed25519, HMAC, PBKDF2. No OpenSSL.",
      "featured": true,
      "tags": [
        "crypto",
        "ffi",
        "c-api",
        "cross-platform"
      ],
      "version": "0.1.0",
      "release_date": "2026-04-25T18:41:15Z",
      "releases": [
        {
          "tag": "0.1.0",
          "date": "2026-04-25T18:41:15Z",
          "body": "## What's Changed\n* Productionize: build.zig.zon, autodoc, examples, agent metadata by @Jesssullivan in https://github.com/Jesssullivan/zig-crypto/pull/1\n\n## New Contributors\n* @Jesssullivan made their first contribution in https://github.com/Jesssullivan/zig-crypto/pull/1\n\n**Full Changelog**: https://github.com/Jesssullivan/zig-crypto/commits/v0.1.0"
        }
      ],
      "stars": 0,
      "topics": [
        "attestation-compatibility",
        "cryptography-algorithms",
        "primitives",
        "c-interop",
        "cryptography",
        "ffi",
        "zig",
        "zig-package"
      ],
      "languages": [
        {
          "name": "Zig",
          "color": "#ec915c",
          "bytes": 43100
        },
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 37626
        },
        {
          "name": "C",
          "color": "#555555",
          "bytes": 6130
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 1551
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 1449
        }
      ],
      "primary_language": "Zig",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">zig-crypto</h1><a id=\"user-content-zig-crypto\" class=\"anchor\" aria-label=\"Permalink: zig-crypto\" href=\"#zig-crypto\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Portable cryptographic primitives in Zig with C FFI -- SHA-256, HMAC-SHA-256, AES-CBC, PBKDF2, ECDH P-256, Ed25519, and CSPRNG.</p>\n<p dir=\"auto\"><strong>License:</strong> Zlib OR MIT</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Why</h2><a id=\"user-content-why\" class=\"anchor\" aria-label=\"Permalink: Why\" href=\"#why\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">A minimal, zero-dependency crypto library that compiles to a static C library from Zig. No OpenSSL, no CommonCrypto, no system dependencies. Provides the cryptographic primitives needed by CTAP2 PIN protocol, Sparkle update signing, and general-purpose credential management.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Features</h2><a id=\"user-content-features\" class=\"anchor\" aria-label=\"Permalink: Features\" href=\"#features\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>SHA-256</strong>: Single-shot and incremental hashing, hex output</li>\n<li><strong>HMAC-SHA-256</strong>: RFC 4231-conformant message authentication</li>\n<li><strong>AES-128-CBC / AES-256-CBC</strong>: Encrypt/decrypt with PKCS#7 padding</li>\n<li><strong>AES-256-CBC raw</strong>: No-padding mode for CTAP2 PIN protocol</li>\n<li><strong>PBKDF2-SHA1</strong>: RFC 6070-conformant key derivation</li>\n<li><strong>ECDH P-256</strong>: Key generation and shared secret derivation</li>\n<li><strong>Ed25519</strong>: Key generation, signing, verification</li>\n<li><strong>CSPRNG</strong>: OS-backed cryptographically secure random bytes</li>\n<li><strong>C FFI</strong>: 19 exported functions</li>\n<li><strong>Property-based tests</strong>: Roundtrip tests for SHA-256, AES, ECDH, Ed25519</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Installation</h2><a id=\"user-content-installation\" class=\"anchor\" aria-label=\"Permalink: Installation\" href=\"#installation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Zig Package Manager (recommended)</h3><a id=\"user-content-zig-package-manager-recommended\" class=\"anchor\" aria-label=\"Permalink: Zig Package Manager (recommended)\" href=\"#zig-package-manager-recommended\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"zig fetch --save git+https://github.com/Jesssullivan/zig-crypto.git\"><pre>zig fetch --save git+https://github.com/Jesssullivan/zig-crypto.git</pre></div>\n<p dir=\"auto\">Then in your <code>build.zig</code>:</p>\n<div class=\"highlight highlight-source-zig notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"const dep = b.dependency(&quot;zig-crypto&quot;, .{ .target = target, .optimize = optimize });\nexe.root_module.addImport(&quot;zig-crypto&quot;, dep.module(&quot;zig-crypto&quot;));\"><pre><span class=\"pl-k\">const</span> <span class=\"pl-v\">dep</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">b</span>.<span class=\"pl-v\">dependency</span>(<span class=\"pl-s\">\"zig-crypto\"</span>, .{ .<span class=\"pl-v\">target</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">target</span>, .<span class=\"pl-v\">optimize</span> <span class=\"pl-k\">=</span> <span class=\"pl-v\">optimize</span> });\n<span class=\"pl-v\">exe</span>.<span class=\"pl-v\">root_module</span>.<span class=\"pl-v\">addImport</span>(<span class=\"pl-s\">\"zig-crypto\"</span>, <span class=\"pl-v\">dep</span>.<span class=\"pl-v\">module</span>(<span class=\"pl-s\">\"zig-crypto\"</span>));</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Git Submodule (C FFI consumers)</h3><a id=\"user-content-git-submodule-c-ffi-consumers\" class=\"anchor\" aria-label=\"Permalink: Git Submodule (C FFI consumers)\" href=\"#git-submodule-c-ffi-consumers\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"git submodule add https://github.com/Jesssullivan/zig-crypto.git vendor/crypto\ncd vendor/crypto &amp;&amp; zig build -Doptimize=ReleaseFast\"><pre>git submodule add https://github.com/Jesssullivan/zig-crypto.git vendor/crypto\n<span class=\"pl-c1\">cd</span> vendor/crypto <span class=\"pl-k\">&amp;&amp;</span> zig build -Doptimize=ReleaseFast</pre></div>\n<p dir=\"auto\">Link <code>-lzig-crypto</code> and include <code>#include \"zig_crypto.h\"</code>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Requirements</h2><a id=\"user-content-requirements\" class=\"anchor\" aria-label=\"Permalink: Requirements\" href=\"#requirements\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>Zig 0.15.2+</li>\n<li>No platform-specific dependencies (pure Zig std.crypto)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Architecture</h2><a id=\"user-content-architecture\" class=\"anchor\" aria-label=\"Permalink: Architecture\" href=\"#architecture\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<section class=\"js-render-needs-enrichment render-needs-enrichment position-relative\" data-identity=\"531573fa-a47e-418e-bcd9-937ceabb412e\" data-host=\"https://viewscreen.githubusercontent.com\" data-src=\"https://viewscreen.githubusercontent.com/markdown/mermaid?docs_host=https%3A%2F%2Fdocs.github.com\" data-type=\"mermaid\" aria-label=\"mermaid rendered output container\">\n  <div class=\"js-render-enrichment-target\" data-json=\"{&quot;data&quot;:&quot;graph TD\\n    A[Application] --&amp;gt;|C FFI| B[ffi.zig&amp;lt;br/&amp;gt;19 exported functions]\\n    B --&amp;gt; C[sha256.zig]\\n    B --&amp;gt; D[hmac.zig]\\n    B --&amp;gt; E[aes.zig]\\n    B --&amp;gt; F[pbkdf2.zig]\\n    B --&amp;gt; G[ecdh.zig]\\n    B --&amp;gt; H[ed25519.zig]\\n    B --&amp;gt; I[random.zig]\\n    C --&amp;gt; Z[std.crypto]\\n    D --&amp;gt; Z\\n    E --&amp;gt; Z\\n    F --&amp;gt; Z\\n    G --&amp;gt; Z\\n    H --&amp;gt; Z\\n    I --&amp;gt; Z\\n&quot;}\" data-plain=\"graph TD\n    A[Application] --&gt;|C FFI| B[ffi.zig&lt;br/&gt;19 exported functions]\n    B --&gt; C[sha256.zig]\n    B --&gt; D[hmac.zig]\n    B --&gt; E[aes.zig]\n    B --&gt; F[pbkdf2.zig]\n    B --&gt; G[ecdh.zig]\n    B --&gt; H[ed25519.zig]\n    B --&gt; I[random.zig]\n    C --&gt; Z[std.crypto]\n    D --&gt; Z\n    E --&gt; Z\n    F --&gt; Z\n    G --&gt; Z\n    H --&gt; Z\n    I --&gt; Z\n\" dir=\"auto\">\n    <div class=\"render-plaintext-hidden\" dir=\"auto\">\n      <pre lang=\"mermaid\" aria-label=\"Raw mermaid code\">graph TD\n    A[Application] --&gt;|C FFI| B[ffi.zig&lt;br/&gt;19 exported functions]\n    B --&gt; C[sha256.zig]\n    B --&gt; D[hmac.zig]\n    B --&gt; E[aes.zig]\n    B --&gt; F[pbkdf2.zig]\n    B --&gt; G[ecdh.zig]\n    B --&gt; H[ed25519.zig]\n    B --&gt; I[random.zig]\n    C --&gt; Z[std.crypto]\n    D --&gt; Z\n    E --&gt; Z\n    F --&gt; Z\n    G --&gt; Z\n    H --&gt; Z\n    I --&gt; Z\n</pre>\n    </div>\n  </div>\n  <span class=\"js-render-enrichment-loader d-flex flex-justify-center flex-items-center width-full\" style=\"min-height:100px\" role=\"presentation\">\n    <span data-view-component=\"true\">\n  <svg style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" data-view-component=\"true\" class=\"octospinner mx-auto tmp-mx-auto anim-rotate\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n    <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n</svg>    <span class=\"sr-only\">Loading</span>\n</span>\n  </span>\n</section>\n\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Build</h2><a id=\"user-content-build\" class=\"anchor\" aria-label=\"Permalink: Build\" href=\"#build\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"zig build -Doptimize=ReleaseFast   # static library\nzig build test                      # unit tests\nzig build test-pbt                  # property-based tests\"><pre>zig build -Doptimize=ReleaseFast   <span class=\"pl-c\"><span class=\"pl-c\">#</span> static library</span>\nzig build <span class=\"pl-c1\">test</span>                      <span class=\"pl-c\"><span class=\"pl-c\">#</span> unit tests</span>\nzig build test-pbt                  <span class=\"pl-c\"><span class=\"pl-c\">#</span> property-based tests</span></pre></div>\n<p dir=\"auto\">With <a href=\"https://just.systems\" rel=\"nofollow\">just</a>: <code>just test-all</code>, <code>just build</code>, <code>just info</code>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Platform Support</h2><a id=\"user-content-platform-support\" class=\"anchor\" aria-label=\"Permalink: Platform Support\" href=\"#platform-support\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Platform</th>\n<th>Status</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>macOS (arm64/x86_64)</td>\n<td>Tested</td>\n<td>No frameworks needed</td>\n</tr>\n<tr>\n<td>Linux (x86_64/arm64)</td>\n<td>Supported</td>\n<td>No system libraries needed</td>\n</tr>\n<tr>\n<td>Cross-compilation</td>\n<td>Supported</td>\n<td>Pure Zig, no platform dependencies</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">C API Reference</h2><a id=\"user-content-c-api-reference\" class=\"anchor\" aria-label=\"Permalink: C API Reference\" href=\"#c-api-reference\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Header: <a href=\"include/zig_crypto.h\"><code>include/zig_crypto.h</code></a>. All functions are thread-safe and stateless.</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Function</th>\n<th>Returns</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>zig_crypto_sha256</code></td>\n<td>void</td>\n<td>SHA-256 hash (out: 32 bytes)</td>\n</tr>\n<tr>\n<td><code>zig_crypto_sha256_hex</code></td>\n<td>size_t (64)</td>\n<td>SHA-256 as hex (out: 64 bytes)</td>\n</tr>\n<tr>\n<td><code>zig_crypto_hmac_sha256</code></td>\n<td>void</td>\n<td>HMAC-SHA-256 (out: 32 bytes)</td>\n</tr>\n<tr>\n<td><code>zig_crypto_aes128_cbc_encrypt</code></td>\n<td>int</td>\n<td>AES-128-CBC encrypt, PKCS#7</td>\n</tr>\n<tr>\n<td><code>zig_crypto_aes128_cbc_decrypt</code></td>\n<td>int</td>\n<td>AES-128-CBC decrypt, PKCS#7</td>\n</tr>\n<tr>\n<td><code>zig_crypto_aes256_cbc_encrypt</code></td>\n<td>int</td>\n<td>AES-256-CBC encrypt, PKCS#7</td>\n</tr>\n<tr>\n<td><code>zig_crypto_aes256_cbc_decrypt</code></td>\n<td>int</td>\n<td>AES-256-CBC decrypt, PKCS#7</td>\n</tr>\n<tr>\n<td><code>zig_crypto_aes256_cbc_encrypt_raw</code></td>\n<td>int</td>\n<td>AES-256-CBC no padding (CTAP2)</td>\n</tr>\n<tr>\n<td><code>zig_crypto_aes256_cbc_decrypt_raw</code></td>\n<td>int</td>\n<td>AES-256-CBC no unpadding (CTAP2)</td>\n</tr>\n<tr>\n<td><code>zig_crypto_pbkdf2_sha1</code></td>\n<td>void</td>\n<td>PBKDF2-HMAC-SHA1 key derivation</td>\n</tr>\n<tr>\n<td><code>zig_crypto_p256_generate</code></td>\n<td>int (0/-1)</td>\n<td>Generate P-256 key pair</td>\n</tr>\n<tr>\n<td><code>zig_crypto_p256_ecdh</code></td>\n<td>int (0/-1)</td>\n<td>ECDH shared secret</td>\n</tr>\n<tr>\n<td><code>zig_crypto_ed25519_generate</code></td>\n<td>void</td>\n<td>Generate Ed25519 key pair</td>\n</tr>\n<tr>\n<td><code>zig_crypto_ed25519_from_seed</code></td>\n<td>int (0/-1)</td>\n<td>Deterministic key from seed</td>\n</tr>\n<tr>\n<td><code>zig_crypto_ed25519_sign</code></td>\n<td>int (0/-1)</td>\n<td>Sign message (sig: 64 bytes)</td>\n</tr>\n<tr>\n<td><code>zig_crypto_ed25519_verify</code></td>\n<td>bool</td>\n<td>Verify signature</td>\n</tr>\n<tr>\n<td><code>zig_crypto_random</code></td>\n<td>bool</td>\n<td>Fill with secure random bytes</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Integration</h2><a id=\"user-content-integration\" class=\"anchor\" aria-label=\"Permalink: Integration\" href=\"#integration\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"git submodule add https://github.com/Jesssullivan/zig-crypto.git vendor/crypto\ncd vendor/crypto &amp;&amp; zig build -Doptimize=ReleaseFast\"><pre>git submodule add https://github.com/Jesssullivan/zig-crypto.git vendor/crypto\n<span class=\"pl-c1\">cd</span> vendor/crypto <span class=\"pl-k\">&amp;&amp;</span> zig build -Doptimize=ReleaseFast</pre></div>\n<p dir=\"auto\">Link: <code>-lzig-crypto</code>. Include: <code>#include \"zig_crypto.h\"</code>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Dual-licensed under <a href=\"https://opensource.org/licenses/Zlib\" rel=\"nofollow\">Zlib</a> and <a href=\"https://opensource.org/licenses/MIT\" rel=\"nofollow\">MIT</a>. Choose whichever you prefer.</p>\n</article></div>",
      "readme_excerpt": "Portable cryptographic primitives in Zig with C FFI -- SHA-256, HMAC-SHA-256, AES-CBC, PBKDF2, ECDH P-256, Ed25519, and CSPRNG.\nLicense: Zlib OR MIT\nA minimal, zero-dependency crypto library that compiles to a static C library from Zig. No OpenSSL, no CommonCrypto, no system dependencies. Provides the cryptographic primitives needed by CTAP2 PIN protocol, Sparkle update signing, and general-purpose credential management.\n- SHA-256: Single-shot and incremental hashing, hex output\n- HMAC-SHA-256:...",
      "install_commands": {
        "zig": "zig fetch --save git+https://github.com/jesssullivan/zig-crypto.git"
      },
      "repo_url": "https://github.com/jesssullivan/zig-crypto",
      "website_url": "http://transscendsurvival.org/zig-crypto/",
      "docs_url": "https://jesssullivan.github.io/zig-crypto/",
      "registry_url": "https://zigistry.dev/package/{repo}",
      "releases_url": "https://github.com/jesssullivan/zig-crypto/releases",
      "og_image_url": "https://opengraph.githubassets.com/5da519295725192da7baf8a217d843d0693618728da805646475c1c97f0bf76e/Jesssullivan/zig-crypto",
      "license": "NOASSERTION",
      "pushed_at": "2026-04-25T18:39:57Z",
      "enriched_at": "2026-04-26T17:17:14.338Z",
      "gate": "pages"
    },
    {
      "slug": "jesssullivan-betterkvm",
      "name": "betterkvm",
      "repo": "jesssullivan/betterkvm",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "systems",
      "description": "The converged multiarch KVM for Tinyland NoneX86 contributions",
      "featured": false,
      "tags": [
        "kvm",
        "pikvm",
        "multiarch"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [
        "pikvm",
        "remote-development",
        "riscv",
        "serial-over-ip",
        "nonex86"
      ],
      "languages": [
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 42987
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 34129
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 17631
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 13239
        }
      ],
      "primary_language": "Python",
      "readme_html": "<div id=\"readme\" class=\"txt\" data-path=\"README.txt\"><div class=\"plain\"><pre style=\"white-space: pre-wrap\">Hey!  This is a work in progress.\n\nThis project spawned from the need to expand remote developement to support NoneX86 initiatives.\n\n\nThis project converges tinyland.dev KVM hardware via a HKS1601A1U 16-port KVM; this allows us\nto provide Risc-V hardware (musebooks and V300, Spacemit K1 dev boards etc), ARM boards,\nx86 servers, and their debug interfaces to folks outside of the lab.\n\n16-port machine mapping:\n  1: honey           9: sdr-1\n  2: bumble (ATX)   10: g2-1\n  3: petting-zoo    11: g2-2\n  4: xoxd-bates     12: t-deck\n  5: yoga           13: tdeck-pro\n  6: mbp-13         14-16: (unassigned)\n  7: betsy\n  8: musey\n\n\nTinyland folk\n    |\n    | Tailscale VPN (WireGuard)\n    | mTLS admin mgmt (Nebula)\n    |\n+---+---------------------------------+\n|   Tailscale Subnet &amp; local DERP     |  Pi #2 (NixOS)\n|   serial-console                    |  advertises &lt;ipv4&gt;/24\n+---+---------------------------------+\n    |\n    | IoT Management Network (&lt;ipv4&gt;/24)\n    |\n+---+--------+----------+------+----------+\n|            |          |      |          |\nv            v          v      v          v\nPi #1        TESmart    Gearmo Smart     Lab\n(PiKVM A3)   HKS1601A1U 16-port PDU     Machines\n             KVM Switch Serial (NUT)     (16 ports)\n             16-port    Hub\n             + RS232\n</pre></div></div>",
      "readme_excerpt": "Hey!  This is a work in progress.\nThis project spawned from the need to expand remote developement to support NoneX86 initiatives.\nThis project converges tinyland.dev KVM hardware via a HKS1601A1U 16-port KVM; this allows us\nto provide Risc-V hardware (musebooks and V300, Spacemit K1 dev boards etc), ARM boards,\nx86 servers, and their debug interfaces to folks outside of the lab.\n16-port machine mapping:\n  1: honey           9: sdr-1\n  2: bumble (ATX)   10: g2-1\n  3: petting-zoo    11: g2-2\n ...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/betterkvm",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/betterkvm/releases",
      "og_image_url": "https://opengraph.githubassets.com/4db589b54b6d0a58bb6bb9f66e57851ac8080a4c47ae9719771b13dac501a63f/tinyland-inc/betterkvm",
      "license": "MIT",
      "pushed_at": "2026-04-25T04:06:49Z",
      "enriched_at": "2026-04-26T17:17:14.757Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-prompt-pulse-tui",
      "name": "prompt-pulse-tui",
      "repo": "jesssullivan/prompt-pulse-tui",
      "org": "jesssullivan",
      "ecosystem": "rust",
      "category": "ai-tools",
      "description": "Ratatui terminal dashboard for prompt-pulse system monitoring",
      "featured": false,
      "tags": [
        "tui",
        "ratatui",
        "monitoring",
        "tailscale"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [
        "dashboard",
        "monitoring",
        "ratatui",
        "rust",
        "terminal",
        "tui"
      ],
      "languages": [
        {
          "name": "Rust",
          "color": "#dea584",
          "bytes": 239611
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 487
        }
      ],
      "primary_language": "Rust",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">prompt-pulse-tui</h1><a id=\"user-content-prompt-pulse-tui\" class=\"anchor\" aria-label=\"Permalink: prompt-pulse-tui\" href=\"#prompt-pulse-tui\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Canonical writable source for the Rust <code>prompt-pulse-tui</code> application.</p>\n<p dir=\"auto\">This repo owns the Rust TUI source. <code>tinyland-inc/lab</code> owns packaging, pinned\nconsumption, shell-start integration, and the wider workstation/runtime\ncontract.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Authority</h2><a id=\"user-content-authority\" class=\"anchor\" aria-label=\"Permalink: Authority\" href=\"#authority\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>Rust TUI source authority: this repo</li>\n<li>Go app authority: <code>tinyland-inc/prompt-pulse</code></li>\n<li>Fleet/runtime integration authority: <code>tinyland-inc/lab</code></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Canonical Home</h2><a id=\"user-content-canonical-home\" class=\"anchor\" aria-label=\"Permalink: Canonical Home\" href=\"#canonical-home\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>canonical repo: <code>https://github.com/Jesssullivan/prompt-pulse-tui</code></li>\n</ul>\n<p dir=\"auto\">The older <code>tinyland-inc/prompt-pulse-tui</code> repo is archive/history only. It is\nnot the active writable source.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Contribution Flow</h2><a id=\"user-content-contribution-flow\" class=\"anchor\" aria-label=\"Permalink: Contribution Flow\" href=\"#contribution-flow\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ol dir=\"auto\">\n<li>Edit source here.</li>\n<li>Validate source here.</li>\n<li>Test integration from <code>lab</code>.</li>\n<li>Repin <code>lab</code> to the landed upstream revision.</li>\n</ol>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Local Remote Policy</h2><a id=\"user-content-local-remote-policy\" class=\"anchor\" aria-label=\"Permalink: Local Remote Policy\" href=\"#local-remote-policy\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><code>origin</code> = canonical repo</li>\n<li>any archived duplicate remote should use an explicit name such as\n<code>org-archive</code></li>\n</ul>\n</article></div>",
      "readme_excerpt": "Canonical writable source for the Rust prompt-pulse-tui application.\nThis repo owns the Rust TUI source. tinyland-inc/lab owns packaging, pinned\nconsumption, shell-start integration, and the wider workstation/runtime\ncontract.\n- Rust TUI source authority: this repo\n- Go app authority: tinyland-inc/prompt-pulse\n- Fleet/runtime integration authority: tinyland-inc/lab\n- canonical repo: https://github.com/Jesssullivan/prompt-pulse-tui\nThe older tinyland-inc/prompt-pulse-tui repo is archive/history...",
      "install_commands": {
        "rust": "cargo add prompt-pulse-tui"
      },
      "repo_url": "https://github.com/jesssullivan/prompt-pulse-tui",
      "website_url": "https://tinyland.dev",
      "docs_url": null,
      "registry_url": "https://crates.io/crates/prompt-pulse-tui",
      "releases_url": "https://github.com/jesssullivan/prompt-pulse-tui/releases",
      "og_image_url": "https://opengraph.githubassets.com/08ac37583942f5dcb71f156a8badc1eec07dcb7cb329309093374ff06aa5e08c/Jesssullivan/prompt-pulse-tui",
      "license": "",
      "pushed_at": "2026-04-23T18:14:53Z",
      "enriched_at": "2026-04-26T17:17:14.994Z",
      "gate": "homepage"
    },
    {
      "slug": "jesssullivan-tinyland-auth-pg",
      "name": "tinyland-auth-pg",
      "repo": "jesssullivan/tinyland-auth-pg",
      "org": "jesssullivan",
      "ecosystem": "npm",
      "category": "auth",
      "description": "PostgreSQL storage adapter for @tummycrypt/tinyland-auth (Neon + Drizzle)",
      "featured": false,
      "tags": [
        "auth",
        "postgres",
        "drizzle",
        "sessions"
      ],
      "version": "0.2.1",
      "release_date": "2026-04-22T22:14:39Z",
      "releases": [
        {
          "tag": "0.2.1",
          "date": "2026-04-22T22:14:39Z",
          "body": "## v0.2.1\n\n### Added\n- first-class `NodePgStorageAdapter`\n- `createNodePgStorageAdapter({ connectionString, poolConfig?, closeOnDispose? })`\n- Docker-backed node-postgres smoke coverage\n\n### Fixed\n- shared package now has a truthful self-hosted PostgreSQL path for CNPG/local PG without requiring consumer-local adapter hacks\n- CI/publish workflow now publishes to npm without the stale GitHub Packages branch\n\n### Notes\n- this release preserves the tenant-scoped `0.2.x` API introduced in `v0.2.0`\n- self-hosted consumers can now use node-postgres directly through the shared package\n"
        },
        {
          "tag": "0.2.0",
          "date": "2026-04-17T19:14:11Z",
          "body": "Breaking release. Every adapter method now takes `tenantId: string` as its first parameter. Every row-bearing table carries `tenant_id uuid NOT NULL`.\n\n**Full migration guide:** [CHANGELOG.md](./CHANGELOG.md#020--2026-04-17)\n\n## Highlights\n\n- **Driver-agnostic construction** via `createPgStorageAdapter({ db })` — accepts any drizzle client (`drizzle-orm/neon-http`, `drizzle-orm/postgres-js`, `drizzle-orm/node-postgres`). Callers own the client lifecycle. PgBouncer-compatible with `postgres.js` and `{ prepare: false }`.\n- **Pattern B tenant isolation** — `tenant_id` on every row-bearing table, composite uniques scoped to `(tenant_id, <col>)`, `TenantScoped<T>` return types.\n- **Construction-time validation** on both `{ db }` and `{ connectionString }` branches.\n- **Driver-agnostic delete counts** via `.returning({...}).length` (fixes silent zero on postgres.js).\n- **29 unit tests + 3 testcontainers smoke tests** exercising the postgres.js path end-to-end.\n\n## New exports\n\n- `Database` — union of supported drizzle client types\n- `TenantScoped<T>` — widens a domain type with `{ tenantId: string }`\n- `PgStorageConfig` — discriminated union accepted by the factory\n\n## Deprecated\n\n`createPgStorageAdapter({ connectionString })` still works but only resolves to neon-http. Prefer `{ db }` for all new code — required for PgBouncer, node-postgres, and self-hosted Postgres."
        },
        {
          "tag": "0.1.1",
          "date": "2026-03-30T03:13:47Z",
          "body": "Fix publishConfig access, add test gate to publish workflow."
        }
      ],
      "stars": 0,
      "topics": [],
      "languages": [
        {
          "name": "TypeScript",
          "color": "#3178c6",
          "bytes": 111251
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 871
        },
        {
          "name": "Starlark",
          "color": "#76d275",
          "bytes": 650
        }
      ],
      "primary_language": "TypeScript",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">@tummycrypt/tinyland-auth-pg</h1><a id=\"user-content-tummycrypttinyland-auth-pg\" class=\"anchor\" aria-label=\"Permalink: @tummycrypt/tinyland-auth-pg\" href=\"#tummycrypttinyland-auth-pg\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">PostgreSQL storage adapter for <a href=\"https://github.com/Jesssullivan/tinyland-auth\">@tummycrypt/tinyland-auth</a>, backed by <a href=\"https://orm.drizzle.team\" rel=\"nofollow\">Drizzle ORM</a> with driver-agnostic construction and multi-tenant scoping.</p>\n<p dir=\"auto\">Supports Neon HTTP, <code>postgres.js</code>, and <code>node-postgres</code>. Use <code>createNodePgStorageAdapter()</code>\nwhen you want the package to own a <code>pg.Pool</code>, or <code>createPgStorageAdapter({ db })</code>\nwhen you already have a pre-built Drizzle client.</p>\n<blockquote>\n<p dir=\"auto\"><strong>0.2.0 is a breaking release.</strong> Every adapter method now takes <code>tenantId: string</code>\nas its first parameter. Every row-bearing table has <code>tenant_id uuid NOT NULL</code>.\nSee <a href=\"./CHANGELOG.md#020--2026-04-17\"><code>CHANGELOG.md</code></a> for the full migration guide.</p>\n</blockquote>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Installation</h2><a id=\"user-content-installation\" class=\"anchor\" aria-label=\"Permalink: Installation\" href=\"#installation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"npm install @tummycrypt/tinyland-auth-pg\n# or\npnpm add @tummycrypt/tinyland-auth-pg\"><pre>npm install @tummycrypt/tinyland-auth-pg\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> or</span>\npnpm add @tummycrypt/tinyland-auth-pg</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Peer Dependencies</h3><a id=\"user-content-peer-dependencies\" class=\"anchor\" aria-label=\"Permalink: Peer Dependencies\" href=\"#peer-dependencies\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"npm install @tummycrypt/tinyland-auth\"><pre>npm install @tummycrypt/tinyland-auth</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick Start (0.2.0+)</h2><a id=\"user-content-quick-start-020\" class=\"anchor\" aria-label=\"Permalink: Quick Start (0.2.0+)\" href=\"#quick-start-020\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">With <code>node-postgres</code> (owned pool; recommended for CNPG / local PG)</h3><a id=\"user-content-with-node-postgres-owned-pool-recommended-for-cnpg--local-pg\" class=\"anchor\" aria-label=\"Permalink: With node-postgres (owned pool; recommended for CNPG / local PG)\" href=\"#with-node-postgres-owned-pool-recommended-for-cnpg--local-pg\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { createNodePgStorageAdapter } from '@tummycrypt/tinyland-auth-pg';\n\nconst adapter = createNodePgStorageAdapter({\n  connectionString: process.env.DATABASE_URL!,\n  poolConfig: { max: 10 },\n});\n\nconst user = await adapter.getUser('&lt;tenant-uuid&gt;', '&lt;user-id&gt;');\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">createNodePgStorageAdapter</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/tinyland-auth-pg'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">adapter</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createNodePgStorageAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">connectionString</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">DATABASE_URL</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">poolConfig</span>: <span class=\"pl-kos\">{</span> <span class=\"pl-c1\">max</span>: <span class=\"pl-c1\">10</span> <span class=\"pl-kos\">}</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">user</span> <span class=\"pl-c1\">=</span> <span class=\"pl-k\">await</span> <span class=\"pl-s1\">adapter</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">getUser</span><span class=\"pl-kos\">(</span><span class=\"pl-s\">'&lt;tenant-uuid&gt;'</span><span class=\"pl-kos\">,</span> <span class=\"pl-s\">'&lt;user-id&gt;'</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">With <code>postgres.js</code> (recommended for PgBouncer transaction mode)</h3><a id=\"user-content-with-postgresjs-recommended-for-pgbouncer-transaction-mode\" class=\"anchor\" aria-label=\"Permalink: With postgres.js (recommended for PgBouncer transaction mode)\" href=\"#with-postgresjs-recommended-for-pgbouncer-transaction-mode\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import postgres from 'postgres';\nimport { drizzle } from 'drizzle-orm/postgres-js';\nimport { createPgStorageAdapter } from '@tummycrypt/tinyland-auth-pg';\nimport * as schema from '@tummycrypt/tinyland-auth-pg/schema';\n\n// prepare: false is required when talking to PgBouncer in transaction mode\nconst sql = postgres(process.env.DATABASE_URL!, { prepare: false, max: 10 });\nconst db = drizzle(sql, { schema });\n\nconst storage = createPgStorageAdapter({ db });\n\n// Every method takes tenantId first\nconst user = await storage.getUser('&lt;tenant-uuid&gt;', '&lt;user-id&gt;');\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-s1\">postgres</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'postgres'</span><span class=\"pl-kos\">;</span>\n<span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">drizzle</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'drizzle-orm/postgres-js'</span><span class=\"pl-kos\">;</span>\n<span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">createPgStorageAdapter</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/tinyland-auth-pg'</span><span class=\"pl-kos\">;</span>\n<span class=\"pl-k\">import</span> <span class=\"pl-c1\">*</span> <span class=\"pl-k\">as</span> <span class=\"pl-s1\">schema</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/tinyland-auth-pg/schema'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-c\">// prepare: false is required when talking to PgBouncer in transaction mode</span>\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">sql</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">postgres</span><span class=\"pl-kos\">(</span><span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">DATABASE_URL</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span> <span class=\"pl-kos\">{</span> <span class=\"pl-c1\">prepare</span>: <span class=\"pl-c1\">false</span><span class=\"pl-kos\">,</span> <span class=\"pl-c1\">max</span>: <span class=\"pl-c1\">10</span> <span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">db</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">drizzle</span><span class=\"pl-kos\">(</span><span class=\"pl-s1\">sql</span><span class=\"pl-kos\">,</span> <span class=\"pl-kos\">{</span> schema <span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">storage</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createPgStorageAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span> db <span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-c\">// Every method takes tenantId first</span>\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">user</span> <span class=\"pl-c1\">=</span> <span class=\"pl-k\">await</span> <span class=\"pl-s1\">storage</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">getUser</span><span class=\"pl-kos\">(</span><span class=\"pl-s\">'&lt;tenant-uuid&gt;'</span><span class=\"pl-kos\">,</span> <span class=\"pl-s\">'&lt;user-id&gt;'</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">With Neon HTTP (legacy, still supported)</h3><a id=\"user-content-with-neon-http-legacy-still-supported\" class=\"anchor\" aria-label=\"Permalink: With Neon HTTP (legacy, still supported)\" href=\"#with-neon-http-legacy-still-supported\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { createPgStorageAdapter } from '@tummycrypt/tinyland-auth-pg';\n\nconst storage = createPgStorageAdapter({\n  connectionString: process.env.DATABASE_URL!,\n  sessionMaxAge: 7 * 24 * 60 * 60 * 1000, // 7 days (default)\n});\n\nconst user = await storage.getUser('&lt;tenant-uuid&gt;', '&lt;user-id&gt;');\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">createPgStorageAdapter</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/tinyland-auth-pg'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">storage</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createPgStorageAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">connectionString</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">DATABASE_URL</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">sessionMaxAge</span>: <span class=\"pl-c1\">7</span> <span class=\"pl-c1\">*</span> <span class=\"pl-c1\">24</span> <span class=\"pl-c1\">*</span> <span class=\"pl-c1\">60</span> <span class=\"pl-c1\">*</span> <span class=\"pl-c1\">60</span> <span class=\"pl-c1\">*</span> <span class=\"pl-c1\">1000</span><span class=\"pl-kos\">,</span> <span class=\"pl-c\">// 7 days (default)</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">user</span> <span class=\"pl-c1\">=</span> <span class=\"pl-k\">await</span> <span class=\"pl-s1\">storage</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">getUser</span><span class=\"pl-kos\">(</span><span class=\"pl-s\">'&lt;tenant-uuid&gt;'</span><span class=\"pl-kos\">,</span> <span class=\"pl-s\">'&lt;user-id&gt;'</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Row-Level Security recommended pattern</h3><a id=\"user-content-row-level-security-recommended-pattern\" class=\"anchor\" aria-label=\"Permalink: Row-Level Security recommended pattern\" href=\"#row-level-security-recommended-pattern\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Pair the adapter with a <code>withTenant</code> wrapper at the app-layer so every query\nalso flows through an RLS <code>SET LOCAL</code>. The explicit <code>tenantId</code> param is your\nfirst line of defense; the <code>SET LOCAL</code> is belt-and-suspenders if a call-site\never forgets to scope.</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"await sql.begin(async (tx) =&gt; {\n  await tx`SELECT set_config('app.tenant_id', ${tenantId}, true)`;\n  return storage.getUser(tenantId, userId);\n});\"><pre><span class=\"pl-k\">await</span> <span class=\"pl-s1\">sql</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">begin</span><span class=\"pl-kos\">(</span><span class=\"pl-k\">async</span> <span class=\"pl-kos\">(</span><span class=\"pl-s1\">tx</span><span class=\"pl-kos\">)</span> <span class=\"pl-c1\">=&gt;</span> <span class=\"pl-kos\">{</span>\n  <span class=\"pl-k\">await</span> <span class=\"pl-en\">tx</span><span class=\"pl-s\">`SELECT set_config('app.tenant_id', <span class=\"pl-s1\"><span class=\"pl-kos\">${</span><span class=\"pl-s1\">tenantId</span><span class=\"pl-kos\">}</span></span>, true)`</span><span class=\"pl-kos\">;</span>\n  <span class=\"pl-k\">return</span> <span class=\"pl-s1\">storage</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">getUser</span><span class=\"pl-kos\">(</span><span class=\"pl-s1\">tenantId</span><span class=\"pl-kos\">,</span> <span class=\"pl-s1\">userId</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Schema Overview</h2><a id=\"user-content-schema-overview\" class=\"anchor\" aria-label=\"Permalink: Schema Overview\" href=\"#schema-overview\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The package exports six Drizzle schema modules, each targeting a specific domain:</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Export</th>\n<th>Schema</th>\n<th>Tables</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>./schema</code></td>\n<td><code>auth</code></td>\n<td>users, sessions, totp_secrets, backup_codes, invitations, audit_events</td>\n<td>Authentication and authorization</td>\n</tr>\n<tr>\n<td><code>./content-schema</code></td>\n<td><code>public</code></td>\n<td>business_profile, services, business_hours, reviews, practitioners</td>\n<td>CMS content</td>\n</tr>\n<tr>\n<td><code>./booking-schema</code></td>\n<td><code>public</code></td>\n<td>clients, bookings, time_blocks, business_hours_overrides, slot_reservations</td>\n<td>Scheduling and appointments</td>\n</tr>\n<tr>\n<td><code>./giftcert-schema</code></td>\n<td><code>public</code></td>\n<td>gift_certificates, gift_certificate_redemptions</td>\n<td>Gift certificate tracking</td>\n</tr>\n<tr>\n<td><code>./intake-schema</code></td>\n<td><code>public</code></td>\n<td>intake_submissions</td>\n<td>Patient intake forms</td>\n</tr>\n<tr>\n<td><code>./business-schema</code></td>\n<td><code>public</code></td>\n<td>(composite re-export)</td>\n<td>Business domain aggregation</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Auth Schema (<code>auth.*</code>)</h3><a id=\"user-content-auth-schema-auth\" class=\"anchor\" aria-label=\"Permalink: Auth Schema (auth.*)\" href=\"#auth-schema-auth\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>users</strong> -- Admin users with roles (viewer, editor, business_owner, developer), PIN hashes, TOTP state, onboarding tracking</li>\n<li><strong>sessions</strong> -- DB-backed sessions with HMAC-signed UUIDs, metadata (IP, user agent), configurable TTL</li>\n<li><strong>totp_secrets</strong> -- AES-encrypted TOTP secrets, linked to users</li>\n<li><strong>backup_codes</strong> -- Bcrypt-hashed one-time recovery codes</li>\n<li><strong>invitations</strong> -- Email-based user invitations with token + expiry</li>\n<li><strong>audit_events</strong> -- Timestamped auth event log (login, logout, failed attempts, role changes)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Booking Schema (<code>public.*</code>)</h3><a id=\"user-content-booking-schema-public\" class=\"anchor\" aria-label=\"Permalink: Booking Schema (public.*)\" href=\"#booking-schema-public\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>clients</strong> -- Client directory (name, email, phone, notes)</li>\n<li><strong>bookings</strong> -- Appointment records with status (confirmed, cancelled, completed, no_show), payment tracking</li>\n<li><strong>time_blocks</strong> -- Practitioner availability blocks (break, vacation, hold)</li>\n<li><strong>business_hours_overrides</strong> -- Date-specific hour overrides</li>\n<li><strong>slot_reservations</strong> -- Temporary slot holds during booking flow (TTL-based)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Drizzle Migrations</h2><a id=\"user-content-drizzle-migrations\" class=\"anchor\" aria-label=\"Permalink: Drizzle Migrations\" href=\"#drizzle-migrations\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Push schema changes directly (development):</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Auth schema\nDATABASE_URL=&quot;postgresql://...&quot; pnpm db:push\n\n# Public schema (booking, content)\nDATABASE_URL=&quot;postgresql://...&quot; npx drizzle-kit push --config=drizzle.public.config.ts\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Auth schema</span>\nDATABASE_URL=<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>postgresql://...<span class=\"pl-pds\">\"</span></span> pnpm db:push\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Public schema (booking, content)</span>\nDATABASE_URL=<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>postgresql://...<span class=\"pl-pds\">\"</span></span> npx drizzle-kit push --config=drizzle.public.config.ts</pre></div>\n<p dir=\"auto\">Generate migration files (production):</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"DATABASE_URL=&quot;postgresql://...&quot; pnpm db:generate\nDATABASE_URL=&quot;postgresql://...&quot; pnpm db:migrate\"><pre>DATABASE_URL=<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>postgresql://...<span class=\"pl-pds\">\"</span></span> pnpm db:generate\nDATABASE_URL=<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>postgresql://...<span class=\"pl-pds\">\"</span></span> pnpm db:migrate</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">API Reference</h2><a id=\"user-content-api-reference\" class=\"anchor\" aria-label=\"Permalink: API Reference\" href=\"#api-reference\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>createPgStorageAdapter(config: PgStorageConfig): PgStorageAdapter</code></h3><a id=\"user-content-createpgstorageadapterconfig-pgstorageconfig-pgstorageadapter\" class=\"anchor\" aria-label=\"Permalink: createPgStorageAdapter(config: PgStorageConfig): PgStorageAdapter\" href=\"#createpgstorageadapterconfig-pgstorageconfig-pgstorageadapter\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Factory function that returns a Pattern B tenant-scoped adapter.</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"type PgStorageConfig =\n  | { db: Database; sessionMaxAge?: number }       // driver injection (recommended)\n  | { connectionString: string; sessionMaxAge?: number }; // legacy neon-http\n\ntype Database =\n  | NeonHttpDatabase&lt;typeof schema&gt;\n  | NodePgDatabase&lt;typeof schema&gt;\n  | PostgresJsDatabase&lt;typeof schema&gt;;\"><pre><span class=\"pl-k\">type</span> <span class=\"pl-smi\">PgStorageConfig</span> <span class=\"pl-c1\">=</span>\n  <span class=\"pl-c1\">|</span> <span class=\"pl-kos\">{</span> <span class=\"pl-c1\">db</span>: <span class=\"pl-smi\">Database</span><span class=\"pl-kos\">;</span> <span class=\"pl-c1\">sessionMaxAge</span>?: <span class=\"pl-smi\">number</span> <span class=\"pl-kos\">}</span>       <span class=\"pl-c\">// driver injection (recommended)</span>\n  <span class=\"pl-c1\">|</span> <span class=\"pl-kos\">{</span> <span class=\"pl-c1\">connectionString</span>: <span class=\"pl-smi\">string</span><span class=\"pl-kos\">;</span> <span class=\"pl-c1\">sessionMaxAge</span>?: <span class=\"pl-smi\">number</span> <span class=\"pl-kos\">}</span><span class=\"pl-kos\">;</span> <span class=\"pl-c\">// legacy neon-http</span>\n\n<span class=\"pl-k\">type</span> <span class=\"pl-smi\">Database</span> <span class=\"pl-c1\">=</span>\n  <span class=\"pl-c1\">|</span> <span class=\"pl-smi\">NeonHttpDatabase</span><span class=\"pl-c1\">&lt;</span><span class=\"pl-k\">typeof</span> <span class=\"pl-s1\">schema</span><span class=\"pl-c1\">&gt;</span>\n  <span class=\"pl-c1\">|</span> <span class=\"pl-smi\">NodePgDatabase</span><span class=\"pl-c1\">&lt;</span><span class=\"pl-k\">typeof</span> <span class=\"pl-s1\">schema</span><span class=\"pl-c1\">&gt;</span>\n  <span class=\"pl-c1\">|</span> <span class=\"pl-smi\">PostgresJsDatabase</span><span class=\"pl-c1\">&lt;</span><span class=\"pl-k\">typeof</span> <span class=\"pl-s1\">schema</span><span class=\"pl-c1\">&gt;</span><span class=\"pl-kos\">;</span></pre></div>\n<p dir=\"auto\">Both branches validate their input at construction time and throw loudly on\nnullish <code>db</code> or empty <code>connectionString</code> rather than deferring to the first\nquery.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>createNodePgStorageAdapter(config: NodePgStorageConfig): NodePgStorageAdapter</code></h3><a id=\"user-content-createnodepgstorageadapterconfig-nodepgstorageconfig-nodepgstorageadapter\" class=\"anchor\" aria-label=\"Permalink: createNodePgStorageAdapter(config: NodePgStorageConfig): NodePgStorageAdapter\" href=\"#createnodepgstorageadapterconfig-nodepgstorageconfig-nodepgstorageadapter\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Factory function that constructs and owns a <code>pg.Pool</code> for standard PostgreSQL.</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"interface NodePgStorageConfig {\n  connectionString: string;\n  sessionMaxAge?: number;\n  poolConfig?: PoolConfig;\n  closeOnDispose?: boolean; // default true\n}\"><pre><span class=\"pl-k\">interface</span> <span class=\"pl-smi\">NodePgStorageConfig</span> <span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">connectionString</span>: <span class=\"pl-smi\">string</span><span class=\"pl-kos\">;</span>\n  <span class=\"pl-c1\">sessionMaxAge</span>?: <span class=\"pl-smi\">number</span><span class=\"pl-kos\">;</span>\n  <span class=\"pl-c1\">poolConfig</span>?: <span class=\"pl-smi\">PoolConfig</span><span class=\"pl-kos\">;</span>\n  <span class=\"pl-c1\">closeOnDispose</span>?: <span class=\"pl-smi\">boolean</span><span class=\"pl-kos\">;</span> <span class=\"pl-c\">// default true</span>\n<span class=\"pl-kos\">}</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>PgStorageAdapter</code></h3><a id=\"user-content-pgstorageadapter\" class=\"anchor\" aria-label=\"Permalink: PgStorageAdapter\" href=\"#pgstorageadapter\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Every method accepts <code>tenantId: string</code> as its <strong>first parameter</strong> and returns\n<code>TenantScoped&lt;T&gt;</code> where the domain type carries <code>tenantId</code>. Key methods:</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">User Management</h4><a id=\"user-content-user-management\" class=\"anchor\" aria-label=\"Permalink: User Management\" href=\"#user-management\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><code>getUser(tenantId, id): Promise&lt;TenantScoped&lt;AdminUser&gt; | null&gt;</code></li>\n<li><code>getUserByHandle(tenantId, handle): Promise&lt;TenantScoped&lt;AdminUser&gt; | null&gt;</code></li>\n<li><code>getUserByEmail(tenantId, email): Promise&lt;TenantScoped&lt;AdminUser&gt; | null&gt;</code></li>\n<li><code>createUser(tenantId, user): Promise&lt;TenantScoped&lt;AdminUser&gt;&gt;</code></li>\n<li><code>updateUser(tenantId, id, updates): Promise&lt;TenantScoped&lt;AdminUser&gt;&gt;</code></li>\n<li><code>deleteUser(tenantId, id): Promise&lt;void&gt;</code></li>\n<li><code>getAllUsers(tenantId): Promise&lt;TenantScoped&lt;AdminUser&gt;[]&gt;</code></li>\n<li><code>hasUsers(tenantId): Promise&lt;boolean&gt;</code></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">Session Management</h4><a id=\"user-content-session-management\" class=\"anchor\" aria-label=\"Permalink: Session Management\" href=\"#session-management\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><code>createSession(tenantId, userId, metadata?): Promise&lt;TenantScoped&lt;Session&gt;&gt;</code></li>\n<li><code>getSession(tenantId, sessionId): Promise&lt;TenantScoped&lt;Session&gt; | null&gt;</code></li>\n<li><code>updateSession(tenantId, sessionId, updates): Promise&lt;TenantScoped&lt;Session&gt;&gt;</code></li>\n<li><code>deleteSession(tenantId, sessionId): Promise&lt;void&gt;</code></li>\n<li><code>deleteUserSessions(tenantId, userId): Promise&lt;void&gt;</code></li>\n<li><code>getSessionsByUser(tenantId, userId): Promise&lt;TenantScoped&lt;Session&gt;[]&gt;</code></li>\n<li><code>getAllSessions(tenantId): Promise&lt;TenantScoped&lt;Session&gt;[]&gt;</code></li>\n<li><code>cleanupExpiredSessions(tenantId): Promise&lt;number&gt;</code></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">TOTP / Backup Codes</h4><a id=\"user-content-totp--backup-codes\" class=\"anchor\" aria-label=\"Permalink: TOTP / Backup Codes\" href=\"#totp--backup-codes\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><code>saveTOTPSecret(tenantId, handle, secret): Promise&lt;void&gt;</code></li>\n<li><code>getTOTPSecret(tenantId, handle): Promise&lt;EncryptedTOTPSecret | null&gt;</code></li>\n<li><code>deleteTOTPSecret(tenantId, handle): Promise&lt;void&gt;</code></li>\n<li><code>saveBackupCodes(tenantId, userId, codes): Promise&lt;void&gt;</code></li>\n<li><code>getBackupCodes(tenantId, userId): Promise&lt;BackupCodeSet | null&gt;</code></li>\n<li><code>deleteBackupCodes(tenantId, userId): Promise&lt;void&gt;</code></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">Invitations</h4><a id=\"user-content-invitations\" class=\"anchor\" aria-label=\"Permalink: Invitations\" href=\"#invitations\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><code>createInvitation(tenantId, invitation): Promise&lt;TenantScoped&lt;Invitation&gt;&gt;</code></li>\n<li><code>getInvitation(tenantId, token): Promise&lt;TenantScoped&lt;Invitation&gt; | null&gt;</code></li>\n<li><code>getInvitationById(tenantId, id): Promise&lt;TenantScoped&lt;Invitation&gt; | null&gt;</code></li>\n<li><code>getAllInvitations(tenantId): Promise&lt;TenantScoped&lt;Invitation&gt;[]&gt;</code></li>\n<li><code>getPendingInvitations(tenantId): Promise&lt;TenantScoped&lt;Invitation&gt;[]&gt;</code></li>\n<li><code>updateInvitation(tenantId, token, updates): Promise&lt;TenantScoped&lt;Invitation&gt;&gt;</code></li>\n<li><code>deleteInvitation(tenantId, token): Promise&lt;void&gt;</code></li>\n<li><code>cleanupExpiredInvitations(tenantId): Promise&lt;number&gt;</code></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">Audit Log</h4><a id=\"user-content-audit-log\" class=\"anchor\" aria-label=\"Permalink: Audit Log\" href=\"#audit-log\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><code>logAuditEvent(tenantId, event): Promise&lt;void&gt;</code></li>\n<li><code>getAuditEvents(tenantId, filters?): Promise&lt;AuditEvent[]&gt;</code></li>\n</ul>\n<blockquote>\n<p dir=\"auto\"><strong>Interface note:</strong> the class does not <code>implements IStorageAdapter</code> from\n<code>@tummycrypt/tinyland-auth@0.2.x</code> because the peer package predates Pattern B.\nAn interface uplift will ship with tinyland-auth 0.3.0 and this adapter will\nre-implement it then. Until then, consume the concrete class or type against\nthe exported method signatures directly.</p>\n</blockquote>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>NodePgStorageAdapter</code></h3><a id=\"user-content-nodepgstorageadapter\" class=\"anchor\" aria-label=\"Permalink: NodePgStorageAdapter\" href=\"#nodepgstorageadapter\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Subclass of <code>PgStorageAdapter</code> that exposes its owned <code>pool: Pool</code> and closes\nthat pool by default when <code>adapter.close()</code> is called.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Environment Variables</h2><a id=\"user-content-environment-variables\" class=\"anchor\" aria-label=\"Permalink: Environment Variables\" href=\"#environment-variables\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Variable</th>\n<th>Required</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>DATABASE_URL</code></td>\n<td>Yes</td>\n<td>PostgreSQL connection string for Neon, CNPG, local PG, or other supported deployments</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Development</h2><a id=\"user-content-development\" class=\"anchor\" aria-label=\"Permalink: Development\" href=\"#development\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm install\npnpm test          # Run tests\npnpm build         # Compile TypeScript\npnpm test:watch    # Watch mode\"><pre>pnpm install\npnpm <span class=\"pl-c1\">test</span>          <span class=\"pl-c\"><span class=\"pl-c\">#</span> Run tests</span>\npnpm build         <span class=\"pl-c\"><span class=\"pl-c\">#</span> Compile TypeScript</span>\npnpm test:watch    <span class=\"pl-c\"><span class=\"pl-c\">#</span> Watch mode</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Nix</h3><a id=\"user-content-nix\" class=\"anchor\" aria-label=\"Permalink: Nix\" href=\"#nix\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix develop        # Enter dev shell with Node 20 + pnpm + tsc\"><pre>nix develop        <span class=\"pl-c\"><span class=\"pl-c\">#</span> Enter dev shell with Node 20 + pnpm + tsc</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">MIT</p>\n</article></div>",
      "readme_excerpt": "PostgreSQL storage adapter for @tummycrypt/tinyland-auth, backed by Drizzle ORM with driver-agnostic construction and multi-tenant scoping.\nSupports Neon HTTP, postgres.js, and node-postgres. Use createNodePgStorageAdapter()\nwhen you want the package to own a pg.Pool, or createPgStorageAdapter({ db })\nwhen you already have a pre-built Drizzle client.\n> 0.2.0 is a breaking release. Every adapter method now takes tenantId: string\n> as its first parameter. Every row-bearing table has tenantid uuid...",
      "install_commands": {
        "npm": "npm install @tummycrypt/tinyland-auth-pg"
      },
      "repo_url": "https://github.com/jesssullivan/tinyland-auth-pg",
      "website_url": null,
      "docs_url": null,
      "registry_url": "https://www.npmjs.com/package/@tummycrypt/tinyland-auth-pg",
      "releases_url": "https://github.com/jesssullivan/tinyland-auth-pg/releases",
      "og_image_url": "https://opengraph.githubassets.com/59053bec1ddd122bcdb4fb66c1f3d53863d6262a481008df92d5a8356d03f83f/tinyland-inc/tinyland-auth-pg",
      "license": "",
      "pushed_at": "2026-04-22T22:14:39Z",
      "enriched_at": "2026-04-26T17:17:15.372Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-pixelwise-research",
      "name": "pixelwise-research",
      "repo": "jesssullivan/pixelwise-research",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "ai-tools",
      "description": "WIP, danger be lurking!  Novel glyph compositor research with Futhark webGPU investigating vectorized gradient direction monads",
      "featured": false,
      "tags": [
        "boundary-detection",
        "wasm",
        "research"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 1,
      "topics": [
        "boundary-detection",
        "emscripten",
        "esdt",
        "futhark",
        "just-for-fun",
        "skeleton-ui",
        "sobel-gradient",
        "sveltekit",
        "wasm",
        "dangerous-permissions"
      ],
      "languages": [
        {
          "name": "TypeScript",
          "color": "#3178c6",
          "bytes": 1086545
        },
        {
          "name": "JavaScript",
          "color": "#f1e05a",
          "bytes": 176024
        },
        {
          "name": "Svelte",
          "color": "#ff3e00",
          "bytes": 160937
        },
        {
          "name": "HTML",
          "color": "#e34c26",
          "bytes": 38865
        },
        {
          "name": "Starlark",
          "color": "#76d275",
          "bytes": 37995
        }
      ],
      "primary_language": "TypeScript",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">Pixelwise</h1><a id=\"user-content-pixelwise\" class=\"anchor\" aria-label=\"Permalink: Pixelwise\" href=\"#pixelwise\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">ESDT-based WCAG contrast computation research implementation in Futhark targeting WebGPU.</p>\n<p dir=\"auto\"><strong><a href=\"tex_research/pixelwise/dist/pixelwise.pdf\">Research Paper (PDF)</a></strong> -- Mathematical foundations with verification status.</p>\n<p dir=\"auto\">Pixelwise originally used precomputed WGSL shaders for GPU contrast computation with\nFuthark WASM multicore as the reference implementation. I am now working toward\na unified Futhark WebGPU backend that generates both GPU (WebGPU/WGSL) and CPU\n(WASM multicore) code from a single source.</p>\n<p dir=\"auto\"><strong>Foundation</strong>: <a href=\"https://futhark-lang.org/student-projects/sebastian-msc-thesis.pdf\" rel=\"nofollow\">Sebastian Paarmann's MSc Thesis (2024)</a>\nintroduced the Futhark WebGPU backend as part of his research at DIKU.</p>\n<p dir=\"auto\"><strong>Current Status</strong>: Experimental fork at <a href=\"https://github.com/jesssullivan/futhark\">jesssullivan/futhark</a>\n(branch <code>development-webgpu</code>) with Emscripten 4.x compatibility patches.</p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Why Offset Vectors Matter</h2><a id=\"user-content-why-offset-vectors-matter\" class=\"anchor\" aria-label=\"Permalink: Why Offset Vectors Matter\" href=\"#why-offset-vectors-matter\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">The Problem with Scalar Distance Transforms</h3><a id=\"user-content-the-problem-with-scalar-distance-transforms\" class=\"anchor\" aria-label=\"Permalink: The Problem with Scalar Distance Transforms\" href=\"#the-problem-with-scalar-distance-transforms\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Classical distance transforms store <code>d^2</code> (squared distance to nearest edge) for each pixel.\nThis is efficient but loses information: you know <em>how far</em> but not <em>which direction</em>.</p>\n<p dir=\"auto\">For WCAG contrast enhancement, we need to sample background colors <em>outward</em> from text.\nWith only <code>d^2</code>, you need a separate gradient computation pass (Sobel filter, finite differences).</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">The ESDT Solution: Track Offset Vectors</h3><a id=\"user-content-the-esdt-solution-track-offset-vectors\" class=\"anchor\" aria-label=\"Permalink: The ESDT Solution: Track Offset Vectors\" href=\"#the-esdt-solution-track-offset-vectors\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Instead of storing <code>d^2 = dx^2 + dy^2</code>, ESDT stores the offset vector <code>(dx, dy)</code> directly.</p>\n<p dir=\"auto\"><strong>What you get for free:</strong></p>\n<ul dir=\"auto\">\n<li><strong>Distance</strong>: <code>d = sqrt(dx^2 + dy^2)</code> -- same as before</li>\n<li><strong>Gradient direction</strong>: <code>(dx, dy) / d</code> -- the direction to the nearest edge</li>\n<li><strong>Background sampling</strong>: Follow the gradient outward to find background pixels</li>\n</ul>\n<p dir=\"auto\">This eliminates one pipeline pass and provides mathematically correct gradients.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Anti-Aliased Text: The Gray Pixel Trap</h3><a id=\"user-content-anti-aliased-text-the-gray-pixel-trap\" class=\"anchor\" aria-label=\"Permalink: Anti-Aliased Text: The Gray Pixel Trap\" href=\"#anti-aliased-text-the-gray-pixel-trap\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Anti-aliased fonts produce \"gray pixels\" at edges where opacity <code>L in (0, 1)</code> encodes\nsub-pixel edge position. A common mistake is to add the gray offset as:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"d^2 = x^2 + y^2 + (L - 0.5)^2    // WRONG: This is 3D distance!\"><pre class=\"notranslate\"><code>d^2 = x^2 + y^2 + (L - 0.5)^2    // WRONG: This is 3D distance!\n</code></pre></div>\n<p dir=\"auto\">This treats opacity as a third spatial dimension. Instead, ESDT applies the offset\n<em>along</em> the 2D gradient direction during initialization:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"offset = L - 0.5\n(dx, dy) = (offset * gx, offset * gy)  // where (gx, gy) is normalized gradient\"><pre class=\"notranslate\"><code>offset = L - 0.5\n(dx, dy) = (offset * gx, offset * gy)  // where (gx, gy) is normalized gradient\n</code></pre></div>\n<p dir=\"auto\">This maintains correct 2D geometry.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Visual Intuition</h3><a id=\"user-content-visual-intuition\" class=\"anchor\" aria-label=\"Permalink: Visual Intuition\" href=\"#visual-intuition\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"Traditional EDT (scalar d^2):          ESDT (offset vectors):\n+---------------------+               +---------------------------------+\n| 9  4  1  0  1  4  9 |               | (-3,0) (-2,0) (-1,0) (0,0) ... |\n| 4  1  0  0  0  1  4 |  Only         | (-2,0) (-1,0)  (0,0) (0,0) ... |  Distance\n| 1  0  0  0  0  0  1 |  distances    | (-1,0)  (0,0)  (0,0) (0,0) ... |  AND direction\n+---------------------+               +---------------------------------+\n     v Need Sobel pass                      v Gradient = normalize(dx,dy)\n     for gradient                           (no extra pass needed)\"><pre class=\"notranslate\"><code>Traditional EDT (scalar d^2):          ESDT (offset vectors):\n+---------------------+               +---------------------------------+\n| 9  4  1  0  1  4  9 |               | (-3,0) (-2,0) (-1,0) (0,0) ... |\n| 4  1  0  0  0  1  4 |  Only         | (-2,0) (-1,0)  (0,0) (0,0) ... |  Distance\n| 1  0  0  0  0  0  1 |  distances    | (-1,0)  (0,0)  (0,0) (0,0) ... |  AND direction\n+---------------------+               +---------------------------------+\n     v Need Sobel pass                      v Gradient = normalize(dx,dy)\n     for gradient                           (no extra pass needed)\n</code></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Comparison</h3><a id=\"user-content-comparison\" class=\"anchor\" aria-label=\"Permalink: Comparison\" href=\"#comparison\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Aspect</th>\n<th>Scalar d^2</th>\n<th>Offset Vectors (dx, dy)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Storage</td>\n<td>1 float</td>\n<td>2 floats</td>\n</tr>\n<tr>\n<td>Distance</td>\n<td><code>sqrt(d^2)</code></td>\n<td><code>sqrt(dx^2 + dy^2)</code></td>\n</tr>\n<tr>\n<td>Gradient</td>\n<td>Requires Sobel/FD pass</td>\n<td><code>(dx, dy) / d</code> (free)</td>\n</tr>\n<tr>\n<td>Gray pixels</td>\n<td>Often incorrect (3D)</td>\n<td>Correct 2D displacement</td>\n</tr>\n<tr>\n<td>Pipeline passes</td>\n<td>7+</td>\n<td>6</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Core Algorithm</h2><a id=\"user-content-core-algorithm\" class=\"anchor\" aria-label=\"Permalink: Core Algorithm\" href=\"#core-algorithm\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Exact Signed Distance Transform (ESDT)</h3><a id=\"user-content-exact-signed-distance-transform-esdt\" class=\"anchor\" aria-label=\"Permalink: Exact Signed Distance Transform (ESDT)\" href=\"#exact-signed-distance-transform-esdt\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">ESDT computes offset vectors <code>(dx, dy)</code> to the nearest edge for each pixel.</p>\n<p dir=\"auto\"><strong>Distance:</strong></p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"d = sqrt(dx^2 + dy^2)\"><pre class=\"notranslate\"><code>d = sqrt(dx^2 + dy^2)\n</code></pre></div>\n<p dir=\"auto\"><strong>Gradient (direction to nearest edge):</strong></p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"grad(d) = (dx, dy) / d    when d &gt; epsilon\"><pre class=\"notranslate\"><code>grad(d) = (dx, dy) / d    when d &gt; epsilon\n</code></pre></div>\n<p dir=\"auto\"><strong>Gray pixel initialization</strong> (Definition 2.3 in paper):</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"offset = L - 0.5\"><pre class=\"notranslate\"><code>offset = L - 0.5\n</code></pre></div>\n<p dir=\"auto\">Where <code>L in (0, 1)</code> is pixel opacity. The offset is applied in the Sobel gradient direction.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">WCAG 2.1 Formulas</h3><a id=\"user-content-wcag-21-formulas\" class=\"anchor\" aria-label=\"Permalink: WCAG 2.1 Formulas\" href=\"#wcag-21-formulas\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><strong>sRGB Linearization:</strong></p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"C_lin = C / 12.92                        if C &lt;= 0.03928\nC_lin = ((C + 0.055) / 1.055)^2.4        otherwise\"><pre class=\"notranslate\"><code>C_lin = C / 12.92                        if C &lt;= 0.03928\nC_lin = ((C + 0.055) / 1.055)^2.4        otherwise\n</code></pre></div>\n<p dir=\"auto\"><strong>Relative Luminance:</strong></p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"L = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin\"><pre class=\"notranslate\"><code>L = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin\n</code></pre></div>\n<p dir=\"auto\"><strong>Contrast Ratio:</strong></p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"CR = (L_lighter + 0.05) / (L_darker + 0.05)\"><pre class=\"notranslate\"><code>CR = (L_lighter + 0.05) / (L_darker + 0.05)\n</code></pre></div>\n<p dir=\"auto\">Bounds: <code>CR in [1, 21]</code>. Black/white yields CR ~ 21.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Edge Weight</h3><a id=\"user-content-edge-weight\" class=\"anchor\" aria-label=\"Permalink: Edge Weight\" href=\"#edge-weight\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"w = 4 * alpha * (1 - alpha)\"><pre class=\"notranslate\"><code>w = 4 * alpha * (1 - alpha)\n</code></pre></div>\n<p dir=\"auto\">Where <code>alpha = clamp(1 - d/d_max, 0, 1)</code>. Peaks at <code>alpha = 0.5</code> (glyph boundaries).</p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Architecture</h2><a id=\"user-content-architecture\" class=\"anchor\" aria-label=\"Permalink: Architecture\" href=\"#architecture\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Backend Priority</h3><a id=\"user-content-backend-priority\" class=\"anchor\" aria-label=\"Permalink: Backend Priority\" href=\"#backend-priority\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"1. Futhark WebGPU (GPU)   -&gt; Fastest, requires GPU adapter + WebGPU browser\n2. Futhark WASM (CPU)     -&gt; Multicore, requires COOP/COEP headers\n3. JavaScript Fallback    -&gt; Single-threaded, always works\"><pre class=\"notranslate\"><code>1. Futhark WebGPU (GPU)   -&gt; Fastest, requires GPU adapter + WebGPU browser\n2. Futhark WASM (CPU)     -&gt; Multicore, requires COOP/COEP headers\n3. JavaScript Fallback    -&gt; Single-threaded, always works\n</code></pre></div>\n<p dir=\"auto\">Both GPU and CPU backends are generated from a single Futhark source\n(<code>futhark/pipeline.fut</code>), ensuring algorithmic equivalence.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">6-Pass Pipeline</h3><a id=\"user-content-6-pass-pipeline\" class=\"anchor\" aria-label=\"Permalink: 6-Pass Pipeline\" href=\"#6-pass-pipeline\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"Pass 1: Grayscale -&gt; Sobel gradient computation\nPass 2: ESDT X-pass (horizontal propagation, O(w) per row)\nPass 3: ESDT Y-pass (vertical propagation, O(h) per column)\nPass 4: Glyph extraction (distance &lt; threshold)\nPass 5: Background sampling (outward along grad(d))\nPass 6: WCAG contrast check + luminance adjustment\"><pre class=\"notranslate\"><code>Pass 1: Grayscale -&gt; Sobel gradient computation\nPass 2: ESDT X-pass (horizontal propagation, O(w) per row)\nPass 3: ESDT Y-pass (vertical propagation, O(h) per column)\nPass 4: Glyph extraction (distance &lt; threshold)\nPass 5: Background sampling (outward along grad(d))\nPass 6: WCAG contrast check + luminance adjustment\n</code></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">WebGPU Shader Pipeline</h3><a id=\"user-content-webgpu-shader-pipeline\" class=\"anchor\" aria-label=\"Permalink: WebGPU Shader Pipeline\" href=\"#webgpu-shader-pipeline\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">When WebGPU is available, a 6-pass GPU compute pipeline is used:</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Pass</th>\n<th>Shader</th>\n<th>Workgroup</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>CPU</td>\n<td>-</td>\n<td>sRGB -&gt; Linear, grayscale, Sobel</td>\n</tr>\n<tr>\n<td>1</td>\n<td><code>esdt-x-pass.wgsl</code></td>\n<td>256</td>\n<td>Horizontal distance propagation</td>\n</tr>\n<tr>\n<td>2</td>\n<td><code>esdt-y-pass.wgsl</code></td>\n<td>256</td>\n<td>Vertical distance propagation</td>\n</tr>\n<tr>\n<td>3</td>\n<td><code>esdt-extract-pixels.wgsl</code></td>\n<td>8x8</td>\n<td>Glyph pixel extraction</td>\n</tr>\n<tr>\n<td>4</td>\n<td><code>esdt-background-sample.wgsl</code></td>\n<td>256</td>\n<td>Background color sampling</td>\n</tr>\n<tr>\n<td>5</td>\n<td><code>esdt-contrast-analysis.wgsl</code></td>\n<td>256</td>\n<td>WCAG ratio computation</td>\n</tr>\n<tr>\n<td>6</td>\n<td><code>esdt-color-adjust.wgsl</code></td>\n<td>256</td>\n<td>Hue-preserving adjustment</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick Start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick Start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix develop          # Enter environment (includes Futhark, Emscripten, Node 22)\npnpm install         # Install dependencies\njust dev             # Start server at localhost:5175 (with COOP/COEP headers)\"><pre>nix develop          <span class=\"pl-c\"><span class=\"pl-c\">#</span> Enter environment (includes Futhark, Emscripten, Node 22)</span>\npnpm install         <span class=\"pl-c\"><span class=\"pl-c\">#</span> Install dependencies</span>\njust dev             <span class=\"pl-c\"><span class=\"pl-c\">#</span> Start server at localhost:5175 (with COOP/COEP headers)</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Commands</h2><a id=\"user-content-commands\" class=\"anchor\" aria-label=\"Permalink: Commands\" href=\"#commands\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Development</h3><a id=\"user-content-development\" class=\"anchor\" aria-label=\"Permalink: Development\" href=\"#development\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Command</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>just dev</code></td>\n<td>Start dev server (port 5175, rebuilds research PDF on start)</td>\n</tr>\n<tr>\n<td><code>just dev-bazel</code></td>\n<td>Start dev server via Bazel (full reproducible builds)</td>\n</tr>\n<tr>\n<td><code>just dev-container</code></td>\n<td>Start dev server in container with HMR</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Testing</h3><a id=\"user-content-testing\" class=\"anchor\" aria-label=\"Permalink: Testing\" href=\"#testing\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Command</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>just test-quick</code></td>\n<td>Run vitest directly (fast iteration)</td>\n</tr>\n<tr>\n<td><code>just test</code></td>\n<td>Run all tests via Bazel</td>\n</tr>\n<tr>\n<td><code>just test-unit</code></td>\n<td>Unit tests only (Bazel)</td>\n</tr>\n<tr>\n<td><code>just test-pbt</code></td>\n<td>Property-based tests (Bazel)</td>\n</tr>\n<tr>\n<td><code>just test-futhark</code></td>\n<td>Futhark algorithm tests (Bazel)</td>\n</tr>\n<tr>\n<td><code>just test-wgsl-quick</code></td>\n<td>WGSL shader tests (pnpm, fast)</td>\n</tr>\n<tr>\n<td><code>just test-e2e</code></td>\n<td>End-to-end Playwright tests</td>\n</tr>\n<tr>\n<td><code>just check</code></td>\n<td>TypeScript + Svelte type check</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Build</h3><a id=\"user-content-build\" class=\"anchor\" aria-label=\"Permalink: Build\" href=\"#build\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Command</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>just build</code></td>\n<td>Build all targets via Bazel</td>\n</tr>\n<tr>\n<td><code>just build-prod</code></td>\n<td>Production build (release config)</td>\n</tr>\n<tr>\n<td><code>just build-futhark</code></td>\n<td>Build Futhark WASM modules (Bazel)</td>\n</tr>\n<tr>\n<td><code>just futhark-rebuild</code></td>\n<td>Rebuild Futhark WASM directly (bypasses Bazel, fast)</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Futhark</h3><a id=\"user-content-futhark\" class=\"anchor\" aria-label=\"Permalink: Futhark\" href=\"#futhark\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Command</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>just futhark-check</code></td>\n<td>Type check all Futhark sources</td>\n</tr>\n<tr>\n<td><code>just futhark-test-all</code></td>\n<td>Run Futhark built-in tests (C backend)</td>\n</tr>\n<tr>\n<td><code>just futhark-bench</code></td>\n<td>Benchmark ESDT (C backend)</td>\n</tr>\n<tr>\n<td><code>just futhark-esdt</code></td>\n<td>Compile ESDT to WASM multicore</td>\n</tr>\n<tr>\n<td><code>just futhark-pipeline</code></td>\n<td>Compile pipeline to WASM multicore</td>\n</tr>\n<tr>\n<td><code>just futhark-watch</code></td>\n<td>Watch and rebuild on changes</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Futhark WebGPU</h3><a id=\"user-content-futhark-webgpu\" class=\"anchor\" aria-label=\"Permalink: Futhark WebGPU\" href=\"#futhark-webgpu\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Command</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>just futhark-webgpu-check</code></td>\n<td>Check if WebGPU compiler is available</td>\n</tr>\n<tr>\n<td><code>just futhark-webgpu-build</code></td>\n<td>Build Futhark from source with WebGPU backend</td>\n</tr>\n<tr>\n<td><code>just futhark-webgpu-compile</code></td>\n<td>Compile pipeline to WebGPU and install</td>\n</tr>\n<tr>\n<td><code>just test-futhark-webgpu</code></td>\n<td>Run WebGPU equivalence tests</td>\n</tr>\n<tr>\n<td><code>just bench-webgpu</code></td>\n<td>Benchmark WebGPU vs WASM backends</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Research Paper</h3><a id=\"user-content-research-paper\" class=\"anchor\" aria-label=\"Permalink: Research Paper\" href=\"#research-paper\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Command</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>just tex</code></td>\n<td>Compile research paper PDF (latexmk)</td>\n</tr>\n<tr>\n<td><code>just docs-watch</code></td>\n<td>Watch and rebuild paper on changes</td>\n</tr>\n<tr>\n<td><code>just docs-view</code></td>\n<td>Open the compiled PDF</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Container &amp; Cache</h3><a id=\"user-content-container--cache\" class=\"anchor\" aria-label=\"Permalink: Container &amp; Cache\" href=\"#container--cache\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Command</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>just container-build</code></td>\n<td>Build all container tarballs (Bazel + nix2container)</td>\n</tr>\n<tr>\n<td><code>just container-push</code></td>\n<td>Push production container to registry</td>\n</tr>\n<tr>\n<td><code>just cache-push</code></td>\n<td>Push Nix build outputs to Attic cache</td>\n</tr>\n<tr>\n<td><code>just info</code></td>\n<td>Show build tool versions</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Demos</h2><a id=\"user-content-demos\" class=\"anchor\" aria-label=\"Permalink: Demos\" href=\"#demos\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Route</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>/demo/compositor</code></td>\n<td>Full 6-pass ESDT pipeline with Screen Capture API</td>\n</tr>\n<tr>\n<td><code>/demo/gradient-direction</code></td>\n<td>ESDT offset vector visualization</td>\n</tr>\n<tr>\n<td><code>/demo/contrast-analysis</code></td>\n<td>WCAG 2.1 contrast ratio computation</td>\n</tr>\n<tr>\n<td><code>/demo/performance</code></td>\n<td>Real-time pipeline benchmarks</td>\n</tr>\n<tr>\n<td><code>/demo/before-after</code></td>\n<td>Side-by-side contrast enhancement comparison</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Key Files</h2><a id=\"user-content-key-files\" class=\"anchor\" aria-label=\"Permalink: Key Files\" href=\"#key-files\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Path</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>futhark/esdt.fut</code></td>\n<td>ESDT algorithm (Def 2.1, 2.3, Thm 2.4)</td>\n</tr>\n<tr>\n<td><code>futhark/wcag.fut</code></td>\n<td>WCAG formulas (Sec 3.1)</td>\n</tr>\n<tr>\n<td><code>futhark/pipeline.fut</code></td>\n<td>6-pass pipeline composition</td>\n</tr>\n<tr>\n<td><code>futhark/Makefile</code></td>\n<td>WASM build targets</td>\n</tr>\n<tr>\n<td><code>src/lib/core/ComputeDispatcher.ts</code></td>\n<td>Backend selection + WebGPU pipeline</td>\n</tr>\n<tr>\n<td><code>src/lib/futhark/</code></td>\n<td>WASM module exports</td>\n</tr>\n<tr>\n<td><code>src/lib/futhark-webgpu/</code></td>\n<td>Futhark-generated WebGPU pipeline</td>\n</tr>\n<tr>\n<td><code>src/lib/pixelwise/shaders/</code></td>\n<td>WGSL compute shaders (6 passes)</td>\n</tr>\n<tr>\n<td><code>tests/theorem-verification/</code></td>\n<td>Property-based tests for formulas</td>\n</tr>\n<tr>\n<td><code>vite.config.ts</code></td>\n<td>Dev server config, COOP/COEP headers</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Verification</h2><a id=\"user-content-verification\" class=\"anchor\" aria-label=\"Permalink: Verification\" href=\"#verification\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Tests in <code>tests/theorem-verification/</code> verify:</p>\n<ul dir=\"auto\">\n<li>Linearization threshold <code>0.03928</code> (not <code>0.04045</code>)</li>\n<li>Gamma exponent <code>2.4</code> (not <code>2.5</code>)</li>\n<li>CR bounds <code>[1, 21]</code></li>\n<li>Edge weight peak at <code>alpha = 0.5</code></li>\n<li>Offset vector distance/gradient derivation</li>\n</ul>\n<p dir=\"auto\">Run: <code>pnpm test tests/theorem-verification/</code></p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">COOP/COEP Headers</h2><a id=\"user-content-coopcoep-headers\" class=\"anchor\" aria-label=\"Permalink: COOP/COEP Headers\" href=\"#coopcoep-headers\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Futhark's WASM multicore backend uses <code>SharedArrayBuffer</code> for parallel execution.\nBrowsers require Cross-Origin Isolation headers:</p>\n<div class=\"highlight highlight-source-httpspec notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"Cross-Origin-Opener-Policy: same-origin\nCross-Origin-Embedder-Policy: credentialless\"><pre><span class=\"pl-s\"><span class=\"pl-v\">Cross-Origin-Opener-Policy:</span> same-origin</span>\n<span class=\"pl-s\"><span class=\"pl-v\">Cross-Origin-Embedder-Policy:</span> credentialless</span></pre></div>\n<p dir=\"auto\">These are configured in <code>vite.config.ts</code> (dev), <code>src/hooks.server.ts</code> (production),\nand nginx ingress annotations (Kubernetes).</p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Paper</h2><a id=\"user-content-paper\" class=\"anchor\" aria-label=\"Permalink: Paper\" href=\"#paper\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Mathematical foundations with verification status in tex source; not finalized.</p>\n<p dir=\"auto\">Originally developed with Rust SIMD as a project to learn Rust SIMD; this project\nreceived autonomous assistance with PBT constraining, fuzzing, verification and\nfunction composition as well as some GPU integration work performed within <strong>Tinyland</strong>\nwith the xoxd.ai stack.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">zlib</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Author</h2><a id=\"user-content-author\" class=\"anchor\" aria-label=\"Permalink: Author\" href=\"#author\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Jess Sullivan <a href=\"mailto:jess@sulliwood.org\">jess@sulliwood.org</a></p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Citations</h2><a id=\"user-content-citations\" class=\"anchor\" aria-label=\"Permalink: Citations\" href=\"#citations\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-text-bibtex notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"@software{pixelwise2026,\n  author = {Sullivan, Jess},\n  title = {Pixelwise: ESDT-Based WCAG Contrast Enhancement},\n  year = {2026},\n  url = {https://github.com/Jesssullivan/pixelwise-research}\n}\"><pre><span class=\"pl-k\">@software</span>{<span class=\"pl-en\">pixelwise2026</span>,\n  <span class=\"pl-s\">author</span> = <span class=\"pl-s\"><span class=\"pl-pds\">{</span>Sullivan, Jess<span class=\"pl-pds\">}</span></span>,\n  <span class=\"pl-s\">title</span> = <span class=\"pl-s\"><span class=\"pl-pds\">{</span>Pixelwise: ESDT-Based WCAG Contrast Enhancement<span class=\"pl-pds\">}</span></span>,\n  <span class=\"pl-s\">year</span> = <span class=\"pl-s\"><span class=\"pl-pds\">{</span>2026<span class=\"pl-pds\">}</span></span>,\n  <span class=\"pl-s\">url</span> = <span class=\"pl-s\"><span class=\"pl-pds\">{</span>https://github.com/Jesssullivan/pixelwise-research<span class=\"pl-pds\">}</span></span>\n}</pre></div>\n<p dir=\"auto\"><strong>References</strong>:</p>\n<ul dir=\"auto\">\n<li>Danielsson, P.E. (1980). Euclidean Distance Mapping. CGIP 14(3):227-248.</li>\n<li>Meijster, A. et al. (2000). A General Algorithm for Computing Distance Transforms in Linear Time.</li>\n<li>Wittens, S. (2023). Subpixel Distance Transform. <a href=\"https://acko.net/blog/subpixel-distance-transform/\" rel=\"nofollow\">https://acko.net/blog/subpixel-distance-transform/</a></li>\n<li>Henriksen, T. et al. (2017). Futhark: Purely Functional GPU-Programming. PLDI '17.</li>\n<li>Paarmann, S. (2024). A WebGPU Backend for Futhark. MSc Thesis, DIKU.</li>\n</ul>\n</article></div>",
      "readme_excerpt": "ESDT-based WCAG contrast computation research implementation in Futhark targeting WebGPU.\nResearch Paper (PDF) -- Mathematical foundations with verification status.\nPixelwise originally used precomputed WGSL shaders for GPU contrast computation with\nFuthark WASM multicore as the reference implementation. I am now working toward\na unified Futhark WebGPU backend that generates both GPU (WebGPU/WGSL) and CPU\n(WASM multicore) code from a single source.\nFoundation: Sebastian Paarmann's MSc Thesis...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/pixelwise-research",
      "website_url": "https://pixelwise.ephemera.xoxd.ai/",
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/pixelwise-research/releases",
      "og_image_url": "https://opengraph.githubassets.com/87f480a06628bd3ca3355f5b7ff11baa8d52a97377fa0c73df9f4d4984795811/Jesssullivan/pixelwise-research",
      "license": "NOASSERTION",
      "pushed_at": "2026-04-22T18:43:34Z",
      "enriched_at": "2026-04-26T17:17:15.682Z",
      "gate": "homepage"
    },
    {
      "slug": "jesssullivan-hiberpower-ntfs",
      "name": "hiberpower-ntfs",
      "repo": "jesssullivan/hiberpower-ntfs",
      "org": "jesssullivan",
      "ecosystem": "zig",
      "category": "systems",
      "description": "ASM2362 NVMe recovery experiments and research around FTL corruption",
      "featured": false,
      "tags": [
        "ntfs",
        "firmware",
        "usb",
        "nvme"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 5,
      "topics": [
        "opcode-analysis",
        "opcode-manipulation",
        "frida-capture",
        "queue-attack",
        "xram-injection"
      ],
      "languages": [
        {
          "name": "Zig",
          "color": "#ec915c",
          "bytes": 200008
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 71467
        },
        {
          "name": "JavaScript",
          "color": "#f1e05a",
          "bytes": 59560
        },
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 25856
        },
        {
          "name": "PowerShell",
          "color": "#012456",
          "bytes": 16142
        }
      ],
      "primary_language": "Zig",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">ASM2362 NVMe Research Toys</h1><a id=\"user-content-asm2362-nvme-research-toys\" class=\"anchor\" aria-label=\"Permalink: ASM2362 NVMe Research Toys\" href=\"#asm2362-nvme-research-toys\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Experimental research into recovering NVMe SSDs exhibiting firmware-level write protection after FTL corruption. <strong>This is a project</strong></p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">BLog posts:</h3><a id=\"user-content-blog-posts\" class=\"anchor\" aria-label=\"Permalink: BLog posts:\" href=\"#blog-posts\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><a href=\"https://transscendsurvival.org/blog/xram-injection-bypassing-usb-bridge-whitelists-to-recover-nvme-drives\" rel=\"nofollow\">xram injection technical blog post</a></li>\n<li><a href=\"https://transscendsurvival.org/blog/from-bricked-to-recovered-the-story-of-hacking-an-nvme-ssd-back-to-life\" rel=\"nofollow\">overview / storytime blog post</a></li>\n<li><a href=\"https://transscendsurvival.org/papers/recovery-paper.pdf\" rel=\"nofollow\">Full paper - <em>and tell your friends to also write papers for fun</em></a></li>\n</ul>\n<p dir=\"auto\">I used this nvme stick and the ASM2362 every day all day between 2017 and 2020 for all my laptop computing.  It happpily ran Tails, ubuntu budgie, and even windows via a little usb 3 enclosure velcroed to my laptop during this time.  I haven't thrown it away because it kept me safe under political duress, because I am stubborn, and because I think that it should continue to work forever.</p>\n<ul dir=\"auto\">\n<li>256GB Silicon Power NVMe SSD connected via ASMedia ASM2362 USB bridge now exhibits silent write failure after a Windows hibernate + power loss event.</li>\n<li>It is impossible to write zeros to this drive.  Amazing!  Never seen this before.</li>\n</ul>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Operation</th>\n<th>Reports</th>\n<th>Actual</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>dd if=/dev/zero of=/dev/sdb</code></td>\n<td>Success</td>\n<td>Data unchanged</td>\n</tr>\n<tr>\n<td><code>wipefs --all</code></td>\n<td>Success</td>\n<td>Signatures remain</td>\n</tr>\n<tr>\n<td>SCSI READ</td>\n<td>Success</td>\n<td>Data readable</td>\n</tr>\n<tr>\n<td>SCSI WRITE</td>\n<td>Success</td>\n<td>Silent failure</td>\n</tr>\n<tr>\n<td>NVMe admin commands</td>\n<td>Fail</td>\n<td>\"Medium not present\" (ASC=0x3A)</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">Standard Linux disk tools (fdisk, gdisk, blkdiscard, sg_format) all fail to modify the drive despite reporting success.</p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Hardware Stack</h2><a id=\"user-content-hardware-stack\" class=\"anchor\" aria-label=\"Permalink: Hardware Stack\" href=\"#hardware-stack\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"+---------------------------------------------+\n|  NVMe SSD: Silicon Power 256GB              |\n|  Controller: Phison PS5012-E12              |\n|  State: FTL corrupted, read-only mode       |\n+----------------------+----------------------+\n                       | M.2 PCIe\n+----------------------+----------------------+\n|  USB Bridge: ASMedia ASM2362                |\n|  VID:PID = 0x174c:0x2362                    |\n|  Passthrough: 0xe6 CDB (NVMe tunneling)     |\n+----------------------+----------------------+\n                       | USB 3.1 Gen 2\n+----------------------+----------------------+\n|  Linux: UAS driver -&gt; /dev/sdb              |\n+---------------------------------------------+\"><pre class=\"notranslate\"><code>+---------------------------------------------+\n|  NVMe SSD: Silicon Power 256GB              |\n|  Controller: Phison PS5012-E12              |\n|  State: FTL corrupted, read-only mode       |\n+----------------------+----------------------+\n                       | M.2 PCIe\n+----------------------+----------------------+\n|  USB Bridge: ASMedia ASM2362                |\n|  VID:PID = 0x174c:0x2362                    |\n|  Passthrough: 0xe6 CDB (NVMe tunneling)     |\n+----------------------+----------------------+\n                       | USB 3.1 Gen 2\n+----------------------+----------------------+\n|  Linux: UAS driver -&gt; /dev/sdb              |\n+---------------------------------------------+\n</code></pre></div>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Technical Reference</h2><a id=\"user-content-technical-reference\" class=\"anchor\" aria-label=\"Permalink: Technical Reference\" href=\"#technical-reference\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">SMART Critical Warning Bits</h3><a id=\"user-content-smart-critical-warning-bits\" class=\"anchor\" aria-label=\"Permalink: SMART Critical Warning Bits\" href=\"#smart-critical-warning-bits\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The NVMe SMART/Health log contains critical warning flags at byte offset 0:</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Bit</th>\n<th>Mask</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>0x01</td>\n<td>Available spare capacity below threshold</td>\n</tr>\n<tr>\n<td>1</td>\n<td>0x02</td>\n<td>Temperature exceeded threshold</td>\n</tr>\n<tr>\n<td>2</td>\n<td>0x04</td>\n<td>NVM subsystem reliability degraded</td>\n</tr>\n<tr>\n<td><strong>3</strong></td>\n<td><strong>0x08</strong></td>\n<td><strong>Media placed in read-only mode</strong></td>\n</tr>\n<tr>\n<td>4</td>\n<td>0x10</td>\n<td>Volatile memory backup failed</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\"><strong>Bit 3 (0x08) is our target.</strong> When set, the controller has entered firmware-level protection mode. Per NVMe spec:</p>\n<blockquote>\n<p dir=\"auto\">\"The media has been placed in read only mode. Vendor specific recovery may be required.\"</p>\n</blockquote>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">SCSI Sense Data</h3><a id=\"user-content-scsi-sense-data\" class=\"anchor\" aria-label=\"Permalink: SCSI Sense Data\" href=\"#scsi-sense-data\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">When NVMe admin commands fail through the USB bridge:</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Value</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Sense Key</td>\n<td>0x02</td>\n<td>NOT READY</td>\n</tr>\n<tr>\n<td>ASC</td>\n<td>0x3A</td>\n<td>MEDIUM NOT PRESENT</td>\n</tr>\n<tr>\n<td>ASCQ</td>\n<td>0x00</td>\n<td>-</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">This is generated by the USB bridge, not the NVMe controller. The bridge returns this SCSI error when the NVMe controller doesn't respond as expected during initialization.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">ASM2362 Passthrough Protocol</h3><a id=\"user-content-asm2362-passthrough-protocol\" class=\"anchor\" aria-label=\"Permalink: ASM2362 Passthrough Protocol\" href=\"#asm2362-passthrough-protocol\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The ASMedia ASM2362 uses a proprietary 16-byte CDB with opcode <code>0xe6</code> to tunnel NVMe commands:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"Byte 0:     0xe6        ASMedia passthrough opcode\nByte 1:     NVMe opcode (0x06=Identify, 0x02=GetLog, etc.)\nByte 2:     Reserved\nByte 3:     CDW10[7:0]\nByte 4-5:   Reserved\nByte 6:     CDW10[23:16]\nByte 7:     CDW10[31:24]\nBytes 8-11: CDW13 (big-endian)\nBytes 12-15: CDW12 (big-endian)\"><pre class=\"notranslate\"><code>Byte 0:     0xe6        ASMedia passthrough opcode\nByte 1:     NVMe opcode (0x06=Identify, 0x02=GetLog, etc.)\nByte 2:     Reserved\nByte 3:     CDW10[7:0]\nByte 4-5:   Reserved\nByte 6:     CDW10[23:16]\nByte 7:     CDW10[31:24]\nBytes 8-11: CDW13 (big-endian)\nBytes 12-15: CDW12 (big-endian)\n</code></pre></div>\n<p dir=\"auto\"><strong>Limitation</strong>: CDW11, CDW14, CDW15 cannot be passed, restricting some admin commands.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">NVMe Opcodes via 0xe6 Passthrough</h3><a id=\"user-content-nvme-opcodes-via-0xe6-passthrough\" class=\"anchor\" aria-label=\"Permalink: NVMe Opcodes via 0xe6 Passthrough\" href=\"#nvme-opcodes-via-0xe6-passthrough\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Opcode</th>\n<th>Command</th>\n<th>Status</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0x02</td>\n<td>Get Log Page</td>\n<td><strong>Works</strong></td>\n</tr>\n<tr>\n<td>0x06</td>\n<td>Identify</td>\n<td><strong>Works</strong></td>\n</tr>\n<tr>\n<td>0x09</td>\n<td>Set Features</td>\n<td>Blocked by whitelist</td>\n</tr>\n<tr>\n<td>0x0A</td>\n<td>Get Features</td>\n<td>Blocked by whitelist</td>\n</tr>\n<tr>\n<td>0x80</td>\n<td>Format NVM</td>\n<td>Blocked by whitelist</td>\n</tr>\n<tr>\n<td>0x81</td>\n<td>Security Receive</td>\n<td>Blocked by whitelist</td>\n</tr>\n<tr>\n<td>0x82</td>\n<td>Security Send</td>\n<td>Blocked by whitelist</td>\n</tr>\n<tr>\n<td>0x84</td>\n<td>Sanitize</td>\n<td>Blocked by whitelist</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">Blocked opcodes can be sent via <strong>XRAM injection</strong> (0xE4/0xE5) instead. See <a href=\"docs/archive/dead-ends-e6-whitelist.md\">docs/archive/dead-ends-e6-whitelist.md</a> for details.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Write Protection Feature (FID 0x84)</h3><a id=\"user-content-write-protection-feature-fid-0x84\" class=\"anchor\" aria-label=\"Permalink: Write Protection Feature (FID 0x84)\" href=\"#write-protection-feature-fid-0x84\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">NVMe 1.4+ namespace write protection states:</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Value</th>\n<th>State</th>\n<th>Persistence</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0x00</td>\n<td>No Write Protect</td>\n<td>N/A</td>\n</tr>\n<tr>\n<td>0x01</td>\n<td>Write Protect</td>\n<td>Survives power cycles</td>\n</tr>\n<tr>\n<td>0x02</td>\n<td>Write Protect Until Power Cycle</td>\n<td>Clears on reboot</td>\n</tr>\n<tr>\n<td>0x03</td>\n<td>Permanent Write Protect</td>\n<td><strong>Irreversible</strong></td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Security Protocols</h3><a id=\"user-content-security-protocols\" class=\"anchor\" aria-label=\"Permalink: Security Protocols\" href=\"#security-protocols\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Protocol</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0x00</td>\n<td>Discovery - list supported protocols</td>\n</tr>\n<tr>\n<td>0xEF</td>\n<td>ATA Device Server Password</td>\n</tr>\n<tr>\n<td>0x01-0x06</td>\n<td>TCG Opal</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">ASM2362 Vendor SCSI Commands (Beyond 0xe6)</h3><a id=\"user-content-asm2362-vendor-scsi-commands-beyond-0xe6\" class=\"anchor\" aria-label=\"Permalink: ASM2362 Vendor SCSI Commands (Beyond 0xe6)\" href=\"#asm2362-vendor-scsi-commands-beyond-0xe6\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Opcode</th>\n<th>Direction</th>\n<th>Command</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0xE0</td>\n<td>Read</td>\n<td>Read Config</td>\n<td>128 bytes bridge configuration</td>\n</tr>\n<tr>\n<td>0xE1</td>\n<td>Write</td>\n<td>Write Config</td>\n<td>Modify bridge configuration</td>\n</tr>\n<tr>\n<td>0xE2</td>\n<td>Read</td>\n<td>Flash Read</td>\n<td>Read SPI flash contents</td>\n</tr>\n<tr>\n<td>0xE3</td>\n<td>Write</td>\n<td>Firmware Write</td>\n<td>Flash firmware (from address 0x80)</td>\n</tr>\n<tr>\n<td>0xE4</td>\n<td>Read</td>\n<td><strong>XDATA Read</strong></td>\n<td>Read up to 255 bytes from bridge XRAM</td>\n</tr>\n<tr>\n<td>0xE5</td>\n<td>Write</td>\n<td><strong>XDATA Write</strong></td>\n<td>Write single byte to bridge XRAM</td>\n</tr>\n<tr>\n<td>0xE6</td>\n<td>Read</td>\n<td>NVMe Admin</td>\n<td>NVMe passthrough (only 0x02, 0x06)</td>\n</tr>\n<tr>\n<td>0xE8</td>\n<td>None</td>\n<td>Reset</td>\n<td>0x00=CPU reset, 0x01=PCIe/soft reset</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">ASM2362 XRAM Memory Map</h3><a id=\"user-content-asm2362-xram-memory-map\" class=\"anchor\" aria-label=\"Permalink: ASM2362 XRAM Memory Map\" href=\"#asm2362-xram-memory-map\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Address</th>\n<th>Size</th>\n<th>Contents</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0x07F0-0x07F5</td>\n<td>6B</td>\n<td>Firmware version string</td>\n</tr>\n<tr>\n<td>0xA000-0xAFFF</td>\n<td>4KB</td>\n<td>NVMe I/O Submission Queue</td>\n</tr>\n<tr>\n<td>0xB000-0xB1FF</td>\n<td>512B</td>\n<td>NVMe Admin Submission Queue (depth=4, slots 0-3 active)</td>\n</tr>\n<tr>\n<td>0xB200-0xB296</td>\n<td>~150B</td>\n<td>PCIe TLP engine registers</td>\n</tr>\n<tr>\n<td>0xB800-0xBBFF</td>\n<td>1KB</td>\n<td>NVMe I/O Completion Queue</td>\n</tr>\n<tr>\n<td>0xBC00-0xBFFF</td>\n<td>1KB</td>\n<td>NVMe Admin Completion Queue</td>\n</tr>\n<tr>\n<td>0xF000-0xFFFF</td>\n<td>4KB</td>\n<td>NVMe data buffer (Identify cache)</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">ASM2362 Hardware</h3><a id=\"user-content-asm2362-hardware\" class=\"anchor\" aria-label=\"Permalink: ASM2362 Hardware\" href=\"#asm2362-hardware\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>CPU: 8051-compatible core, ~114.3 MHz</li>\n<li>XRAM: 64KB mapped memory</li>\n<li>No firmware signature verification</li>\n<li>UART debug: 921600 8N1, 3.3V (pins 62/63)</li>\n</ul>\n<p dir=\"auto\">Source: <a href=\"https://github.com/cyrozap/usb-to-pcie-re\">cyrozap/usb-to-pcie-re</a></p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Recovery (Completed 2026-03-04)</h2><a id=\"user-content-recovery-completed-2026-03-04\" class=\"anchor\" aria-label=\"Permalink: Recovery (Completed 2026-03-04)\" href=\"#recovery-completed-2026-03-04\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><strong>The drive was successfully recovered</strong> via XRAM-based NVMe command injection. Sanitize Block Erase (SANACT=2), injected directly into the Admin Submission Queue at XRAM 0xB000 and triggered via PCIe TLP doorbell, cleared the FTL corruption and restored full write capability.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">What Worked</h3><a id=\"user-content-what-worked\" class=\"anchor\" aria-label=\"Permalink: What Worked\" href=\"#what-worked\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Approach</th>\n<th>Result</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>XRAM SQ injection + PCIe TLP doorbell</strong></td>\n<td>Sanitize Block Erase succeeded, FTL rebuilt</td>\n</tr>\n<tr>\n<td><strong>Sanitize Block Erase</strong> (opcode 0x84, SANACT=2)</td>\n<td>Cleared write protection</td>\n</tr>\n<tr>\n<td><strong>BOT mode</strong> (usb-storage quirks)</td>\n<td>Required for 0xE4/0xE5 vendor commands</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">What Didn't Work</h3><a id=\"user-content-what-didnt-work\" class=\"anchor\" aria-label=\"Permalink: What Didn't Work\" href=\"#what-didnt-work\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Approach</th>\n<th>Why</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0xE6 passthrough for Format/Sanitize</td>\n<td>Silently dropped by bridge firmware whitelist</td>\n</tr>\n<tr>\n<td>Format NVM via injection</td>\n<td>Accepted by controller but didn't clear FTL corruption</td>\n</tr>\n<tr>\n<td>Sanitize Crypto Erase</td>\n<td>Not supported (SANICAP=0x02, block erase only)</td>\n</tr>\n<tr>\n<td>Wine + SP Toolbox</td>\n<td>Wine lacks IOCTL_SCSI_PASS_THROUGH_DIRECT</td>\n</tr>\n<tr>\n<td>Set Features 0x84</td>\n<td>Separate mechanism from firmware read-only mode</td>\n</tr>\n<tr>\n<td>Injection to SQ slots 4-7</td>\n<td>Admin SQ depth is 4, not 8 — slots beyond 3 are ignored</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Key Discovery: Admin SQ Depth</h3><a id=\"user-content-key-discovery-admin-sq-depth\" class=\"anchor\" aria-label=\"Permalink: Key Discovery: Admin SQ Depth\" href=\"#key-discovery-admin-sq-depth\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The XRAM buffer at 0xB000-0xB1FF has room for 8 entries (512 bytes), but the firmware configures a queue depth of 4. Commands must be injected into slots 0-3 only. The SQHD field in the Completion Queue reveals the actual depth.</p>\n<p dir=\"auto\">See <a href=\"docs/research/deep-research-synthesis.md\">docs/research/deep-research-synthesis.md</a> for the full research history.</p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Building</h2><a id=\"user-content-building\" class=\"anchor\" aria-label=\"Permalink: Building\" href=\"#building\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Requires Zig 0.13.0 or later.</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"zig build\"><pre>zig build</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Diagnostic Commands (read-only, safe)</h3><a id=\"user-content-diagnostic-commands-read-only-safe\" class=\"anchor\" aria-label=\"Permalink: Diagnostic Commands (read-only, safe)\" href=\"#diagnostic-commands-read-only-safe\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"sudo ./zig-out/bin/asm2362-tool probe /dev/sdX        # Detect bridge type\nsudo ./zig-out/bin/asm2362-tool identify /dev/sdX      # NVMe Identify Controller\nsudo ./zig-out/bin/asm2362-tool smart /dev/sdX --json  # SMART log (JSON output)\"><pre>sudo ./zig-out/bin/asm2362-tool probe /dev/sdX        <span class=\"pl-c\"><span class=\"pl-c\">#</span> Detect bridge type</span>\nsudo ./zig-out/bin/asm2362-tool identify /dev/sdX      <span class=\"pl-c\"><span class=\"pl-c\">#</span> NVMe Identify Controller</span>\nsudo ./zig-out/bin/asm2362-tool smart /dev/sdX --json  <span class=\"pl-c\"><span class=\"pl-c\">#</span> SMART log (JSON output)</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">XRAM Commands (bridge-level access)</h3><a id=\"user-content-xram-commands-bridge-level-access\" class=\"anchor\" aria-label=\"Permalink: XRAM Commands (bridge-level access)\" href=\"#xram-commands-bridge-level-access\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Read-only probe — tests 0xE4 support, dumps Admin SQ, MMIO, data buffer\nsudo ./zig-out/bin/asm2362-tool xram-probe /dev/sdX\n\n# Raw XRAM read/write\nsudo ./zig-out/bin/asm2362-tool xram-read --addr=0xB000 --len=64 /dev/sdX\nsudo ./zig-out/bin/asm2362-tool xram-dump --addr=0xB000 --len=512 /dev/sdX\nsudo ./zig-out/bin/asm2362-tool xram-write --addr=0xB000 --byte=0x00 /dev/sdX\n\n# NVMe command injection via XRAM (dry-run by default)\nsudo ./zig-out/bin/asm2362-tool inject --inject-cmd=format /dev/sdX         # dry-run\nsudo ./zig-out/bin/asm2362-tool inject --inject-cmd=format --force /dev/sdX  # live\n\n# Bridge reset\nsudo ./zig-out/bin/asm2362-tool reset --reset-type=1 /dev/sdX  # PCIe soft reset\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Read-only probe — tests 0xE4 support, dumps Admin SQ, MMIO, data buffer</span>\nsudo ./zig-out/bin/asm2362-tool xram-probe /dev/sdX\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Raw XRAM read/write</span>\nsudo ./zig-out/bin/asm2362-tool xram-read --addr=0xB000 --len=64 /dev/sdX\nsudo ./zig-out/bin/asm2362-tool xram-dump --addr=0xB000 --len=512 /dev/sdX\nsudo ./zig-out/bin/asm2362-tool xram-write --addr=0xB000 --byte=0x00 /dev/sdX\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> NVMe command injection via XRAM (dry-run by default)</span>\nsudo ./zig-out/bin/asm2362-tool inject --inject-cmd=format /dev/sdX         <span class=\"pl-c\"><span class=\"pl-c\">#</span> dry-run</span>\nsudo ./zig-out/bin/asm2362-tool inject --inject-cmd=format --force /dev/sdX  <span class=\"pl-c\"><span class=\"pl-c\">#</span> live</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Bridge reset</span>\nsudo ./zig-out/bin/asm2362-tool reset --reset-type=1 /dev/sdX  <span class=\"pl-c\"><span class=\"pl-c\">#</span> PCIe soft reset</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Running Tests</h3><a id=\"user-content-running-tests\" class=\"anchor\" aria-label=\"Permalink: Running Tests\" href=\"#running-tests\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"zig build test       # main.zig tests\nzig build test-all   # all module tests (sg_io, sense, passthrough, replay, xram)\"><pre>zig build <span class=\"pl-c1\">test</span>       <span class=\"pl-c\"><span class=\"pl-c\">#</span> main.zig tests</span>\nzig build test-all   <span class=\"pl-c\"><span class=\"pl-c\">#</span> all module tests (sg_io, sense, passthrough, replay, xram)</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Implementation Status</h2><a id=\"user-content-implementation-status\" class=\"anchor\" aria-label=\"Permalink: Implementation Status\" href=\"#implementation-status\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Status</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>SCSI SG_IO layer</td>\n<td>Complete</td>\n</tr>\n<tr>\n<td>ASM2362 0xe6 passthrough</td>\n<td>Complete (Identify + SMART work; others blocked by whitelist)</td>\n</tr>\n<tr>\n<td>XRAM access (0xE4/0xE5/0xE8)</td>\n<td>Complete</td>\n</tr>\n<tr>\n<td>XRAM NVMe command injection</td>\n<td>Complete (dry-run default, doorbell via PCIe reset)</td>\n</tr>\n<tr>\n<td>NVMe Identify + SMART</td>\n<td>Complete</td>\n</tr>\n<tr>\n<td>Format NVM / Sanitize (via 0xe6)</td>\n<td>Archived — <a href=\"docs/archive/dead-ends-e6-whitelist.md\">dead end</a></td>\n</tr>\n<tr>\n<td>Frida hooks for Windows</td>\n<td>Complete</td>\n</tr>\n<tr>\n<td>Command replay</td>\n<td>Complete</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">~4,000 lines of Zig with 35 unit tests.</p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Project Structure</h2><a id=\"user-content-project-structure\" class=\"anchor\" aria-label=\"Permalink: Project Structure\" href=\"#project-structure\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"src/\n  main.zig             CLI entry point\n  scsi/                SG_IO wrapper, sense parsing\n  asm2362/\n    passthrough.zig    0xe6 CDB passthrough (Identify, SMART only)\n    xram.zig           0xE4/0xE5/0xE8 XRAM access, NVMe SQ injection, PCIe TLP doorbell\n    commands.zig       NVMe command helpers (SMART log parsing)\n  nvme/                NVMe command implementations (identify)\n  frida/               Windows DeviceIoControl hooks + replay\n  analysis/            Capability probing\n\ndocs/\n  blog/                Blog posts (mdsvex-compatible .md)\n  paper/               LaTeX technical paper\n  notes/               Research findings\n  research/            Deep research synthesis\n  archive/             Dead-end code + writeups (format.zig, sanitize.zig)\"><pre class=\"notranslate\"><code>src/\n  main.zig             CLI entry point\n  scsi/                SG_IO wrapper, sense parsing\n  asm2362/\n    passthrough.zig    0xe6 CDB passthrough (Identify, SMART only)\n    xram.zig           0xE4/0xE5/0xE8 XRAM access, NVMe SQ injection, PCIe TLP doorbell\n    commands.zig       NVMe command helpers (SMART log parsing)\n  nvme/                NVMe command implementations (identify)\n  frida/               Windows DeviceIoControl hooks + replay\n  analysis/            Capability probing\n\ndocs/\n  blog/                Blog posts (mdsvex-compatible .md)\n  paper/               LaTeX technical paper\n  notes/               Research findings\n  research/            Deep research synthesis\n  archive/             Dead-end code + writeups (format.zig, sanitize.zig)\n</code></pre></div>\n<p dir=\"auto\">See <a href=\"docs/INDEX.md\">docs/INDEX.md</a> for full documentation navigation.</p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">References</h2><a id=\"user-content-references\" class=\"anchor\" aria-label=\"Permalink: References\" href=\"#references\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><a href=\"https://github.com/cyrozap/usb-to-pcie-re\">cyrozap/usb-to-pcie-re</a> -- ASM2362 firmware RE, XRAM map, vendor commands</li>\n<li><a href=\"https://nvmexpress.org/specifications/\" rel=\"nofollow\">NVMe Base Specification 2.0</a> -- Admin commands, queue structures, doorbell registers</li>\n<li><a href=\"https://github.com/smartmontools/smartmontools/blob/master/smartmontools/scsinvme.cpp\">smartmontools sntasmedia</a> -- Reference 0xe6 implementation</li>\n<li><a href=\"https://github.com/smx-smx/ASMTool\">smx-smx/ASMTool</a> -- ASMedia firmware dumper</li>\n<li><a href=\"https://sg.danny.cz/sg/sg3_utils.html\" rel=\"nofollow\">sg3_utils</a> -- SG_IO ioctl reference</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Research code - use at your own risk. No warranty implied.</p>\n</article></div>",
      "readme_excerpt": "Experimental research into recovering NVMe SSDs exhibiting firmware-level write protection after FTL corruption. This is a project\n- xram injection technical blog post\n- overview / storytime blog post\n- Full paper - and tell your friends to also write papers for fun\nI used this nvme stick and the ASM2362 every day all day between 2017 and 2020 for all my laptop computing.  It happpily ran Tails, ubuntu budgie, and even windows via a little usb 3 enclosure velcroed to my laptop during this time....",
      "install_commands": {
        "zig": "zig fetch --save git+https://github.com/jesssullivan/hiberpower-ntfs.git"
      },
      "repo_url": "https://github.com/jesssullivan/hiberpower-ntfs",
      "website_url": "https://transscendsurvival.org/blog/xram-injection-bypassing-usb-bridge-whitelists-to-recover-nvme-drives",
      "docs_url": null,
      "registry_url": "https://zigistry.dev/package/{repo}",
      "releases_url": "https://github.com/jesssullivan/hiberpower-ntfs/releases",
      "og_image_url": "https://opengraph.githubassets.com/4eab5096441a20ccea4169263849bf62923d40b602e045da6446f5ada499f200/Jesssullivan/hiberpower-ntfs",
      "license": "",
      "pushed_at": "2026-04-22T04:13:49Z",
      "enriched_at": "2026-04-26T17:17:15.937Z",
      "gate": "homepage"
    },
    {
      "slug": "jesssullivan-tummycrypt",
      "name": "tummycrypt",
      "repo": "jesssullivan/tummycrypt",
      "org": "jesssullivan",
      "ecosystem": "rust",
      "category": "cryptography",
      "description": "Self-hosted encrypted file sync with on-demand hydration and fleet sync. FOSS odrive replacement.",
      "featured": true,
      "tags": [
        "crypto",
        "filesystem",
        "sync",
        "monorepo"
      ],
      "version": "0.12.2",
      "release_date": "2026-04-16T14:21:24Z",
      "releases": [
        {
          "tag": "0.12.2",
          "date": "2026-04-16T14:21:24Z",
          "body": "## tcfs v0.12.2\n\n### Quick Install\n\n**macOS (.pkg installer — recommended):**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.2/tcfs-0.12.2-macos-aarch64.pkg\nsudo installer -pkg tcfs-0.12.2-macos-aarch64.pkg -target /\n```\n\n**Linux / macOS (shell script):**\n```bash\ncurl -fsSL https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.2/install.sh | sh\n```\n\n**Homebrew:**\n```bash\nbrew tap --custom-remote Jesssullivan/tummycrypt https://github.com/Jesssullivan/tummycrypt.git\ngit -C \"$(brew --repo Jesssullivan/tummycrypt)\" fetch origin homebrew-tap\ngit -C \"$(brew --repo Jesssullivan/tummycrypt)\" checkout homebrew-tap\nbrew install Jesssullivan/tummycrypt/tcfs\n```\n\n**Container image:**\n```bash\npodman pull ghcr.io/jesssullivan/tcfsd:v0.12.2\n```\n\n**Debian/Ubuntu:**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.2/tcfsd-0.12.2-amd64.deb\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.2/tcfs-0.12.2-amd64.deb\nsudo dpkg -i tcfsd-0.12.2-amd64.deb tcfs-0.12.2-amd64.deb\n```\n\n**RPM (Fedora/RHEL/Rocky, daemon-only today):**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.2/tcfsd-0.12.2-x86_64.rpm\nsudo rpm -i tcfsd-0.12.2-x86_64.rpm\n```\n\n### Verify checksums\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.2/SHA256SUMS.txt\nsha256sum -c SHA256SUMS.txt\n```\n\n### Verify signature (Sigstore Cosign)\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.2/SHA256SUMS.txt.sig\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.2/SHA256SUMS.txt.pem\ncosign verify-blob \\\n  --signature SHA256SUMS.txt.sig \\\n  --certificate SHA256SUMS.txt.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp 'github.com/Jesssullivan/tummycrypt' \\\n  SHA256SUMS.txt\n```\n\n### macOS artifacts\nmacOS CLI binaries are Developer ID signed when Apple credentials are configured.\nFileProvider and `.pkg` notarization is attempted for Apple artifacts, but release\npublication does not fail if Apple's notarization service or account agreements are unavailable.\n\n### Binaries included\n| Binary | Purpose |\n|--------|---------|\n| `tcfs` | CLI: push, pull, sync-status, mount, unsync |\n| `tcfsd` | Daemon: gRPC socket, FUSE, Prometheus metrics |\n| `tcfs-tui` | Terminal UI for interactive management |\n| `tcfs-mcp` | MCP server for AI agent integration |\n\n\n## What's Changed\n* [codex] Build release FileProvider staticlib with grpc backend by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/274\n* [codex] Vendor OpenSSL for macOS arm64 release artifacts by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/282\n* [codex] Pin gitleaks in Secret Scan CI by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/284\n* feat(greptile): add review configuration and coding standards by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/286\n* [codex] Ignore flaky Apple FileProvider API link in docs check by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/287\n* [codex] Correct Homebrew tap flow documentation by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/283\n* [codex] Define neo-honey live acceptance contract by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/285\n* [codex] Preserve state keying after delete by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/288\n* [codex] Define distribution smoke matrix for releases by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/289\n* [codex] Guard macOS release tcfsd linkage by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/291\n* [codex] Align Apple support claims with current evidence by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/292\n* [codex] Define macOS Finder and FileProvider reality by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/294\n* [codex] Define active iOS surface posture by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/293\n* [codex] Refresh odrive parity analysis for v0.12.1 by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/295\n* [codex] Prepare v0.12.2 metadata by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/296\n\n\n**Full Changelog**: https://github.com/Jesssullivan/tummycrypt/compare/v0.12.1...v0.12.2"
        },
        {
          "tag": "0.12.1",
          "date": "2026-04-15T21:00:45Z",
          "body": "## tcfs v0.12.1\n\n### Quick Install\n\n**macOS (.pkg installer — recommended):**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.1/tcfs-0.12.1-macos-aarch64.pkg\nsudo installer -pkg tcfs-0.12.1-macos-aarch64.pkg -target /\n```\n\n**Linux / macOS (shell script):**\n```bash\ncurl -fsSL https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.1/install.sh | sh\n```\n\n**Homebrew:**\n```bash\nbrew tap Jesssullivan/tummycrypt https://github.com/Jesssullivan/tummycrypt --branch homebrew-tap\nbrew install tcfs\n```\n\n**Container image:**\n```bash\npodman pull ghcr.io/jesssullivan/tcfsd:v0.12.1\n```\n\n**Debian/Ubuntu:**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.1/tcfs-0.12.1-amd64.deb\nsudo dpkg -i tcfs-0.12.1-amd64.deb\n```\n\n**RPM (Fedora/RHEL/Rocky):**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.1/tcfsd-0.12.1-x86_64.rpm\nsudo rpm -i tcfsd-0.12.1-x86_64.rpm\n```\n\n### Verify checksums\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.1/SHA256SUMS.txt\nsha256sum -c SHA256SUMS.txt\n```\n\n### Verify signature (Sigstore Cosign)\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.1/SHA256SUMS.txt.sig\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.1/SHA256SUMS.txt.pem\ncosign verify-blob \\\n  --signature SHA256SUMS.txt.sig \\\n  --certificate SHA256SUMS.txt.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp 'github.com/Jesssullivan/tummycrypt' \\\n  SHA256SUMS.txt\n```\n\n### macOS artifacts\nmacOS CLI binaries are Developer ID signed when Apple credentials are configured.\nFileProvider and `.pkg` notarization is attempted for Apple artifacts, but release\npublication does not fail if Apple's notarization service or account agreements are unavailable.\n\n### Binaries included\n| Binary | Purpose |\n|--------|---------|\n| `tcfs` | CLI: push, pull, sync-status, mount, unsync |\n| `tcfsd` | Daemon: gRPC socket, FUSE, Prometheus metrics |\n| `tcfs-tui` | Terminal UI for interactive management |\n| `tcfs-mcp` | MCP server for AI agent integration |\n\n\n## What's Changed\n* fix(sync): P0 sync engine — empty files, permissions, delete by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/176\n* feat(sync): symlink handling with cycle detection by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/177\n* feat(sync): streaming chunker for large files (>64MB) by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/178\n* fix(nats): sync_always documentation + stream health verification by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/181\n* fix(sync): eliminate panics — unwrap, bounds check, parse validation by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/182\n* fix(daemon): race guards for FileDeleted, deferred vclock, active-file protection by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/184\n* fix(fuse): implement flush via VFS fsync + improve error-to-errno mapping by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/187\n* fix(vfs): OOM prevention, RwLock for concurrent reads, bounded negative cache by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/188\n* feat: unsync dehydration + auto-unsync with disk pressure by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/191\n* feat: selective sync policies + D-Bus gRPC backend by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/192\n* feat(sync): sync trash + bandwidth throttling by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/193\n* feat(cli): tcfs trash list/restore/purge commands by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/194\n* fix(storage): centralize prefix resolution, fix 5 namespace bugs by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/195\n* fix(sync): recursive remote index listing + ListFiles fallback by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/196\n* feat(daemon): hydration state in ListFiles + macOS watcher disable by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/197\n* feat(daemon): periodic reconciliation loop by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/198\n* feat(fileprovider): badge decorations + custom actions, remove FinderSync by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/199\n* fix(security): NATS TLS default, trash path traversal, auth bypass warning by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/215\n* fix(sync): clear both conflict and status on resolution by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/211\n* fix(sync): sync empty directories via .tcfs_dir markers by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/212\n* feat(menubar): TCFSStatus.app conflict monitor by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/200\n* test(sync): unit tests for engine, NATS serialization, reconcile by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/213\n* fix(sync): retry chunk upload/download with exponential backoff by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/214\n* chore: repo hygiene — stale advisories + unreachable!() by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/216\n* fix(secrets): unified credential discovery, AWS file support by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/217\n* test(fileprovider): 9 functional tests with memory backend by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/218\n* test(daemon): 8 gRPC unit tests + fix production unwraps by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/219\n* fix(cli): replace bare unwrap() with expect() by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/220\n* test(fuzz): cargo-fuzz targets + proptest for manifest, crypto, paths by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/221\n* [codex] Reject traversal paths in gRPC push by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/239\n* [codex] Persist and recover StateCache metadata by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/240\n* [codex] Converge toolchain and CI by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/242\n* [codex] Align canonical home and release surfaces by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/244\n* [codex] Retry manifest reads during download by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/241\n* [codex] Add recovery-aware manifest/index publish durability by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/243\n* [codex] Add failure-injection retry coverage by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/245\n* [codex] Expand tcfsd gRPC coverage by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/246\n* [codex] Add tcfs-mcp tool coverage by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/247\n* [codex] Add resilience replay simulation coverage by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/248\n* [codex] Fix PathLocks cleanup under contention by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/249\n* [codex] Add orphaned chunk visibility by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/250\n* [codex] Add orphan chunk grace-period cleanup by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/251\n* [codex] Prevent upload TOCTOU races by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/252\n* [codex] Normalize Unicode rel paths by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/254\n* [codex] Make key rotation resumable by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/253\n* [codex] Cover NATS retry delivery decisions by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/255\n* [codex] Add CLI workflow coverage by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/256\n* [codex] Wire orphan chunk cleanup into reconcile surfaces by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/257\n* [codex] Add guarded FUSE mount roundtrip coverage by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/258\n* [codex] Cover live NATS durable replay semantics by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/259\n* [codex] Expand FUSE lifecycle coverage across rename and delete by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/260\n* [codex] Cover live storage outage recovery by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/261\n* [codex] Add named neo-honey live smoke lane by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/268\n* [codex] Sign release container image by digest by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/266\n* [codex] Record Apple surfaces as experimental by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/270\n* [codex] Backfill changelog through v0.12.0 by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/269\n* [codex] Reduce overstatement in platform support docs by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/267\n* [codex] Let manual release dispatch exercise tag-gated jobs by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/271\n* [codex] Harden release proof dispatch and Apple notarization by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/272\n* [codex] Prepare v0.12.1 release metadata by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/273\n\n\n**Full Changelog**: https://github.com/Jesssullivan/tummycrypt/compare/v0.12.0...v0.12.1"
        },
        {
          "tag": "0.12.0",
          "date": "2026-04-08T21:00:52Z",
          "body": "## tcfs v0.12.0\n\n### Quick Install\n\n**macOS (.pkg installer — recommended):**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.0/tcfs-0.12.0-macos-aarch64.pkg\nsudo installer -pkg tcfs-0.12.0-macos-aarch64.pkg -target /\n```\n\n**Linux / macOS (shell script):**\n```bash\ncurl -fsSL https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.0/install.sh | sh\n```\n\n**Homebrew:**\n```bash\nbrew install tinyland-inc/tap/tcfs\n```\n\n**Container image:**\n```bash\npodman pull ghcr.io/tcfsd:v0.12.0\n```\n\n**Debian/Ubuntu:**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.0/tcfs-0.12.0-amd64.deb\nsudo dpkg -i tcfs-0.12.0-amd64.deb\n```\n\n**RPM (Fedora/RHEL/Rocky):**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.0/tcfsd-0.12.0-x86_64.rpm\nsudo rpm -i tcfsd-0.12.0-x86_64.rpm\n```\n\n### Verify checksums\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.0/SHA256SUMS.txt\nsha256sum -c SHA256SUMS.txt\n```\n\n### Verify signature (Sigstore Cosign)\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.0/SHA256SUMS.txt.sig\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.12.0/SHA256SUMS.txt.pem\ncosign verify-blob \\\n  --signature SHA256SUMS.txt.sig \\\n  --certificate SHA256SUMS.txt.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp 'github.com/tinyland-inc/tummycrypt' \\\n  SHA256SUMS.txt\n```\n\n### macOS binaries\nAll macOS binaries are signed with Developer ID and notarized by Apple.\nThe FileProvider extension (`TCFSProvider.app`) is stapled for offline verification.\n\n### Binaries included\n| Binary | Purpose |\n|--------|---------|\n| `tcfs` | CLI: push, pull, sync-status, mount, unsync |\n| `tcfsd` | Daemon: gRPC socket, FUSE, Prometheus metrics |\n| `tcfs-tui` | Terminal UI for interactive management |\n| `tcfs-mcp` | MCP server for AI agent integration |\n\n\n**Full Changelog**: https://github.com/Jesssullivan/tummycrypt/compare/v0.11.1...v0.12.0"
        },
        {
          "tag": "0.11.1",
          "date": "2026-04-08T20:21:49Z",
          "body": "## tcfs v0.11.1\n\n### Quick Install\n\n**macOS (.pkg installer — recommended):**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.1/tcfs-0.11.1-macos-aarch64.pkg\nsudo installer -pkg tcfs-0.11.1-macos-aarch64.pkg -target /\n```\n\n**Linux / macOS (shell script):**\n```bash\ncurl -fsSL https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.1/install.sh | sh\n```\n\n**Homebrew:**\n```bash\nbrew install tinyland-inc/tap/tcfs\n```\n\n**Container image:**\n```bash\npodman pull ghcr.io/tcfsd:v0.11.1\n```\n\n**Debian/Ubuntu:**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.1/tcfs-0.11.1-amd64.deb\nsudo dpkg -i tcfs-0.11.1-amd64.deb\n```\n\n**RPM (Fedora/RHEL/Rocky):**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.1/tcfsd-0.11.1-x86_64.rpm\nsudo rpm -i tcfsd-0.11.1-x86_64.rpm\n```\n\n### Verify checksums\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.1/SHA256SUMS.txt\nsha256sum -c SHA256SUMS.txt\n```\n\n### Verify signature (Sigstore Cosign)\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.1/SHA256SUMS.txt.sig\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.1/SHA256SUMS.txt.pem\ncosign verify-blob \\\n  --signature SHA256SUMS.txt.sig \\\n  --certificate SHA256SUMS.txt.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp 'github.com/tinyland-inc/tummycrypt' \\\n  SHA256SUMS.txt\n```\n\n### macOS binaries\nAll macOS binaries are signed with Developer ID and notarized by Apple.\nThe FileProvider extension (`TCFSProvider.app`) is stapled for offline verification.\n\n### Binaries included\n| Binary | Purpose |\n|--------|---------|\n| `tcfs` | CLI: push, pull, sync-status, mount, unsync |\n| `tcfsd` | Daemon: gRPC socket, FUSE, Prometheus metrics |\n| `tcfs-tui` | Terminal UI for interactive management |\n| `tcfs-mcp` | MCP server for AI agent integration |\n\n\n**Full Changelog**: https://github.com/Jesssullivan/tummycrypt/commits/v0.11.1"
        },
        {
          "tag": "0.11.0",
          "date": "2026-04-07T23:51:06Z",
          "body": "## tcfs v0.11.0\n\n### Quick Install\n\n**macOS (.pkg installer — recommended):**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.0/tcfs-0.11.0-macos-aarch64.pkg\nsudo installer -pkg tcfs-0.11.0-macos-aarch64.pkg -target /\n```\n\n**Linux / macOS (shell script):**\n```bash\ncurl -fsSL https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.0/install.sh | sh\n```\n\n**Homebrew:**\n```bash\nbrew install tinyland-inc/tap/tcfs\n```\n\n**Container image:**\n```bash\npodman pull ghcr.io/jesssullivan/tcfsd:v0.11.0\n```\n\n**Debian/Ubuntu:**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.0/tcfs-0.11.0-amd64.deb\nsudo dpkg -i tcfs-0.11.0-amd64.deb\n```\n\n**RPM (Fedora/RHEL/Rocky):**\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.0/tcfsd-0.11.0-x86_64.rpm\nsudo rpm -i tcfsd-0.11.0-x86_64.rpm\n```\n\n### Verify checksums\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.0/SHA256SUMS.txt\nsha256sum -c SHA256SUMS.txt\n```\n\n### Verify signature (Sigstore Cosign)\n```bash\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.0/SHA256SUMS.txt.sig\ncurl -LO https://github.com/Jesssullivan/tummycrypt/releases/download/v0.11.0/SHA256SUMS.txt.pem\ncosign verify-blob \\\n  --signature SHA256SUMS.txt.sig \\\n  --certificate SHA256SUMS.txt.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp 'github.com/tinyland-inc/tummycrypt' \\\n  SHA256SUMS.txt\n```\n\n### macOS binaries\nAll macOS binaries are signed with Developer ID and notarized by Apple.\nThe FileProvider extension (`TCFSProvider.app`) is stapled for offline verification.\n\n### Binaries included\n| Binary | Purpose |\n|--------|---------|\n| `tcfs` | CLI: push, pull, sync-status, mount, unsync |\n| `tcfsd` | Daemon: gRPC socket, FUSE, Prometheus metrics |\n| `tcfs-tui` | Terminal UI for interactive management |\n| `tcfs-mcp` | MCP server for AI agent integration |\n\n\n## What's Changed\n* fix(fileprovider): add TcfsChangeEvent to cbindgen exports by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/72\n* fix(fileprovider): fix EDEADLK hydration deadlock + CLI pull prefix by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/73\n* feat(ios): QR enrollment view for alpha onboarding by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/74\n* fix(ios): wrap conditional body in Group for sheet modifier by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/75\n* feat(ios): fix QR enrollment + encryption + deep links by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/76\n* chore(ios): bump build number to 5 by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/77\n* fix(ios): ATS exception + credential form + TOTP handoff by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/78\n* feat(auth): credential broker for zero-touch enrollment by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/79\n* fix(ios): use build setting variables for version in Info.plist by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/80\n* fix(ios): add version + ATS to xcodegen properties by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/81\n* chore(ios): auto-skip encryption compliance prompt by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/82\n* feat(ios): build info section with git SHA by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/83\n* fix(ios): auto-unlock keychain in pipeline for SSH by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/84\n* chore(ios): bump to v1.1.0 build 9 by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/85\n* feat(auth): compact QR encoding for device enrollment by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/86\n* refactor(tcfs): retire FUSE crates — remove 11,418 LOC by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/87\n* style: cargo fmt by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/89\n* fix(tcfsd): skip directories in watcher push path by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/88\n* fix(nfs): use sudo for NFS mount on Linux by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/90\n* fix(grpc): run NFS mount in-process instead of subprocess by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/91\n* fix(nfs): add diagnostic logging for NFS server exit by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/92\n* fix(nfs): detect task panics via JoinHandle watcher by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/93\n* fix(nfs): add 10s timeout to VFS/S3 calls in NFS adapter by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/94\n* feat(mount): resurrect FUSE3 as default, NFS as fallback by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/95\n* chore: regenerate Cargo.lock with fuse3 dependency by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/96\n* chore: regenerate Cargo.lock with fuse3 dependency by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/97\n* fix(vfs): add diagnostic logging to hydration path by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/98\n* fix(vfs): diagnostic logging for hydration path by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/99\n* fix(vfs): version bump 0.9.1 + hydration diagnostics by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/100\n* fix(vfs): parse JSON v2 manifests in hydration path by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/101\n* fix(flake): disable stale attic binary cache by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/102\n* fix(flake): disable stale attic cache by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/103\n* fix: add missing serde_json to Cargo.lock by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/104\n* fix: add missing serde_json to Cargo.lock by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/105\n* feat(vfs): read-write FUSE support by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/106\n* fix: add blake3 to Cargo.lock by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/107\n* fix: add blake3 to Cargo.lock by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/108\n* feat(vfs): FastCDC chunking for FUSE writes by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/109\n* feat(vfs): FastCDC chunking for FUSE writes by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/110\n* fix(daemon): index-first auto-pull (skip FUSE mount writes) by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/111\n* feat: NATS publish on FUSE write + index-first auto-pull by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/112\n* feat(vfs): make .tc suffix transparent to userspace by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/113\n* fix(sync): prevent orphaned index entries on skipped uploads by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/114\n* fix(flake): remove stale fuse-t feature flag for Darwin by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/115\n* feat(vfs): mkdir, rename, rmdir for FUSE directory ops by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/116\n* feat(vfs): wire vector clock conflict detection into FUSE writes by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/117\n* fix(nats): use create_or_update_stream to apply subject filter changes by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/118\n* fix(nats): include storage prefix in FUSE flush manifest_path by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/119\n* ci: add Cargo.lock freshness check by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/120\n* fix(sync): resolve pull 404 for files pushed with absolute paths (#122) by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/123\n* fix(daemon): write index entry on gRPC push + normalize rel_path by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/124\n* fix(sync): fallback filename search for cross-host pull by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/134\n* fix(crypto): enable crypto feature flag + wire EncryptionContext into all push paths by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/132\n* fix(fileprovider): enumerate immediate children for subdirectory navigation by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/133\n* fix(daemon): update state cache on NATS auto-pull events by @Jesssullivan in https://github.com/Jesssullivan/tummycrypt/pull/135\n\n\n**Full Changelog**: https://github.com/Jesssullivan/tummycrypt/compare/v0.9.3...v0.11.0"
        },
        {
          "tag": "0.10.0",
          "date": "2026-04-05T22:03:39Z",
          "body": "## tcfs v0.10.0\n\n### Quick Install\n\n**macOS (.pkg installer — recommended):**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.10.0/tcfs-0.10.0-macos-aarch64.pkg\nsudo installer -pkg tcfs-0.10.0-macos-aarch64.pkg -target /\n```\n\n**Linux / macOS (shell script):**\n```bash\ncurl -fsSL https://github.com/tinyland-inc/tummycrypt/releases/download/v0.10.0/install.sh | sh\n```\n\n**Homebrew:**\n```bash\nbrew install tinyland-inc/tap/tcfs\n```\n\n**Container image:**\n```bash\npodman pull ghcr.io/tinyland-inc/tcfsd:v0.10.0\n```\n\n**Debian/Ubuntu:**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.10.0/tcfs-0.10.0-amd64.deb\nsudo dpkg -i tcfs-0.10.0-amd64.deb\n```\n\n**RPM (Fedora/RHEL/Rocky):**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.10.0/tcfsd-0.10.0-x86_64.rpm\nsudo rpm -i tcfsd-0.10.0-x86_64.rpm\n```\n\n### Verify checksums\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.10.0/SHA256SUMS.txt\nsha256sum -c SHA256SUMS.txt\n```\n\n### Verify signature (Sigstore Cosign)\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.10.0/SHA256SUMS.txt.sig\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.10.0/SHA256SUMS.txt.pem\ncosign verify-blob \\\n  --signature SHA256SUMS.txt.sig \\\n  --certificate SHA256SUMS.txt.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp 'github.com/tinyland-inc/tummycrypt' \\\n  SHA256SUMS.txt\n```\n\n### macOS binaries\nAll macOS binaries are signed with Developer ID and notarized by Apple.\nThe FileProvider extension (`TCFSProvider.app`) is stapled for offline verification.\n\n### Binaries included\n| Binary | Purpose |\n|--------|---------|\n| `tcfs` | CLI: push, pull, sync-status, mount, unsync |\n| `tcfsd` | Daemon: gRPC socket, FUSE, Prometheus metrics |\n| `tcfs-tui` | Terminal UI for interactive management |\n| `tcfs-mcp` | MCP server for AI agent integration |\n\n\n## What's Changed\n* fix(fileprovider): add TcfsChangeEvent to cbindgen exports by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/72\n* fix(fileprovider): fix EDEADLK hydration deadlock + CLI pull prefix by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/73\n* feat(ios): QR enrollment view for alpha onboarding by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/74\n* fix(ios): wrap conditional body in Group for sheet modifier by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/75\n* feat(ios): fix QR enrollment + encryption + deep links by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/76\n* chore(ios): bump build number to 5 by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/77\n* fix(ios): ATS exception + credential form + TOTP handoff by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/78\n* feat(auth): credential broker for zero-touch enrollment by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/79\n* fix(ios): use build setting variables for version in Info.plist by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/80\n* fix(ios): add version + ATS to xcodegen properties by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/81\n* chore(ios): auto-skip encryption compliance prompt by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/82\n* feat(ios): build info section with git SHA by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/83\n* fix(ios): auto-unlock keychain in pipeline for SSH by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/84\n* chore(ios): bump to v1.1.0 build 9 by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/85\n* feat(auth): compact QR encoding for device enrollment by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/86\n* refactor(tcfs): retire FUSE crates — remove 11,418 LOC by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/87\n* style: cargo fmt by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/89\n* fix(tcfsd): skip directories in watcher push path by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/88\n* fix(nfs): use sudo for NFS mount on Linux by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/90\n* fix(grpc): run NFS mount in-process instead of subprocess by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/91\n* fix(nfs): add diagnostic logging for NFS server exit by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/92\n* fix(nfs): detect task panics via JoinHandle watcher by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/93\n* fix(nfs): add 10s timeout to VFS/S3 calls in NFS adapter by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/94\n* feat(mount): resurrect FUSE3 as default, NFS as fallback by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/95\n* chore: regenerate Cargo.lock with fuse3 dependency by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/96\n* chore: regenerate Cargo.lock with fuse3 dependency by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/97\n* fix(vfs): add diagnostic logging to hydration path by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/98\n* fix(vfs): diagnostic logging for hydration path by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/99\n* fix(vfs): version bump 0.9.1 + hydration diagnostics by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/100\n* fix(vfs): parse JSON v2 manifests in hydration path by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/101\n* fix(flake): disable stale attic binary cache by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/102\n* fix(flake): disable stale attic cache by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/103\n* fix: add missing serde_json to Cargo.lock by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/104\n* fix: add missing serde_json to Cargo.lock by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/105\n* feat(vfs): read-write FUSE support by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/106\n* fix: add blake3 to Cargo.lock by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/107\n* fix: add blake3 to Cargo.lock by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/108\n* feat(vfs): FastCDC chunking for FUSE writes by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/109\n* feat(vfs): FastCDC chunking for FUSE writes by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/110\n* fix(daemon): index-first auto-pull (skip FUSE mount writes) by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/111\n* feat: NATS publish on FUSE write + index-first auto-pull by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/112\n\n\n**Full Changelog**: https://github.com/tinyland-inc/tummycrypt/compare/v0.9.3...v0.10.0"
        },
        {
          "tag": "0.9.3",
          "date": "2026-03-09T23:18:42Z",
          "body": "## tcfs v0.9.3\n\n### Quick Install\n\n**macOS (.pkg installer — recommended):**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.3/tcfs-0.9.3-macos-aarch64.pkg\nsudo installer -pkg tcfs-0.9.3-macos-aarch64.pkg -target /\n```\n\n**Linux / macOS (shell script):**\n```bash\ncurl -fsSL https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.3/install.sh | sh\n```\n\n**Homebrew:**\n```bash\nbrew install tinyland-inc/tap/tcfs\n```\n\n**Container image:**\n```bash\npodman pull ghcr.io/tinyland-inc/tcfsd:v0.9.3\n```\n\n**Debian/Ubuntu:**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.3/tcfs-0.9.3-amd64.deb\nsudo dpkg -i tcfs-0.9.3-amd64.deb\n```\n\n**RPM (Fedora/RHEL/Rocky):**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.3/tcfsd-0.9.3-x86_64.rpm\nsudo rpm -i tcfsd-0.9.3-x86_64.rpm\n```\n\n### Verify checksums\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.3/SHA256SUMS.txt\nsha256sum -c SHA256SUMS.txt\n```\n\n### Verify signature (Sigstore Cosign)\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.3/SHA256SUMS.txt.sig\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.3/SHA256SUMS.txt.pem\ncosign verify-blob \\\n  --signature SHA256SUMS.txt.sig \\\n  --certificate SHA256SUMS.txt.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp 'github.com/tinyland-inc/tummycrypt' \\\n  SHA256SUMS.txt\n```\n\n### macOS binaries\nAll macOS binaries are signed with Developer ID and notarized by Apple.\nThe FileProvider extension (`TCFSProvider.app`) is stapled for offline verification.\n\n### Binaries included\n| Binary | Purpose |\n|--------|---------|\n| `tcfs` | CLI: push, pull, sync-status, mount, unsync |\n| `tcfsd` | Daemon: gRPC socket, FUSE, Prometheus metrics |\n| `tcfs-tui` | Terminal UI for interactive management |\n| `tcfs-mcp` | MCP server for AI agent integration |\n\n\n**Full Changelog**: https://github.com/tinyland-inc/tummycrypt/compare/v0.9.2...v0.9.3"
        },
        {
          "tag": "0.9.2",
          "date": "2026-03-09T22:55:56Z",
          "body": "## tcfs v0.9.2\n\n### Quick Install\n\n**macOS (.pkg installer — recommended):**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.2/tcfs-0.9.2-macos-aarch64.pkg\nsudo installer -pkg tcfs-0.9.2-macos-aarch64.pkg -target /\n```\n\n**Linux / macOS (shell script):**\n```bash\ncurl -fsSL https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.2/install.sh | sh\n```\n\n**Homebrew:**\n```bash\nbrew install tinyland-inc/tap/tcfs\n```\n\n**Container image:**\n```bash\npodman pull ghcr.io/tinyland-inc/tcfsd:v0.9.2\n```\n\n**Debian/Ubuntu:**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.2/tcfs-0.9.2-amd64.deb\nsudo dpkg -i tcfs-0.9.2-amd64.deb\n```\n\n**RPM (Fedora/RHEL/Rocky):**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.2/tcfsd-0.9.2-x86_64.rpm\nsudo rpm -i tcfsd-0.9.2-x86_64.rpm\n```\n\n### Verify checksums\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.2/SHA256SUMS.txt\nsha256sum -c SHA256SUMS.txt\n```\n\n### Verify signature (Sigstore Cosign)\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.2/SHA256SUMS.txt.sig\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.2/SHA256SUMS.txt.pem\ncosign verify-blob \\\n  --signature SHA256SUMS.txt.sig \\\n  --certificate SHA256SUMS.txt.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp 'github.com/tinyland-inc/tummycrypt' \\\n  SHA256SUMS.txt\n```\n\n### macOS binaries\nAll macOS binaries are signed with Developer ID and notarized by Apple.\nThe FileProvider extension (`TCFSProvider.app`) is stapled for offline verification.\n\n### Binaries included\n| Binary | Purpose |\n|--------|---------|\n| `tcfs` | CLI: push, pull, sync-status, mount, unsync |\n| `tcfsd` | Daemon: gRPC socket, FUSE, Prometheus metrics |\n| `tcfs-tui` | Terminal UI for interactive management |\n| `tcfs-mcp` | MCP server for AI agent integration |\n\n\n## What's Changed\n* fix(ci): resolve remaining release build failures by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/71\n\n\n**Full Changelog**: https://github.com/tinyland-inc/tummycrypt/compare/v0.9.1...v0.9.2"
        },
        {
          "tag": "0.9.1",
          "date": "2026-03-09T22:20:11Z",
          "body": "## tcfs v0.9.1\n\n### Quick Install\n\n**macOS (.pkg installer — recommended):**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.1/tcfs-0.9.1-macos-aarch64.pkg\nsudo installer -pkg tcfs-0.9.1-macos-aarch64.pkg -target /\n```\n\n**Linux / macOS (shell script):**\n```bash\ncurl -fsSL https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.1/install.sh | sh\n```\n\n**Homebrew:**\n```bash\nbrew install tinyland-inc/tap/tcfs\n```\n\n**Container image:**\n```bash\npodman pull ghcr.io/tinyland-inc/tcfsd:v0.9.1\n```\n\n**Debian/Ubuntu:**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.1/tcfs-0.9.1-amd64.deb\nsudo dpkg -i tcfs-0.9.1-amd64.deb\n```\n\n**RPM (Fedora/RHEL/Rocky):**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.1/tcfsd-0.9.1-x86_64.rpm\nsudo rpm -i tcfsd-0.9.1-x86_64.rpm\n```\n\n### Verify checksums\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.1/SHA256SUMS.txt\nsha256sum -c SHA256SUMS.txt\n```\n\n### Verify signature (Sigstore Cosign)\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.1/SHA256SUMS.txt.sig\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.1/SHA256SUMS.txt.pem\ncosign verify-blob \\\n  --signature SHA256SUMS.txt.sig \\\n  --certificate SHA256SUMS.txt.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp 'github.com/tinyland-inc/tummycrypt' \\\n  SHA256SUMS.txt\n```\n\n### macOS binaries\nAll macOS binaries are signed with Developer ID and notarized by Apple.\nThe FileProvider extension (`TCFSProvider.app`) is stapled for offline verification.\n\n### Binaries included\n| Binary | Purpose |\n|--------|---------|\n| `tcfs` | CLI: push, pull, sync-status, mount, unsync |\n| `tcfsd` | Daemon: gRPC socket, FUSE, Prometheus metrics |\n| `tcfs-tui` | Terminal UI for interactive management |\n| `tcfs-mcp` | MCP server for AI agent integration |\n\n\n## What's Changed\n* fix(ci): resolve release workflow build failures by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/70\n\n\n**Full Changelog**: https://github.com/tinyland-inc/tummycrypt/compare/v0.9.0...v0.9.1"
        },
        {
          "tag": "0.9.0",
          "date": "2026-03-09T21:25:38Z",
          "body": "## tcfs v0.9.0\n\n### Quick Install\n\n**macOS (.pkg installer — recommended):**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.0/tcfs-0.9.0-macos-aarch64.pkg\nsudo installer -pkg tcfs-0.9.0-macos-aarch64.pkg -target /\n```\n\n**Linux / macOS (shell script):**\n```bash\ncurl -fsSL https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.0/install.sh | sh\n```\n\n**Homebrew:**\n```bash\nbrew install tinyland-inc/tap/tcfs\n```\n\n**Container image:**\n```bash\npodman pull ghcr.io/tinyland-inc/tcfsd:v0.9.0\n```\n\n**Debian/Ubuntu:**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.0/tcfs-0.9.0-amd64.deb\nsudo dpkg -i tcfs-0.9.0-amd64.deb\n```\n\n**RPM (Fedora/RHEL/Rocky):**\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.0/tcfsd-0.9.0-x86_64.rpm\nsudo rpm -i tcfsd-0.9.0-x86_64.rpm\n```\n\n### Verify checksums\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.0/SHA256SUMS.txt\nsha256sum -c SHA256SUMS.txt\n```\n\n### Verify signature (Sigstore Cosign)\n```bash\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.0/SHA256SUMS.txt.sig\ncurl -LO https://github.com/tinyland-inc/tummycrypt/releases/download/v0.9.0/SHA256SUMS.txt.pem\ncosign verify-blob \\\n  --signature SHA256SUMS.txt.sig \\\n  --certificate SHA256SUMS.txt.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp 'github.com/tinyland-inc/tummycrypt' \\\n  SHA256SUMS.txt\n```\n\n### macOS binaries\nAll macOS binaries are signed with Developer ID and notarized by Apple.\nThe FileProvider extension (`TCFSProvider.app`) is stapled for offline verification.\n\n### Binaries included\n| Binary | Purpose |\n|--------|---------|\n| `tcfs` | CLI: push, pull, sync-status, mount, unsync |\n| `tcfsd` | Daemon: gRPC socket, FUSE, Prometheus metrics |\n| `tcfs-tui` | Terminal UI for interactive management |\n| `tcfs-mcp` | MCP server for AI agent integration |\n\n\n## What's Changed\n* feat: UniFFI bindings for iOS FileProvider (Phase 7a) by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/62\n* feat(ios): scaffold iOS FileProvider project (Phase 7b) by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/63\n* feat(ios): end-to-end xcodebuild pipeline via xcodegen by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/64\n* feat(ios): progress reporting + sync status (Phase 7e) by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/65\n* feat(ios): TestFlight pipeline + simulator fixes by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/66\n* feat(fuse): FUSE-T support for macOS by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/67\n* feat(vfs): extract VirtualFilesystem trait + NFS loopback server by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/68\n* chore: bump workspace version to 0.9.0 by @Jesssullivan in https://github.com/tinyland-inc/tummycrypt/pull/69\n\n\n**Full Changelog**: https://github.com/tinyland-inc/tummycrypt/compare/v0.8.1...v0.9.0"
        }
      ],
      "stars": 3,
      "topics": [
        "encryption",
        "nats",
        "rust",
        "seaweedfs",
        "self-hosted",
        "sync",
        "cleanroom-engineering"
      ],
      "languages": [
        {
          "name": "Rust",
          "color": "#dea584",
          "bytes": 1689288
        },
        {
          "name": "Swift",
          "color": "#F05138",
          "bytes": 214544
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 93021
        },
        {
          "name": "HCL",
          "color": "#844FBA",
          "bytes": 50310
        },
        {
          "name": "C",
          "color": "#555555",
          "bytes": 33641
        }
      ],
      "primary_language": "Rust",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">tummycrypt / tcfs</h1><a id=\"user-content-tummycrypt--tcfs\" class=\"anchor\" aria-label=\"Permalink: tummycrypt / tcfs\" href=\"#tummycrypt--tcfs\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<blockquote>\n<p dir=\"auto\">Under active development. Not yet stable. Expect breaking changes.</p>\n</blockquote>\n<p dir=\"auto\">Self-hosted encrypted file sync with on-demand hydration. Mounts SeaweedFS as a local directory — files appear as zero-byte <code>.tc</code> stubs until accessed, then transparently download and decrypt. FOSS odrive/Dropbox replacement.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Canonical Home</h2><a id=\"user-content-canonical-home\" class=\"anchor\" aria-label=\"Permalink: Canonical Home\" href=\"#canonical-home\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><code>Jesssullivan/tummycrypt</code> is the canonical source repository for tcfs.\nIf <code>tinyland-inc/tummycrypt</code> exists, treat it as a fork or downstream\ndistribution surface rather than the source of truth for planning, issues,\nreleases, or contributor workflow.</p>\n<p dir=\"auto\">Operational policy: <a href=\"docs/ops/remote-governance.md\"><code>docs/ops/remote-governance.md</code></a>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Features</h2><a id=\"user-content-features\" class=\"anchor\" aria-label=\"Permalink: Features\" href=\"#features\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>On-demand hydration</strong>: Files appear as <code>.tc</code> stubs, hydrate transparently on open</li>\n<li><strong>E2E encryption</strong>: XChaCha20-Poly1305 per-chunk, Argon2id KDF, BIP-39 recovery keys</li>\n<li><strong>Fleet sync</strong>: Multi-machine sync via NATS JetStream with vector clock conflict detection</li>\n<li><strong>Content-addressed storage</strong>: FastCDC chunking, BLAKE3 hashing, zstd compression</li>\n<li><strong>Git-safe</strong>: Syncs <code>.git/</code> directories as atomic bundles with lock detection</li>\n<li><strong>Cross-platform</strong>: Linux is the best-supported runtime; macOS has packaged but still experimental desktop surfaces; Windows remains planned</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick Start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick Start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Nix devShell (recommended)\nnix develop\n# Or auto-load the committed devShell + env on cd\ndirenv allow\n\n# Or manual: install the pinned Rust 1.93.0 toolchain, protobuf compiler, fuse3 (Linux)\n\n# Start local dev infrastructure (SeaweedFS + NATS + Prometheus + Grafana)\ntask dev\n\n# Build + test\ntask check\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Nix devShell (recommended)</span>\nnix develop\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Or auto-load the committed devShell + env on cd</span>\ndirenv allow\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Or manual: install the pinned Rust 1.93.0 toolchain, protobuf compiler, fuse3 (Linux)</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Start local dev infrastructure (SeaweedFS + NATS + Prometheus + Grafana)</span>\ntask dev\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Build + test</span>\ntask check</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Installation</h2><a id=\"user-content-installation\" class=\"anchor\" aria-label=\"Permalink: Installation\" href=\"#installation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Linux/macOS tarball convenience installer\n# Fast CLI install, but not part of the canonical release-proof surface.\ncurl -fsSL https://github.com/Jesssullivan/tummycrypt/releases/latest/download/install.sh | sh\n\n# macOS (Homebrew, current manual tap flow)\nbrew tap --custom-remote Jesssullivan/tummycrypt https://github.com/Jesssullivan/tummycrypt.git\ngit -C &quot;$(brew --repo Jesssullivan/tummycrypt)&quot; fetch origin homebrew-tap\ngit -C &quot;$(brew --repo Jesssullivan/tummycrypt)&quot; checkout homebrew-tap\nbrew install Jesssullivan/tummycrypt/tcfs\n\n# Debian/Ubuntu\nsudo dpkg -i tcfsd-*.deb tcfs-*.deb\n\n# RPM (Fedora/RHEL/Rocky, daemon-only today)\nsudo rpm -i tcfsd-*.rpm\n\n# Container (K8s worker mode)\npodman pull ghcr.io/jesssullivan/tcfsd:latest\n\n# Nix\nnix build github:Jesssullivan/tummycrypt\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Linux/macOS tarball convenience installer</span>\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Fast CLI install, but not part of the canonical release-proof surface.</span>\ncurl -fsSL https://github.com/Jesssullivan/tummycrypt/releases/latest/download/install.sh <span class=\"pl-k\">|</span> sh\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> macOS (Homebrew, current manual tap flow)</span>\nbrew tap --custom-remote Jesssullivan/tummycrypt https://github.com/Jesssullivan/tummycrypt.git\ngit -C <span class=\"pl-s\"><span class=\"pl-pds\">\"</span><span class=\"pl-s\"><span class=\"pl-pds\">$(</span>brew --repo Jesssullivan/tummycrypt<span class=\"pl-pds\">)</span></span><span class=\"pl-pds\">\"</span></span> fetch origin homebrew-tap\ngit -C <span class=\"pl-s\"><span class=\"pl-pds\">\"</span><span class=\"pl-s\"><span class=\"pl-pds\">$(</span>brew --repo Jesssullivan/tummycrypt<span class=\"pl-pds\">)</span></span><span class=\"pl-pds\">\"</span></span> checkout homebrew-tap\nbrew install Jesssullivan/tummycrypt/tcfs\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Debian/Ubuntu</span>\nsudo dpkg -i tcfsd-<span class=\"pl-k\">*</span>.deb tcfs-<span class=\"pl-k\">*</span>.deb\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> RPM (Fedora/RHEL/Rocky, daemon-only today)</span>\nsudo rpm -i tcfsd-<span class=\"pl-k\">*</span>.rpm\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Container (K8s worker mode)</span>\npodman pull ghcr.io/jesssullivan/tcfsd:latest\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Nix</span>\nnix build github:Jesssullivan/tummycrypt</pre></div>\n<p dir=\"auto\">For the supported post-release proof contract across Homebrew, <code>.pkg</code>, <code>.deb</code>,\n<code>.rpm</code>, container, and Nix, see\n<a href=\"docs/ops/distribution-smoke-matrix.md\">docs/ops/distribution-smoke-matrix.md</a>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">CLI</h2><a id=\"user-content-cli\" class=\"anchor\" aria-label=\"Permalink: CLI\" href=\"#cli\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"tcfs status                    # Daemon status, device identity, NATS connection\ntcfs push &lt;path&gt;               # Upload with encryption + vector clock tick\ntcfs pull &lt;manifest&gt; &lt;local&gt;   # Download with conflict detection + decryption\ntcfs mount &lt;remote&gt; &lt;target&gt;   # FUSE mount with on-demand hydration\ntcfs unsync &lt;path&gt;             # Convert hydrated file back to .tc stub\ntcfs device enroll             # Register device with age keypair\ntcfs device list               # Show enrolled fleet devices\"><pre>tcfs status                    <span class=\"pl-c\"><span class=\"pl-c\">#</span> Daemon status, device identity, NATS connection</span>\ntcfs push <span class=\"pl-k\">&lt;</span>path<span class=\"pl-k\">&gt;</span>               <span class=\"pl-c\"><span class=\"pl-c\">#</span> Upload with encryption + vector clock tick</span>\ntcfs pull <span class=\"pl-k\">&lt;</span>manifest<span class=\"pl-k\">&gt;</span> <span class=\"pl-k\">&lt;</span>local<span class=\"pl-k\">&gt;</span>   <span class=\"pl-c\"><span class=\"pl-c\">#</span> Download with conflict detection + decryption</span>\ntcfs mount <span class=\"pl-k\">&lt;</span>remote<span class=\"pl-k\">&gt;</span> <span class=\"pl-k\">&lt;</span>target<span class=\"pl-k\">&gt;</span>   <span class=\"pl-c\"><span class=\"pl-c\">#</span> FUSE mount with on-demand hydration</span>\ntcfs unsync <span class=\"pl-k\">&lt;</span>path<span class=\"pl-k\">&gt;</span>             <span class=\"pl-c\"><span class=\"pl-c\">#</span> Convert hydrated file back to .tc stub</span>\ntcfs device enroll             <span class=\"pl-c\"><span class=\"pl-c\">#</span> Register device with age keypair</span>\ntcfs device list               <span class=\"pl-c\"><span class=\"pl-c\">#</span> Show enrolled fleet devices</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Binaries</h2><a id=\"user-content-binaries\" class=\"anchor\" aria-label=\"Permalink: Binaries\" href=\"#binaries\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Binary</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>tcfs</code></td>\n<td>CLI: push, pull, mount, unsync, device management</td>\n</tr>\n<tr>\n<td><code>tcfsd</code></td>\n<td>Daemon: gRPC, FUSE mounts, NATS fleet sync, Prometheus metrics</td>\n</tr>\n<tr>\n<td><code>tcfs-tui</code></td>\n<td>Terminal UI: dashboard with sync status, conflicts, mounts</td>\n</tr>\n<tr>\n<td><code>tcfs-mcp</code></td>\n<td>MCP server: AI agent integration (8 tools, stdio transport)</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Architecture</h2><a id=\"user-content-architecture\" class=\"anchor\" aria-label=\"Permalink: Architecture\" href=\"#architecture\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">18 workspace crates organized in layers:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"crates/\n├── tcfs-core/           # Shared types, config, protobuf (gRPC service)\n├── tcfs-crypto/         # XChaCha20-Poly1305, Argon2id, HKDF, BIP-39\n├── tcfs-secrets/        # SOPS/age decryption, KeePassXC, device identity\n├── tcfs-storage/        # OpenDAL S3/SeaweedFS operator + health checks\n├── tcfs-chunks/         # FastCDC chunking, BLAKE3 hashing, zstd compression\n├── tcfs-sync/           # Sync engine, vector clocks, NATS JetStream, reconciliation\n├── tcfs-auth/           # TOTP, WebAuthn/FIDO2, device enrollment\n├── tcfs-vfs/            # Virtual filesystem: hydration, disk cache, negative cache\n├── tcfs-fuse/           # Linux FUSE3 driver\n├── tcfs-nfs/            # NFS loopback mount (no kernel modules)\n├── tcfs-cloudfilter/    # Windows Cloud Files API (planned)\n├── tcfs-file-provider/  # macOS/iOS FileProvider FFI (cbindgen + UniFFI)\n├── tcfs-sops/           # SOPS+age fleet secret propagation\n├── tcfs-dbus/           # D-Bus integration (Linux)\n├── tcfsd/               # Daemon binary (gRPC + metrics + systemd)\n├── tcfs-cli/            # CLI binary\n├── tcfs-tui/            # Terminal UI (ratatui)\n└── tcfs-mcp/            # MCP server (rmcp, stdio transport)\"><pre class=\"notranslate\"><code>crates/\n├── tcfs-core/           # Shared types, config, protobuf (gRPC service)\n├── tcfs-crypto/         # XChaCha20-Poly1305, Argon2id, HKDF, BIP-39\n├── tcfs-secrets/        # SOPS/age decryption, KeePassXC, device identity\n├── tcfs-storage/        # OpenDAL S3/SeaweedFS operator + health checks\n├── tcfs-chunks/         # FastCDC chunking, BLAKE3 hashing, zstd compression\n├── tcfs-sync/           # Sync engine, vector clocks, NATS JetStream, reconciliation\n├── tcfs-auth/           # TOTP, WebAuthn/FIDO2, device enrollment\n├── tcfs-vfs/            # Virtual filesystem: hydration, disk cache, negative cache\n├── tcfs-fuse/           # Linux FUSE3 driver\n├── tcfs-nfs/            # NFS loopback mount (no kernel modules)\n├── tcfs-cloudfilter/    # Windows Cloud Files API (planned)\n├── tcfs-file-provider/  # macOS/iOS FileProvider FFI (cbindgen + UniFFI)\n├── tcfs-sops/           # SOPS+age fleet secret propagation\n├── tcfs-dbus/           # D-Bus integration (Linux)\n├── tcfsd/               # Daemon binary (gRPC + metrics + systemd)\n├── tcfs-cli/            # CLI binary\n├── tcfs-tui/            # Terminal UI (ratatui)\n└── tcfs-mcp/            # MCP server (rmcp, stdio transport)\n</code></pre></div>\n<p dir=\"auto\">See <a href=\"docs/ARCHITECTURE.md\">docs/ARCHITECTURE.md</a> for the full system design.</p>\n<p dir=\"auto\">For packaged release proof across Homebrew, <code>.pkg</code>, <code>.deb</code>, <code>.rpm</code>, container,\nand Nix surfaces, see <a href=\"docs/ops/distribution-smoke-matrix.md\">docs/ops/distribution-smoke-matrix.md</a>.\nFor the bar after install succeeds, see\n<a href=\"docs/ops/packaged-install-first-use.md\">docs/ops/packaged-install-first-use.md</a>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Platform Support</h2><a id=\"user-content-platform-support\" class=\"anchor\" aria-label=\"Permalink: Platform Support\" href=\"#platform-support\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Linux</th>\n<th>macOS</th>\n<th>Windows</th>\n<th>iOS</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>CLI (push/pull/reconcile)</td>\n<td>Full</td>\n<td>Full</td>\n<td>Planned</td>\n<td>-</td>\n</tr>\n<tr>\n<td>Daemon (gRPC + metrics)</td>\n<td>Full</td>\n<td>Available, lightly validated</td>\n<td>Planned</td>\n<td>-</td>\n</tr>\n<tr>\n<td>Filesystem mount</td>\n<td>Full (FUSE3, NFS fallback)</td>\n<td>Experimental</td>\n<td>Cloud Files API (skeleton)</td>\n<td>-</td>\n</tr>\n<tr>\n<td>FileProvider</td>\n<td>-</td>\n<td>Experimental</td>\n<td>-</td>\n<td>Proof-of-concept, read-only</td>\n</tr>\n<tr>\n<td>Finder/Explorer badges</td>\n<td>-</td>\n<td>Experimental</td>\n<td>-</td>\n<td>-</td>\n</tr>\n<tr>\n<td>D-Bus integration</td>\n<td>Full</td>\n<td>-</td>\n<td>-</td>\n<td>-</td>\n</tr>\n<tr>\n<td>Fleet sync (NATS)</td>\n<td>Full</td>\n<td>Core path available, not continuously acceptance-tested</td>\n<td>Planned</td>\n<td>-</td>\n</tr>\n<tr>\n<td>E2E encryption</td>\n<td>Full</td>\n<td>Full</td>\n<td>Planned</td>\n<td>Core crypto path available</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">See <a href=\"docs/platform-support.md\">docs/platform-support.md</a> for details.\nFor the dated Apple posture, see\n<a href=\"docs/ops/apple-surface-status.md\">docs/ops/apple-surface-status.md</a>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Development</h2><a id=\"user-content-development\" class=\"anchor\" aria-label=\"Permalink: Development\" href=\"#development\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"task build          # Build all crates\ntask test           # Run all tests (424 tests)\ntask lint           # Clippy + rustfmt\ntask deny           # License + advisory check\ntask check          # All of the above\"><pre>task build          <span class=\"pl-c\"><span class=\"pl-c\">#</span> Build all crates</span>\ntask <span class=\"pl-c1\">test</span>           <span class=\"pl-c\"><span class=\"pl-c\">#</span> Run all tests (424 tests)</span>\ntask lint           <span class=\"pl-c\"><span class=\"pl-c\">#</span> Clippy + rustfmt</span>\ntask deny           <span class=\"pl-c\"><span class=\"pl-c\">#</span> License + advisory check</span>\ntask check          <span class=\"pl-c\"><span class=\"pl-c\">#</span> All of the above</span></pre></div>\n<p dir=\"auto\">See <a href=\"docs/CONTRIBUTING.md\">docs/CONTRIBUTING.md</a> for setup details and PR workflow.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Credential Setup</h2><a id=\"user-content-credential-setup\" class=\"anchor\" aria-label=\"Permalink: Credential Setup\" href=\"#credential-setup\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"task sops:init       # Generate age key + configure .sops.yaml\ntask sops:migrate    # Migrate credentials to SOPS-encrypted files\"><pre>task sops:init       <span class=\"pl-c\"><span class=\"pl-c\">#</span> Generate age key + configure .sops.yaml</span>\ntask sops:migrate    <span class=\"pl-c\"><span class=\"pl-c\">#</span> Migrate credentials to SOPS-encrypted files</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">MIT OR Apache-2.0</p>\n</article></div>",
      "readme_excerpt": "> Under active development. Not yet stable. Expect breaking changes.\nSelf-hosted encrypted file sync with on-demand hydration. Mounts SeaweedFS as a local directory — files appear as zero-byte .tc stubs until accessed, then transparently download and decrypt. FOSS odrive/Dropbox replacement.\nJesssullivan/tummycrypt is the canonical source repository for tcfs.\nIf tinyland-inc/tummycrypt exists, treat it as a fork or downstream\ndistribution surface rather than the source of truth for planning,...",
      "install_commands": {
        "rust": "cargo add tummycrypt"
      },
      "repo_url": "https://github.com/jesssullivan/tummycrypt",
      "website_url": "https://tinyland-inc.github.io/tummycrypt",
      "docs_url": null,
      "registry_url": "https://crates.io/crates/tummycrypt",
      "releases_url": "https://github.com/jesssullivan/tummycrypt/releases",
      "og_image_url": "https://opengraph.githubassets.com/14b8de50aa9e1a473d43e19cec8ca28cf8bc823e521ac024fbe6562296d8a481/Jesssullivan/tummycrypt",
      "license": "Apache-2.0",
      "pushed_at": "2026-04-18T14:39:58Z",
      "enriched_at": "2026-04-26T17:17:16.321Z",
      "gate": "homepage"
    },
    {
      "slug": "jesssullivan-tinyland-huskycat",
      "name": "tinyland-huskycat",
      "repo": "jesssullivan/tinyland-huskycat",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "ai-tools",
      "description": "A multimodal, deterministic verification middleware for unsupervised, domain-driven iteration",
      "featured": false,
      "tags": [
        "testing",
        "tdd",
        "automation"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [
        "asychronous",
        "autoverification",
        "domain-driven-design",
        "githook",
        "parallel-tests",
        "tdd",
        "validator"
      ],
      "languages": [
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 1504987
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 101742
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 24572
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 4626
        },
        {
          "name": "HTML",
          "color": "#e34c26",
          "bytes": 4026
        }
      ],
      "primary_language": "Python",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">huskycat</h1><a id=\"user-content-huskycat\" class=\"anchor\" aria-label=\"Permalink: huskycat\" href=\"#huskycat\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Universal Code Validation Platform with MCP Server Integration.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Install</h2><a id=\"user-content-install\" class=\"anchor\" aria-label=\"Permalink: Install\" href=\"#install\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pip install huskycat\"><pre>pip install huskycat</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Apache-2.0</p>\n</article></div>",
      "readme_excerpt": "Universal Code Validation Platform with MCP Server Integration.\nbash\npip install huskycat\nApache-2.0",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/tinyland-huskycat",
      "website_url": "https://gitlab.com/tinyland/ai/huskycat",
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/tinyland-huskycat/releases",
      "og_image_url": "https://opengraph.githubassets.com/a303ac7749e9468d32320f6f35a3b0712b54409c416157d6b7598af97897ea63/Jesssullivan/tinyland-huskycat",
      "license": "Zlib",
      "pushed_at": "2026-04-15T15:00:43Z",
      "enriched_at": "2026-04-26T17:17:16.629Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-tinyland-auth-redis",
      "name": "tinyland-auth-redis",
      "repo": "jesssullivan/tinyland-auth-redis",
      "org": "jesssullivan",
      "ecosystem": "npm",
      "category": "auth",
      "description": "Redis storage adapter for @tummycrypt/tinyland-auth (Upstash)",
      "featured": false,
      "tags": [
        "auth",
        "redis",
        "upstash",
        "sessions"
      ],
      "version": "0.1.1",
      "release_date": "2026-03-30T03:13:50Z",
      "releases": [
        {
          "tag": "0.1.1",
          "date": "2026-03-30T03:13:50Z",
          "body": "Fix publishConfig access, add release trigger, test gate, npm provenance."
        }
      ],
      "stars": 0,
      "topics": [],
      "languages": [
        {
          "name": "TypeScript",
          "color": "#3178c6",
          "bytes": 62114
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 810
        },
        {
          "name": "Starlark",
          "color": "#76d275",
          "bytes": 667
        }
      ],
      "primary_language": "TypeScript",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">@tummycrypt/tinyland-auth-redis</h1><a id=\"user-content-tummycrypttinyland-auth-redis\" class=\"anchor\" aria-label=\"Permalink: @tummycrypt/tinyland-auth-redis\" href=\"#tummycrypttinyland-auth-redis\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Redis storage adapter for <a href=\"https://github.com/Jesssullivan/tinyland-auth\">@tummycrypt/tinyland-auth</a>, backed by <a href=\"https://upstash.com/\" rel=\"nofollow\">Upstash Redis</a> (<code>@upstash/redis</code>).</p>\n<p dir=\"auto\">Implements the full <code>IStorageAdapter</code> interface: users, sessions, TOTP secrets, backup codes, invitations, and audit events.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Installation</h2><a id=\"user-content-installation\" class=\"anchor\" aria-label=\"Permalink: Installation\" href=\"#installation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"npm install @tummycrypt/tinyland-auth-redis\n# or\npnpm add @tummycrypt/tinyland-auth-redis\"><pre>npm install @tummycrypt/tinyland-auth-redis\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> or</span>\npnpm add @tummycrypt/tinyland-auth-redis</pre></div>\n<p dir=\"auto\">Peer dependency:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"npm install @tummycrypt/tinyland-auth\"><pre>npm install @tummycrypt/tinyland-auth</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick Start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick Start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { createRedisStorageAdapter } from '@tummycrypt/tinyland-auth-redis';\n\nconst storage = createRedisStorageAdapter({\n  url: process.env.KV_REST_API_URL,\n  token: process.env.KV_REST_API_TOKEN,\n  prefix: 'auth',           // optional, default: 'auth'\n  sessionMaxAge: 7 * 24 * 60 * 60 * 1000, // optional, default: 7 days\n});\n\nawait storage.init(); // verifies connectivity with PING\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">createRedisStorageAdapter</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/tinyland-auth-redis'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">storage</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createRedisStorageAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">url</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">KV_REST_API_URL</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">token</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">KV_REST_API_TOKEN</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">prefix</span>: <span class=\"pl-s\">'auth'</span><span class=\"pl-kos\">,</span>           <span class=\"pl-c\">// optional, default: 'auth'</span>\n  <span class=\"pl-c1\">sessionMaxAge</span>: <span class=\"pl-c1\">7</span> <span class=\"pl-c1\">*</span> <span class=\"pl-c1\">24</span> <span class=\"pl-c1\">*</span> <span class=\"pl-c1\">60</span> <span class=\"pl-c1\">*</span> <span class=\"pl-c1\">60</span> <span class=\"pl-c1\">*</span> <span class=\"pl-c1\">1000</span><span class=\"pl-kos\">,</span> <span class=\"pl-c\">// optional, default: 7 days</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">await</span> <span class=\"pl-s1\">storage</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">init</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span> <span class=\"pl-c\">// verifies connectivity with PING</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Using an Existing Redis Instance</h3><a id=\"user-content-using-an-existing-redis-instance\" class=\"anchor\" aria-label=\"Permalink: Using an Existing Redis Instance\" href=\"#using-an-existing-redis-instance\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { Redis } from '@upstash/redis';\nimport { createRedisStorageAdapter } from '@tummycrypt/tinyland-auth-redis';\n\nconst redis = new Redis({\n  url: process.env.KV_REST_API_URL!,\n  token: process.env.KV_REST_API_TOKEN!,\n});\n\nconst storage = createRedisStorageAdapter({ redis });\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-v\">Redis</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@upstash/redis'</span><span class=\"pl-kos\">;</span>\n<span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">createRedisStorageAdapter</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/tinyland-auth-redis'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">redis</span> <span class=\"pl-c1\">=</span> <span class=\"pl-k\">new</span> <span class=\"pl-v\">Redis</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span>\n  <span class=\"pl-c1\">url</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">KV_REST_API_URL</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>\n  <span class=\"pl-c1\">token</span>: <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">KV_REST_API_TOKEN</span><span class=\"pl-c1\">!</span><span class=\"pl-kos\">,</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">storage</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createRedisStorageAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span> redis <span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Graceful Fallback</h2><a id=\"user-content-graceful-fallback\" class=\"anchor\" aria-label=\"Permalink: Graceful Fallback\" href=\"#graceful-fallback\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">When Redis is unavailable (e.g., local development without Upstash credentials), fall back to an in-memory Map:</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { createRedisStorageAdapter } from '@tummycrypt/tinyland-auth-redis';\n\nconst createStorage = () =&gt; {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (url &amp;&amp; token) {\n    return createRedisStorageAdapter({ url, token });\n  }\n\n  console.warn('Redis not configured, using in-memory fallback');\n  // Use your own in-memory adapter or the one from tinyland-auth\n  return createInMemoryAdapter();\n};\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">createRedisStorageAdapter</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/tinyland-auth-redis'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-en\">createStorage</span> <span class=\"pl-c1\">=</span> <span class=\"pl-kos\">(</span><span class=\"pl-kos\">)</span> <span class=\"pl-c1\">=&gt;</span> <span class=\"pl-kos\">{</span>\n  <span class=\"pl-k\">const</span> <span class=\"pl-s1\">url</span> <span class=\"pl-c1\">=</span> <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">KV_REST_API_URL</span><span class=\"pl-kos\">;</span>\n  <span class=\"pl-k\">const</span> <span class=\"pl-s1\">token</span> <span class=\"pl-c1\">=</span> <span class=\"pl-s1\">process</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">env</span><span class=\"pl-kos\">.</span><span class=\"pl-c1\">KV_REST_API_TOKEN</span><span class=\"pl-kos\">;</span>\n\n  <span class=\"pl-k\">if</span> <span class=\"pl-kos\">(</span><span class=\"pl-s1\">url</span> <span class=\"pl-c1\">&amp;&amp;</span> <span class=\"pl-s1\">token</span><span class=\"pl-kos\">)</span> <span class=\"pl-kos\">{</span>\n    <span class=\"pl-k\">return</span> <span class=\"pl-en\">createRedisStorageAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">{</span> url<span class=\"pl-kos\">,</span> token <span class=\"pl-kos\">}</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n  <span class=\"pl-kos\">}</span>\n\n  <span class=\"pl-smi\">console</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">warn</span><span class=\"pl-kos\">(</span><span class=\"pl-s\">'Redis not configured, using in-memory fallback'</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n  <span class=\"pl-c\">// Use your own in-memory adapter or the one from tinyland-auth</span>\n  <span class=\"pl-k\">return</span> <span class=\"pl-en\">createInMemoryAdapter</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n<span class=\"pl-kos\">}</span><span class=\"pl-kos\">;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Key Namespacing</h2><a id=\"user-content-key-namespacing\" class=\"anchor\" aria-label=\"Permalink: Key Namespacing\" href=\"#key-namespacing\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">All keys are namespaced under a configurable prefix (default: <code>auth</code>). This allows multiple applications to share a single Redis instance without collisions.</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Pattern</th>\n<th>Example</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>{prefix}:user:{id}</code></td>\n<td><code>auth:user:abc-123</code></td>\n<td>User entity</td>\n</tr>\n<tr>\n<td><code>{prefix}:user:handle:{handle}</code></td>\n<td><code>auth:user:handle:jen</code></td>\n<td>Handle lookup index</td>\n</tr>\n<tr>\n<td><code>{prefix}:user:email:{email}</code></td>\n<td><code>auth:user:email:jen@example.com</code></td>\n<td>Email lookup index</td>\n</tr>\n<tr>\n<td><code>{prefix}:users:all</code></td>\n<td><code>auth:users:all</code></td>\n<td>Set of all user IDs</td>\n</tr>\n<tr>\n<td><code>{prefix}:session:{id}</code></td>\n<td><code>auth:session:sess-1</code></td>\n<td>Session entity</td>\n</tr>\n<tr>\n<td><code>{prefix}:sessions:user:{userId}</code></td>\n<td><code>auth:sessions:user:abc-123</code></td>\n<td>Sorted set of session IDs by expiry</td>\n</tr>\n<tr>\n<td><code>{prefix}:totp:{handle}</code></td>\n<td><code>auth:totp:jen</code></td>\n<td>Encrypted TOTP secret</td>\n</tr>\n<tr>\n<td><code>{prefix}:backup:{userId}</code></td>\n<td><code>auth:backup:abc-123</code></td>\n<td>Backup codes</td>\n</tr>\n<tr>\n<td><code>{prefix}:invite:{token}</code></td>\n<td><code>auth:invite:tok-abc</code></td>\n<td>Invitation entity</td>\n</tr>\n<tr>\n<td><code>{prefix}:invite:id:{id}</code></td>\n<td><code>auth:invite:id:inv-1</code></td>\n<td>Invitation ID to token index</td>\n</tr>\n<tr>\n<td><code>{prefix}:invites:all</code></td>\n<td><code>auth:invites:all</code></td>\n<td>Set of all invitation tokens</td>\n</tr>\n<tr>\n<td><code>{prefix}:invites:pending</code></td>\n<td><code>auth:invites:pending</code></td>\n<td>Set of pending invitation tokens</td>\n</tr>\n<tr>\n<td><code>{prefix}:audit:{id}</code></td>\n<td><code>auth:audit:evt_123_abc</code></td>\n<td>Audit event entity</td>\n</tr>\n<tr>\n<td><code>{prefix}:audit:log</code></td>\n<td><code>auth:audit:log</code></td>\n<td>Sorted set of audit event IDs by timestamp</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">API Reference</h2><a id=\"user-content-api-reference\" class=\"anchor\" aria-label=\"Permalink: API Reference\" href=\"#api-reference\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>createRedisStorageAdapter(config: RedisStorageConfig): RedisStorageAdapter</code></h3><a id=\"user-content-createredisstorageadapterconfig-redisstorageconfig-redisstorageadapter\" class=\"anchor\" aria-label=\"Permalink: createRedisStorageAdapter(config: RedisStorageConfig): RedisStorageAdapter\" href=\"#createredisstorageadapterconfig-redisstorageconfig-redisstorageadapter\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Factory function. Accepts:</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Option</th>\n<th>Type</th>\n<th>Default</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>redis</code></td>\n<td><code>Redis</code></td>\n<td>-</td>\n<td>Existing Upstash Redis instance</td>\n</tr>\n<tr>\n<td><code>url</code></td>\n<td><code>string</code></td>\n<td>-</td>\n<td>Upstash REST URL (used if <code>redis</code> not provided)</td>\n</tr>\n<tr>\n<td><code>token</code></td>\n<td><code>string</code></td>\n<td>-</td>\n<td>Upstash REST token (used if <code>redis</code> not provided)</td>\n</tr>\n<tr>\n<td><code>prefix</code></td>\n<td><code>string</code></td>\n<td><code>'auth'</code></td>\n<td>Key namespace prefix</td>\n</tr>\n<tr>\n<td><code>sessionMaxAge</code></td>\n<td><code>number</code></td>\n<td><code>604800000</code></td>\n<td>Session TTL in milliseconds (7 days)</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>RedisStorageAdapter</code> (implements <code>IStorageAdapter</code>)</h3><a id=\"user-content-redisstorageadapter-implements-istorageadapter\" class=\"anchor\" aria-label=\"Permalink: RedisStorageAdapter (implements IStorageAdapter)\" href=\"#redisstorageadapter-implements-istorageadapter\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><strong>Lifecycle:</strong></p>\n<ul dir=\"auto\">\n<li><code>init()</code> - Verify Redis connectivity (PING)</li>\n<li><code>close()</code> - No-op (Upstash uses HTTP, no persistent connection)</li>\n</ul>\n<p dir=\"auto\"><strong>Users:</strong> <code>getUser</code>, <code>getUserByHandle</code>, <code>getUserByEmail</code>, <code>getAllUsers</code>, <code>createUser</code>, <code>updateUser</code>, <code>deleteUser</code>, <code>hasUsers</code></p>\n<p dir=\"auto\"><strong>Sessions:</strong> <code>getSession</code>, <code>getSessionsByUser</code>, <code>getAllSessions</code>, <code>createSession</code>, <code>updateSession</code>, <code>deleteSession</code>, <code>deleteUserSessions</code>, <code>cleanupExpiredSessions</code></p>\n<p dir=\"auto\"><strong>TOTP:</strong> <code>getTOTPSecret</code>, <code>saveTOTPSecret</code>, <code>deleteTOTPSecret</code></p>\n<p dir=\"auto\"><strong>Backup Codes:</strong> <code>getBackupCodes</code>, <code>saveBackupCodes</code>, <code>deleteBackupCodes</code></p>\n<p dir=\"auto\"><strong>Invitations:</strong> <code>getInvitation</code>, <code>getInvitationById</code>, <code>getAllInvitations</code>, <code>getPendingInvitations</code>, <code>createInvitation</code>, <code>updateInvitation</code>, <code>deleteInvitation</code>, <code>cleanupExpiredInvitations</code></p>\n<p dir=\"auto\"><strong>Audit:</strong> <code>logAuditEvent</code>, <code>getAuditEvents</code>, <code>getRecentAuditEvents</code></p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Serialization Utilities</h3><a id=\"user-content-serialization-utilities\" class=\"anchor\" aria-label=\"Permalink: Serialization Utilities\" href=\"#serialization-utilities\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Exported for advanced use cases:</p>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { serialize, deserialize, toHashFields, fromHashFields } from '@tummycrypt/tinyland-auth-redis';\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">serialize</span><span class=\"pl-kos\">,</span> <span class=\"pl-s1\">deserialize</span><span class=\"pl-kos\">,</span> <span class=\"pl-s1\">toHashFields</span><span class=\"pl-kos\">,</span> <span class=\"pl-s1\">fromHashFields</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/tinyland-auth-redis'</span><span class=\"pl-kos\">;</span></pre></div>\n<ul dir=\"auto\">\n<li><code>serialize&lt;T&gt;(value: T): string</code> - Safe JSON.stringify wrapper</li>\n<li><code>deserialize&lt;T&gt;(value: string | null): T | null</code> - Safe JSON.parse with null handling</li>\n<li><code>toHashFields(obj: Record&lt;string, unknown&gt;): Record&lt;string, string&gt;</code> - Convert to Redis HSET-compatible flat map</li>\n<li><code>fromHashFields&lt;T&gt;(hash: Record&lt;string, string&gt; | null): T | null</code> - Parse Redis HGETALL result back to typed object</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Key Generators</h3><a id=\"user-content-key-generators\" class=\"anchor\" aria-label=\"Permalink: Key Generators\" href=\"#key-generators\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-ts notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"import { createKeys } from '@tummycrypt/tinyland-auth-redis';\n\nconst keys = createKeys('myapp');\nkeys.user('abc-123');      // 'myapp:user:abc-123'\nkeys.session('sess-1');    // 'myapp:session:sess-1'\nkeys.auditLog();           // 'myapp:audit:log'\"><pre><span class=\"pl-k\">import</span> <span class=\"pl-kos\">{</span> <span class=\"pl-s1\">createKeys</span> <span class=\"pl-kos\">}</span> <span class=\"pl-k\">from</span> <span class=\"pl-s\">'@tummycrypt/tinyland-auth-redis'</span><span class=\"pl-kos\">;</span>\n\n<span class=\"pl-k\">const</span> <span class=\"pl-s1\">keys</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">createKeys</span><span class=\"pl-kos\">(</span><span class=\"pl-s\">'myapp'</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>\n<span class=\"pl-s1\">keys</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">user</span><span class=\"pl-kos\">(</span><span class=\"pl-s\">'abc-123'</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>      <span class=\"pl-c\">// 'myapp:user:abc-123'</span>\n<span class=\"pl-s1\">keys</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">session</span><span class=\"pl-kos\">(</span><span class=\"pl-s\">'sess-1'</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>    <span class=\"pl-c\">// 'myapp:session:sess-1'</span>\n<span class=\"pl-s1\">keys</span><span class=\"pl-kos\">.</span><span class=\"pl-en\">auditLog</span><span class=\"pl-kos\">(</span><span class=\"pl-kos\">)</span><span class=\"pl-kos\">;</span>           <span class=\"pl-c\">// 'myapp:audit:log'</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Environment Variables</h2><a id=\"user-content-environment-variables\" class=\"anchor\" aria-label=\"Permalink: Environment Variables\" href=\"#environment-variables\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Variable</th>\n<th>Required</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>KV_REST_API_URL</code></td>\n<td>Yes</td>\n<td>Upstash Redis REST URL</td>\n</tr>\n<tr>\n<td><code>KV_REST_API_TOKEN</code></td>\n<td>Yes</td>\n<td>Upstash Redis REST token</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">These are the standard environment variable names used by Vercel KV (Upstash integration).</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">MIT</p>\n</article></div>",
      "readme_excerpt": "Redis storage adapter for @tummycrypt/tinyland-auth, backed by Upstash Redis (@upstash/redis).\nImplements the full IStorageAdapter interface: users, sessions, TOTP secrets, backup codes, invitations, and audit events.\nbash\nnpm install @tummycrypt/tinyland-auth-redis\npnpm add @tummycrypt/tinyland-auth-redis\nPeer dependency:\nbash\nnpm install @tummycrypt/tinyland-auth\ntypescript\nimport { createRedisStorageAdapter } from '@tummycrypt/tinyland-auth-redis';\nconst storage =...",
      "install_commands": {
        "npm": "npm install @tummycrypt/tinyland-auth-redis"
      },
      "repo_url": "https://github.com/jesssullivan/tinyland-auth-redis",
      "website_url": null,
      "docs_url": null,
      "registry_url": "https://www.npmjs.com/package/@tummycrypt/tinyland-auth-redis",
      "releases_url": "https://github.com/jesssullivan/tinyland-auth-redis/releases",
      "og_image_url": "https://opengraph.githubassets.com/c8efdff71adda5cad1e49ec9dd565dbe36ad5534a6af5cdb635de5739669a366/tinyland-inc/tinyland-auth-redis",
      "license": "",
      "pushed_at": "2026-04-06T17:51:54Z",
      "enriched_at": "2026-04-26T17:17:17.014Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-dsa-study-asi",
      "name": "DSA-study-ASI",
      "repo": "jesssullivan/DSA-study-ASI",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "devex",
      "description": "",
      "featured": false,
      "tags": [
        "algorithms",
        "study"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [
        "study-guide",
        "leetcode",
        "optout",
        "study-materials"
      ],
      "languages": [
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 378556
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 8301
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 1547
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 43
        }
      ],
      "primary_language": "Python",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick Start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick Start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"direnv allow        # nix devshell + python 3.14 venv\njust test           # 628 tests\njust lint           # ruff + mypy strict\njust docs           # mkdocs site at localhost:8000\"><pre>direnv allow        <span class=\"pl-c\"><span class=\"pl-c\">#</span> nix devshell + python 3.14 venv</span>\njust <span class=\"pl-c1\">test</span>           <span class=\"pl-c\"><span class=\"pl-c\">#</span> 628 tests</span>\njust lint           <span class=\"pl-c\"><span class=\"pl-c\">#</span> ruff + mypy strict</span>\njust docs           <span class=\"pl-c\"><span class=\"pl-c\">#</span> mkdocs site at localhost:8000</span></pre></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"just challenge arrays two_sum     # strip solution → implement\njust study arrays                 # watch mode — tests re-run on save\njust solution arrays two_sum      # peek if stuck\njust challenge-done arrays two_sum\njust challenge-progress\"><pre>just challenge arrays two_sum     <span class=\"pl-c\"><span class=\"pl-c\">#</span> strip solution → implement</span>\njust study arrays                 <span class=\"pl-c\"><span class=\"pl-c\">#</span> watch mode — tests re-run on save</span>\njust solution arrays two_sum      <span class=\"pl-c\"><span class=\"pl-c\">#</span> peek if stuck</span>\njust challenge-done arrays two_sum\njust challenge-progress</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Core 42</h3><a id=\"user-content-core-42\" class=\"anchor\" aria-label=\"Permalink: Core 42\" href=\"#core-42\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Topic</th>\n<th>#</th>\n<th>Drill set</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>arrays</td>\n<td>3</td>\n<td>two_sum, group_anagrams, product_except_self</td>\n</tr>\n<tr>\n<td>two_pointers</td>\n<td>2</td>\n<td>three_sum, trapping_rain_water</td>\n</tr>\n<tr>\n<td>sliding_window</td>\n<td>2</td>\n<td>min_window_substring, longest_substring_no_repeat</td>\n</tr>\n<tr>\n<td>stacks_queues</td>\n<td>2</td>\n<td>valid_parentheses, daily_temperatures</td>\n</tr>\n<tr>\n<td>searching</td>\n<td>2</td>\n<td>binary_search, search_rotated_array</td>\n</tr>\n<tr>\n<td>linked_lists</td>\n<td>2</td>\n<td>reverse_linked_list, lru_cache</td>\n</tr>\n<tr>\n<td>trees</td>\n<td>3</td>\n<td>validate_bst, level_order_traversal, trie</td>\n</tr>\n<tr>\n<td>graphs</td>\n<td>7</td>\n<td>dijkstra, a_star, bellman_ford, topo_sort, islands, MST, course_schedule</td>\n</tr>\n<tr>\n<td>dp</td>\n<td>5</td>\n<td>coin_change, edit_distance, knapsack, LIS, LCS</td>\n</tr>\n<tr>\n<td>heaps</td>\n<td>2</td>\n<td>kth_largest, merge_k_sorted_lists</td>\n</tr>\n<tr>\n<td>backtracking</td>\n<td>3</td>\n<td>subsets, combination_sum, n_queens</td>\n</tr>\n<tr>\n<td>greedy</td>\n<td>2</td>\n<td>merge_intervals, jump_game</td>\n</tr>\n<tr>\n<td>strings</td>\n<td>2</td>\n<td>valid_palindrome, longest_palindromic_substring</td>\n</tr>\n<tr>\n<td>recursion</td>\n<td>2</td>\n<td>generate_parentheses, flatten_nested_list</td>\n</tr>\n<tr>\n<td>bit_manipulation</td>\n<td>1</td>\n<td>single_number</td>\n</tr>\n<tr>\n<td>sorting</td>\n<td>1</td>\n<td>quickselect</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">The remaining 27 implementations stay in the repo as extended practice and reference.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Commands</h2><a id=\"user-content-commands\" class=\"anchor\" aria-label=\"Permalink: Commands\" href=\"#commands\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"just                              # list all commands\n\n# ── Testing ──\njust test                         # all 628 tests\njust practice &lt;topic&gt;             # one topic\njust study &lt;topic&gt;                # watch mode\njust test-concepts                # concept modules (numpy/scipy/flask/pydantic)\n\n# ── Challenge ──\njust challenge &lt;topic&gt; &lt;problem&gt;  # strip solution, show failing tests\njust solution &lt;topic&gt; &lt;problem&gt;   # restore solution\njust challenge-done &lt;t&gt; &lt;p&gt;       # mark complete\njust challenge-progress           # show progress\n\n# ── Quality ──\njust lint                         # ruff + mypy strict\njust fmt                          # auto-format\n\n# ── Docs ──\njust docs                         # mkdocs serve\njust pdf-booklet                  # printable reference booklet\"><pre>just                              <span class=\"pl-c\"><span class=\"pl-c\">#</span> list all commands</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> ── Testing ──</span>\njust <span class=\"pl-c1\">test</span>                         <span class=\"pl-c\"><span class=\"pl-c\">#</span> all 628 tests</span>\njust practice <span class=\"pl-k\">&lt;</span>topic<span class=\"pl-k\">&gt;</span>             <span class=\"pl-c\"><span class=\"pl-c\">#</span> one topic</span>\njust study <span class=\"pl-k\">&lt;</span>topic<span class=\"pl-k\">&gt;</span>                <span class=\"pl-c\"><span class=\"pl-c\">#</span> watch mode</span>\njust test-concepts                <span class=\"pl-c\"><span class=\"pl-c\">#</span> concept modules (numpy/scipy/flask/pydantic)</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> ── Challenge ──</span>\njust challenge <span class=\"pl-k\">&lt;</span>topic<span class=\"pl-k\">&gt;</span> <span class=\"pl-k\">&lt;</span>problem<span class=\"pl-k\">&gt;</span>  <span class=\"pl-c\"><span class=\"pl-c\">#</span> strip solution, show failing tests</span>\njust solution <span class=\"pl-k\">&lt;</span>topic<span class=\"pl-k\">&gt;</span> <span class=\"pl-k\">&lt;</span>problem<span class=\"pl-k\">&gt;</span>   <span class=\"pl-c\"><span class=\"pl-c\">#</span> restore solution</span>\njust challenge-done <span class=\"pl-k\">&lt;</span>t<span class=\"pl-k\">&gt;</span> <span class=\"pl-k\">&lt;</span>p<span class=\"pl-k\">&gt;</span>       <span class=\"pl-c\"><span class=\"pl-c\">#</span> mark complete</span>\njust challenge-progress           <span class=\"pl-c\"><span class=\"pl-c\">#</span> show progress</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> ── Quality ──</span>\njust lint                         <span class=\"pl-c\"><span class=\"pl-c\">#</span> ruff + mypy strict</span>\njust fmt                          <span class=\"pl-c\"><span class=\"pl-c\">#</span> auto-format</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> ── Docs ──</span>\njust docs                         <span class=\"pl-c\"><span class=\"pl-c\">#</span> mkdocs serve</span>\njust pdf-booklet                  <span class=\"pl-c\"><span class=\"pl-c\">#</span> printable reference booklet</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Structure</h2><a id=\"user-content-structure\" class=\"anchor\" aria-label=\"Permalink: Structure\" href=\"#structure\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"src/algo/         69 implementations, 15 topics (42 core + 27 extended)\nsrc/concepts/     6 modules: t-strings, typing, hypothesis, FFT, Flask, Pydantic\nsrc/practice/     3 code reading + 3 decomposition exercises\ntests/            628 tests (pytest + hypothesis)\nreference-sheets/ 9 printable reference sheets\ndocs/             mkdocs site\"><pre class=\"notranslate\"><code>src/algo/         69 implementations, 15 topics (42 core + 27 extended)\nsrc/concepts/     6 modules: t-strings, typing, hypothesis, FFT, Flask, Pydantic\nsrc/practice/     3 code reading + 3 decomposition exercises\ntests/            628 tests (pytest + hypothesis)\nreference-sheets/ 9 printable reference sheets\ndocs/             mkdocs site\n</code></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Reference</h2><a id=\"user-content-reference\" class=\"anchor\" aria-label=\"Permalink: Reference\" href=\"#reference\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><a href=\"reference-sheets/08-cross-reference-guide.md\">Cross-Reference Guide</a> — pattern → implementation lookup</li>\n<li><a href=\"reference-sheets/07-interview-day-guide.md\">Interview Day Guide</a> — printouts, tabs, strategy</li>\n<li><a href=\"reference-sheets/03-algorithm-templates.md\">Algorithm Templates</a> — binary search, BFS/DFS, DP, backtracking</li>\n</ul>\n</article></div>",
      "readme_excerpt": "----|---|-----------|\n| arrays | 3 | twosum, groupanagrams, productexceptself |\n| twopointers | 2 | threesum, trappingrainwater |\n| slidingwindow | 2 | minwindowsubstring, longestsubstringnorepeat |\n| stacksqueues | 2 | validparentheses, dailytemperatures |\n| searching | 2 | binarysearch, searchrotatedarray |\n| linkedlists | 2 | reverselinkedlist, lrucache |\n| trees | 3 | validatebst, levelordertraversal, trie |\n| graphs | 7 | dijkstra, astar, bellmanford, toposort, islands, MST, courseschedule...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/DSA-study-ASI",
      "website_url": "http://transscendsurvival.org/DSA-study-ASI/",
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/DSA-study-ASI/releases",
      "og_image_url": "https://opengraph.githubassets.com/f67125389321c4464328a98fcd86ffaab451ea7963bf3d88a9de4f51eedc5e1d/Jesssullivan/DSA-study-ASI",
      "license": "",
      "pushed_at": "2026-04-03T12:49:02Z",
      "enriched_at": "2026-04-26T17:17:17.267Z",
      "gate": "pages"
    },
    {
      "slug": "jesssullivan-gnucashr",
      "name": "gnucashr",
      "repo": "jesssullivan/gnucashr",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "integrations",
      "description": "A high performance accounting and financial modeling R package and MCP tool surface for GNUCash, for people and robots",
      "featured": false,
      "tags": [
        "gnucash",
        "mcp",
        "plaid",
        "finance"
      ],
      "version": "0.2.0",
      "release_date": "2026-01-26T16:37:57Z",
      "releases": [
        {
          "tag": "0.2.0",
          "date": "2026-01-26T16:37:57Z",
          "body": "**Full Changelog**: https://github.com/Jesssullivan/gnucashr/commits/v0.2.0"
        }
      ],
      "stars": 1,
      "topics": [
        "agent-skills",
        "finops",
        "gnucash",
        "mcp",
        "plaid"
      ],
      "languages": [
        {
          "name": "C++",
          "color": "#f34b7d",
          "bytes": 1722990
        },
        {
          "name": "R",
          "color": "#198CE7",
          "bytes": 659046
        },
        {
          "name": "Dhall",
          "color": "#dfafff",
          "bytes": 29878
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 14782
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 10831
        }
      ],
      "primary_language": "C++",
      "readme_html": "",
      "readme_excerpt": "",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/gnucashr",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/gnucashr/releases",
      "og_image_url": "https://opengraph.githubassets.com/5e7a6e8761ba03b76e3c1b7566211a7ca99f1975b2a019534e091096ab039b70/Jesssullivan/gnucashr",
      "license": "",
      "pushed_at": "2026-03-20T05:23:47Z",
      "enriched_at": "2026-04-26T17:17:17.571Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-winrm-molecule-forkbomb-demo",
      "name": "winrm-molecule-forkbomb-demo",
      "repo": "jesssullivan/winrm-molecule-forkbomb-demo",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "systems",
      "description": "Fast and dirty demo of winrm molecule fork bomb behavior; when trying to go fast goes wrong",
      "featured": false,
      "tags": [
        "ansible",
        "molecule",
        "windows"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [],
      "languages": [
        {
          "name": "Jinja",
          "color": "#a52a22",
          "bytes": 13138
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 12262
        },
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 10332
        },
        {
          "name": "Dhall",
          "color": "#dfafff",
          "bytes": 8178
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 7715
        }
      ],
      "primary_language": "Jinja",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">winrm-molecule-forkbomb-demo</h1><a id=\"user-content-winrm-molecule-forkbomb-demo\" class=\"anchor\" aria-label=\"Permalink: winrm-molecule-forkbomb-demo\" href=\"#winrm-molecule-forkbomb-demo\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Research project demonstrating how Ansible's WinRM connection model causes a \"forkbomb\"\nof authentication failures that exhaust Windows shell quotas and trigger Active Directory\naccount lockouts.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">The Problem</h2><a id=\"user-content-the-problem\" class=\"anchor\" aria-label=\"Permalink: The Problem\" href=\"#the-problem\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"Ansible forks=50 × 15 tasks/role = 750 WinRM shell attempts\nWindows MaxShellsPerUser = 30 → 720 failures\nEach failure = failed NTLM auth → AD lockout after 5 failures\"><pre class=\"notranslate\"><code>Ansible forks=50 × 15 tasks/role = 750 WinRM shell attempts\nWindows MaxShellsPerUser = 30 → 720 failures\nEach failure = failed NTLM auth → AD lockout after 5 failures\n</code></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">The Fix</h2><a id=\"user-content-the-fix\" class=\"anchor\" aria-label=\"Permalink: The Fix\" href=\"#the-fix\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ol dir=\"auto\">\n<li><strong>Admin toggle</strong>: Raise WinRM quotas before parallel testing (<code>winrm_quota_config</code> role)</li>\n<li><strong>Session cleanup</strong>: Auto-terminate stale WinRM shells (<code>winrm_session_cleanup</code> role)</li>\n<li><strong>Use pypsrp</strong>: Better connection pooling via PowerShell Remoting Protocol</li>\n</ol>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick Start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick Start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"direnv allow                 # Enter nix dev shell\njust setup                   # Install deps + collections\nsops secrets/winrm-creds.enc.yaml  # Configure credentials\njust tunnel-start            # SSH tunnel to win-target\njust audit                   # Verify connectivity + baseline quotas\n\n# Demo the forkbomb\njust benchmark-safe          # forks=5, works fine\njust benchmark-unsafe        # forks=50, demonstrates the problem\njust deploy-quotas           # Apply the fix (raise quotas)\njust benchmark-unsafe        # forks=50, now works!\njust benchmark-psrp          # Compare pypsrp connection behavior\"><pre>direnv allow                 <span class=\"pl-c\"><span class=\"pl-c\">#</span> Enter nix dev shell</span>\njust setup                   <span class=\"pl-c\"><span class=\"pl-c\">#</span> Install deps + collections</span>\nsops secrets/winrm-creds.enc.yaml  <span class=\"pl-c\"><span class=\"pl-c\">#</span> Configure credentials</span>\njust tunnel-start            <span class=\"pl-c\"><span class=\"pl-c\">#</span> SSH tunnel to win-target</span>\njust audit                   <span class=\"pl-c\"><span class=\"pl-c\">#</span> Verify connectivity + baseline quotas</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Demo the forkbomb</span>\njust benchmark-safe          <span class=\"pl-c\"><span class=\"pl-c\">#</span> forks=5, works fine</span>\njust benchmark-unsafe        <span class=\"pl-c\"><span class=\"pl-c\">#</span> forks=50, demonstrates the problem</span>\njust deploy-quotas           <span class=\"pl-c\"><span class=\"pl-c\">#</span> Apply the fix (raise quotas)</span>\njust benchmark-unsafe        <span class=\"pl-c\"><span class=\"pl-c\">#</span> forks=50, now works!</span>\njust benchmark-psrp          <span class=\"pl-c\"><span class=\"pl-c\">#</span> Compare pypsrp connection behavior</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Stack</h2><a id=\"user-content-stack\" class=\"anchor\" aria-label=\"Permalink: Stack\" href=\"#stack\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>Nix flake</strong> + <strong>direnv</strong> - Reproducible dev shell</li>\n<li><strong>UV</strong> + <strong>pyproject.toml</strong> - Python 3.13 dependency management</li>\n<li><strong>Dhall</strong> - Type-safe configuration generation (quotas, benchmarks, role manifests)</li>\n<li><strong>SOPS</strong> + <strong>age</strong> - Encrypted credential management</li>\n<li><strong>Ansible</strong> + <strong>Molecule</strong> - Infrastructure automation and testing</li>\n<li><strong>just</strong> - Task orchestration</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Roles</h2><a id=\"user-content-roles\" class=\"anchor\" aria-label=\"Permalink: Roles\" href=\"#roles\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Role</th>\n<th>Purpose</th>\n<th>Tags</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>winrm_quota_config</code></td>\n<td>Raise WinRM shell quotas (admin toggle)</td>\n<td><code>winrm-quota</code></td>\n</tr>\n<tr>\n<td><code>winrm_session_cleanup</code></td>\n<td>Detect + terminate stale sessions</td>\n<td><code>winrm-cleanup</code></td>\n</tr>\n<tr>\n<td><code>firewall_rules</code></td>\n<td>Windows firewall for IIS/WinRM</td>\n<td><code>firewall</code></td>\n</tr>\n<tr>\n<td><code>iis_site</code></td>\n<td>Demo IIS site displaying repo contents</td>\n<td><code>iis-site</code></td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Documentation</h2><a id=\"user-content-documentation\" class=\"anchor\" aria-label=\"Permalink: Documentation\" href=\"#documentation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><a href=\"docs/forkbomb-mechanism.md\">Forkbomb Mechanism</a> - Root cause analysis</li>\n<li><a href=\"docs/winrm-quotas.md\">WinRM Quotas</a> - Quota reference and presets</li>\n<li><a href=\"docs/pywinrm-vs-pypsrp.md\">pywinrm vs pypsrp</a> - Connection plugin comparison</li>\n<li><a href=\"docs/ad-lockout-prevention.md\">AD Lockout Prevention</a> - Safety procedures</li>\n<li><a href=\"docs/upstream-issues.md\">Upstream Issues</a> - Contribution targets</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Upstream Issues</h2><a id=\"user-content-upstream-issues\" class=\"anchor\" aria-label=\"Permalink: Upstream Issues\" href=\"#upstream-issues\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><a href=\"https://github.com/diyan/pywinrm/issues/277\" data-hovercard-type=\"issue\" data-hovercard-url=\"/diyan/pywinrm/issues/277/hovercard\">pywinrm#277</a> - Multi-threaded requests fail</li>\n<li><a href=\"https://github.com/ansible/molecule/issues/607\" data-hovercard-type=\"issue\" data-hovercard-url=\"/ansible/molecule/issues/607/hovercard\">molecule#607</a> - WinRM connection plugin gaps</li>\n<li><a href=\"https://github.com/ansible-collections/ansible.windows/issues/597\" data-hovercard-type=\"issue\" data-hovercard-url=\"/ansible-collections/ansible.windows/issues/597/hovercard\">ansible.windows#597</a> - Intermittent failures at scale</li>\n</ul>\n</article></div>",
      "readme_excerpt": "Research project demonstrating how Ansible's WinRM connection model causes a \"forkbomb\"\nof authentication failures that exhaust Windows shell quotas and trigger Active Directory\naccount lockouts.\nAnsible forks=50 × 15 tasks/role = 750 WinRM shell attempts\nWindows MaxShellsPerUser = 30 → 720 failures\nEach failure = failed NTLM auth → AD lockout after 5 failures\n1. Admin toggle: Raise WinRM quotas before parallel testing (winrmquotaconfig role)\n2. Session cleanup: Auto-terminate stale WinRM...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/winrm-molecule-forkbomb-demo",
      "website_url": "http://transscendsurvival.org/winrm-molecule-forkbomb-demo/",
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/winrm-molecule-forkbomb-demo/releases",
      "og_image_url": "https://opengraph.githubassets.com/6515983a34de4aa60ab6d1ada15950ad8e06f435b3d073d445cde70c68baba2d/Jesssullivan/winrm-molecule-forkbomb-demo",
      "license": "",
      "pushed_at": "2026-03-13T22:40:37Z",
      "enriched_at": "2026-04-26T17:17:17.925Z",
      "gate": "pages"
    },
    {
      "slug": "jesssullivan-tinyland-hexstrunk",
      "name": "tinyland-hexstrunk",
      "repo": "jesssullivan/tinyland-hexstrunk",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "ai-tools",
      "description": "Formal, tracable, auditable tool surface for playing the bad guy",
      "featured": false,
      "tags": [
        "mcp",
        "formal-methods"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [
        "formal-methods",
        "mcp",
        "tailscale-aperture"
      ],
      "languages": [
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 239495
        },
        {
          "name": "OCaml",
          "color": "#ef7a08",
          "bytes": 151950
        },
        {
          "name": "Go",
          "color": "#00ADD8",
          "bytes": 49625
        },
        {
          "name": "Dhall",
          "color": "#dfafff",
          "bytes": 40692
        },
        {
          "name": "F*",
          "color": "#572e30",
          "bytes": 11925
        }
      ],
      "primary_language": "Python",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">HexStrunk ^w^</h1><a id=\"user-content-hexstrunk-w\" class=\"anchor\" aria-label=\"Permalink: HexStrunk ^w^\" href=\"#hexstrunk-w\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Auditable, provable cybersecurity tool surface for agents and humans. 42 tools across 13 domains, with verified dispatch, hash-chain audit, and grants-as-capabilities policy enforcement.  Inspired by HexStrike.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Architecture</h2><a id=\"user-content-architecture\" class=\"anchor\" aria-label=\"Permalink: Architecture\" href=\"#architecture\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"AI Agent (Claude, GPT, etc.)\n    | MCP Protocol (stdio or SSE)\n    v\nGo Gateway (hexstrike-gateway)\n    |-- tsnet: Tailscale identity authentication\n    |-- Dhall policies: grants-as-capabilities enforcement\n    |-- Aperture: token metering + circuit breaking\n    | JSON-RPC stdio\n    v\nOCaml MCP Server (hexstrike-mcp)\n    |-- F*-verified dispatch, sanitization, audit\n    |-- Hash-chain audit log\n    |-- Futhark C FFI: GPU-accelerated analysis\n    | subprocess\n    v\nSecurity Tools (nmap, nuclei, trivy, curl, etc.)\"><pre class=\"notranslate\"><code>AI Agent (Claude, GPT, etc.)\n    | MCP Protocol (stdio or SSE)\n    v\nGo Gateway (hexstrike-gateway)\n    |-- tsnet: Tailscale identity authentication\n    |-- Dhall policies: grants-as-capabilities enforcement\n    |-- Aperture: token metering + circuit breaking\n    | JSON-RPC stdio\n    v\nOCaml MCP Server (hexstrike-mcp)\n    |-- F*-verified dispatch, sanitization, audit\n    |-- Hash-chain audit log\n    |-- Futhark C FFI: GPU-accelerated analysis\n    | subprocess\n    v\nSecurity Tools (nmap, nuclei, trivy, curl, etc.)\n</code></pre></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Layer</th>\n<th>Technology</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Configuration</td>\n<td>Dhall</td>\n<td>Tool schemas, typed policies</td>\n</tr>\n<tr>\n<td>Verification</td>\n<td>F*</td>\n<td>Proved dispatch, sanitization, audit integrity</td>\n</tr>\n<tr>\n<td>Acceleration</td>\n<td>Futhark</td>\n<td>GPU-parallel batch analysis (sequential C fallback)</td>\n</tr>\n<tr>\n<td>MCP Server</td>\n<td>OCaml</td>\n<td>F*-extracted core, JSON-RPC 2.0 stdio</td>\n</tr>\n<tr>\n<td>Gateway</td>\n<td>Go</td>\n<td>tsnet auth, policy, metering, Prometheus</td>\n</tr>\n<tr>\n<td>Build System</td>\n<td>Nix</td>\n<td>Reproducible builds, devShells, OCI container</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Tool Inventory (42 tools, 13 domains)</h2><a id=\"user-content-tool-inventory-42-tools-13-domains\" class=\"anchor\" aria-label=\"Permalink: Tool Inventory (42 tools, 13 domains)\" href=\"#tool-inventory-42-tools-13-domains\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Domain</th>\n<th>Tools</th>\n<th>Count</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>WebSecurity</td>\n<td><code>dir_discovery</code>, <code>vuln_scan</code>, <code>sqli_test</code>, <code>xss_test</code>, <code>waf_detect</code>, <code>web_crawl</code></td>\n<td>6</td>\n</tr>\n<tr>\n<td>NetworkRecon</td>\n<td><code>port_scan</code>, <code>host_discovery</code>, <code>nmap_scan</code>, <code>network_posture</code></td>\n<td>4</td>\n</tr>\n<tr>\n<td>CloudSecurity</td>\n<td><code>cloud_posture</code>, <code>container_vuln</code>, <code>iac_scan</code>, <code>k8s_audit</code></td>\n<td>4</td>\n</tr>\n<tr>\n<td>CredentialAudit</td>\n<td><code>credential_scan</code>, <code>sops_rotation</code>, <code>brute_force</code>, <code>hash_crack</code></td>\n<td>4</td>\n</tr>\n<tr>\n<td>BinaryAnalysis</td>\n<td><code>disassemble</code>, <code>debug_tool</code>, <code>gadget_search</code>, <code>firmware_analyze</code></td>\n<td>4</td>\n</tr>\n<tr>\n<td>Forensics</td>\n<td><code>memory_forensics</code>, <code>file_carving</code>, <code>steganography</code>, <code>metadata_extract</code></td>\n<td>4</td>\n</tr>\n<tr>\n<td>SMBEnum</td>\n<td><code>smb_enum</code>, <code>network_exec</code>, <code>rpc_enum</code></td>\n<td>3</td>\n</tr>\n<tr>\n<td>Intelligence</td>\n<td><code>cve_monitor</code>, <code>exploit_gen</code>, <code>threat_correlate</code></td>\n<td>3</td>\n</tr>\n<tr>\n<td>APITesting</td>\n<td><code>api_fuzz</code>, <code>graphql_scan</code>, <code>jwt_analyze</code></td>\n<td>3</td>\n</tr>\n<tr>\n<td>DNSRecon</td>\n<td><code>subdomain_enum</code>, <code>dns_recon</code></td>\n<td>2</td>\n</tr>\n<tr>\n<td>Orchestration</td>\n<td><code>smart_scan</code>, <code>analyze_target</code></td>\n<td>2</td>\n</tr>\n<tr>\n<td>Meta</td>\n<td><code>server_health</code>, <code>execute_command</code></td>\n<td>2</td>\n</tr>\n<tr>\n<td>CryptoAnalysis</td>\n<td><code>tls_check</code></td>\n<td>1</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">Tool names are canonical in <code>dhall/policies/constants/tools.dhall</code>. An OCaml parity test catches drift.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick Start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick Start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Enter dev shell (OCaml, Go, Dhall, Futhark, security tools)\nnix develop\n\n# Build everything\njust build\n\n# Run all tests\njust test\n\n# Fast feedback (type-check + vet)\njust check\n\n# Start MCP server on stdio\njust serve\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Enter dev shell (OCaml, Go, Dhall, Futhark, security tools)</span>\nnix develop\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Build everything</span>\njust build\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Run all tests</span>\njust <span class=\"pl-c1\">test</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Fast feedback (type-check + vet)</span>\njust check\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Start MCP server on stdio</span>\njust serve</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">F* verification (optional)</h3><a id=\"user-content-f-verification-optional\" class=\"anchor\" aria-label=\"Permalink: F* verification (optional)\" href=\"#f-verification-optional\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix develop .#fstar    # shell with F* + Z3\njust fstar-verify      # verify all modules\"><pre>nix develop .<span class=\"pl-c\"><span class=\"pl-c\">#</span>fstar    # shell with F* + Z3</span>\njust fstar-verify      <span class=\"pl-c\"><span class=\"pl-c\">#</span> verify all modules</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">MCP Client Configuration</h2><a id=\"user-content-mcp-client-configuration\" class=\"anchor\" aria-label=\"Permalink: MCP Client Configuration\" href=\"#mcp-client-configuration\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Claude Desktop / Cursor</h3><a id=\"user-content-claude-desktop--cursor\" class=\"anchor\" aria-label=\"Permalink: Claude Desktop / Cursor\" href=\"#claude-desktop--cursor\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-json notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"{\n  &quot;mcpServers&quot;: {\n    &quot;hexstrike-ai&quot;: {\n      &quot;command&quot;: &quot;nix&quot;,\n      &quot;args&quot;: [&quot;run&quot;, &quot;github:tinyland-inc/hexstrike-ai&quot;],\n      &quot;description&quot;: &quot;HexStrike-AI cybersecurity platform&quot;\n    }\n  }\n}\"><pre>{\n  <span class=\"pl-ent\">\"mcpServers\"</span>: {\n    <span class=\"pl-ent\">\"hexstrike-ai\"</span>: {\n      <span class=\"pl-ent\">\"command\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>nix<span class=\"pl-pds\">\"</span></span>,\n      <span class=\"pl-ent\">\"args\"</span>: [<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>run<span class=\"pl-pds\">\"</span></span>, <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>github:tinyland-inc/hexstrike-ai<span class=\"pl-pds\">\"</span></span>],\n      <span class=\"pl-ent\">\"description\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>HexStrike-AI cybersecurity platform<span class=\"pl-pds\">\"</span></span>\n    }\n  }\n}</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Container</h3><a id=\"user-content-container\" class=\"anchor\" aria-label=\"Permalink: Container\" href=\"#container\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix build .#container\ndocker load &lt; result\ndocker run -v /workspace:/workspace ghcr.io/tinyland-inc/hexstrike-ai:edge\"><pre>nix build .<span class=\"pl-c\"><span class=\"pl-c\">#</span>container</span>\ndocker load <span class=\"pl-k\">&lt;</span> result\ndocker run -v /workspace:/workspace ghcr.io/tinyland-inc/hexstrike-ai:edge</pre></div>\n<p dir=\"auto\">The container image includes all 42 tools and their runtime dependencies (~2.9 GB).</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Project Structure</h2><a id=\"user-content-project-structure\" class=\"anchor\" aria-label=\"Permalink: Project Structure\" href=\"#project-structure\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"dhall/           Dhall tool schemas + grants-as-capabilities policies\nfstar/           F* verified modules (dispatch, policy, sanitize, audit)\nfuthark/         GPU-parallel analysis kernels (scan, pattern, graph)\nocaml/           OCaml MCP server (42 tools, audit, policy, FFI bridge)\ngateway/         Go gateway (tsnet, Aperture, credential broker)\nflake.nix        Nix build system (packages, devShells, OCI container)\njustfile         Task runner recipes\"><pre class=\"notranslate\"><code>dhall/           Dhall tool schemas + grants-as-capabilities policies\nfstar/           F* verified modules (dispatch, policy, sanitize, audit)\nfuthark/         GPU-parallel analysis kernels (scan, pattern, graph)\nocaml/           OCaml MCP server (42 tools, audit, policy, FFI bridge)\ngateway/         Go gateway (tsnet, Aperture, credential broker)\nflake.nix        Nix build system (packages, devShells, OCI container)\njustfile         Task runner recipes\n</code></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Security Model</h2><a id=\"user-content-security-model\" class=\"anchor\" aria-label=\"Permalink: Security Model\" href=\"#security-model\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>Authentication</strong>: Tailscale identity (tsnet) at the gateway</li>\n<li><strong>Authorization</strong>: Dhall-compiled grants-as-capabilities policies</li>\n<li><strong>Input sanitization</strong>: F*-proved sanitization rejects shell metacharacters</li>\n<li><strong>Audit</strong>: Hash-chain log with SHA-256 integrity verification</li>\n<li><strong>Binary allowlisting</strong>: Only declared binaries can be executed</li>\n<li><strong>No arbitrary execution</strong>: File ops, Python exec, and payload generation removed from legacy</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">MIT</p>\n</article></div>",
      "readme_excerpt": "Auditable, provable cybersecurity tool surface for agents and humans. 42 tools across 13 domains, with verified dispatch, hash-chain audit, and grants-as-capabilities policy enforcement.  Inspired by HexStrike.\nAI Agent (Claude, GPT, etc.)\n    | MCP Protocol (stdio or SSE)\n    v\nGo Gateway (hexstrike-gateway)\n    |-- tsnet: Tailscale identity authentication\n    |-- Dhall policies: grants-as-capabilities enforcement\n    |-- Aperture: token metering + circuit breaking\n    | JSON-RPC stdio\n   ...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/tinyland-hexstrunk",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/tinyland-hexstrunk/releases",
      "og_image_url": "https://opengraph.githubassets.com/64e0a8bddfc0fd956c10fda2e4754a2b555cca89f4bd3312358f42ec1c8b2250/Jesssullivan/tinyland-hexstrunk",
      "license": "",
      "pushed_at": "2026-02-27T05:49:47Z",
      "enriched_at": "2026-04-26T17:17:18.231Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-aperture-bootstrap",
      "name": "aperture-bootstrap",
      "repo": "jesssullivan/aperture-bootstrap",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "systems",
      "description": "Bootstrap Tailscale Aperture config from tagged devices using tsnet — How to resolve WhoIs identity gap",
      "featured": false,
      "tags": [
        "tailscale",
        "aperture",
        "networking"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [
        "dhall",
        "nix",
        "tailscale",
        "tailscale-aperture",
        "tsnet",
        "demo",
        "ephemera"
      ],
      "languages": [
        {
          "name": "Go",
          "color": "#00ADD8",
          "bytes": 4144
        },
        {
          "name": "Dhall",
          "color": "#dfafff",
          "bytes": 3379
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 2731
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 1072
        }
      ],
      "primary_language": "Go",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">aperture-bootstrap</h1><a id=\"user-content-aperture-bootstrap\" class=\"anchor\" aria-label=\"Permalink: aperture-bootstrap\" href=\"#aperture-bootstrap\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Demo Bootstrap <a href=\"https://tailscale.com/aperture\" rel=\"nofollow\">Tailscale Aperture</a> config from tagged devices using <a href=\"https://pkg.go.dev/tailscale.com/tsnet\" rel=\"nofollow\">tsnet</a>.</p>\n<p dir=\"auto\">Aperture uses Tailscale WhoIs to identify who's connecting.  If your devices are <strong>tagged</strong> (as most K8s workloads are), Aperture doesn't recognize the literal string <code>\"tagged-devices\"</code> or tag names like <code>\"tag:dev\"</code> in its <code>temp_grants</code> config.  Only explicit user emails and the <code>\"*\"</code> wildcard match.</p>\n<p dir=\"auto\">This creates a chicken-and-egg: you can't access the config API to fix the grants because you don't have a grant.</p>\n<section class=\"js-render-needs-enrichment render-needs-enrichment position-relative\" data-identity=\"fa94cdca-ec1a-4463-b81c-2360b4ed2ada\" data-host=\"https://viewscreen.githubusercontent.com\" data-src=\"https://viewscreen.githubusercontent.com/markdown/mermaid?docs_host=https%3A%2F%2Fdocs.github.com\" data-type=\"mermaid\" aria-label=\"mermaid rendered output container\">\n  <div class=\"js-render-enrichment-target\" data-json=\"{&quot;data&quot;:&quot;graph LR\\n    subgraph \\&quot;Tailnet\\&quot;\\n        Yoga[\\&quot;Yoga&amp;lt;br/&amp;gt;&amp;lt;small&amp;gt;tag:dev, tag:dollhouse&amp;lt;/small&amp;gt;\\&quot;]\\n        K8s[\\&quot;K8s Agents&amp;lt;br/&amp;gt;&amp;lt;small&amp;gt;tag:k8s, tag:k8s-operator&amp;lt;/small&amp;gt;\\&quot;]\\n        AI[\\&quot;Aperture&amp;lt;br/&amp;gt;&amp;lt;small&amp;gt;ai.example.ts.net&amp;lt;/small&amp;gt;\\&quot;]\\n    end\\n\\n    Yoga --&amp;gt;|\\&quot;curl http://ai/api/config\\&quot;| AI\\n    K8s --&amp;gt;|\\&quot;ANTHROPIC_BASE_URL\\&quot;| AI\\n\\n    AI --&amp;gt;|\\&quot;WhoIs: tagged-devices\\&quot;| X[\\&quot;403 no role granted\\&quot;]\\n\\n    style X fill:#f66,stroke:#900,color:white\\n    style AI fill:#4a9,stroke:#2a7,color:white\\n&quot;}\" data-plain=\"graph LR\n    subgraph &quot;Tailnet&quot;\n        Yoga[&quot;Yoga&lt;br/&gt;&lt;small&gt;tag:dev, tag:dollhouse&lt;/small&gt;&quot;]\n        K8s[&quot;K8s Agents&lt;br/&gt;&lt;small&gt;tag:k8s, tag:k8s-operator&lt;/small&gt;&quot;]\n        AI[&quot;Aperture&lt;br/&gt;&lt;small&gt;ai.example.ts.net&lt;/small&gt;&quot;]\n    end\n\n    Yoga --&gt;|&quot;curl http://ai/api/config&quot;| AI\n    K8s --&gt;|&quot;ANTHROPIC_BASE_URL&quot;| AI\n\n    AI --&gt;|&quot;WhoIs: tagged-devices&quot;| X[&quot;403 no role granted&quot;]\n\n    style X fill:#f66,stroke:#900,color:white\n    style AI fill:#4a9,stroke:#2a7,color:white\n\" dir=\"auto\">\n    <div class=\"render-plaintext-hidden\" dir=\"auto\">\n      <pre lang=\"mermaid\" aria-label=\"Raw mermaid code\">graph LR\n    subgraph \"Tailnet\"\n        Yoga[\"Yoga&lt;br/&gt;&lt;small&gt;tag:dev, tag:dollhouse&lt;/small&gt;\"]\n        K8s[\"K8s Agents&lt;br/&gt;&lt;small&gt;tag:k8s, tag:k8s-operator&lt;/small&gt;\"]\n        AI[\"Aperture&lt;br/&gt;&lt;small&gt;ai.example.ts.net&lt;/small&gt;\"]\n    end\n\n    Yoga --&gt;|\"curl http://ai/api/config\"| AI\n    K8s --&gt;|\"ANTHROPIC_BASE_URL\"| AI\n\n    AI --&gt;|\"WhoIs: tagged-devices\"| X[\"403 no role granted\"]\n\n    style X fill:#f66,stroke:#900,color:white\n    style AI fill:#4a9,stroke:#2a7,color:white\n</pre>\n    </div>\n  </div>\n  <span class=\"js-render-enrichment-loader d-flex flex-justify-center flex-items-center width-full\" style=\"min-height:100px\" role=\"presentation\">\n    <span data-view-component=\"true\">\n  <svg style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" data-view-component=\"true\" class=\"octospinner mx-auto tmp-mx-auto anim-rotate\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n    <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n</svg>    <span class=\"sr-only\">Loading</span>\n</span>\n  </span>\n</section>\n\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">The solution</h2><a id=\"user-content-the-solution\" class=\"anchor\" aria-label=\"Permalink: The solution\" href=\"#the-solution\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Spin up an <strong>ephemeral, user-owned tsnet node</strong> — no tags, proper user identity.  Aperture's WhoIs sees the real user email, grants access, and you push the config.</p>\n<section class=\"js-render-needs-enrichment render-needs-enrichment position-relative\" data-identity=\"fee11d24-985b-4c85-8b0a-2174c6bc2f1a\" data-host=\"https://viewscreen.githubusercontent.com\" data-src=\"https://viewscreen.githubusercontent.com/markdown/mermaid?docs_host=https%3A%2F%2Fdocs.github.com\" data-type=\"mermaid\" aria-label=\"mermaid rendered output container\">\n  <div class=\"js-render-enrichment-target\" data-json=\"{&quot;data&quot;:&quot;sequenceDiagram\\n    participant API as Tailscale API\\n    participant Boot as tsnet node&amp;lt;br/&amp;gt;(ephemeral)\\n    participant AI as Aperture\\n\\n    Note over API: 1. Create auth key (no tags)\\n    API-&amp;gt;&amp;gt;Boot: Auth key (user-owned)\\n    Boot-&amp;gt;&amp;gt;AI: GET /api/config\\n    AI--&amp;gt;&amp;gt;Boot: {config, hash}\\n    Note over Boot: 2. Modify temp_grants\\n    Boot-&amp;gt;&amp;gt;AI: PUT /api/config {config, hash}\\n    AI--&amp;gt;&amp;gt;Boot: {success, new_hash}\\n    Note over Boot: 3. Ephemeral node auto-cleans up\\n&quot;}\" data-plain=\"sequenceDiagram\n    participant API as Tailscale API\n    participant Boot as tsnet node&lt;br/&gt;(ephemeral)\n    participant AI as Aperture\n\n    Note over API: 1. Create auth key (no tags)\n    API-&gt;&gt;Boot: Auth key (user-owned)\n    Boot-&gt;&gt;AI: GET /api/config\n    AI--&gt;&gt;Boot: {config, hash}\n    Note over Boot: 2. Modify temp_grants\n    Boot-&gt;&gt;AI: PUT /api/config {config, hash}\n    AI--&gt;&gt;Boot: {success, new_hash}\n    Note over Boot: 3. Ephemeral node auto-cleans up\n\" dir=\"auto\">\n    <div class=\"render-plaintext-hidden\" dir=\"auto\">\n      <pre lang=\"mermaid\" aria-label=\"Raw mermaid code\">sequenceDiagram\n    participant API as Tailscale API\n    participant Boot as tsnet node&lt;br/&gt;(ephemeral)\n    participant AI as Aperture\n\n    Note over API: 1. Create auth key (no tags)\n    API-&gt;&gt;Boot: Auth key (user-owned)\n    Boot-&gt;&gt;AI: GET /api/config\n    AI--&gt;&gt;Boot: {config, hash}\n    Note over Boot: 2. Modify temp_grants\n    Boot-&gt;&gt;AI: PUT /api/config {config, hash}\n    AI--&gt;&gt;Boot: {success, new_hash}\n    Note over Boot: 3. Ephemeral node auto-cleans up\n</pre>\n    </div>\n  </div>\n  <span class=\"js-render-enrichment-loader d-flex flex-justify-center flex-items-center width-full\" style=\"min-height:100px\" role=\"presentation\">\n    <span data-view-component=\"true\">\n  <svg style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" data-view-component=\"true\" class=\"octospinner mx-auto tmp-mx-auto anim-rotate\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n    <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n</svg>    <span class=\"sr-only\">Loading</span>\n</span>\n  </span>\n</section>\n\n<p dir=\"auto\">After bootstrapping, the network looks like this:</p>\n<section class=\"js-render-needs-enrichment render-needs-enrichment position-relative\" data-identity=\"2023b000-cc2a-47f2-b502-5e5e5306d1ad\" data-host=\"https://viewscreen.githubusercontent.com\" data-src=\"https://viewscreen.githubusercontent.com/markdown/mermaid?docs_host=https%3A%2F%2Fdocs.github.com\" data-type=\"mermaid\" aria-label=\"mermaid rendered output container\">\n  <div class=\"js-render-enrichment-target\" data-json=\"{&quot;data&quot;:&quot;graph TB\\n    subgraph \\&quot;Kubernetes Cluster\\&quot;\\n        IC[\\&quot;IronClaw Agent\\&quot;]\\n        PC[\\&quot;PicoClaw Agent\\&quot;]\\n        HS[\\&quot;HexStrike Agent\\&quot;]\\n        Egress[\\&quot;Egress Proxy&amp;lt;br/&amp;gt;&amp;lt;small&amp;gt;aperture.fuzzy-dev.svc&amp;lt;/small&amp;gt;\\&quot;]\\n    end\\n\\n    subgraph \\&quot;Tailnet\\&quot;\\n        AI[\\&quot;Aperture&amp;lt;br/&amp;gt;&amp;lt;small&amp;gt;ai.example.ts.net&amp;lt;/small&amp;gt;\\&quot;]\\n        GW[\\&quot;rj-gateway\\&quot;]\\n    end\\n\\n    Anthropic[\\&quot;Anthropic API\\&quot;]\\n\\n    IC --&amp;gt;|\\&quot;ANTHROPIC_BASE_URL\\&quot;| Egress\\n    PC --&amp;gt;|\\&quot;ANTHROPIC_BASE_URL\\&quot;| Egress\\n    HS --&amp;gt;|\\&quot;ANTHROPIC_BASE_URL\\&quot;| Egress\\n    Egress --&amp;gt;|\\&quot;Tailscale tunnel\\&quot;| AI\\n    AI --&amp;gt;|\\&quot;Proxied + metered\\&quot;| Anthropic\\n    AI --&amp;gt;|\\&quot;Webhook\\&quot;| GW\\n\\n    style AI fill:#4a9,stroke:#2a7,color:white\\n    style Anthropic fill:#d4a,stroke:#a27,color:white\\n&quot;}\" data-plain=\"graph TB\n    subgraph &quot;Kubernetes Cluster&quot;\n        IC[&quot;IronClaw Agent&quot;]\n        PC[&quot;PicoClaw Agent&quot;]\n        HS[&quot;HexStrike Agent&quot;]\n        Egress[&quot;Egress Proxy&lt;br/&gt;&lt;small&gt;aperture.fuzzy-dev.svc&lt;/small&gt;&quot;]\n    end\n\n    subgraph &quot;Tailnet&quot;\n        AI[&quot;Aperture&lt;br/&gt;&lt;small&gt;ai.example.ts.net&lt;/small&gt;&quot;]\n        GW[&quot;rj-gateway&quot;]\n    end\n\n    Anthropic[&quot;Anthropic API&quot;]\n\n    IC --&gt;|&quot;ANTHROPIC_BASE_URL&quot;| Egress\n    PC --&gt;|&quot;ANTHROPIC_BASE_URL&quot;| Egress\n    HS --&gt;|&quot;ANTHROPIC_BASE_URL&quot;| Egress\n    Egress --&gt;|&quot;Tailscale tunnel&quot;| AI\n    AI --&gt;|&quot;Proxied + metered&quot;| Anthropic\n    AI --&gt;|&quot;Webhook&quot;| GW\n\n    style AI fill:#4a9,stroke:#2a7,color:white\n    style Anthropic fill:#d4a,stroke:#a27,color:white\n\" dir=\"auto\">\n    <div class=\"render-plaintext-hidden\" dir=\"auto\">\n      <pre lang=\"mermaid\" aria-label=\"Raw mermaid code\">graph TB\n    subgraph \"Kubernetes Cluster\"\n        IC[\"IronClaw Agent\"]\n        PC[\"PicoClaw Agent\"]\n        HS[\"HexStrike Agent\"]\n        Egress[\"Egress Proxy&lt;br/&gt;&lt;small&gt;aperture.fuzzy-dev.svc&lt;/small&gt;\"]\n    end\n\n    subgraph \"Tailnet\"\n        AI[\"Aperture&lt;br/&gt;&lt;small&gt;ai.example.ts.net&lt;/small&gt;\"]\n        GW[\"rj-gateway\"]\n    end\n\n    Anthropic[\"Anthropic API\"]\n\n    IC --&gt;|\"ANTHROPIC_BASE_URL\"| Egress\n    PC --&gt;|\"ANTHROPIC_BASE_URL\"| Egress\n    HS --&gt;|\"ANTHROPIC_BASE_URL\"| Egress\n    Egress --&gt;|\"Tailscale tunnel\"| AI\n    AI --&gt;|\"Proxied + metered\"| Anthropic\n    AI --&gt;|\"Webhook\"| GW\n\n    style AI fill:#4a9,stroke:#2a7,color:white\n    style Anthropic fill:#d4a,stroke:#a27,color:white\n</pre>\n    </div>\n  </div>\n  <span class=\"js-render-enrichment-loader d-flex flex-justify-center flex-items-center width-full\" style=\"min-height:100px\" role=\"presentation\">\n    <span data-view-component=\"true\">\n  <svg style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\" data-view-component=\"true\" class=\"octospinner mx-auto tmp-mx-auto anim-rotate\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n    <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n</svg>    <span class=\"sr-only\">Loading</span>\n</span>\n  </span>\n</section>\n\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Enter dev shell (Go, Dhall, just, jq)\nnix develop\n\n# Or install dependencies manually:\n#   go 1.22+, just, dhall, dhall-json, jq, curl\n\n# 1. Create an ephemeral auth key\nexport TS_KEY   # set to your Tailscale management key (tskey-api-...)\nexport TAILNET      # set to your tailnet name (e.g. example.ts.net)\nexport TS_AUTHKEY=$(just key)\n\n# 2. Read current config\njust read\n# Prints JSON to stdout, hash to stderr\n\n# 3. Edit config/default.dhall, then render\njust render\n\n# 4. Push the rendered config\njust write config/rendered.json &lt;hash-from-step-2&gt;\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Enter dev shell (Go, Dhall, just, jq)</span>\nnix develop\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Or install dependencies manually:</span>\n<span class=\"pl-c\"><span class=\"pl-c\">#</span>   go 1.22+, just, dhall, dhall-json, jq, curl</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> 1. Create an ephemeral auth key</span>\n<span class=\"pl-k\">export</span> TS_KEY   <span class=\"pl-c\"><span class=\"pl-c\">#</span> set to your Tailscale management key (tskey-api-...)</span>\n<span class=\"pl-k\">export</span> TAILNET      <span class=\"pl-c\"><span class=\"pl-c\">#</span> set to your tailnet name (e.g. example.ts.net)</span>\n<span class=\"pl-k\">export</span> TS_AUTHKEY=<span class=\"pl-s\"><span class=\"pl-pds\">$(</span>just key<span class=\"pl-pds\">)</span></span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> 2. Read current config</span>\njust <span class=\"pl-c1\">read</span>\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Prints JSON to stdout, hash to stderr</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> 3. Edit config/default.dhall, then render</span>\njust render\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> 4. Push the rendered config</span>\njust write config/rendered.json <span class=\"pl-k\">&lt;</span>hash-from-step-<span class=\"pl-k\">2&gt;</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Full bootstrap (one command)</h2><a id=\"user-content-full-bootstrap-one-command\" class=\"anchor\" aria-label=\"Permalink: Full bootstrap (one command)\" href=\"#full-bootstrap-one-command\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"export TS_KEY   # set to your Tailscale management key\nexport TAILNET      # set to your tailnet name\njust bootstrap\"><pre><span class=\"pl-k\">export</span> TS_KEY   <span class=\"pl-c\"><span class=\"pl-c\">#</span> set to your Tailscale management key</span>\n<span class=\"pl-k\">export</span> TAILNET      <span class=\"pl-c\"><span class=\"pl-c\">#</span> set to your tailnet name</span>\njust bootstrap</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Config management with Dhall</h2><a id=\"user-content-config-management-with-dhall\" class=\"anchor\" aria-label=\"Permalink: Config management with Dhall\" href=\"#config-management-with-dhall\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The <code>config/</code> directory uses <a href=\"https://dhall-lang.org\" rel=\"nofollow\">Dhall</a> for type-safe config:</p>\n<ul dir=\"auto\">\n<li><code>config/types.dhall</code> — Aperture config type definitions</li>\n<li><code>config/default.dhall</code> — Your config template (edit this)</li>\n<li><code>just render</code> — Compiles Dhall to <code>config/rendered.json</code></li>\n<li><code>just check</code> — Type-checks without producing output</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Why tsnet?</h2><a id=\"user-content-why-tsnet\" class=\"anchor\" aria-label=\"Permalink: Why tsnet?\" href=\"#why-tsnet\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Method</th>\n<th>Identity seen by Aperture</th>\n<th>Works?</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>curl</code> from tagged device</td>\n<td>(empty / tagged-devices)</td>\n<td>No</td>\n</tr>\n<tr>\n<td>SOCKS proxy in container</td>\n<td>(empty / tagged-devices)</td>\n<td>No</td>\n</tr>\n<tr>\n<td><code>tsnet.Server.HTTPClient()</code></td>\n<td><code>jess@example.com</code></td>\n<td>Yes</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<p dir=\"auto\">Aperture's WhoIs only recognizes <strong>user login names</strong> (emails) and the <code>\"*\"</code> wildcard.  Tagged devices don't have a login name — they have tags.  tsnet creates a proper user-owned node whose <code>HTTPClient()</code> presents the correct WhoIs identity.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Blog posts</h2><a id=\"user-content-blog-posts\" class=\"anchor\" aria-label=\"Permalink: Blog posts\" href=\"#blog-posts\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><a href=\"blog/part-1-identity.md\">Part 1: Aperture and the tagged-device identity gap</a></li>\n<li><a href=\"blog/part-2-bootstrap.md\">Part 2: Bootstrapping Aperture config with tsnet</a></li>\n</ul>\n</article></div>",
      "readme_excerpt": "Demo Bootstrap Tailscale Aperture config from tagged devices using tsnet.\nAperture uses Tailscale WhoIs to identify who's connecting.  If your devices are tagged (as most K8s workloads are), Aperture doesn't recognize the literal string \"tagged-devices\" or tag names like \"tag:dev\" in its tempgrants config.  Only explicit user emails and the \"\" wildcard match.\nThis creates a chicken-and-egg: you can't access the config API to fix the grants because you don't have a grant.\nmermaid\ngraph LR\n   ...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/aperture-bootstrap",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/aperture-bootstrap/releases",
      "og_image_url": "https://opengraph.githubassets.com/1c403c4cf6b397a54bc8aa03d43144292256b4907de4a9b9e456c26697ae8140/Jesssullivan/aperture-bootstrap",
      "license": "",
      "pushed_at": "2026-02-26T14:26:27Z",
      "enriched_at": "2026-04-26T17:17:18.535Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-la-mesh",
      "name": "LA-Mesh",
      "repo": "jesssullivan/LA-Mesh",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "systems",
      "description": "LoRa infrastructure projects for Southern Maine.",
      "featured": false,
      "tags": [
        "lora",
        "mesh",
        "firmware",
        "svelte"
      ],
      "version": "la-mesh-v2.7.15.567b8ea",
      "release_date": "2026-02-13T05:53:38Z",
      "releases": [
        {
          "tag": "la-mesh-v2.7.15.567b8ea",
          "date": "2026-02-13T05:53:38Z",
          "body": "## LA-Mesh Custom Firmware v2.7.15.567b8ea\n\nCustom Meshtastic firmware with LA-Mesh boot splash and network defaults.\nBased on [Meshtastic v2.7.15.567b8ea](https://github.com/meshtastic/firmware/releases/tag/v2.7.15.567b8ea).\n\n### Changes from Stock Firmware\n- Custom boot splash: Maine state silhouette + \"LA-Mesh\" text (5s after default splash)\n- Default LoRa region: US\n- Default owner: LA-Mesh / LAM\n\n### Downloads\n\n| Binary | SHA256 |\n|--------|--------|\n| firmware-station-g2-2.7.15.567b8ea-lamesh.bin | `d49ec0afb3adfd475c4b7b25e0e280cab4b218e5cdfeef53af5beb010aecdb06` |\n| firmware-t-deck-2.7.15.567b8ea-lamesh.bin | `7af26345fb24eedf2a9f578d5c011854f7b513cd7fe0c3d110bf00ff74259cc0` |\n| firmware-t-deck-pro-2.7.15.567b8ea-lamesh.bin | `0c4d06bf16bb0b9403c58fcdd6cf4c8cca08469842fef6cdfd91c320e713aa30` |\n\n\n### Verify Downloads\n\n```bash\n# Download checksums file\ncurl -fSLO https://github.com/Jesssullivan/LA-Mesh/releases/download/la-mesh-v2.7.15.567b8ea/SHA256SUMS.txt\n\n# Verify all binaries\nsha256sum -c SHA256SUMS.txt\n```\n\n### Flash\n\n```bash\njust fetch-firmware --source custom\njust flash-g2\n```\n\nBLE OTA and LittleFS partitions use stock Meshtastic binaries (unchanged).\nOnly the main firmware binary includes LA-Mesh customizations."
        }
      ],
      "stars": 0,
      "topics": [
        "comms",
        "curriculum",
        "lora",
        "sdr",
        "infrastructure"
      ],
      "languages": [
        {
          "name": "Svelte",
          "color": "#ff3e00",
          "bytes": 158392
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 63477
        },
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 43472
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 28622
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 3268
        }
      ],
      "primary_language": "Svelte",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">LA-Mesh</h1><a id=\"user-content-la-mesh\" class=\"anchor\" aria-label=\"Permalink: LA-Mesh\" href=\"#la-mesh\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Work in progress LoRa mesh network and encrypted communication bridge infrastructure for Southern Maine.</p>\n<p dir=\"auto\">Community-driven encrypted mesh communications covering the Lewiston-Auburn area, supporting Meshtastic, MeshCore, and LoRa radio education.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">What is LA-Mesh?</h2><a id=\"user-content-what-is-la-mesh\" class=\"anchor\" aria-label=\"Permalink: What is LA-Mesh?\" href=\"#what-is-la-mesh\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">LA-Mesh deploys a resilient, encrypted mesh network using LoRa radio technology. Our goals:</p>\n<ul dir=\"auto\">\n<li><strong>Encrypted communications</strong> for community members via Meshtastic mesh devices; 3 private encrypted channels plus the default public Meshtastic channel (LongFast) so LA-Mesh nodes also participate in the broader Meshtastic network.</li>\n<li><strong>Infrastructure relay nodes</strong> We aspire to distribute, establish and help maintain high quality router nodes on rooftops and towers for wide coverage!</li>\n<li><strong>Education</strong> We aspire to meet fortnightly in the LA area</li>\n<li><strong>Bridges, eventually</strong> Jess wants to connect SMS, email, and internet to the mesh network</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Supported Devices</h2><a id=\"user-content-supported-devices\" class=\"anchor\" aria-label=\"Permalink: Supported Devices\" href=\"#supported-devices\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Device</th>\n<th>Role</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Station G2</strong></td>\n<td>Base station / relay</td>\n<td>High-power (up to 4.46W), rooftop/tower deployment (want one?  have a roof, pole or sunny spot?  We'll giev you one)</td>\n</tr>\n<tr>\n<td><strong>T-Deck Pro</strong></td>\n<td>Mobile client</td>\n<td>Full keyboard, GPS, portable encrypted comms; turn key (want one and in the LA area?  We'll give you one)</td>\n</tr>\n<tr>\n<td><strong>T-Deck Pro (E-Ink)</strong></td>\n<td>Low-power client</td>\n<td>Battery-optimized, sunlight-readable</td>\n</tr>\n<tr>\n<td><strong>FireElmo-SDR</strong></td>\n<td>Custom PCB + Pi HAT gateway</td>\n<td>SMS/email bridge, runs meshtasticd on Linux (custom PCB/software project)</td>\n</tr>\n<tr>\n<td><strong>HackRF H4M</strong></td>\n<td>SDR spectrum analysis for teaching</td>\n<td>Jess has curricula for basic TEMPEST attacks, packet capture, interference and range testing.</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Device Provisioning</h2><a id=\"user-content-device-provisioning\" class=\"anchor\" aria-label=\"Permalink: Device Provisioning\" href=\"#device-provisioning\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Three commands to go from a blank device to a deployed mesh node:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# 1. Flash firmware (interactive — prompts for bootloader mode)\njust flash-g2\n\n# 2. Configure everything: LoRa, channels, owner name\njust configure-g2 &quot;LA-Mesh RTR-01&quot; &quot;R01&quot;\n\n# 3. Set ROUTER role (final step — kills USB serial on ESP32-S3)\njust mesh-set-role ROUTER\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> 1. Flash firmware (interactive — prompts for bootloader mode)</span>\njust flash-g2\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> 2. Configure everything: LoRa, channels, owner name</span>\njust configure-g2 <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>LA-Mesh RTR-01<span class=\"pl-pds\">\"</span></span> <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>R01<span class=\"pl-pds\">\"</span></span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> 3. Set ROUTER role (final step — kills USB serial on ESP32-S3)</span>\njust mesh-set-role ROUTER</pre></div>\n<p dir=\"auto\">Step 2 reads PSK values from <code>.env</code> if you are into that life. (auto-sourced by justfile). Generate\nfresh keys with <code>just generate-psks</code> and store them in KeePassXC.  We reccomend using a proper secret\nmanagment system, not <code>.env</code>.</p>\n<p dir=\"auto\">Port is auto-detected (<code>/dev/ttyACM0</code> &gt; <code>ttyUSB0</code>). Pass explicitly if\nneeded: <code>just flash-g2 /dev/ttyACM1</code>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Development</h2><a id=\"user-content-development\" class=\"anchor\" aria-label=\"Permalink: Development\" href=\"#development\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><strong>Prerequisites</strong>: Nix with flakes enabled, or manually install: Node.js 22, pnpm, just, meshtastic CLI, esptool</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Clone and enter dev shell\ngit clone https://github.com/Jesssullivan/LA-Mesh.git\ncd LA-Mesh\nnix develop\n\n# First-time setup\njust setup\n\n# Build documentation site\njust build\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Clone and enter dev shell</span>\ngit clone https://github.com/Jesssullivan/LA-Mesh.git\n<span class=\"pl-c1\">cd</span> LA-Mesh\nnix develop\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> First-time setup</span>\njust setup\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Build documentation site</span>\njust build</pre></div>\n</article></div>",
      "readme_excerpt": "Work in progress LoRa mesh network and encrypted communication bridge infrastructure for Southern Maine.\nCommunity-driven encrypted mesh communications covering the Lewiston-Auburn area, supporting Meshtastic, MeshCore, and LoRa radio education.\nLA-Mesh deploys a resilient, encrypted mesh network using LoRa radio technology. Our goals:\n- Encrypted communications for community members via Meshtastic mesh devices; 3 private encrypted channels plus the default public Meshtastic channel (LongFast)...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/LA-Mesh",
      "website_url": "http://la-mesh.me/",
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/LA-Mesh/releases",
      "og_image_url": "https://opengraph.githubassets.com/51fd1d3c0cd2c523cb6ea11f19e52a607d66493c59dcbfb99285ed73077d1748/Jesssullivan/LA-Mesh",
      "license": "MIT",
      "pushed_at": "2026-02-13T15:27:10Z",
      "enriched_at": "2026-04-26T17:17:18.870Z",
      "gate": "homepage"
    },
    {
      "slug": "jesssullivan-gis_shortcuts",
      "name": "GIS_Shortcuts",
      "repo": "jesssullivan/GIS_Shortcuts",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "devex",
      "description": "Jess's miscellaneous GIS notes and related tomfoolery ",
      "featured": false,
      "tags": [
        "gis",
        "r",
        "spatial"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 1,
      "topics": [
        "gdal",
        "gis",
        "esri",
        "wsl",
        "rscript",
        "virtualbox",
        "ebird-api",
        "rshiny"
      ],
      "languages": [
        {
          "name": "R",
          "color": "#198CE7",
          "bytes": 5475
        },
        {
          "name": "HTML",
          "color": "#e34c26",
          "bytes": 1369
        },
        {
          "name": "CSS",
          "color": "#663399",
          "bytes": 958
        },
        {
          "name": "Stylus",
          "color": "#ff6347",
          "bytes": 860
        },
        {
          "name": "JavaScript",
          "color": "#f1e05a",
          "bytes": 681
        }
      ],
      "primary_language": "R",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">GIS Shortcuts</h1><a id=\"user-content-gis-shortcuts\" class=\"anchor\" aria-label=\"Permalink: GIS Shortcuts\" href=\"#gis-shortcuts\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"Python_Results.png\"><img title=\"3d From the command line....\" src=\"Python_Results.png\" width=\"500px\" style=\"max-width: 100%;\"></a></p>\n<hr>\n<p dir=\"auto\"><em>Miscellaneous gis notes, mostly derived from <a href=\"https://www.transscendsurvival.org/\" rel=\"nofollow\">my blog over here</a></em></p>\n<hr>\n<p dir=\"auto\"><strong>Index:</strong>  <br></p>\n<p dir=\"auto\"><a href=\"#ebird\"><strong>eBird API stuff</strong></a> <br>\n<a href=\"#widget\"><strong>R / Shiny Web Experiments</strong></a> <br>\n<a href=\"#rmacros\"><strong>Shell macros from R</strong></a> <br>\n<a href=\"#windows\"><strong>When it must be Windows</strong></a> <br>\n<a href=\"#wsl\"><strong>Windows WSL - Ubuntu GDAL setup</strong></a> <br>\n<a href=\"#cartopy\"><strong>Ubuntu &amp; Jupyter cartopy setup</strong></a> <br>\n<a href=\"#osx\"><strong>Mac OSX - GDAL setup</strong></a> <br>\n<a href=\"#demstitch\"><strong>Bash Example - DEM stitching</strong></a> <br>\n<a href=\"https://github.com/Jesssullivan/rJDKmanager\"><strong>Link to rJDK management info</strong></a> <br></p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 id=\"user-content-ebird\" class=\"heading-element\" dir=\"auto\"> </h4><a id=\"user-content--\" class=\"anchor\" aria-label=\"Permalink: \" href=\"#-\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>     \n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\"><em>eBird regionCode</em></h2><a id=\"user-content-ebird-regioncode\" class=\"anchor\" aria-label=\"Permalink: eBird regionCode\" href=\"#ebird-regioncode\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The <a href=\"https://ebird.org/science/download-ebird-data-products\" rel=\"nofollow\">Ebird dataset</a> is awesome.  While directly handling data as a <strong>massive</strong> delimited file- as distributed by <a href=\"https://ebird.org/data/download\" rel=\"nofollow\">the eBird people-</a> is cumbersome at best, the <a href=\"https://documenter.getpostman.com/view/664302/S1ENwy59?version=latest#e18ea3b5-e80c-479f-87db-220ce8d9f3b6\" rel=\"nofollow\">ebird api</a> offers a fairly straightforward and efficient alternative for a few choice bits and batches of data.</p>\n<ul dir=\"auto\">\n<li>\n<p dir=\"auto\">The eBird <code>AWK</code> tool for filtering the actual delimited data can be <a href=\"https://cornelllabofornithology.github.io/auk/\" rel=\"nofollow\">found over here</a>:</p>\n<p dir=\"auto\"><code>install.packages(\"auk\")</code></p>\n</li>\n</ul>\n<p dir=\"auto\">It is worth noting R + <code>auk</code> (or frankly any R centered filtering method) will quickly become limited by the single-threaded approach of R, and how you're managing memory as you iterate.  Working and querying the data from a proper database quickly becomes necessary.</p>\n<p dir=\"auto\">Most conveniently, the <a href=\"https://documenter.getpostman.com/view/664302/S1ENwy59?version=latest#e18ea3b5-e80c-479f-87db-220ce8d9f3b6\" rel=\"nofollow\">eBird API already exists.</a></p>\n<p dir=\"auto\">...The API package for R is <a href=\"https://cran.r-project.org/web/packages/rebird/index.html\" rel=\"nofollow\">over here</a>:<br>\n<code>install.packages(\"rebird\")</code></p>\n<p dir=\"auto\">...There is also a neat Python wrapper <a href=\"https://pypi.org/project/ebird-api/\" rel=\"nofollow\">over here</a>:<br>\n<code>pip3 install ebird-api</code></p>\n<p dir=\"auto\"><em><strong>Region Codes:</strong></em></p>\n<p dir=\"auto\">I'm not sure why, but some methods use normal latitude / longitude in decimal degrees while some others use <code>\"regionCode\"</code>, which seems to be some kind of eBird special.  Only ever seen this format in ebird data.</p>\n<p dir=\"auto\">For example, recent observations uses <code>regionCode</code>:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# GET Recent observations in a region:\n# https://api.ebird.org/v2/data/obs/{{regionCode}}/recent\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> GET Recent observations in a region:</span>\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> https://api.ebird.org/v2/data/obs/{{regionCode}}/recent</span></pre></div>\n<p dir=\"auto\">...But nearby recent observations uses latitude / longitude:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# GET Recent nearby observations:\n# https://api.ebird.org/v2/data/obs/geo/recent?lat={{lat}}&amp;lng={{lng}}\"><pre class=\"notranslate\"><code># GET Recent nearby observations:\n# https://api.ebird.org/v2/data/obs/geo/recent?lat={{lat}}&amp;lng={{lng}}\n</code></pre></div>\n<p dir=\"auto\">Regardless, lets just write a function to convert decimal degrees to this <code>regionCode</code> thing.  Here's mine:</p>\n<div class=\"highlight highlight-source-python notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"#!/usr/bin/env python3\n&quot;&quot;&quot;\n# provide latitude &amp; longitude, return eBird &quot;regionCode&quot;\nWritten by Jess Sullivan\n@ https://transscendsurvival.org/\n&quot;&quot;&quot;\nimport requests\nimport json\n\n\ndef get_regioncode(lat, lon):\n\n    # this municipal api is a publicly available, no keys needed afaict\n    census_url = str('https://geo.fcc.gov/api/census/area?lat=' +\n                     str(lat) +\n                     '&amp;lon=' +\n                     str(lon) +\n                     '&amp;format=json')\n\n    # send out a GET request:\n    payload = {}\n    get = requests.request(&quot;GET&quot;, census_url, data=payload)\n\n    # parse the response, all api values are contained in list 'results':\n    response = json.loads(get.content)['results'][0]\n\n    # use the last three digits from the in-state fips code as the &quot;subnational 2&quot; identifier:\n    fips = response['county_fips']\n\n    # assemble and return the &quot;subnational type 2&quot; code:\n    regioncode = 'US-' + response['state_code'] + '-' + fips[2] + fips[3] + fips[4]\n    print('formed region code: ' + regioncode)\n    return regioncode\n\"><pre><span class=\"pl-c\">#!/usr/bin/env python3</span>\n<span class=\"pl-s\">\"\"\"</span>\n<span class=\"pl-s\"># provide latitude &amp; longitude, return eBird \"regionCode\"</span>\n<span class=\"pl-s\">Written by Jess Sullivan</span>\n<span class=\"pl-s\">@ https://transscendsurvival.org/</span>\n<span class=\"pl-s\">\"\"\"</span>\n<span class=\"pl-k\">import</span> <span class=\"pl-s1\">requests</span>\n<span class=\"pl-k\">import</span> <span class=\"pl-s1\">json</span>\n\n\n<span class=\"pl-k\">def</span> <span class=\"pl-en\">get_regioncode</span>(<span class=\"pl-s1\">lat</span>, <span class=\"pl-s1\">lon</span>):\n\n    <span class=\"pl-c\"># this municipal api is a publicly available, no keys needed afaict</span>\n    <span class=\"pl-s1\">census_url</span> <span class=\"pl-c1\">=</span> <span class=\"pl-en\">str</span>(<span class=\"pl-s\">'https://geo.fcc.gov/api/census/area?lat='</span> <span class=\"pl-c1\">+</span>\n                     <span class=\"pl-en\">str</span>(<span class=\"pl-s1\">lat</span>) <span class=\"pl-c1\">+</span>\n                     <span class=\"pl-s\">'&amp;lon='</span> <span class=\"pl-c1\">+</span>\n                     <span class=\"pl-en\">str</span>(<span class=\"pl-s1\">lon</span>) <span class=\"pl-c1\">+</span>\n                     <span class=\"pl-s\">'&amp;format=json'</span>)\n\n    <span class=\"pl-c\"># send out a GET request:</span>\n    <span class=\"pl-s1\">payload</span> <span class=\"pl-c1\">=</span> {}\n    <span class=\"pl-s1\">get</span> <span class=\"pl-c1\">=</span> <span class=\"pl-s1\">requests</span>.<span class=\"pl-c1\">request</span>(<span class=\"pl-s\">\"GET\"</span>, <span class=\"pl-s1\">census_url</span>, <span class=\"pl-s1\">data</span><span class=\"pl-c1\">=</span><span class=\"pl-s1\">payload</span>)\n\n    <span class=\"pl-c\"># parse the response, all api values are contained in list 'results':</span>\n    <span class=\"pl-s1\">response</span> <span class=\"pl-c1\">=</span> <span class=\"pl-s1\">json</span>.<span class=\"pl-c1\">loads</span>(<span class=\"pl-s1\">get</span>.<span class=\"pl-c1\">content</span>)[<span class=\"pl-s\">'results'</span>][<span class=\"pl-c1\">0</span>]\n\n    <span class=\"pl-c\"># use the last three digits from the in-state fips code as the \"subnational 2\" identifier:</span>\n    <span class=\"pl-s1\">fips</span> <span class=\"pl-c1\">=</span> <span class=\"pl-s1\">response</span>[<span class=\"pl-s\">'county_fips'</span>]\n\n    <span class=\"pl-c\"># assemble and return the \"subnational type 2\" code:</span>\n    <span class=\"pl-s1\">regioncode</span> <span class=\"pl-c1\">=</span> <span class=\"pl-s\">'US-'</span> <span class=\"pl-c1\">+</span> <span class=\"pl-s1\">response</span>[<span class=\"pl-s\">'state_code'</span>] <span class=\"pl-c1\">+</span> <span class=\"pl-s\">'-'</span> <span class=\"pl-c1\">+</span> <span class=\"pl-s1\">fips</span>[<span class=\"pl-c1\">2</span>] <span class=\"pl-c1\">+</span> <span class=\"pl-s1\">fips</span>[<span class=\"pl-c1\">3</span>] <span class=\"pl-c1\">+</span> <span class=\"pl-s1\">fips</span>[<span class=\"pl-c1\">4</span>]\n    <span class=\"pl-en\">print</span>(<span class=\"pl-s\">'formed region code: '</span> <span class=\"pl-c1\">+</span> <span class=\"pl-s1\">regioncode</span>)\n    <span class=\"pl-k\">return</span> <span class=\"pl-s1\">regioncode</span></pre></div>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 id=\"user-content-widget\" class=\"heading-element\" dir=\"auto\"> </h4><a id=\"user-content---1\" class=\"anchor\" aria-label=\"Permalink: \" href=\"#--1\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>     \n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\"><em>GIS Wigdets, Web Apps &amp; R/ Shiny</em></h2><a id=\"user-content-gis-wigdets-web-apps--r-shiny\" class=\"anchor\" aria-label=\"Permalink: GIS Wigdets, Web Apps &amp; R/ Shiny\" href=\"#gis-wigdets-web-apps--r-shiny\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Some experiments with simple web utilities for GIS tasks.</p>\n<p dir=\"auto\">Process manager experiments for R threads in <a href=\"/Flask-Manager\">/Flask-Manager</a>.</p>\n<p dir=\"auto\">Visit the <a href=\"https://kml-tools.herokuapp.com/\" rel=\"nofollow\">single threaded example here</a>; (Not load balanced, just for an example view).  These are wrapped in a Node.JS application on Heroku, which loads each utility through <a href=\"https://www.shinyapps.io/\" rel=\"nofollow\">shinyapps.io</a>.  These functions are hosted entirely through (Heroku / shinyapps) free tiers.</p>\n<p dir=\"auto\">See /<a href=\"https://github.com/Jesssullivan/Shiny-Apps/tree/master/Docker-App\">Docker-App</a> for deployment in the GCP app engine.</p>\n<ul dir=\"auto\">\n<li>Raster2stl - converts raster data (image- a .jpg taken from a DEM file for instance) to a 3d STL file showing exagerated terrain.  (added 3/6/19)</li>\n<li>KML2SHP_Converter - generates a zip archive of shape files based on KML file layers</li>\n<li>Centroid_KML - \"KML-Centroid-Generator\"</li>\n<li>KML2CSV - converts KML (XML) to .csv spreadsheet format</li>\n<li>KMLSubsetFilter - from a KML file, this tool returns a subset KML based on two query strings (searches the description field of the KML)</li>\n</ul>\n<p dir=\"auto\"><em>Some more Bits, Bobs, Widgets &amp; R / Shiny demos:</em></p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th><a href=\"https://www.youtube.com/watch?v=79XI5FTxD2E\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/3e86ff3798543e9c9931e53f550bcd7404d3361a946d94bacc9d5922cf00800e/68747470733a2f2f696d672e796f75747562652e636f6d2f76692f37395849354654784432452f302e6a7067\" alt=\" \" data-canonical-src=\"https://img.youtube.com/vi/79XI5FTxD2E/0.jpg\" style=\"max-width: 100%;\"></a></th>\n<th><a href=\"https://www.youtube.com/watch?v=sf7rDg86CJE\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/a016c3c44ed630932486cabfb68c0072506057caaf0564937378d697f9d2ccef/68747470733a2f2f696d672e796f75747562652e636f6d2f76692f7366377244673836434a452f302e6a7067\" alt=\" \" data-canonical-src=\"https://img.youtube.com/vi/sf7rDg86CJE/0.jpg\" style=\"max-width: 100%;\"></a></th>\n</tr>\n</thead>\n</table></markdown-accessiblity-table>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\"><em>Some GDAL shell macros from R instead of rgdal</em></h2><a id=\"user-content-some-gdal-shell-macros-from-r-instead-of-rgdal\" class=\"anchor\" aria-label=\"Permalink: Some GDAL shell macros from R instead of rgdal\" href=\"#some-gdal-shell-macros-from-r-instead-of-rgdal\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><a href=\"https://www.transscendsurvival.org/2020/03/01/1607/\" rel=\"nofollow\">Visit this blog post</a></p>\n<p dir=\"auto\"><em>it's not R sacrilege if nobody knows</em></p>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 id=\"user-content-rmacros\" class=\"heading-element\" dir=\"auto\"> </h4><a id=\"user-content---2\" class=\"anchor\" aria-label=\"Permalink: \" href=\"#--2\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>     \n<p dir=\"auto\">Even the little stuff benefits from some organizational scripting, even if it’s just to catalog one’s actions.  Here are some examples for common tasks.</p>\n<p dir=\"auto\">Get all the source data into a R-friendly format like csv.  <code>ogr2ogr</code> has a nifty option <code>-lco GEOMETRY=AS_WKT</code> (Well-Known-Text) to keep track of spatial data throughout abstractions- we can add the WKT as a cell until it is time to write the data out again.</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# define a shapefile conversion to csv from system's shell:\nsys_SHP2CSV &lt;- function(shp) {\n  csvfile &lt;- paste0(shp, '.csv')\n  shpfile &lt;-paste0(shp, '.shp')\n  if (!file.exists(csvfile)) {\n    # use -lco GEOMETRY to maintain location\n    # for reference, shp --&gt; geojson would look like:\n    # system('ogr2ogr -f geojson output.geojson input.shp')\n    # keeps geometry as WKT:\n    cmd &lt;- paste('ogr2ogr -f CSV', csvfile, shpfile, '-lco GEOMETRY=AS_WKT')\n    system(cmd)  # executes command\n  } else {\n    print(paste('output file already exists, please delete', csvfile, 'before converting again'))\n  }\n  return(csvfile)\n}\"><pre class=\"notranslate\"><code># define a shapefile conversion to csv from system's shell:\nsys_SHP2CSV &lt;- function(shp) {\n  csvfile &lt;- paste0(shp, '.csv')\n  shpfile &lt;-paste0(shp, '.shp')\n  if (!file.exists(csvfile)) {\n    # use -lco GEOMETRY to maintain location\n    # for reference, shp --&gt; geojson would look like:\n    # system('ogr2ogr -f geojson output.geojson input.shp')\n    # keeps geometry as WKT:\n    cmd &lt;- paste('ogr2ogr -f CSV', csvfile, shpfile, '-lco GEOMETRY=AS_WKT')\n    system(cmd)  # executes command\n  } else {\n    print(paste('output file already exists, please delete', csvfile, 'before converting again'))\n  }\n  return(csvfile)\n}\n</code></pre></div>\n<p dir=\"auto\">Read the new csv into R:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# for file 'foo.shp':\nfoo_raw &lt;- read.csv(sys_SHP2CSV(shp='foo'), sep = ',')\"><pre class=\"notranslate\"><code># for file 'foo.shp':\nfoo_raw &lt;- read.csv(sys_SHP2CSV(shp='foo'), sep = ',')\n</code></pre></div>\n<p dir=\"auto\">One might do any number of things now, some here lets snag some columns and rename them:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# rename the subset of data &quot;foo&quot; we want in a data.frame:\nfoo &lt;- data.frame(foo_raw[1:5])\ncolnames(foo) &lt;- c('bar', 'eggs', 'ham', 'hello', 'world')\"><pre class=\"notranslate\"><code># rename the subset of data \"foo\" we want in a data.frame:\nfoo &lt;- data.frame(foo_raw[1:5])\ncolnames(foo) &lt;- c('bar', 'eggs', 'ham', 'hello', 'world')\n</code></pre></div>\n<p dir=\"auto\">We could do some more careful parsing too, here a semicolon in cell strings can be converted to a comma:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# replace ` ; ` to ` , ` in col &quot;bar&quot;:\nfoo$bar &lt;- gsub(pattern=&quot;;&quot;, replacement=&quot;,&quot;, foo$bar)\"><pre class=\"notranslate\"><code># replace ` ; ` to ` , ` in col \"bar\":\nfoo$bar &lt;- gsub(pattern=\";\", replacement=\",\", foo$bar)\n</code></pre></div>\n<p dir=\"auto\">Do whatever you do for an output directory:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# make a output file directory if you're into that\n# my preference is to only keep one set of output files per run\n# here, we'd reset the directory before adding any new output files\nredir &lt;- function(outdir) {\n  if (dir.exists(outdir)) {\n    system(paste('rm -rf', outdir))\n  }\n  dir.create(outdir)\n}\n\"><pre class=\"notranslate\"><code># make a output file directory if you're into that\n# my preference is to only keep one set of output files per run\n# here, we'd reset the directory before adding any new output files\nredir &lt;- function(outdir) {\n  if (dir.exists(outdir)) {\n    system(paste('rm -rf', outdir))\n  }\n  dir.create(outdir)\n}\n\n</code></pre></div>\n<p dir=\"auto\">Of course, once your data is in R there are countless \"R things\" one could do...</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# iterate to fill empty cells with preceding values\nfor (i in 1:length(foo[,1])) {\n  if (nchar(foo$bar[i]) &lt; 1) {\n    foo$bar[i] &lt;- foo$bar[i-1]\n  }\n  # fill incomplete rows with NA values:\n  if (nchar(foo$bar[i]) &lt; 1) {\n    foo[i,] &lt;- NA  \n  }\n}\n\n# remove NA rows if there is nothing better to do:\nnewfoo &lt;- na.omit(foo)\"><pre class=\"notranslate\"><code># iterate to fill empty cells with preceding values\nfor (i in 1:length(foo[,1])) {\n  if (nchar(foo$bar[i]) &lt; 1) {\n    foo$bar[i] &lt;- foo$bar[i-1]\n  }\n  # fill incomplete rows with NA values:\n  if (nchar(foo$bar[i]) &lt; 1) {\n    foo[i,] &lt;- NA  \n  }\n}\n\n# remove NA rows if there is nothing better to do:\nnewfoo &lt;- na.omit(foo)\n</code></pre></div>\n<p dir=\"auto\">Even though this is totally adding a level of complexity to what could be a single <code>ogr2ogr</code>  command, I've decided it is still worth it- I'd definitely rather keep track of everything I do over forget what I did.... xD</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# make some methods to write out various kinds of files via gdal:\nto_GEO &lt;- function(target) {\n  print(paste('converting', target, 'to geojson .... '))\n  system(paste('ogr2ogr -f', &quot; geojson &quot;,  paste0(target, '.geojson'), paste0(target, '.csv')))\n}\n\nto_SHP &lt;- function(target) {\n  print(paste('converting ', target, ' to ESRI Shapefile .... '))\n  system(paste('ogr2ogr -f', &quot; 'ESRI Shapefile' &quot;,  paste0(target, '.shp'), paste0(target, '.csv')))\n}\n\n# name files:\nfoo_name &lt;- 'output_foo'\n\n# for table data 'foo', first:\nwrite.csv(foo, paste0(foo_name, '.csv'))\n\n# convert with the above csv:\nto_SHP(foo_name)\"><pre class=\"notranslate\"><code># make some methods to write out various kinds of files via gdal:\nto_GEO &lt;- function(target) {\n  print(paste('converting', target, 'to geojson .... '))\n  system(paste('ogr2ogr -f', \" geojson \",  paste0(target, '.geojson'), paste0(target, '.csv')))\n}\n\nto_SHP &lt;- function(target) {\n  print(paste('converting ', target, ' to ESRI Shapefile .... '))\n  system(paste('ogr2ogr -f', \" 'ESRI Shapefile' \",  paste0(target, '.shp'), paste0(target, '.csv')))\n}\n\n# name files:\nfoo_name &lt;- 'output_foo'\n\n# for table data 'foo', first:\nwrite.csv(foo, paste0(foo_name, '.csv'))\n\n# convert with the above csv:\nto_SHP(foo_name)\n</code></pre></div>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 id=\"user-content-wsl\" class=\"heading-element\" dir=\"auto\"> </h4><a id=\"user-content---3\" class=\"anchor\" aria-label=\"Permalink: \" href=\"#--3\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>     \n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\"><em>Using Ubuntu GDAL on Windows w/ WSL</em></h2><a id=\"user-content-using-ubuntu-gdal-on-windows-w-wsl\" class=\"anchor\" aria-label=\"Permalink: Using Ubuntu GDAL on Windows w/ WSL\" href=\"#using-ubuntu-gdal-on-windows-w-wsl\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><a href=\"https://docs.microsoft.com/en-us/windows/wsl/install-win10\" rel=\"nofollow\">LINK: get the WSL shell from Microsoft</a></p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# In the WSL shell:\n\nsudo apt-get install python3.6-dev -y\nsudo add-apt-repository ppa:ubuntugis/ppa &amp;&amp; sudo apt-get update\nsudo apt-get install libgdal-dev -y\nsudo apt-get install gdal-bin -y\n\n# See here for more notes including Python bindings:\n# https://mothergeo-py.readthedocs.io/en/latest/development/how-to/gdal-ubuntu-pkg.html\"><pre class=\"notranslate\"><code># In the WSL shell:\n\nsudo apt-get install python3.6-dev -y\nsudo add-apt-repository ppa:ubuntugis/ppa &amp;&amp; sudo apt-get update\nsudo apt-get install libgdal-dev -y\nsudo apt-get install gdal-bin -y\n\n# See here for more notes including Python bindings:\n# https://mothergeo-py.readthedocs.io/en/latest/development/how-to/gdal-ubuntu-pkg.html\n</code></pre></div>\n<p dir=\"auto\"><em>In a new Shell:</em></p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# Double check the shell does indeed have GDAL in $PATH:\ngdalinfo --version\n\"><pre class=\"notranslate\"><code># Double check the shell does indeed have GDAL in $PATH:\ngdalinfo --version\n\n</code></pre></div>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\"><em>Ubuntu &amp; Jupyter: <code>cartopy</code> setup</em></h2><a id=\"user-content-ubuntu--jupyter-cartopy-setup\" class=\"anchor\" aria-label=\"Permalink: Ubuntu &amp; Jupyter: cartopy setup\" href=\"#ubuntu--jupyter-cartopy-setup\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 id=\"user-content-cartopy\" class=\"heading-element\" dir=\"auto\"> </h4><a id=\"user-content---4\" class=\"anchor\" aria-label=\"Permalink: \" href=\"#--4\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# install libraries:\nsudo apt install libproj-dev proj-data proj-bin\nsudo apt install libgeos-dev\"><pre class=\"notranslate\"><code># install libraries:\nsudo apt install libproj-dev proj-data proj-bin\nsudo apt install libgeos-dev\n</code></pre></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# install cython, cartopy:\npip3 install cython \npip3 install cartopy\"><pre class=\"notranslate\"><code># install cython, cartopy:\npip3 install cython \npip3 install cartopy\n</code></pre></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# fix shapely:\npip3 uninstall shapely\npip3 install shapely --no-binary shapely\"><pre class=\"notranslate\"><code># fix shapely:\npip3 uninstall shapely\npip3 install shapely --no-binary shapely\n</code></pre></div>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\"><em>Using GDAL on Mac OSX</em></h2><a id=\"user-content-using-gdal-on-mac-osx\" class=\"anchor\" aria-label=\"Permalink: Using GDAL on Mac OSX\" href=\"#using-gdal-on-mac-osx\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 id=\"user-content-osx\" class=\"heading-element\" dir=\"auto\"> </h4><a id=\"user-content---5\" class=\"anchor\" aria-label=\"Permalink: \" href=\"#--5\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>     \n<p dir=\"auto\"><a href=\"https://www.transscendsurvival.org/2019/10/07/gdal-for-gis-on-unix-using-a-mac-or-better-linux/\" rel=\"nofollow\">Visit this blog post</a></p>\n<p dir=\"auto\">Note: in my opinion, homebrew and macPorts are good ideas- try them!  If you don’t have Homebrew, get it now:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"/usr/bin/ruby -e &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)&quot;\"><pre class=\"notranslate\"><code>/usr/bin/ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"\n</code></pre></div>\n<p dir=\"auto\">(….However, port or brew installing QGIS and GDAL (primarily surrounding the delicate links between QGIS / GDAL / Python 2 &amp; 3 / OSX local paths) can cause baffling issues.  If possible, don’t do that.  Use QGIS installers from the official site or build from source!)</p>\n<p dir=\"auto\">if you need to resolve issues with your GDAL packages via removal:\non MacPorts, try similar:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"sudo port uninstall qgis py37-gdal\n\n# on homebrew, list then remove (follow its instructions):\n\nbrew list\nbrew uninstall gdal geos gdal2  \"><pre class=\"notranslate\"><code>sudo port uninstall qgis py37-gdal\n\n# on homebrew, list then remove (follow its instructions):\n\nbrew list\nbrew uninstall gdal geos gdal2  \n</code></pre></div>\n<p dir=\"auto\"><em>!!! NOTE: I am investigating more reliable built-from-source solutions for gdal on mac.</em></p>\n<p dir=\"auto\">Really!</p>\n<p dir=\"auto\">There are numerous issues with brew-installed gdal.  Those I have run into include:</p>\n<ul dir=\"auto\">\n<li>linking issues with the crucial directory “gdal-data” (libraries)</li>\n<li>linking issues Python bindings and python 2 vs. 3 getting confused</li>\n<li>internal raster library conflicts against the gdal requirements</li>\n<li>Proj.4 inconsistencies (see source notes below)</li>\n<li>OSX Framework conflicts with source / brew / port (<a href=\"http://www.kyngchaos.com/software/frameworks/\" rel=\"nofollow\">http://www.kyngchaos.com/software/frameworks/</a>)</li>\n<li>Linking conflicts with old, qgis default / LTR libraries against new ones</li>\n<li>Major KML discrepancies: expat standard vs libkml.</li>\n</ul>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"brew install gdal\n#\n# brew install qgis can work well too.  At least you can unbrew it!\n#\"><pre class=\"notranslate\"><code>brew install gdal\n#\n# brew install qgis can work well too.  At least you can unbrew it!\n#\n</code></pre></div>\n<p dir=\"auto\">Next, assuming your GDAL is not broken (on Mac OS this is rare and considered a miracle):</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# double check CLI is working:\ngdalinfo --version\n# “GDAL 2.4.0, released 2018/12/14”\ngdal_merge.py\n# list of args\"><pre class=\"notranslate\"><code># double check CLI is working:\ngdalinfo --version\n# “GDAL 2.4.0, released 2018/12/14”\ngdal_merge.py\n# list of args\n</code></pre></div>\n<hr>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 id=\"user-content-windows\" class=\"heading-element\" dir=\"auto\"> </h4><a id=\"user-content---6\" class=\"anchor\" aria-label=\"Permalink: \" href=\"#--6\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>     \n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\"><em>Regarding Windows-specific software, such as ArcMap</em></h2><a id=\"user-content-regarding-windows-specific-software-such-as-arcmap\" class=\"anchor\" aria-label=\"Permalink: Regarding Windows-specific software, such as ArcMap\" href=\"#regarding-windows-specific-software-such-as-arcmap\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><em>Remote Desktop:</em><br>\nThe greatest solution I've settled on for ArcMap use continues to be <a href=\"https://remotedesktop.google.com/home\" rel=\"nofollow\">Chrome Remote Desktop</a>, coupled with an <a href=\"https://www.plymouth.edu/webapp/itsurplus/\" rel=\"nofollow\">IT Surplus</a> desktop purchased for ~$50. Once Chrome is good to go on the remote Windows computer, one can operate everything from a web browser from anywhere else (even reboot and share files to and from the remote computer).  While adding an additional, dedicated computer like this may not be possible for many students, it is certainly the simplest and most dependable solution.</p>\n<p dir=\"auto\"><em>VirtualBox, Bootcamp, etc:</em><br>\n<a href=\"https://www.virtualbox.org/wiki/Downloads\" rel=\"nofollow\">Oracle's VirtualBox</a> is a longstanding (and free!) virtualization software.  A Windows virtual machine is vastly preferable over <a href=\"https://support.apple.com/boot-camp\" rel=\"nofollow\">Bootcamp</a> or further <a href=\"https://www.digitalocean.com/community/tutorials/how-to-partition-and-format-storage-devices-in-linux\" rel=\"nofollow\">partition tomfoolery</a>.\nOne can start / stop the VM only when its needed, store it on a usb stick, avoid <a href=\"https://www.transscendsurvival.org/2019/02/27/mac-osx-fixing-gpt-and-pmbr-tables/\" rel=\"nofollow\">insane pmbr issues</a>, etc.</p>\n<ul dir=\"auto\">\n<li>Bootcamp will consume at least 40gb of space at all times before even attempting to function, whereas even a fully configured Windows VirtualBox VDI will only consume ~22gb, and can be moved elsewhere if not in use.</li>\n<li>There are better (not free) virtualization tools such as <a href=\"https://www.parallels.com/\" rel=\"nofollow\">Parallels</a>, though any way you slice it a dedicated machine will almost always be a better solution.</li>\n</ul>\n<br>\n<p dir=\"auto\"><strong>Setup &amp; Configure VirtualBox:</strong></p>\n<ul dir=\"auto\">\n<li><a href=\"https://www.virtualbox.org/wiki/Downloads\" rel=\"nofollow\">Install VirtualBox- link</a></li>\n<li><a href=\"https://www.microsoft.com/en-us/software-download/windows10ISO\" rel=\"nofollow\">Download a Windows 10 ISO- link</a></li>\n</ul>\n<p dir=\"auto\">There are numerous sites with VirtualBox guides, so I will not go into detail here.</p>\n<p dir=\"auto\"><em>Extra bits on setup-</em></p>\n<ul dir=\"auto\">\n<li>\n<p dir=\"auto\"><a href=\"https://www.virtualbox.org/manual/ch04.html\" rel=\"nofollow\">Guest Additions</a> are not necessary, despite what some folks may suggest.</p>\n</li>\n<li>\n<p dir=\"auto\">Dynamically Allocated VDI is the way to go as a virtual disk.  There is no reason not to set the allocated disk size to the biggest value allowed, as it will never consume any more space than the virtual machine actually needs.</p>\n</li>\n<li>\n<p dir=\"auto\">Best to click through all the other machine settings just to see what is available, it is easy enough to make changes over time.</p>\n</li>\n<li>\n<p dir=\"auto\">There are many more levels of convoluted not worth stooping to, ranging from ArcMap via <a href=\"https://aws.amazon.com/ec2/\" rel=\"nofollow\">AWS EC2</a> or <a href=\"https://www.openstack.org/\" rel=\"nofollow\">openstack</a> to <a href=\"https://www.linux-kvm.org/page/Main_Page\" rel=\"nofollow\">KVM/QEMU</a> to <a href=\"https://www.winehq.org/about\" rel=\"nofollow\">WINE</a>. <em>Take it from me</em></p>\n</li>\n</ul>\n<br>\n<p dir=\"auto\"><strong>Using ESRI ArcGIS / ArcMap on Macs, continued...</strong></p>\n<p dir=\"auto\"><span>I need to run ESRI products on my MacBook Pro.   QGIS is always the prefered solution- open source, excellent free plugins, works on mac natively- but in a college / research environment, the only option that supports other people and school machines is ESRI.  Despite the annoying bureaucracy and expense of the software, some things are faster (but not better!) in ESRI, like dealing with raster / multiband data. </span></p>\n<p dir=\"auto\"><strong>First, you need a license. </strong></p>\n<p dir=\"auto\"><span>I went about this two ways; </span></p>\n<p dir=\"auto\"><span>My first solution was to buy an ESRI Press textbook on amazon.  A 180 day trial for $50- when taken as a college course, this isn't to bad.  :)   The book is slow and recursive, but a 180 days to play with all the plugins and whistles allows for way deeper learning via the internet.   :)</span><a href=\"https://transscendsurvival.org/wp-content/uploads/2018/04/Screen-Shot-2018-04-03-at-9.36.59-AM.png\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/cceb20954a501ac62329f2bfc81df7bfd88cb2866d1e0b49cb662f1b00a855d3/68747470733a2f2f7472616e737363656e64737572766976616c2e6f72672f77702d636f6e74656e742f75706c6f6164732f323031382f30342f53637265656e2d53686f742d323031382d30342d30332d61742d392e33362e35392d414d2d323533783330302e706e67\" alt=\"\" width=\"253\" height=\"300\" data-canonical-src=\"https://transscendsurvival.org/wp-content/uploads/2018/04/Screen-Shot-2018-04-03-at-9.36.59-AM-253x300.png\" style=\"max-width: 100%; height: auto; max-height: 300px;; aspect-ratio: 253 / 300; background-color: var(--bgColor-muted); border-radius: 6px; display: block\" class=\"js-gh-image-fallback\"></a></p>\n<p dir=\"auto\"><span>Do know there is a little-documented limit to the number of license transfers you may perform before getting either lock in or out of your software.  I hit this limit, as I was also figuring out my virtual machine situation, which would occasionally need a re-installation. </span></p>\n<p dir=\"auto\"><span>My current solution is “just buy a student license”.   $100 per year is less than any adobe situation- so really not that bad.  </span></p>\n<p dir=\"auto\"><strong>Now you need a windows ISO.  </strong></p>\n<p dir=\"auto\"><a href=\"https://www.microsoft.com/en-us/software-download/windows10ISO\" rel=\"nofollow\"></a><a href=\"https://www.microsoft.com/en-us/software-download/windows10ISO\" rel=\"nofollow\">https://www.microsoft.com/en-us/software-download/windows10ISO</a></p>\n<p dir=\"auto\">Follow that link for the window 10, 64 bit ISO.  YOU DO NOT NEED TO BUY WINDOWS.  It will sometimes complain about not having an  authentication, but in the months of using windows via VMs, never have I been prohibited to do... anything.  When prompted for a license when configuring your VM, click the button that says \"I don't have a license\".  Done.</p>\n<p dir=\"auto\"> </p>\n<p dir=\"auto\"><strong>Option one:  VirtualBox VM on a thumbdrive</strong></p>\n<p dir=\"auto\"><a href=\"https://www.virtualbox.org/wiki/Downloads\" rel=\"nofollow\"><span></span></a><a href=\"https://www.virtualbox.org/wiki/Downloads\" rel=\"nofollow\">https://www.virtualbox.org/wiki/Downloads</a><span> - VirtualBox downloads- <em>the VM will take up most of a 128gb flash drive- ~70 gb just for windows and all the stuff you'll want from a PC.  Add ESRI software and allocated space for a cache (where your GIS project works!), bigger is better.   Format all drives in disk utility as ExFat!  this is important, any other file system either won't fly or could wreak havoc (other FAT based ones may have too small file allocations!</em></span></p>\n<p dir=\"auto\"><span>I used two drives, a 128 and a 64- this is great because I can store all my work on the 64, so I can easily plug it into other (school) machines running windows ArcMap and keep going, without causing issues with the massive VM in the 128.  </span></p>\n<p dir=\"auto\">Installation is straightforward- added more screenshots of the process in <a href=\"https://github.com/Jesssullivan/GIS_Shortcuts/tree/master/vmsetup\">vmsetup</a></p>\n<p dir=\"auto\"><a href=\"https://transscendsurvival.org/wp-content/uploads/2018/04/Screen-Shot-2018-04-03-at-9.52.43-AM.png\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/d0c8db32b8a5c54b436193a6c626e4e6002972df573d03534b206e7247e94f80/68747470733a2f2f7472616e737363656e64737572766976616c2e6f72672f77702d636f6e74656e742f75706c6f6164732f323031382f30342f53637265656e2d53686f742d323031382d30342d30332d61742d392e35322e34332d414d2d333030783230372e706e67\" alt=\"\" width=\"300\" height=\"207\" data-canonical-src=\"https://transscendsurvival.org/wp-content/uploads/2018/04/Screen-Shot-2018-04-03-at-9.52.43-AM-300x207.png\" style=\"max-width: 100%; height: auto; max-height: 207px;; aspect-ratio: 300 / 207; background-color: var(--bgColor-muted); border-radius: 6px; display: block\" class=\"js-gh-image-fallback\"></a> <a href=\"https://transscendsurvival.org/wp-content/uploads/2018/04/Screen-Shot-2018-04-03-at-9.52.38-AM.png\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/4b8cfac0c9eeb3ad496225ed871cd1c3bb39dfc0f3b5cf2f8451d88758cefa68/68747470733a2f2f7472616e737363656e64737572766976616c2e6f72672f77702d636f6e74656e742f75706c6f6164732f323031382f30342f53637265656e2d53686f742d323031382d30342d30332d61742d392e35322e33382d414d2d333030783231332e706e67\" alt=\"\" width=\"300\" height=\"213\" data-canonical-src=\"https://transscendsurvival.org/wp-content/uploads/2018/04/Screen-Shot-2018-04-03-at-9.52.38-AM-300x213.png\" style=\"max-width: 100%; height: auto; max-height: 213px;; aspect-ratio: 300 / 213; background-color: var(--bgColor-muted); border-radius: 6px; display: block\" class=\"js-gh-image-fallback\"></a></p>\n<p dir=\"auto\"><strong>Problems</strong>:   Stability.   Crashes, and python / some other script modules do not work well.  This is a problem.  ArcAdministrator gets confused about all kinds of things- FWIW, if you are googling to delete the FLEXnet folder to solve authentication file issues, move to option 2 :)</p>\n<p dir=\"auto\">Speed is down, but actually the ~same speed as our school \"super\" PCs- (though I happened to know they are essentially glorified \"hybrid\" VMs too!) .</p>\n<p dir=\"auto\"><a href=\"https://transscendsurvival.org/wp-content/uploads/2018/04/Screen-Shot-2018-04-03-at-9.52.20-AM.png\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/2c6924967e29c44da6fa287449631d87ff27111e0c8652f5bb552ac52cecf8d4/68747470733a2f2f7472616e737363656e64737572766976616c2e6f72672f77702d636f6e74656e742f75706c6f6164732f323031382f30342f53637265656e2d53686f742d323031382d30342d30332d61742d392e35322e32302d414d2d333030783137312e706e67\" alt=\"\" width=\"300\" height=\"171\" data-canonical-src=\"https://transscendsurvival.org/wp-content/uploads/2018/04/Screen-Shot-2018-04-03-at-9.52.20-AM-300x171.png\" style=\"max-width: 100%; height: auto; max-height: 171px;; aspect-ratio: 300 / 171; background-color: var(--bgColor-muted); border-radius: 6px; display: block\" class=\"js-gh-image-fallback\"></a></p>\n<p dir=\"auto\"><strong>Option two: OSX Bootcamp </strong></p>\n<p dir=\"auto\"><a href=\"https://support.apple.com/boot-camp\" rel=\"nofollow\"></a><a href=\"https://support.apple.com/boot-camp\" rel=\"nofollow\">https://support.apple.com/boot-camp</a></p>\n<p dir=\"auto\"><a href=\"https://support.apple.com/en-us/HT201468\" rel=\"nofollow\"></a><a href=\"https://support.apple.com/en-us/HT201468\" rel=\"nofollow\">https://support.apple.com/en-us/HT201468</a></p>\n<p dir=\"auto\">This way, you will hit \"option/alt\" each time you restart/boot your computer to choose from win/osx.   This is easy to install, as it is mac and mac = easy.</p>\n<p dir=\"auto\"><strong>Big Caveat:</strong>  it is much harder to install windows externally  (on a usb, etc) from bootcamp.  I didn't succeed in my efforts, but there could be a way....   The thing is, it really wants to run everything like a normal intel based PC, with all installations in the usual place.  This is good for the mac performance, but terrible for the tiny SSD hard drives we get as mac users.  I have a 256gb SSD.  I have an average of &lt; 15 gb wiggle room here, and use every cloud service in the book.</p>\n<p dir=\"auto\">If you need to manage your cloud storage because of a itsy mac SSD, my solution is still ODrive.   <a href=\"https://www.odrive.com/\" rel=\"nofollow\"></a><a href=\"https://www.odrive.com/\" rel=\"nofollow\">https://www.odrive.com/</a></p>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\"><em>Example DEM stiching from GDAL</em></h2><a id=\"user-content-example-dem-stiching-from-gdal\" class=\"anchor\" aria-label=\"Permalink: Example DEM stiching from GDAL\" href=\"#example-dem-stiching-from-gdal\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 id=\"user-content-demstitch\" class=\"heading-element\" dir=\"auto\"> </h4><a id=\"user-content---7\" class=\"anchor\" aria-label=\"Permalink: \" href=\"#--7\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>    \n<p dir=\"auto\"><em>To begin- try a recent GIS assignment that would otherwise rely on the ESRI mosaic system:</em></p>\n<p dir=\"auto\">Data source: ftp://ftp.granit.sr.unh.edu/pub/GRANIT_Data/Vector_Data/Elevation_and_Derived_Products/d-elevationdem/d-10m/</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"# wget is great, and is included in many distributions- it is not installed by default on Mac OSX however.\n\nbrew install wget\"><pre class=\"notranslate\"><code># wget is great, and is included in many distributions- it is not installed by default on Mac OSX however.\n\nbrew install wget\n</code></pre></div>\n<p dir=\"auto\">make some folders</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"mkdir GIS_Projects &amp;&amp; cd GIS_Projects\"><pre class=\"notranslate\"><code>mkdir GIS_Projects &amp;&amp; cd GIS_Projects\n</code></pre></div>\n<p dir=\"auto\">use wget to download every .dem file (-A .dem) from the specified folder and sub-folders (-r)</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"wget -r -A .dem ftp://ftp.granit.sr.unh.edu/pub/GRANIT_Data/Vector_Data/Elevation_and_Derived_Products/d-elevationdem/\n\ncd ftp.granit.sr.unh.edu/pub/GRANIT_Data/Vector_Data/Elevation_and_Derived_Products/d-elevationdem\"><pre class=\"notranslate\"><code>wget -r -A .dem ftp://ftp.granit.sr.unh.edu/pub/GRANIT_Data/Vector_Data/Elevation_and_Derived_Products/d-elevationdem/\n\ncd ftp.granit.sr.unh.edu/pub/GRANIT_Data/Vector_Data/Elevation_and_Derived_Products/d-elevationdem\n</code></pre></div>\n<p dir=\"auto\">make an index file of only .dem files.<br>\n(If we needed to download other files and keep them from our wget (more common)\nthis way we can still sort the various files for .dem)</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"ls -1 *.dem &gt; dem_list.txt\"><pre class=\"notranslate\"><code>ls -1 *.dem &gt; dem_list.txt\n</code></pre></div>\n<p dir=\"auto\">use gdal to make state-plane referenced “Output_merged.tif” from the list of files in the index we made.\nWe will use a single generic \"0 0 255\" band to show gradient.</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"gdal_merge.py -init &quot;0 0 255&quot; -o Output_Merged.tif --optfile dem_list.txt\"><pre class=\"notranslate\"><code>gdal_merge.py -init \"0 0 255\" -o Output_Merged.tif --optfile dem_list.txt\n</code></pre></div>\n<p dir=\"auto\">copy the resulting file to desktop, then return home</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"cp Output_Merged.tif ~/desktop &amp;&amp; cd\"><pre class=\"notranslate\"><code>cp Output_Merged.tif ~/desktop &amp;&amp; cd\n</code></pre></div>\n<p dir=\"auto\">if you want (recommended):</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"rm -rf GIS_Projects  # remove .dem files.  Some are huge!\"><pre class=\"notranslate\"><code>rm -rf GIS_Projects  # remove .dem files.  Some are huge!\n</code></pre></div>\n<p dir=\"auto\">In Finder at in ~/desktop, open the new file with QGIS.  A normal photo viewer will NOT show any detail.</p>\n<p dir=\"auto\">Need to make something like this a reusable script?  In Terminal, just a few extra steps:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"mkdir GIS_Scripts &amp;&amp; cd GIS_Scripts\"><pre class=\"notranslate\"><code>mkdir GIS_Scripts &amp;&amp; cd GIS_Scripts\n</code></pre></div>\n<p dir=\"auto\">open an editor + filename.  Nano is generally pre-installed on OSX.</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"nano GDAL_LiveMerge.sh\"><pre class=\"notranslate\"><code>nano GDAL_LiveMerge.sh\n</code></pre></div>\n<p dir=\"auto\">COPY + PASTE THE SCRIPT FROM ABOVE INTO THE WINDOW</p>\n<ul dir=\"auto\">\n<li>ctrl+X , then Y for yes</li>\n</ul>\n<p dir=\"auto\">make your file runnable:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"chmod u+x GDAL_LiveMerge.sh\"><pre class=\"notranslate\"><code>chmod u+x GDAL_LiveMerge.sh\n</code></pre></div>\n<p dir=\"auto\">run with ./</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"./GDAL_LiveMerge.sh\"><pre class=\"notranslate\"><code>./GDAL_LiveMerge.sh\n</code></pre></div>\n<p dir=\"auto\">You can now copy + paste your script anywhere you want and run it there.  scripts   like this should not be exported to your global path / bashrc and will only work if they are in the directory you are calling them:  If you need a global script, there are plenty of ways to do that too.</p>\n<p dir=\"auto\"><em>See /Notes_GDAL/README.md for notes on building GDAL from source on OSX</em></p>\n<p dir=\"auto\"><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"GDAL_Bash_DEM_Merge.png\"><img title=\"Results\" src=\"GDAL_Bash_DEM_Merge.png\" width=\"300px\" style=\"max-width: 100%;\"></a></p>  \n</article></div>",
      "readme_excerpt": "<img title='3d From the command line....' src=\"PythonResults.png\" width='500px' >\n    \nMiscellaneous gis notes, mostly derived from my blog over here\n    \nIndex:  <br>\neBird API stuff <br>\nR / Shiny Web Experiments <br>\nShell macros from R <br>\nWhen it must be Windows <br>\nWindows WSL - Ubuntu GDAL setup <br>\nUbuntu & Jupyter cartopy setup <br>\nMac OSX - GDAL setup <br>\nBash Example - DEM stitching <br>\nLink to rJDK management info <br>\n- - -\n<h4 id=\"ebird\"> </h4>     \nThe Ebird dataset is...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/GIS_Shortcuts",
      "website_url": "https://transscendsurvival.org/GIS_Shortcuts/",
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/GIS_Shortcuts/releases",
      "og_image_url": "https://opengraph.githubassets.com/70bea8545b1df236af9c510e399d92a868a8e60e5cce44f12c0bd6f49c451933/Jesssullivan/GIS_Shortcuts",
      "license": "MIT",
      "pushed_at": "2026-02-10T02:59:48Z",
      "enriched_at": "2026-04-26T17:17:19.115Z",
      "gate": "pages"
    },
    {
      "slug": "jesssullivan-ansible-dag-harness",
      "name": "Ansible-DAG-Harness",
      "repo": "jesssullivan/Ansible-DAG-Harness",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "ai-tools",
      "description": "A disposable self-bootstrapping LangGraph DAG harness for boxing up Ansible iteration cycles in GitLab",
      "featured": false,
      "tags": [
        "langgraph",
        "dag",
        "ansible"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [
        "ansible-role",
        "dag",
        "gitlab",
        "harness",
        "langgraph",
        "minutae"
      ],
      "languages": [
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 2868546
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 37690
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 9106
        },
        {
          "name": "Jinja",
          "color": "#a52a22",
          "bytes": 3840
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 2675
        }
      ],
      "primary_language": "Python",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">DAG Harness</h1><a id=\"user-content-dag-harness\" class=\"anchor\" aria-label=\"Permalink: DAG Harness\" href=\"#dag-harness\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">LangGraph DAG orchestration for Ansible role deployments with GitLab integration.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Features</h2><a id=\"user-content-features\" class=\"anchor\" aria-label=\"Permalink: Features\" href=\"#features\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>17-node workflow</strong> with parallel testing, HITL gates, recovery subgraph</li>\n<li><strong>30 MCP tools</strong> across 9 categories for Claude Code integration</li>\n<li><strong>HOTL mode</strong> - Human Out of The Loop autonomous operation</li>\n<li><strong>Checkpointing</strong> - Resume from any node with SQLite persistence</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Installation</h2><a id=\"user-content-installation\" class=\"anchor\" aria-label=\"Permalink: Installation\" href=\"#installation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# From source (recommended)\ngit clone https://github.com/Jesssullivan/dag-harness.git\ncd dag-harness/harness\nuv sync\n\n# Or via pip\npip install git+https://github.com/Jesssullivan/dag-harness.git#subdirectory=harness\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> From source (recommended)</span>\ngit clone https://github.com/Jesssullivan/dag-harness.git\n<span class=\"pl-c1\">cd</span> dag-harness/harness\nuv sync\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Or via pip</span>\npip install git+https://github.com/Jesssullivan/dag-harness.git#subdirectory=harness</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Usage</h2><a id=\"user-content-usage\" class=\"anchor\" aria-label=\"Permalink: Usage\" href=\"#usage\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"harness init                    # Initialize in repo\nharness box-up-role &lt;role&gt;      # Execute workflow\nharness status                  # Show status\nharness hotl start              # Autonomous mode\"><pre>harness init                    <span class=\"pl-c\"><span class=\"pl-c\">#</span> Initialize in repo</span>\nharness box-up-role <span class=\"pl-k\">&lt;</span>role<span class=\"pl-k\">&gt;</span>      <span class=\"pl-c\"><span class=\"pl-c\">#</span> Execute workflow</span>\nharness status                  <span class=\"pl-c\"><span class=\"pl-c\">#</span> Show status</span>\nharness hotl start              <span class=\"pl-c\"><span class=\"pl-c\">#</span> Autonomous mode</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Workflow</h2><a id=\"user-content-workflow\" class=\"anchor\" aria-label=\"Permalink: Workflow\" href=\"#workflow\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"validate_role → analyze_deps → check_reverse_deps → create_worktree\n                                                          ↓\nrun_molecule ─┬─→ merge_test_results → validate_deploy → create_commit\nrun_pytest  ──┘                                              ↓\n                push_branch → create_issue → create_mr → human_approval\n                                                              ↓\n                              add_to_merge_train → report_summary\n                                        ↓\n                               [recovery subgraph on failure]\"><pre class=\"notranslate\"><code>validate_role → analyze_deps → check_reverse_deps → create_worktree\n                                                          ↓\nrun_molecule ─┬─→ merge_test_results → validate_deploy → create_commit\nrun_pytest  ──┘                                              ↓\n                push_branch → create_issue → create_mr → human_approval\n                                                              ↓\n                              add_to_merge_train → report_summary\n                                        ↓\n                               [recovery subgraph on failure]\n</code></pre></div>\n<p dir=\"auto\">17 nodes: <code>validate_role</code>, <code>analyze_deps</code>, <code>check_reverse_deps</code>, <code>create_worktree</code>, <code>run_molecule</code>, <code>run_pytest</code>, <code>merge_test_results</code>, <code>validate_deploy</code>, <code>create_commit</code>, <code>push_branch</code>, <code>create_issue</code>, <code>create_mr</code>, <code>human_approval</code>, <code>add_to_merge_train</code>, <code>report_summary</code>, <code>notify_failure</code>, <code>recovery</code></p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">MCP Tools</h2><a id=\"user-content-mcp-tools\" class=\"anchor\" aria-label=\"Permalink: MCP Tools\" href=\"#mcp-tools\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">30 tools defined in <code>harness/mcp/server.py</code>:</p>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Category</th>\n<th>Count</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>role_management</td>\n<td>7</td>\n</tr>\n<tr>\n<td>workflow</td>\n<td>5</td>\n</tr>\n<tr>\n<td>agent</td>\n<td>6</td>\n</tr>\n<tr>\n<td>worktree</td>\n<td>3</td>\n</tr>\n<tr>\n<td>costs</td>\n<td>3</td>\n</tr>\n<tr>\n<td>testing</td>\n<td>2</td>\n</tr>\n<tr>\n<td>search</td>\n<td>2</td>\n</tr>\n<tr>\n<td>credentials</td>\n<td>1</td>\n</tr>\n<tr>\n<td>merge_train</td>\n<td>1</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Configuration</h2><a id=\"user-content-configuration\" class=\"anchor\" aria-label=\"Permalink: Configuration\" href=\"#configuration\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-yaml notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# harness.yml\ndb_path: harness.db\nrepo_root: /path/to/ansible/roles\n\ngitlab:\n  project_path: group/project\n  default_assignee: username\n\nworktree:\n  base_path: /path/to/worktrees\n  branch_prefix: feature/\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> harness.yml</span>\n<span class=\"pl-ent\">db_path</span>: <span class=\"pl-s\">harness.db</span>\n<span class=\"pl-ent\">repo_root</span>: <span class=\"pl-s\">/path/to/ansible/roles</span>\n\n<span class=\"pl-ent\">gitlab</span>:\n  <span class=\"pl-ent\">project_path</span>: <span class=\"pl-s\">group/project</span>\n  <span class=\"pl-ent\">default_assignee</span>: <span class=\"pl-s\">username</span>\n\n<span class=\"pl-ent\">worktree</span>:\n  <span class=\"pl-ent\">base_path</span>: <span class=\"pl-s\">/path/to/worktrees</span>\n  <span class=\"pl-ent\">branch_prefix</span>: <span class=\"pl-s\">feature/</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Requirements</h2><a id=\"user-content-requirements\" class=\"anchor\" aria-label=\"Permalink: Requirements\" href=\"#requirements\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>Python 3.11+</li>\n<li>Git with worktree support</li>\n<li>GitLab API access (<code>GITLAB_TOKEN</code>)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Tests</h2><a id=\"user-content-tests\" class=\"anchor\" aria-label=\"Permalink: Tests\" href=\"#tests\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"cd harness\nuv run pytest                           # 1667 tests\nuv run pytest -m &quot;not integration&quot;      # Unit tests only\nuv run pytest -m pbt                    # Property-based tests\"><pre><span class=\"pl-c1\">cd</span> harness\nuv run pytest                           <span class=\"pl-c\"><span class=\"pl-c\">#</span> 1667 tests</span>\nuv run pytest -m <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>not integration<span class=\"pl-pds\">\"</span></span>      <span class=\"pl-c\"><span class=\"pl-c\">#</span> Unit tests only</span>\nuv run pytest -m pbt                    <span class=\"pl-c\"><span class=\"pl-c\">#</span> Property-based tests</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Documentation</h2><a id=\"user-content-documentation\" class=\"anchor\" aria-label=\"Permalink: Documentation\" href=\"#documentation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><a href=\"docs/architecture.md\">Architecture</a></li>\n<li><a href=\"docs/api/cli.md\">CLI Reference</a></li>\n<li><a href=\"docs/api/mcp-tools.md\">MCP Tools</a></li>\n<li><a href=\"docs/llms.md\">LLM Context</a></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">MIT</p>\n</article></div>",
      "readme_excerpt": "LangGraph DAG orchestration for Ansible role deployments with GitLab integration.\n- 17-node workflow with parallel testing, HITL gates, recovery subgraph\n- 30 MCP tools across 9 categories for Claude Code integration\n- HOTL mode - Human Out of The Loop autonomous operation\n- Checkpointing - Resume from any node with SQLite persistence\nbash\ngit clone https://github.com/Jesssullivan/dag-harness.git\ncd dag-harness/harness\nuv sync\npip install...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/Ansible-DAG-Harness",
      "website_url": "https://jesssullivan.github.io/Ansible-DAG-Harness/",
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/Ansible-DAG-Harness/releases",
      "og_image_url": "https://opengraph.githubassets.com/bcdbcb3ccafdd38a9e6eff0ef32698faad60fbfd7002a295ef93dade489e1f65/Jesssullivan/Ansible-DAG-Harness",
      "license": "MIT",
      "pushed_at": "2026-02-08T20:44:11Z",
      "enriched_at": "2026-04-26T17:17:19.376Z",
      "gate": "homepage"
    },
    {
      "slug": "jesssullivan-quickchpl",
      "name": "quickchpl",
      "repo": "jesssullivan/quickchpl",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "devex",
      "description": "Simple Property-Based Testing for Chapel Language",
      "featured": false,
      "tags": [
        "chapel",
        "pbt",
        "testing"
      ],
      "version": "1.0.2",
      "release_date": "2026-01-15T08:14:03Z",
      "releases": [
        {
          "tag": "1.0.2",
          "date": "2026-01-15T08:14:03Z",
          "body": "## quickchpl 1.0.2\r\n\r\n## [1.0.2] - 2026-01-15\r\n\r\n### Fixed\r\n- Generic type warnings: Added `Property(?)` syntax for generic formal parameters\r\n- Unstable symbol warnings: Replaced `_unused` loop variables with bare `for 1..n` syntax\r\n- Mason main module check: Restructured to proper submodule convention\r\n\r\n### Changed\r\n- Moved submodules to `src/quickchpl/` directory per Mason package conventions\r\n- Updated `quickchpl.chpl` to use `include module` statements before `public use`\r\n- Test files now import via `use quickchpl` instead of direct submodule imports\r\n- Fix UnusedFormal: extract helper procs with @chplcheck.ignore\r\n- Fix LineLength: break long lines under 80 chars\r\n- Update CI threshold to 7 (acceptable API design violations)\r\n- Chapel version compatibility extended to 2.8.0\r\n\r\n### Infrastructure\r\n- Updated GitHub Actions and GitLab CI paths for new module structure\r\n- Fixed Mason.toml trailing newline (Mason bug workaround)\r\n\r\n[1.0.2]: https://github.com/Jesssullivan/quickchpl/releases/tag/v1.0.2"
        },
        {
          "tag": "1.0.1",
          "date": "2026-01-12T23:44:39Z",
          "body": "## quickchpl 1.0.1\n\n\n\n### Installation\n\n#### Via Mason (Recommended)\n```bash\nmason add quickchpl@1.0.1\n```\n\n#### Manual\n```bash\ngit clone https://github.com/Jesssullivan/quickchpl.git\ngit checkout v1.0.1\nexport CHPL_MODULE_PATH=$CHPL_MODULE_PATH:$PWD/quickchpl/src\n```\n\n### Documentation\n- [README](https://github.com/Jesssullivan/quickchpl/blob/main/README.md)\n- [Examples](https://github.com/Jesssullivan/quickchpl/tree/main/examples)\n- [API Documentation](https://github.com/Jesssullivan/quickchpl/tree/main/docs)\n"
        }
      ],
      "stars": 2,
      "topics": [
        "chapel-language",
        "mason",
        "property-based-testing",
        "parl"
      ],
      "languages": [
        {
          "name": "Chapel",
          "color": "#8dc63f",
          "bytes": 129066
        },
        {
          "name": "Shell",
          "color": "#89e051",
          "bytes": 4692
        },
        {
          "name": "Dockerfile",
          "color": "#384d54",
          "bytes": 944
        }
      ],
      "primary_language": "Chapel",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">quickchpl</h1><a id=\"user-content-quickchpl\" class=\"anchor\" aria-label=\"Permalink: quickchpl\" href=\"#quickchpl\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><strong>Simple Property-Based Testing for Chapel</strong></p>\n<p dir=\"auto\"><a href=\"https://chapel-lang.org/\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/d47d2562118e0c345115a307c5752ad1771deac704b4bf37aeaa8b85af210010/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f43686170656c2d322e362532422d626c7565\" alt=\"Chapel\" data-canonical-src=\"https://img.shields.io/badge/Chapel-2.6%2B-blue\" style=\"max-width: 100%;\"></a>\n<a href=\"https://opensource.org/licenses/MIT\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667\" alt=\"License: MIT\" data-canonical-src=\"https://img.shields.io/badge/License-MIT-yellow.svg\" style=\"max-width: 100%;\"></a>\n<a href=\"https://gitlab.com/tinyland/projects/quickchpl\" rel=\"nofollow\"><img src=\"https://camo.githubusercontent.com/cfb521129fcf7bad280ff50eafe14c9e6692434079250c4ddeabf9ca12127905/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4769744c61622d74696e796c616e64253246717569636b6368706c2d6f72616e6765\" alt=\"GitLab\" data-canonical-src=\"https://img.shields.io/badge/GitLab-tinyland%2Fquickchpl-orange\" style=\"max-width: 100%;\"></a></p>\n<p dir=\"auto\">Inspired by QuickCheck and my father ^w^</p>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"use quickchpl;\n\nproc main() {\n  // Test that addition is commutative\n  var gen = tupleGen(intGen(-100, 100), intGen(-100, 100));\n  var prop = property(\n    &quot;addition is commutative&quot;,\n    gen,\n    proc(args: (int, int)) {\n      const (a, b) = args;\n      return a + b == b + a;\n    }\n  );\n\n  var result = check(prop);\n  if result.passed {\n    writeln(&quot;✓ &quot;, prop.name, &quot; passed &quot;, result.numTests, &quot; tests&quot;);\n  } else {\n    writeln(&quot;✗ &quot;, prop.name, &quot; FAILED&quot;);\n    writeln(&quot;  Counterexample: &quot;, result.failureInfo);\n  }\n}\"><pre><span class=\"pl-k\">use</span> quickchpl;\n\n<span class=\"pl-k\">proc</span> main() {\n  <span class=\"pl-c\"><span class=\"pl-c\">//</span> Test that addition is commutative</span>\n  <span class=\"pl-k\">var</span> gen <span class=\"pl-k\">=</span> tupleGen(intGen(<span class=\"pl-k\">-</span><span class=\"pl-c1\">100</span>, <span class=\"pl-c1\">100</span>), intGen(<span class=\"pl-k\">-</span><span class=\"pl-c1\">100</span>, <span class=\"pl-c1\">100</span>));\n  <span class=\"pl-k\">var</span> prop <span class=\"pl-k\">=</span> property(\n    <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>addition is commutative<span class=\"pl-pds\">\"</span></span>,\n    gen,\n    <span class=\"pl-k\">proc</span>(args<span class=\"pl-k\">:</span> (<span class=\"pl-k\">int</span>, <span class=\"pl-k\">int</span>)) {\n      <span class=\"pl-k\">const</span> (a, b) <span class=\"pl-k\">=</span> args;\n      <span class=\"pl-k\">return</span> a <span class=\"pl-k\">+</span> b <span class=\"pl-k\">==</span> b <span class=\"pl-k\">+</span> a;\n    }\n  );\n\n  <span class=\"pl-k\">var</span> result <span class=\"pl-k\">=</span> check(prop);\n  <span class=\"pl-k\">if</span> result.passed {\n    <span class=\"pl-c1\">writeln</span>(<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>✓ <span class=\"pl-pds\">\"</span></span>, prop.name, <span class=\"pl-s\"><span class=\"pl-pds\">\"</span> passed <span class=\"pl-pds\">\"</span></span>, result.numTests, <span class=\"pl-s\"><span class=\"pl-pds\">\"</span> tests<span class=\"pl-pds\">\"</span></span>);\n  } <span class=\"pl-k\">else</span> {\n    <span class=\"pl-c1\">writeln</span>(<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>✗ <span class=\"pl-pds\">\"</span></span>, prop.name, <span class=\"pl-s\"><span class=\"pl-pds\">\"</span> FAILED<span class=\"pl-pds\">\"</span></span>);\n    <span class=\"pl-c1\">writeln</span>(<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>  Counterexample: <span class=\"pl-pds\">\"</span></span>, result.failureInfo);\n  }\n}</pre></div>\n<p dir=\"auto\">Compile and run:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"chpl my_test.chpl -M path/to/quickchpl/src\n./my_test\"><pre>chpl my_test.chpl -M path/to/quickchpl/src\n./my_test</pre></div>\n<p dir=\"auto\">Output:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"✓ addition is commutative passed 100 tests\"><pre class=\"notranslate\"><code>✓ addition is commutative passed 100 tests\n</code></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Generators</h2><a id=\"user-content-generators\" class=\"anchor\" aria-label=\"Permalink: Generators\" href=\"#generators\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Primitive Generators</h3><a id=\"user-content-primitive-generators\" class=\"anchor\" aria-label=\"Permalink: Primitive Generators\" href=\"#primitive-generators\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Integers\nvar intG = intGen(-100, 100);           // Range [-100, 100]\nvar natG = natGen(1000);                 // Natural numbers [0, 1000]\nvar posG = positiveIntGen(1000);         // Positive [1, 1000]\n\n// Real numbers\nvar realG = realGen(0.0, 1.0);           // Uniform [0, 1)\nvar normalG = realGen(0.0, 1.0, Distribution.Normal);\n\n// Booleans\nvar boolG = boolGen(0.5);                // 50% true probability\n\n// Strings\nvar strG = stringGen(0, 50);             // Length 0-50, alphanumeric\nvar alphaG = alphaGen(5, 10);            // Alphabetic only\"><pre><span class=\"pl-c\"><span class=\"pl-c\">//</span> Integers</span>\n<span class=\"pl-k\">var</span> intG <span class=\"pl-k\">=</span> intGen(<span class=\"pl-k\">-</span><span class=\"pl-c1\">100</span>, <span class=\"pl-c1\">100</span>);           <span class=\"pl-c\"><span class=\"pl-c\">//</span> Range [-100, 100]</span>\n<span class=\"pl-k\">var</span> natG <span class=\"pl-k\">=</span> natGen(<span class=\"pl-c1\">1000</span>);                 <span class=\"pl-c\"><span class=\"pl-c\">//</span> Natural numbers [0, 1000]</span>\n<span class=\"pl-k\">var</span> posG <span class=\"pl-k\">=</span> positiveIntGen(<span class=\"pl-c1\">1000</span>);         <span class=\"pl-c\"><span class=\"pl-c\">//</span> Positive [1, 1000]</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Real numbers</span>\n<span class=\"pl-k\">var</span> realG <span class=\"pl-k\">=</span> realGen(<span class=\"pl-c1\">0.0</span>, <span class=\"pl-c1\">1.0</span>);           <span class=\"pl-c\"><span class=\"pl-c\">//</span> Uniform [0, 1)</span>\n<span class=\"pl-k\">var</span> normalG <span class=\"pl-k\">=</span> realGen(<span class=\"pl-c1\">0.0</span>, <span class=\"pl-c1\">1.0</span>, Distribution.Normal);\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Booleans</span>\n<span class=\"pl-k\">var</span> boolG <span class=\"pl-k\">=</span> boolGen(<span class=\"pl-c1\">0.5</span>);                <span class=\"pl-c\"><span class=\"pl-c\">//</span> 50% true probability</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Strings</span>\n<span class=\"pl-k\">var</span> strG <span class=\"pl-k\">=</span> stringGen(<span class=\"pl-c1\">0</span>, <span class=\"pl-c1\">50</span>);             <span class=\"pl-c\"><span class=\"pl-c\">//</span> Length 0-50, alphanumeric</span>\n<span class=\"pl-k\">var</span> alphaG <span class=\"pl-k\">=</span> alphaGen(<span class=\"pl-c1\">5</span>, <span class=\"pl-c1\">10</span>);            <span class=\"pl-c\"><span class=\"pl-c\">//</span> Alphabetic only</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Composite Generators</h3><a id=\"user-content-composite-generators\" class=\"anchor\" aria-label=\"Permalink: Composite Generators\" href=\"#composite-generators\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Tuples\nvar pairG = tupleGen(intGen(), stringGen());\nvar tripleG = tupleGen(intGen(), intGen(), intGen());\n\n// Lists\nvar listG = listGen(intGen(), 0, 10);    // List of 0-10 integers\n\n// Fixed values\nvar constG = constantGen(42);\"><pre><span class=\"pl-c\"><span class=\"pl-c\">//</span> Tuples</span>\n<span class=\"pl-k\">var</span> pairG <span class=\"pl-k\">=</span> tupleGen(intGen(), stringGen());\n<span class=\"pl-k\">var</span> tripleG <span class=\"pl-k\">=</span> tupleGen(intGen(), intGen(), intGen());\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Lists</span>\n<span class=\"pl-k\">var</span> listG <span class=\"pl-k\">=</span> listGen(intGen(), <span class=\"pl-c1\">0</span>, <span class=\"pl-c1\">10</span>);    <span class=\"pl-c\"><span class=\"pl-c\">//</span> List of 0-10 integers</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Fixed values</span>\n<span class=\"pl-k\">var</span> constG <span class=\"pl-k\">=</span> constantGen(<span class=\"pl-c1\">42</span>);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Generator Combinators</h3><a id=\"user-content-generator-combinators\" class=\"anchor\" aria-label=\"Permalink: Generator Combinators\" href=\"#generator-combinators\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Transform output\nvar doubledG = map(intGen(), proc(x: int) { return x * 2; });\n\n// Filter values\nvar evenG = filter(intGen(), proc(x: int) { return x % 2 == 0; });\n\n// Combine generators\nvar zippedG = zip(intGen(), stringGen());\n\n// Random choice\nvar choiceG = oneOf(intGen(0, 10), intGen(100, 200));\n\n// Weighted choice\nvar weightedG = frequency(9, intGen(0, 10), 1, intGen(1000, 2000));\"><pre><span class=\"pl-c\"><span class=\"pl-c\">//</span> Transform output</span>\n<span class=\"pl-k\">var</span> doubledG <span class=\"pl-k\">=</span> map(intGen(), <span class=\"pl-k\">proc</span>(x<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> x <span class=\"pl-k\">*</span> <span class=\"pl-c1\">2</span>; });\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Filter values</span>\n<span class=\"pl-k\">var</span> evenG <span class=\"pl-k\">=</span> filter(intGen(), <span class=\"pl-k\">proc</span>(x<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> x <span class=\"pl-k\">%</span> <span class=\"pl-c1\">2</span> <span class=\"pl-k\">==</span> <span class=\"pl-c1\">0</span>; });\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Combine generators</span>\n<span class=\"pl-k\">var</span> zippedG <span class=\"pl-k\">=</span> <span class=\"pl-k\">zip</span>(intGen(), stringGen());\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Random choice</span>\n<span class=\"pl-k\">var</span> choiceG <span class=\"pl-k\">=</span> oneOf(intGen(<span class=\"pl-c1\">0</span>, <span class=\"pl-c1\">10</span>), intGen(<span class=\"pl-c1\">100</span>, <span class=\"pl-c1\">200</span>));\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Weighted choice</span>\n<span class=\"pl-k\">var</span> weightedG <span class=\"pl-k\">=</span> frequency(<span class=\"pl-c1\">9</span>, intGen(<span class=\"pl-c1\">0</span>, <span class=\"pl-c1\">10</span>), <span class=\"pl-c1\">1</span>, intGen(<span class=\"pl-c1\">1000</span>, <span class=\"pl-c1\">2000</span>));</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Properties</h2><a id=\"user-content-properties\" class=\"anchor\" aria-label=\"Permalink: Properties\" href=\"#properties\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Basic Properties</h3><a id=\"user-content-basic-properties\" class=\"anchor\" aria-label=\"Permalink: Basic Properties\" href=\"#basic-properties\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"var prop = property(\n  &quot;absolute value is non-negative&quot;,\n  intGen(),\n  proc(x: int) { return abs(x) &gt;= 0; }\n);\n\nvar result = check(prop);\nassert(result.passed);\"><pre><span class=\"pl-k\">var</span> prop <span class=\"pl-k\">=</span> property(\n  <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>absolute value is non-negative<span class=\"pl-pds\">\"</span></span>,\n  intGen(),\n  <span class=\"pl-k\">proc</span>(x<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> <span class=\"pl-c1\">abs</span>(x) <span class=\"pl-k\">&gt;=</span> <span class=\"pl-c1\">0</span>; }\n);\n\n<span class=\"pl-k\">var</span> result <span class=\"pl-k\">=</span> check(prop);\nassert(result.passed);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Conditional Properties (Implication)</h3><a id=\"user-content-conditional-properties-implication\" class=\"anchor\" aria-label=\"Permalink: Conditional Properties (Implication)\" href=\"#conditional-properties-implication\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Property only checked when condition is true\n// (implies is available from quickchpl module)\nvar prop = property(\n  &quot;division by non-zero&quot;,\n  tupleGen(intGen(), intGen()),\n  proc(args: (int, int)) {\n    const (a, b) = args;\n    return implies(b != 0, a / b * b == a - a % b);\n  }\n);\"><pre><span class=\"pl-c\"><span class=\"pl-c\">//</span> Property only checked when condition is true</span>\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> (implies is available from quickchpl module)</span>\n<span class=\"pl-k\">var</span> prop <span class=\"pl-k\">=</span> property(\n  <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>division by non-zero<span class=\"pl-pds\">\"</span></span>,\n  tupleGen(intGen(), intGen()),\n  <span class=\"pl-k\">proc</span>(args<span class=\"pl-k\">:</span> (<span class=\"pl-k\">int</span>, <span class=\"pl-k\">int</span>)) {\n    <span class=\"pl-k\">const</span> (a, b) <span class=\"pl-k\">=</span> args;\n    <span class=\"pl-k\">return</span> implies(b <span class=\"pl-k\">!=</span> <span class=\"pl-c1\">0</span>, a <span class=\"pl-k\">/</span> b <span class=\"pl-k\">*</span> b <span class=\"pl-k\">==</span> a <span class=\"pl-k\">-</span> a <span class=\"pl-k\">%</span> b);\n  }\n);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Convenience Functions</h3><a id=\"user-content-convenience-functions\" class=\"anchor\" aria-label=\"Permalink: Convenience Functions\" href=\"#convenience-functions\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Quick one-liner check\nassert(quickCheck(intGen(), proc(x: int) { return x + 0 == x; }));\n\n// forAll syntax\nvar result = forAll(intGen(), proc(x: int) { return x * 1 == x; });\"><pre><span class=\"pl-c\"><span class=\"pl-c\">//</span> Quick one-liner check</span>\nassert(quickCheck(intGen(), <span class=\"pl-k\">proc</span>(x<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> x <span class=\"pl-k\">+</span> <span class=\"pl-c1\">0</span> <span class=\"pl-k\">==</span> x; }));\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> forAll syntax</span>\n<span class=\"pl-k\">var</span> result <span class=\"pl-k\">=</span> forAll(intGen(), <span class=\"pl-k\">proc</span>(x<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> x <span class=\"pl-k\">*</span> <span class=\"pl-c1\">1</span> <span class=\"pl-k\">==</span> x; });</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Property Patterns</h2><a id=\"user-content-property-patterns\" class=\"anchor\" aria-label=\"Permalink: Property Patterns\" href=\"#property-patterns\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The <code>Patterns</code> module provides reusable <strong>predicate functions</strong> for testing common properties.\nAll pattern predicates are available when you <code>use quickchpl;</code>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Algebraic Patterns</h3><a id=\"user-content-algebraic-patterns\" class=\"anchor\" aria-label=\"Permalink: Algebraic Patterns\" href=\"#algebraic-patterns\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Test that addition is commutative\nvar prop = property(\n  &quot;addition is commutative&quot;,\n  tupleGen(intGen(-100, 100), intGen(-100, 100)),\n  proc(args: (int, int)) {\n    const (a, b) = args;\n    return isCommutative(a, b, proc(x: int, y: int) { return x + y; });\n  }\n);\n\n// Or use ready-made predicates for integers\nvar commProp = property(\n  &quot;integer addition commutes&quot;,\n  tupleGen(intGen(), intGen()),\n  proc(args: (int, int)) {\n    const (a, b) = args;\n    return intAddCommutative(a, b);\n  }\n);\n\n// Test associativity\nvar assocProp = property(\n  &quot;addition is associative&quot;,\n  tupleGen(intGen(), intGen(), intGen()),\n  proc(args: (int, int, int)) {\n    const (a, b, c) = args;\n    return intAddAssociative(a, b, c);\n  }\n);\n\n// Test identity element\nvar idProp = property(\n  &quot;zero is additive identity&quot;,\n  intGen(),\n  proc(a: int) { return intAddIdentity(a); }\n);\"><pre><span class=\"pl-c\"><span class=\"pl-c\">//</span> Test that addition is commutative</span>\n<span class=\"pl-k\">var</span> prop <span class=\"pl-k\">=</span> property(\n  <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>addition is commutative<span class=\"pl-pds\">\"</span></span>,\n  tupleGen(intGen(<span class=\"pl-k\">-</span><span class=\"pl-c1\">100</span>, <span class=\"pl-c1\">100</span>), intGen(<span class=\"pl-k\">-</span><span class=\"pl-c1\">100</span>, <span class=\"pl-c1\">100</span>)),\n  <span class=\"pl-k\">proc</span>(args<span class=\"pl-k\">:</span> (<span class=\"pl-k\">int</span>, <span class=\"pl-k\">int</span>)) {\n    <span class=\"pl-k\">const</span> (a, b) <span class=\"pl-k\">=</span> args;\n    <span class=\"pl-k\">return</span> isCommutative(a, b, <span class=\"pl-k\">proc</span>(x<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>, y<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> x <span class=\"pl-k\">+</span> y; });\n  }\n);\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Or use ready-made predicates for integers</span>\n<span class=\"pl-k\">var</span> commProp <span class=\"pl-k\">=</span> property(\n  <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>integer addition commutes<span class=\"pl-pds\">\"</span></span>,\n  tupleGen(intGen(), intGen()),\n  <span class=\"pl-k\">proc</span>(args<span class=\"pl-k\">:</span> (<span class=\"pl-k\">int</span>, <span class=\"pl-k\">int</span>)) {\n    <span class=\"pl-k\">const</span> (a, b) <span class=\"pl-k\">=</span> args;\n    <span class=\"pl-k\">return</span> intAddCommutative(a, b);\n  }\n);\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Test associativity</span>\n<span class=\"pl-k\">var</span> assocProp <span class=\"pl-k\">=</span> property(\n  <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>addition is associative<span class=\"pl-pds\">\"</span></span>,\n  tupleGen(intGen(), intGen(), intGen()),\n  <span class=\"pl-k\">proc</span>(args<span class=\"pl-k\">:</span> (<span class=\"pl-k\">int</span>, <span class=\"pl-k\">int</span>, <span class=\"pl-k\">int</span>)) {\n    <span class=\"pl-k\">const</span> (a, b, c) <span class=\"pl-k\">=</span> args;\n    <span class=\"pl-k\">return</span> intAddAssociative(a, b, c);\n  }\n);\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Test identity element</span>\n<span class=\"pl-k\">var</span> idProp <span class=\"pl-k\">=</span> property(\n  <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>zero is additive identity<span class=\"pl-pds\">\"</span></span>,\n  intGen(),\n  <span class=\"pl-k\">proc</span>(a<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> intAddIdentity(a); }\n);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Functional Patterns</h3><a id=\"user-content-functional-patterns\" class=\"anchor\" aria-label=\"Permalink: Functional Patterns\" href=\"#functional-patterns\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Idempotence: f(f(x)) = f(x)\nvar idempProp = property(\n  &quot;abs is idempotent&quot;,\n  intGen(),\n  proc(x: int) {\n    return isIdempotent(x, proc(n: int) { return abs(n); });\n  }\n);\n\n// Involution: f(f(x)) = x\nvar invProp = property(\n  &quot;negation is involution&quot;,\n  intGen(),\n  proc(x: int) {\n    return isInvolution(x, proc(n: int) { return -n; });\n  }\n);\n\n// Round-trip: decode(encode(x)) = x\nvar roundTripProp = property(\n  &quot;int&lt;-&gt;string round-trip&quot;,\n  intGen(),\n  proc(x: int) {\n    return isRoundTrip(x,\n      proc(n: int) { return n:string; },\n      proc(s: string): int { return try! s:int; catch { return 0; } }\n    );\n  }\n);\"><pre><span class=\"pl-c\"><span class=\"pl-c\">//</span> Idempotence: f(f(x)) = f(x)</span>\n<span class=\"pl-k\">var</span> idempProp <span class=\"pl-k\">=</span> property(\n  <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>abs is idempotent<span class=\"pl-pds\">\"</span></span>,\n  intGen(),\n  <span class=\"pl-k\">proc</span>(x<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) {\n    <span class=\"pl-k\">return</span> isIdempotent(x, <span class=\"pl-k\">proc</span>(n<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> <span class=\"pl-c1\">abs</span>(n); });\n  }\n);\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Involution: f(f(x)) = x</span>\n<span class=\"pl-k\">var</span> invProp <span class=\"pl-k\">=</span> property(\n  <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>negation is involution<span class=\"pl-pds\">\"</span></span>,\n  intGen(),\n  <span class=\"pl-k\">proc</span>(x<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) {\n    <span class=\"pl-k\">return</span> isInvolution(x, <span class=\"pl-k\">proc</span>(n<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> <span class=\"pl-k\">-</span>n; });\n  }\n);\n\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> Round-trip: decode(encode(x)) = x</span>\n<span class=\"pl-k\">var</span> roundTripProp <span class=\"pl-k\">=</span> property(\n  <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>int&lt;-&gt;string round-trip<span class=\"pl-pds\">\"</span></span>,\n  intGen(),\n  <span class=\"pl-k\">proc</span>(x<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) {\n    <span class=\"pl-k\">return</span> isRoundTrip(x,\n      <span class=\"pl-k\">proc</span>(n<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> n<span class=\"pl-k\">:</span><span class=\"pl-k\">string</span>; },\n      <span class=\"pl-k\">proc</span>(s<span class=\"pl-k\">:</span> <span class=\"pl-k\">string</span>)<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span> { <span class=\"pl-k\">return</span> <span class=\"pl-k\">try</span>! s<span class=\"pl-k\">:</span><span class=\"pl-k\">int</span>; <span class=\"pl-k\">catch</span> { <span class=\"pl-k\">return</span> <span class=\"pl-c1\">0</span>; } }\n    );\n  }\n);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Shrinking</h2><a id=\"user-content-shrinking\" class=\"anchor\" aria-label=\"Permalink: Shrinking\" href=\"#shrinking\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">When a property fails, quickchpl automatically shrinks the counterexample to find the minimal failing case:</p>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"// Property fails for x &gt;= 50\nvar prop = property(\n  &quot;x is small&quot;,\n  intGen(0, 1000),\n  proc(x: int) { return x &lt; 50; }\n);\n\nvar result = check(prop);\n// result.failureInfo might be &quot;847&quot;\n// result.shrunkInfo will be &quot;50&quot; (minimal failing case)\"><pre><span class=\"pl-c\"><span class=\"pl-c\">//</span> Property fails for x &gt;= 50</span>\n<span class=\"pl-k\">var</span> prop <span class=\"pl-k\">=</span> property(\n  <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>x is small<span class=\"pl-pds\">\"</span></span>,\n  intGen(<span class=\"pl-c1\">0</span>, <span class=\"pl-c1\">1000</span>),\n  <span class=\"pl-k\">proc</span>(x<span class=\"pl-k\">:</span> <span class=\"pl-k\">int</span>) { <span class=\"pl-k\">return</span> x <span class=\"pl-k\">&lt;</span> <span class=\"pl-c1\">50</span>; }\n);\n\n<span class=\"pl-k\">var</span> result <span class=\"pl-k\">=</span> check(prop);\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> result.failureInfo might be \"847\"</span>\n<span class=\"pl-c\"><span class=\"pl-c\">//</span> result.shrunkInfo will be \"50\" (minimal failing case)</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Shrinking Strategies</h3><a id=\"user-content-shrinking-strategies\" class=\"anchor\" aria-label=\"Permalink: Shrinking Strategies\" href=\"#shrinking-strategies\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>Integers</strong>: Binary search towards 0</li>\n<li><strong>Reals</strong>: Try 0, truncated, rounded values</li>\n<li><strong>Strings</strong>: Try empty, remove chars, simplify to 'a'</li>\n<li><strong>Lists</strong>: Try empty, remove elements, shrink elements</li>\n<li><strong>Tuples</strong>: Shrink each component</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">GitLab CI</h3><a id=\"user-content-gitlab-ci\" class=\"anchor\" aria-label=\"Permalink: GitLab CI\" href=\"#gitlab-ci\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-yaml notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"include:\n  - remote: 'https://gitlab.com/tinyland/projects/quickchpl/-/raw/main/ci/.gitlab-ci.yml'\n\nproperty_tests:\n  extends: .property_test_template\n  script:\n    - chpl tests/my_properties.chpl -M quickchpl/src -o /tmp/props\n    - /tmp/props --numTests=1000\"><pre><span class=\"pl-ent\">include</span>:\n  - <span class=\"pl-ent\">remote</span>: <span class=\"pl-s\"><span class=\"pl-pds\">'</span>https://gitlab.com/tinyland/projects/quickchpl/-/raw/main/ci/.gitlab-ci.yml<span class=\"pl-pds\">'</span></span>\n\n<span class=\"pl-ent\">property_tests</span>:\n  <span class=\"pl-ent\">extends</span>: <span class=\"pl-s\">.property_test_template</span>\n  <span class=\"pl-ent\">script</span>:\n    - <span class=\"pl-s\">chpl tests/my_properties.chpl -M quickchpl/src -o /tmp/props</span>\n    - <span class=\"pl-s\">/tmp/props --numTests=1000</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">GitHub Actions</h3><a id=\"user-content-github-actions\" class=\"anchor\" aria-label=\"Permalink: GitHub Actions\" href=\"#github-actions\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-yaml notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"- name: Run property tests\n  run: |\n    chpl tests/my_properties.chpl -M quickchpl/src -o /tmp/props\n    /tmp/props --numTests=1000\"><pre>- <span class=\"pl-ent\">name</span>: <span class=\"pl-s\">Run property tests</span>\n  <span class=\"pl-ent\">run</span>: <span class=\"pl-s\">|</span>\n<span class=\"pl-s\">    chpl tests/my_properties.chpl -M quickchpl/src -o /tmp/props</span>\n<span class=\"pl-s\">    /tmp/props --numTests=1000</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Configuration</h2><a id=\"user-content-configuration\" class=\"anchor\" aria-label=\"Permalink: Configuration\" href=\"#configuration\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"export CHPL_MODULE_PATH=$CHPL_MODULE_PATH:$PWD/quickchpl/src\"><pre><span class=\"pl-k\">export</span> CHPL_MODULE_PATH=<span class=\"pl-smi\">$CHPL_MODULE_PATH</span>:<span class=\"pl-smi\">$PWD</span>/quickchpl/src</pre></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"./my_tests --numTests=1000 --maxShrinkSteps=500 --verbose=true\"><pre>./my_tests --numTests=1000 --maxShrinkSteps=500 --verbose=true</pre></div>\n<p dir=\"auto\">...Or in code:</p>\n<div class=\"highlight highlight-source-chapel notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"var runner = new PropertyRunner(\n  numTests = 1000,\n  maxShrinkSteps = 500,\n  verboseMode = true\n);\nvar result = runner.check(prop);\"><pre><span class=\"pl-k\">var</span> runner <span class=\"pl-k\">=</span> <span class=\"pl-k\">new</span> PropertyRunner(\n  numTests <span class=\"pl-k\">=</span> <span class=\"pl-c1\">1000</span>,\n  maxShrinkSteps <span class=\"pl-k\">=</span> <span class=\"pl-c1\">500</span>,\n  verboseMode <span class=\"pl-k\">=</span> <span class=\"pl-c1\">true</span>\n);\n<span class=\"pl-k\">var</span> result <span class=\"pl-k\">=</span> runner.check(prop);</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">API</h2><a id=\"user-content-api\" class=\"anchor\" aria-label=\"Permalink: API\" href=\"#api\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Core Types</h3><a id=\"user-content-core-types\" class=\"anchor\" aria-label=\"Permalink: Core Types\" href=\"#core-types\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>IntGenerator</code></td>\n<td>Generates random integers</td>\n</tr>\n<tr>\n<td><code>RealGenerator</code></td>\n<td>Generates random real numbers</td>\n</tr>\n<tr>\n<td><code>BoolGenerator</code></td>\n<td>Generates random booleans</td>\n</tr>\n<tr>\n<td><code>StringGenerator</code></td>\n<td>Generates random strings</td>\n</tr>\n<tr>\n<td><code>Property</code></td>\n<td>Defines a property to test</td>\n</tr>\n<tr>\n<td><code>PropertyRunner</code></td>\n<td>Executes property tests</td>\n</tr>\n<tr>\n<td><code>TestResult</code></td>\n<td>Contains test results</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Functions</h3><a id=\"user-content-functions\" class=\"anchor\" aria-label=\"Permalink: Functions\" href=\"#functions\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Function</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>intGen(min, max)</code></td>\n<td>Create integer generator</td>\n</tr>\n<tr>\n<td><code>property(name, gen, pred)</code></td>\n<td>Define a property</td>\n</tr>\n<tr>\n<td><code>check(prop)</code></td>\n<td>Run property test</td>\n</tr>\n<tr>\n<td><code>quickCheck(gen, pred)</code></td>\n<td>One-liner property check</td>\n</tr>\n<tr>\n<td><code>shrink(value)</code></td>\n<td>Generate shrink candidates</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Todos &amp; Future work:</h3><a id=\"user-content-todos--future-work\" class=\"anchor\" aria-label=\"Permalink: Todos &amp; Future work:\" href=\"#todos--future-work\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul class=\"contains-task-list\">\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> Integrate with Chapel Mason package repo (in progress)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Incomplete task\"> Integrate Outbot Harness</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> Integrate with (and <code>[ ]</code> publish) sister projects, <code>chapel-k8s-mail</code>, <code>chapel-git</code>, <code>tinymachines</code>, <code>mariolex</code>)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Incomplete task\"> Add IDE and LLM friendly text and code completions (docs in the works)</li>\n<li class=\"task-list-item\"><input type=\"checkbox\" id=\"\" disabled=\"\" class=\"task-list-item-checkbox\" aria-label=\"Completed task\" checked=\"\"> Provide public demo (<code>aoc-2025</code> <strong>done! one is good for now</strong>)</li>\n</ul>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">Notes:</h3><a id=\"user-content-notes\" class=\"anchor\" aria-label=\"Permalink: Notes:\" href=\"#notes\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><code>module include statements are not yet stable</code> &lt;-- this is expected</li>\n<li><code>list.parSafe is unstable</code> &lt;-- this is expected</li>\n<li><code>use of routines as values is unstable</code> &lt;-- routines are necessary AFAICT to achieve this familiar PBT structure</li>\n</ul>\n</article></div>",
      "readme_excerpt": "Simple Property-Based Testing for Chapel\n[](https://chapel-lang.org/)\n[](https://opensource.org/licenses/MIT)\n[](https://gitlab.com/tinyland/projects/quickchpl)\nInspired by QuickCheck and my father ^w^\nchapel\nuse quickchpl;\nproc main() {\n  // Test that addition is commutative\n  var gen = tupleGen(intGen(-100, 100), intGen(-100, 100));\n  var prop = property(\n    \"addition is commutative\",\n    gen,\n    proc(args: (int, int)) {\n      const (a, b) = args;\n      return a + b == b + a;\n    }\n  );\n ...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/quickchpl",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/quickchpl/releases",
      "og_image_url": "https://opengraph.githubassets.com/9176ab1b95da586e4dc557f1fa4ace81e1c8afb7eb28d4786f148265fc04540e/Jesssullivan/quickchpl",
      "license": "MIT",
      "pushed_at": "2026-01-17T19:12:31Z",
      "enriched_at": "2026-04-26T17:17:19.676Z",
      "gate": "override"
    },
    {
      "slug": "jesssullivan-fastphotoapi",
      "name": "FastPhotoAPI",
      "repo": "jesssullivan/FastPhotoAPI",
      "org": "jesssullivan",
      "ecosystem": "multi",
      "category": "uncategorized",
      "description": "An efficient, flexible, flask-based image server using Lanczos resampling ",
      "featured": false,
      "tags": [
        "flask",
        "lanczos",
        "docker",
        "koyeb",
        "static-api"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 1,
      "topics": [
        "flask",
        "lanczos",
        "docker",
        "koyeb",
        "static-api"
      ],
      "languages": [
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 9848
        },
        {
          "name": "HTML",
          "color": "#e34c26",
          "bytes": 2470
        },
        {
          "name": "CSS",
          "color": "#663399",
          "bytes": 1157
        },
        {
          "name": "Dockerfile",
          "color": "#384d54",
          "bytes": 436
        }
      ],
      "primary_language": "Python",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">FastPhotoAPI</h1><a id=\"user-content-fastphotoapi\" class=\"anchor\" aria-label=\"Permalink: FastPhotoAPI\" href=\"#fastphotoapi\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">An efficient, flexible, flask-based image server that uses lanczos resampling to serve optimized cached photos.</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python3.12 -m venv fast_photo_venv\nsource fast_photo_venv/bin/activate\npip install -r requirements.txt\"><pre>python3.12 -m venv fast_photo_venv\n<span class=\"pl-c1\">source</span> fast_photo_venv/bin/activate\npip install -r requirements.txt</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Usage:</h2><a id=\"user-content-usage\" class=\"anchor\" aria-label=\"Permalink: Usage:\" href=\"#usage\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>Resample &amp; fetch a cached image <code>/image/&lt;yourimage&gt;</code></li>\n<li>Resample and fetch an image with a specific max dimension:\n<ul dir=\"auto\">\n<li><code>/image/&lt;yourimage&gt;?w=69</code> or</li>\n<li><code>/image/&lt;yourimage?h=69&gt;</code> or</li>\n<li><code>/image/&lt;yourimage&gt;?w=69&amp;h=42</code></li>\n</ul>\n</li>\n<li>Fetch the original, unmodified image <code>/full/&lt;yourimage&gt;</code></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Structure:</h2><a id=\"user-content-structure\" class=\"anchor\" aria-label=\"Permalink: Structure:\" href=\"#structure\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">This application adopts the factory pattern; <code>flask run</code> instantiates the built-in development server by executing <code>create_app()</code> at the root of the <code>app/</code> package, while <code>python application.py</code> creates a new production application, served by waitress.</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\".\n├── app\n          ├── __init__.py  #  create and serve development application\n          └── main\n              ├── config\n│             │         ├── conf.py  # Utility configs and methods \n│             │         └── config.cfg  # set directories, max image dimensions, etc\n│             ├── fullsize\n│             │         └── routes.py  # Blueprint routing for serving verbatim image files \n│             ├── __init__.py  # `create_app()` entrypoint\n│             ├── resampled\n│             │         ├── model.py  # Image resampling methods\n│             │         ├── trashd.py  # garbage collector daemon \n│             │         └── routes.py  # Blueprint routing for `/image/`  \n│             └── static\n│                 └── routes.py  # Blueprint routing for `/static/` \n├── application.py  # create and serve production application w/ waitress\n├── cache  # resampled images are dynamically generated adn stored here \n├── Dockerfile  # currently deployed at Koyeb  \n├── pictures # full res pictures go here\n├── README.md  # you are here\n├── static\n│         └── style.css  # index styling\n└── templates\n    ├── index.html  \n    └── upload.html\"><pre><span class=\"pl-c1\">.</span>\n├── app\n          ├── __init__.py  <span class=\"pl-c\"><span class=\"pl-c\">#</span>  create and serve development application</span>\n          └── main\n              ├── config\n│             │         ├── conf.py  <span class=\"pl-c\"><span class=\"pl-c\">#</span> Utility configs and methods </span>\n│             │         └── config.cfg  <span class=\"pl-c\"><span class=\"pl-c\">#</span> set directories, max image dimensions, etc</span>\n│             ├── fullsize\n│             │         └── routes.py  <span class=\"pl-c\"><span class=\"pl-c\">#</span> Blueprint routing for serving verbatim image files </span>\n│             ├── __init__.py  <span class=\"pl-c\"><span class=\"pl-c\">#</span> `create_app()` entrypoint</span>\n│             ├── resampled\n│             │         ├── model.py  <span class=\"pl-c\"><span class=\"pl-c\">#</span> Image resampling methods</span>\n│             │         ├── trashd.py  <span class=\"pl-c\"><span class=\"pl-c\">#</span> garbage collector daemon </span>\n│             │         └── routes.py  <span class=\"pl-c\"><span class=\"pl-c\">#</span> Blueprint routing for `/image/`  </span>\n│             └── static\n│                 └── routes.py  <span class=\"pl-c\"><span class=\"pl-c\">#</span> Blueprint routing for `/static/` </span>\n├── application.py  <span class=\"pl-c\"><span class=\"pl-c\">#</span> create and serve production application w/ waitress</span>\n├── cache  <span class=\"pl-c\"><span class=\"pl-c\">#</span> resampled images are dynamically generated adn stored here </span>\n├── Dockerfile  <span class=\"pl-c\"><span class=\"pl-c\">#</span> currently deployed at Koyeb  </span>\n├── pictures <span class=\"pl-c\"><span class=\"pl-c\">#</span> full res pictures go here</span>\n├── README.md  <span class=\"pl-c\"><span class=\"pl-c\">#</span> you are here</span>\n├── static\n│         └── style.css  <span class=\"pl-c\"><span class=\"pl-c\">#</span> index styling</span>\n└── templates\n    ├── index.html  \n    └── upload.html</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Build</h2><a id=\"user-content-build\" class=\"anchor\" aria-label=\"Permalink: Build\" href=\"#build\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><em>Locally:</em></p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"flask run # 0.0.0.0:5000\"><pre lang=\"dev\" class=\"notranslate\"><code>flask run # 0.0.0.0:5000\n</code></pre></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"flask run # 0.0.0.0:8000\"><pre lang=\"waitress\" class=\"notranslate\"><code>flask run # 0.0.0.0:8000\n</code></pre></div>\n<p dir=\"auto\"><em>Production via Docker:</em></p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"## build production docker image:\ndocker build -t &lt;srv&gt; .\n\n## serve production docker image locally:\ndocker run -d -p 8000:8000 &lt;srv&gt;:latest\n\n## stop local image:\n# docker ps\n# docker stop \n\n## push image to a container registery: \n# docker push &lt;srv&gt;\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span># build production docker image:</span>\ndocker build -t <span class=\"pl-k\">&lt;</span>srv<span class=\"pl-k\">&gt;</span> <span class=\"pl-c1\">.</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span># serve production docker image locally:</span>\ndocker run -d -p 8000:8000 <span class=\"pl-k\">&lt;</span>srv<span class=\"pl-k\">&gt;</span>:latest\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span># stop local image:</span>\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> docker ps</span>\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> docker stop </span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span># push image to a container registery: </span>\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> docker push &lt;srv&gt;</span></pre></div>\n</article></div>",
      "readme_excerpt": "An efficient, flexible, flask-based image server that uses lanczos resampling to serve optimized cached photos.\nshell\npython3.12 -m venv fastphotovenv\nsource fastphotovenv/bin/activate\npip install -r requirements.txt\n- Resample & fetch a cached image /image/<yourimage>\n- Resample and fetch an image with a specific max dimension:\n  - /image/<yourimage>?w=69 or\n  - /image/<yourimage?h=69> or\n  - /image/<yourimage>?w=69&h=42\n- Fetch the original, unmodified image /full/<yourimage>\nThis application...",
      "install_commands": {},
      "repo_url": "https://github.com/jesssullivan/FastPhotoAPI",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/jesssullivan/FastPhotoAPI/releases",
      "og_image_url": "https://opengraph.githubassets.com/73f974263ffc78e88b1a979591d0e0a3281318170fe7034e4e200258f03e0a08/Jesssullivan/FastPhotoAPI",
      "license": "Apache-2.0",
      "pushed_at": "2024-12-20T20:45:58Z",
      "enriched_at": "2026-04-26T17:17:19.921Z",
      "gate": "auto"
    },
    {
      "slug": "jesssullivan-timberbuddy",
      "name": "timberbuddy",
      "repo": "jesssullivan/timberbuddy",
      "org": "jesssullivan",
      "ecosystem": "npm",
      "category": "uncategorized",
      "description": "Archive of Control Package work for Amish Sawmill",
      "featured": false,
      "tags": [
        "i2c",
        "raspberry-pi",
        "robotics",
        "sveltekit"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 1,
      "topics": [
        "i2c",
        "raspberry-pi",
        "robotics",
        "sveltekit"
      ],
      "languages": [
        {
          "name": "TypeScript",
          "color": "#3178c6",
          "bytes": 25013
        },
        {
          "name": "Svelte",
          "color": "#ff3e00",
          "bytes": 15769
        },
        {
          "name": "Jinja",
          "color": "#a52a22",
          "bytes": 5159
        },
        {
          "name": "Cython",
          "color": "#fedf5b",
          "bytes": 2485
        },
        {
          "name": "Python",
          "color": "#3572A5",
          "bytes": 1232
        }
      ],
      "primary_language": "TypeScript",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">Timber Buddy Controller</h1><a id=\"user-content-timber-buddy-controller\" class=\"anchor\" aria-label=\"Permalink: Timber Buddy Controller\" href=\"#timber-buddy-controller\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Thesis:</h2><a id=\"user-content-thesis\" class=\"anchor\" aria-label=\"Permalink: Thesis:\" href=\"#thesis\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><em><strong>Wouldn't it be cool to use a familiar, modern web stack for commercial automation or robotics?</strong></em></p>\n<p dir=\"auto\"><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"media/demo.gif\"><img src=\"media/demo.gif\" alt=\"\" data-animated-image=\"\" style=\"max-width: 100%;\"></a></p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Read on to understand the architecture of this project, why it is built this way and how to build and bend this fun project into your cool product.</h2><a id=\"user-content-read-on-to-understand-the-architecture-of-this-project-why-it-is-built-this-way-and-how-to-build-and-bend-this-fun-project-into-your-cool-product\" class=\"anchor\" aria-label=\"Permalink: Read on to understand the architecture of this project, why it is built this way and how to build and bend this fun project into your cool product.\" href=\"#read-on-to-understand-the-architecture-of-this-project-why-it-is-built-this-way-and-how-to-build-and-bend-this-fun-project-into-your-cool-product\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><strong>User input:</strong></p>\n<ul dir=\"auto\">\n<li>Operator interacts with the controller with any of the following inputs:\n<ul dir=\"auto\">\n<li>the Touch screen UI</li>\n<li>physical front panel buttons</li>\n<li>Graphically via the web (pi connect, web console or with wireguard / port forwarding)</li>\n<li>Via SSH</li>\n</ul>\n</li>\n</ul>\n<p dir=\"auto\"><strong>HID transmission to hardware:</strong></p>\n<ul dir=\"auto\">\n<li>Touch screen button presses are communicated via <strong>websocket</strong> to the node server;\n<ul dir=\"auto\">\n<li>socket communications pass along a boolean value that is treated like an atomic state;</li>\n<li>This allows different / concurrent / incidental requests from the hardware to be made by any number of connected clients (though of course, as a PLC there will only ever by one connected client), with messages only getting accepted if the requested routine is not already active.   This means we do not have to deal with message queues or masquerade what are fundamentally synchronous routines (move the saw up or down by however much, home the machine, toggle a mode etc) as faux asynchronous jobs.  This is the opposite if how most server / client communications would be setup, but is important to understand because the job executed by the server is literally a single set of sensors, lights, solenoids connected to the pi.</li>\n</ul>\n</li>\n<li>Physical button presses are also communicated via websocket on the active client connection.</li>\n</ul>\n<p dir=\"auto\"><strong>Data transmission and storage:</strong></p>\n<ul dir=\"auto\">\n<li>we use a flat json file to store and ultimately log / rotate data as well as browser localStorage to actively handle data, where the localStorage values are managed via a SvelteKit store.  The use of browserStorage allows the touch UI to display, modify and cache data being used by each <em>type</em> of client (stack, core) as well as removes load on the http API and thus the filesystem.</li>\n<li>In the case of timberbuddy, we store Cut size, stack size and total cut count in a static json file.</li>\n<li>Changes to data values to the filesystem are completed via the node POST API.</li>\n<li>If there is a disagreement between client and server about values, the static file values take presidence</li>\n<li>Jobs executed by the server (ie moving a saw) read from the static file directly.</li>\n</ul>\n<p dir=\"auto\"><strong>Type of client (core / stack in the case of the timberbuddy):</strong></p>\n<ul dir=\"auto\">\n<li>Because actions are communicated via active websocket connection, we identify the kind of client and thus kind of job (stack or core, which ingest different cut sizes for example) with the referrer's slug in the socket handshake header.</li>\n</ul>\n<p dir=\"auto\"><strong>Pi Node Server:</strong></p>\n<ul dir=\"auto\">\n<li>The compiled sveltekit application (<em>client + server</em>) is served using a custom express node application.\n<ul dir=\"auto\">\n<li>The sveltkit application uses the <code>node-adapter</code> as part of the vite build process to make this possible.</li>\n<li>The <em>Sveltekit application</em> is responsible for handling the POST API and <strong>creating</strong> and <strong>writing</strong> to the static json data file.  The Sveltekit application is responsible for all http communication.</li>\n</ul>\n</li>\n<li>The <em>node server</em> is responsible for pi GPIO connections (buttons!) websocket connections, managing atomic hardware state and i2c bus and for <strong>reading</strong> the static json file during routine execution.</li>\n<li>includes keyboard support and does its best to allow for graceful socket, GPIO and fs exit (allowing for the systemd service to properly keep the  service alive)</li>\n</ul>\n<p dir=\"auto\"><strong>About The Darwin Server:</strong></p>\n<ul dir=\"auto\">\n<li>I also wrote a development node server for macos that uses keyboard presses as a stand-in for hardware GPIO buttons.  This does not allow for i2c bus communication, but does help troubleshoot socket / http development.</li>\n</ul>\n<p dir=\"auto\"><strong>Can I use a Vite Server?</strong></p>\n<ul dir=\"auto\">\n<li>Yes.  Use the traditional vite dev server for UI development.</li>\n</ul>\n<p dir=\"auto\"><strong>The i2c bus:</strong></p>\n<ul dir=\"auto\">\n<li>Encoder io and relay IO is done via the i2c bus.  I've written drivers for two M5 i2c devices in Typescript depending solely on the <code>raspi-i2c</code> library. These are modeled after the example C++ classes shared by M5 stack on github.  Synchronous methods are used wherever possible.</li>\n</ul>\n<p dir=\"auto\"><strong>Deployment:</strong></p>\n<ul dir=\"auto\">\n<li>Leverages pi connect for initial remote connection and the initial tailscale / wiregaurd daemon setup for SSH.</li>\n<li>Uses ansible to deploy and build the entire application- roles include:\n<ul dir=\"auto\">\n<li><code>kiosk</code>: sets up vanilla bookworm image with:\n<ul dir=\"auto\">\n<li>tailscale daemon, gh &amp; git tokens, custom wayland config, wayfire kiosk mode and executable kiosk mode.</li>\n</ul>\n</li>\n<li><code>pi-setup</code>:\n<ul dir=\"auto\">\n<li>enable i2c, linger support, splashboot configs, support for waveshare DSI display on wayland</li>\n<li>establishes network uptime cron daemon, network logging, wlan config, network depends</li>\n</ul>\n</li>\n<li><code>waveshare-dsi</code>:\n<ul dir=\"auto\">\n<li>sets up OEM brightness control utility for waveshare DSI display</li>\n</ul>\n</li>\n<li><code>node-server</code>:\n<ul dir=\"auto\">\n<li>sets up all kinds of node server related stuff</li>\n<li>fetches source from git w/ token, sets up nodegyp, pnpm</li>\n<li>Compiles sveltekit application and installs as the complete node application as a systemd service</li>\n</ul>\n</li>\n<li><code>splash-kernel</code>:\n<ul dir=\"auto\">\n<li>began work on custom kernel and splash animation (pending completion by local artist) for virtualized dev environment</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n<p dir=\"auto\"><strong>Touchscreen / UI:</strong></p>\n<ul dir=\"auto\">\n<li>SvelteKit web application;\n<ul dir=\"auto\">\n<li>Pages use the edge runtime</li>\n</ul>\n</li>\n<li>Runs directly in a browser in kiosk mode\n<ul dir=\"auto\">\n<li>Utilizes the Skeleton UI component library</li>\n<li>Utilizes Lucide Iconography</li>\n</ul>\n</li>\n</ul>\n<p dir=\"auto\"><strong>Testing</strong></p>\n<ul dir=\"auto\">\n<li>I've included both python and cython implementations of the i2c example c++ classes provided by m5 stack.</li>\n<li>demo encoder logging and relay toggling implementations are included with the typescript class files.</li>\n<li>Currently, demo routines in the node server file are using timers for demo the atomic socket behavior while the saw is moving; switching this to read from the encoder and wait to reach a desired encoder reading is trivial. Similarly, moving these actual machine to class files is inevitable, all the functionality is there and ready to go.</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Why this way?  Doesn't this seem way more complicated than it should be?</h2><a id=\"user-content-why-this-way--doesnt-this-seem-way-more-complicated-than-it-should-be\" class=\"anchor\" aria-label=\"Permalink: Why this way?  Doesn't this seem way more complicated than it should be?\" href=\"#why-this-way--doesnt-this-seem-way-more-complicated-than-it-should-be\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>In order to satisfy the requirements of fully remote development, a network enabled device with well documented GPIO, i2c IO, touchscreen IO etc really calls for a raspberry pi or similarly full-blown SBC.</li>\n<li>Being able to clone SD cards interoperability with long term support from the pi foundation was identified as a plus by collaborators.</li>\n<li>The customer / collaborators indicated a desire for over the air updates or modifications.</li>\n<li>The recent (at the time) beta release of pi connect and wayland screen sharing on pi made boot strapping the setup of a pi completely remotely without needing to coach anyone through major technical hurdles pretty easy.</li>\n<li>Jess is familiar with sveltekit in her professional work.  Using a modern web stack for UI UX as well as robotics with real hardware is sort of a holy grail for her.  There are lots of web engineers, and this stack is not tied to any particular vendor or mutable license.</li>\n<li>My use of a combination of socket / http API protocols and data transmission solutions (localStorage, svelte writable store, flat file) can surely be simplified and improved upon, this is just the solution I've come up with.</li>\n<li>Find some pictures associated with this project in the <code>/media</code> directory.</li>\n</ul>\n<hr>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Setup from scratch:</h2><a id=\"user-content-setup-from-scratch\" class=\"anchor\" aria-label=\"Permalink: Setup from scratch:\" href=\"#setup-from-scratch\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\">What you need:</h3><a id=\"user-content-what-you-need\" class=\"anchor\" aria-label=\"Permalink: What you need:\" href=\"#what-you-need\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>A raspberry pi 5 running the latest pi os using wayland. (at the time of writing, this is the Bookworm release)\n<ul dir=\"auto\">\n<li>If you are doing 100% remote development, you'll need a free pi connect account and a tailnet (or any similar vpn solution with a public control plane)</li>\n</ul>\n</li>\n<li><a href=\"https://www.waveshare.com/8inch-dsi-lcd.htm\" rel=\"nofollow\">this waveshare DSI display: SKU 21229</a></li>\n<li><a href=\"https://shop.m5stack.com/products/4-relay-unit?srsltid=AfmBOopeb1VvwA0WJMs-tyo0icalEB23Ua50P2QP5g7hMfUKjqunKfJv\" rel=\"nofollow\">this i2c relay bank module: SKU U097</a></li>\n<li><a href=\"https://shop.m5stack.com/products/ext-encoder-unit-stm32f030?srsltid=AfmBOopV67NfXJC9uBYsKlOr9E6iXiTUIv2RYjmQ5uZOGiynx3wHIkUh\" rel=\"nofollow\">this i2c encoder module SKU U161</a></li>\n<li>4x momentary push buttons</li>\n<li>4x 10k resistors (for GPIO buttons)</li>\n<li>4x 1k resistors (for GPIO buttons)</li>\n<li>a 5v power supply for the pi (I used a 5v meanwell din module I converted to work as a variable supply)</li>\n<li>a power supply for your solenoids (in my case a 12v meanwell din unit)</li>\n<li>some stuff to toggle (in my dev unit, I used these <a href=\"https://www.amazon.com/dp/B0CYG8YGP3?ref=ppx_yo2ov_dt_b_fed_asin_title&amp;th=1\" rel=\"nofollow\">12v pilot lamps</a> to simulate hydrolytic solenoids firing)</li>\n<li>an encoder to read from</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">Most importantly:</h4><a id=\"user-content-most-importantly\" class=\"anchor\" aria-label=\"Permalink: Most importantly:\" href=\"#most-importantly\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\"><em><strong>Expertise in typescript, sveltekit, postcss, websocket programming, tailwind, vite adapters, ansible deployment strategies, node server development, express applications, debugging with cython, i2c bus programming, atomic execution paradigms, linux init systems, wayland for HID implementations, NIC and SOC power management on SBCs and shell scripting.  This project is not done.  This repo is provided as an as-is archive of work.</strong></em></p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">How to build it:</h2><a id=\"user-content-how-to-build-it\" class=\"anchor\" aria-label=\"Permalink: How to build it:\" href=\"#how-to-build-it\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">for development:</h4><a id=\"user-content-for-development\" class=\"anchor\" aria-label=\"Permalink: for development:\" href=\"#for-development\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li>local depends:\n<ul dir=\"auto\">\n<li>you'll need node v20 and pnpm, npx, tsx installed.</li>\n</ul>\n</li>\n</ul>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"git clone https://github.com/jesssullivan/timberbuddy &amp;&amp; cd timberbuddy\npnpm i\npnpm run build\n\n# just sveltekit applications; live reload:\npnpm run dev\n\n# on macos with node server; no hot reload, no i2c:\npnpm run darwin-start\n\n# on pi with production server:\npnpm run pi-start\"><pre>git clone https://github.com/jesssullivan/timberbuddy <span class=\"pl-k\">&amp;&amp;</span> <span class=\"pl-c1\">cd</span> timberbuddy\npnpm i\npnpm run build\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> just sveltekit applications; live reload:</span>\npnpm run dev\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> on macos with node server; no hot reload, no i2c:</span>\npnpm run darwin-start\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> on pi with production server:</span>\npnpm run pi-start</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">for deployment:</h4><a id=\"user-content-for-deployment\" class=\"anchor\" aria-label=\"Permalink: for deployment:\" href=\"#for-deployment\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# from your local machine:\ngit clone https://github.com/jesssullivan/timberbuddy &amp;&amp; cd timberbuddy/deploy\nsudo chmod +x scripts/setup_venv.sh\n./scripts/setup_venv.sh &amp;&amp; source timber_venv/bin/activate\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> from your local machine:</span>\ngit clone https://github.com/jesssullivan/timberbuddy <span class=\"pl-k\">&amp;&amp;</span> <span class=\"pl-c1\">cd</span> timberbuddy/deploy\nsudo chmod +x scripts/setup_venv.sh\n./scripts/setup_venv.sh <span class=\"pl-k\">&amp;&amp;</span> <span class=\"pl-c1\">source</span> timber_venv/bin/activate</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">over pi connect in it's remote shell:</h4><a id=\"user-content-over-pi-connect-in-its-remote-shell\" class=\"anchor\" aria-label=\"Permalink: over pi connect in it's remote shell:\" href=\"#over-pi-connect-in-its-remote-shell\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# once tailscale is setup, we can ansible the rest of the things:\ncurl -L https://pkgs.tailscale.com/stable/raspbian/$(lsb_release -cs).noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg &gt;/dev/null &amp;&amp; \\\n  echo &quot;deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/raspbian $(lsb_release -cs) main&quot; | sudo tee  /etc/apt/sources.list.d/tailscale.list &amp;&amp; \n  sudo apt update &amp;&amp; sudo apt install tailscale \n\n ## log in:\nsudo tailscale up -ssh # use your local machine to login into to control plane with the generated url\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> once tailscale is setup, we can ansible the rest of the things:</span>\ncurl -L https://pkgs.tailscale.com/stable/raspbian/<span class=\"pl-s\"><span class=\"pl-pds\">$(</span>lsb_release -cs<span class=\"pl-pds\">)</span></span>.noarmor.gpg <span class=\"pl-k\">|</span> sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg <span class=\"pl-k\">&gt;</span>/dev/null <span class=\"pl-k\">&amp;&amp;</span> \\\n  <span class=\"pl-c1\">echo</span> <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/raspbian <span class=\"pl-s\"><span class=\"pl-pds\">$(</span>lsb_release -cs<span class=\"pl-pds\">)</span></span> main<span class=\"pl-pds\">\"</span></span> <span class=\"pl-k\">|</span> sudo tee  /etc/apt/sources.list.d/tailscale.list <span class=\"pl-k\">&amp;&amp;</span> \n  sudo apt update <span class=\"pl-k\">&amp;&amp;</span> sudo apt install tailscale \n\n <span class=\"pl-c\"><span class=\"pl-c\">#</span># log in:</span>\nsudo tailscale up -ssh <span class=\"pl-c\"><span class=\"pl-c\">#</span> use your local machine to login into to control plane with the generated url</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">from controller:</h4><a id=\"user-content-from-controller\" class=\"anchor\" aria-label=\"Permalink: from controller:\" href=\"#from-controller\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# run like any ansible playbook; you'll be prompted for a github key for the remote clone / build / install\n\n# something to the tune of:\nansible-playbook -i inventory_remote_dev -K services.yml --extra-vars &quot;host=remote-dev&quot; -l &quot;remote-dev&quot; -u &quot;TimberBuddy&quot;\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> run like any ansible playbook; you'll be prompted for a github key for the remote clone / build / install</span>\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> something to the tune of:</span>\nansible-playbook -i inventory_remote_dev -K services.yml --extra-vars <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>host=remote-dev<span class=\"pl-pds\">\"</span></span> -l <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>remote-dev<span class=\"pl-pds\">\"</span></span> -u <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>TimberBuddy<span class=\"pl-pds\">\"</span></span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h4 class=\"heading-element\" dir=\"auto\">connect / tailscale troubleshooting:</h4><a id=\"user-content-connect--tailscale-troubleshooting\" class=\"anchor\" aria-label=\"Permalink: connect / tailscale troubleshooting:\" href=\"#connect--tailscale-troubleshooting\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# for pi connect upon local kiosk role run:\nrpi-connect signin\n\n# upon reprovisioning, you may need to play keys time:\nssh-copy-id &lt;TimberBuddy&gt;@&lt;pi-ipv4&gt; \"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> for pi connect upon local kiosk role run:</span>\nrpi-connect signin\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> upon reprovisioning, you may need to play keys time:</span>\nssh-copy-id <span class=\"pl-k\">&lt;</span>TimberBuddy<span class=\"pl-k\">&gt;</span>@<span class=\"pl-k\">&lt;</span>pi-ipv<span class=\"pl-k\">4&gt;</span> </pre></div>\n<p dir=\"auto\"><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"media/IMG_0655.gif\"><img src=\"media/IMG_0655.gif\" alt=\"\" data-animated-image=\"\" style=\"max-width: 100%;\"></a></p>\n</article></div>",
      "readme_excerpt": "Wouldn't it be cool to use a familiar, modern web stack for commercial automation or robotics?\nRead on to understand the architecture of this project, why it is built this way and how to build and bend this fun project into your cool product.\n- A raspberry pi 5 running the latest pi os using wayland. (at the time of writing, this is the Bookworm release)\n  - If you are doing 100% remote development, you'll need a free pi connect account and a tailnet (or any similar vpn solution with a public...",
      "install_commands": {
        "npm": "npm install timberbuddy"
      },
      "repo_url": "https://github.com/jesssullivan/timberbuddy",
      "website_url": null,
      "docs_url": null,
      "registry_url": "https://www.npmjs.com/package/timberbuddy",
      "releases_url": "https://github.com/jesssullivan/timberbuddy/releases",
      "og_image_url": "https://opengraph.githubassets.com/05164d8e594961f7c1fb5187d4d96480ea4269078dc9cba1362900f2bb134245/Jesssullivan/timberbuddy",
      "license": "",
      "pushed_at": "2024-12-07T19:42:40Z",
      "enriched_at": "2026-04-26T17:17:20.203Z",
      "gate": "auto"
    },
    {
      "slug": "tinyland-inc-tinyvectors",
      "name": "tinyvectors",
      "repo": "tinyland-inc/tinyvectors",
      "org": "tinyland-inc",
      "ecosystem": "npm",
      "category": "devex",
      "description": "Lightweight vector math for physics simulations and blob animations",
      "featured": false,
      "tags": [
        "math",
        "vectors",
        "animation"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [],
      "languages": [
        {
          "name": "TypeScript",
          "color": "#3178c6",
          "bytes": 116445
        },
        {
          "name": "Svelte",
          "color": "#ff3e00",
          "bytes": 13068
        },
        {
          "name": "Starlark",
          "color": "#76d275",
          "bytes": 6872
        },
        {
          "name": "CSS",
          "color": "#663399",
          "bytes": 6244
        },
        {
          "name": "HTML",
          "color": "#e34c26",
          "bytes": 2453
        }
      ],
      "primary_language": "TypeScript",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\"><code>@tummycrypt/tinyvectors</code></h1><a id=\"user-content-tummycrypttinyvectors\" class=\"anchor\" aria-label=\"Permalink: @tummycrypt/tinyvectors\" href=\"#tummycrypttinyvectors\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Animated vector blob backgrounds with lil physics for Svelte 5.</p>\n<p dir=\"auto\">This package is what powers the moving background layer on <code>transscendsurvival.org</code>, but it is meant to be useful outside that site too. It ships a small set of Svelte components plus the lower-level motion, theme, and core utilities that drive them.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Install</h2><a id=\"user-content-install\" class=\"anchor\" aria-label=\"Permalink: Install\" href=\"#install\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm add @tummycrypt/tinyvectors\"><pre>pnpm add @tummycrypt/tinyvectors</pre></div>\n<p dir=\"auto\">Peer dependency:</p>\n<ul dir=\"auto\">\n<li><code>svelte@^5</code></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick Start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick Start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-svelte notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"&lt;script lang=&quot;ts&quot;&gt;\n\timport { TinyVectors } from '@tummycrypt/tinyvectors';\n\n\tconst colors = [\n\t\t'rgba(26,188,156,0.35)',\n\t\t'rgba(22,160,133,0.30)',\n\t\t'rgba(39,174,96,0.25)',\n\t\t'rgba(52,152,219,0.20)',\n\t];\n&lt;/script&gt;\n\n&lt;div class=&quot;fixed inset-0 -z-10 pointer-events-none&quot; aria-hidden=&quot;true&quot;&gt;\n\t&lt;TinyVectors\n\t\ttheme=&quot;custom&quot;\n\t\tcolors={colors}\n\t\tblobCount={5}\n\t\topacity={0.4}\n\t\tenableScrollPhysics={true}\n\t\tenableDeviceMotion={false}\n\t/&gt;\n&lt;/div&gt;\"><pre>&lt;<span class=\"pl-ent\">script</span> <span class=\"pl-e\">lang</span>=<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>ts<span class=\"pl-pds\">\"</span></span>&gt;<span class=\"pl-s1\"></span>\n<span class=\"pl-s1\">\t<span class=\"pl-k\">import</span> { <span class=\"pl-smi\">TinyVectors</span> } <span class=\"pl-k\">from</span> <span class=\"pl-s\"><span class=\"pl-pds\">'</span>@tummycrypt/tinyvectors<span class=\"pl-pds\">'</span></span>;</span>\n<span class=\"pl-s1\"></span>\n<span class=\"pl-s1\">\t<span class=\"pl-k\"><span class=\"pl-k\">const</span></span> colors <span class=\"pl-k\">=</span> [</span>\n<span class=\"pl-s1\">\t\t<span class=\"pl-s\"><span class=\"pl-pds\">'</span>rgba(26,188,156,0.35)<span class=\"pl-pds\">'</span></span>,</span>\n<span class=\"pl-s1\">\t\t<span class=\"pl-s\"><span class=\"pl-pds\">'</span>rgba(22,160,133,0.30)<span class=\"pl-pds\">'</span></span>,</span>\n<span class=\"pl-s1\">\t\t<span class=\"pl-s\"><span class=\"pl-pds\">'</span>rgba(39,174,96,0.25)<span class=\"pl-pds\">'</span></span>,</span>\n<span class=\"pl-s1\">\t\t<span class=\"pl-s\"><span class=\"pl-pds\">'</span>rgba(52,152,219,0.20)<span class=\"pl-pds\">'</span></span>,</span>\n<span class=\"pl-s1\">\t];</span>\n<span class=\"pl-s1\"></span>&lt;/<span class=\"pl-ent\">script</span>&gt;\n\n&lt;<span class=\"pl-ent\">div</span> <span class=\"pl-e\">class</span>=<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>fixed inset-0 -z-10 pointer-events-none<span class=\"pl-pds\">\"</span></span> <span class=\"pl-e\">aria-hidden</span>=<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>true<span class=\"pl-pds\">\"</span></span>&gt;\n\t&lt;<span class=\"pl-ent\">TinyVectors</span>\n\t\t<span class=\"pl-e\">theme</span>=<span class=\"pl-s\"><span class=\"pl-pds\">\"</span>custom<span class=\"pl-pds\">\"</span></span>\n\t\t<span class=\"pl-e\">colors</span>={<span class=\"pl-smi\">colors</span>}\n\t\t<span class=\"pl-e\">blobCount</span>={<span class=\"pl-c1\">5</span>}\n\t\t<span class=\"pl-e\">opacity</span>={<span class=\"pl-c1\">0.4</span>}\n\t\t<span class=\"pl-e\">enableScrollPhysics</span>={<span class=\"pl-c1\">true</span>}\n\t\t<span class=\"pl-e\">enableDeviceMotion</span>={<span class=\"pl-c1\">false</span>}\n\t/&gt;\n&lt;/<span class=\"pl-ent\">div</span>&gt;</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Entry Points</h2><a id=\"user-content-entry-points\" class=\"anchor\" aria-label=\"Permalink: Entry Points\" href=\"#entry-points\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The package exports these public entry points:</p>\n<ul dir=\"auto\">\n<li><code>@tummycrypt/tinyvectors</code></li>\n<li><code>@tummycrypt/tinyvectors/core</code></li>\n<li><code>@tummycrypt/tinyvectors/motion</code></li>\n<li><code>@tummycrypt/tinyvectors/themes</code></li>\n<li><code>@tummycrypt/tinyvectors/themes/css</code></li>\n<li><code>@tummycrypt/tinyvectors/svelte</code></li>\n</ul>\n<p dir=\"auto\">Use the top-level entry point when you just want the packaged component surface. Reach for the lower-level paths if you are composing your own vector layer, motion handling, or theme primitives.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Local Development</h2><a id=\"user-content-local-development\" class=\"anchor\" aria-label=\"Permalink: Local Development\" href=\"#local-development\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm install\npnpm check\npnpm test\npnpm build\"><pre>pnpm install\npnpm check\npnpm <span class=\"pl-c1\">test</span>\npnpm build</pre></div>\n<p dir=\"auto\">Useful extra commands:</p>\n<ul dir=\"auto\">\n<li><code>pnpm dev</code> runs the local Vite demo app</li>\n<li><code>pnpm dev:watch</code> rebuilds the library on change</li>\n<li><code>pnpm test:pbt</code> runs the property-based invariants only</li>\n<li><code>pnpm check:release-metadata</code> verifies <code>package.json</code>, <code>BUILD.bazel</code>, and <code>MODULE.bazel</code> stay aligned</li>\n<li><code>pnpm check:package</code> runs <code>publint</code></li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Release Truth</h2><a id=\"user-content-release-truth\" class=\"anchor\" aria-label=\"Permalink: Release Truth\" href=\"#release-truth\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">The supported consumer path today is the published npm package.</p>\n<p dir=\"auto\">This repo also carries Bazel metadata because the broader package ecosystem around it uses Bazel package targets and registry generation. That standalone Bazel surface is being tightened up, but the release metadata in this repo is expected to stay aligned with the published package either way.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">This repository is distributed under the zlib/libpng license. See <a href=\"./LICENSE\">LICENSE</a>.</p>\n</article></div>",
      "readme_excerpt": "Animated vector blob backgrounds with lil physics for Svelte 5.\nThis package is what powers the moving background layer on transscendsurvival.org, but it is meant to be useful outside that site too. It ships a small set of Svelte components plus the lower-level motion, theme, and core utilities that drive them.\nbash\npnpm add @tummycrypt/tinyvectors\nPeer dependency:\n- svelte@^5\nsvelte\n<script lang=\"ts\">\n\timport { TinyVectors } from '@tummycrypt/tinyvectors';\n\tconst colors =...",
      "install_commands": {
        "npm": "npm install @tummycrypt/tinyvectors"
      },
      "repo_url": "https://github.com/tinyland-inc/tinyvectors",
      "website_url": "https://gitlab.com/tinyland/projects/tinyland.dev",
      "docs_url": null,
      "registry_url": "https://www.npmjs.com/package/@tummycrypt/tinyvectors",
      "releases_url": "https://github.com/tinyland-inc/tinyvectors/releases",
      "og_image_url": "https://opengraph.githubassets.com/701f2349c22384feec9b8223885e873d55251eb8b59f81f88ddd927495d73ddd/tinyland-inc/tinyvectors",
      "license": "Zlib",
      "pushed_at": "2026-04-26T03:01:46Z",
      "enriched_at": "2026-04-26T17:17:20.484Z",
      "gate": "override"
    },
    {
      "slug": "tinyland-inc-ci-templates",
      "name": "ci-templates",
      "repo": "tinyland-inc/ci-templates",
      "org": "tinyland-inc",
      "ecosystem": "multi",
      "category": "devex",
      "description": "Reusable GitHub Actions composite actions for Nix, Attic cache, and CI/CD",
      "featured": false,
      "tags": [
        "ci",
        "github-actions",
        "nix",
        "attic"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [],
      "languages": [],
      "primary_language": "",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">ci-templates</h1><a id=\"user-content-ci-templates\" class=\"anchor\" aria-label=\"Permalink: ci-templates\" href=\"#ci-templates\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Reusable GitHub Actions composite actions for tinyland-inc CI/CD.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Actions</h2><a id=\"user-content-actions\" class=\"anchor\" aria-label=\"Permalink: Actions\" href=\"#actions\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>nix-setup</code></h3><a id=\"user-content-nix-setup\" class=\"anchor\" aria-label=\"Permalink: nix-setup\" href=\"#nix-setup\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Configure Nix and cache endpoints. Auto-detects Attic and Bazel on self-hosted ARC runners via cluster DNS.</p>\n<div class=\"highlight highlight-source-yaml notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"- uses: tinyland-inc/ci-templates/.github/actions/nix-setup@main\n  with:\n    attic-cache: &quot;main&quot;  # optional, default: main\"><pre>- <span class=\"pl-ent\">uses</span>: <span class=\"pl-s\">tinyland-inc/ci-templates/.github/actions/nix-setup@main</span>\n  <span class=\"pl-ent\">with</span>:\n    <span class=\"pl-ent\">attic-cache</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>main<span class=\"pl-pds\">\"</span></span>  <span class=\"pl-c\"><span class=\"pl-c\">#</span> optional, default: main</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>nix-build</code></h3><a id=\"user-content-nix-build\" class=\"anchor\" aria-label=\"Permalink: nix-build\" href=\"#nix-build\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Run Nix build with Attic binary cache. Installs Nix, configures caches, runs command.</p>\n<div class=\"highlight highlight-source-yaml notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"- uses: tinyland-inc/ci-templates/.github/actions/nix-build@main\n  with:\n    command: &quot;nix build .#package&quot;\n    push-cache: &quot;true&quot;\n  env:\n    ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}\"><pre>- <span class=\"pl-ent\">uses</span>: <span class=\"pl-s\">tinyland-inc/ci-templates/.github/actions/nix-build@main</span>\n  <span class=\"pl-ent\">with</span>:\n    <span class=\"pl-ent\">command</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>nix build .#package<span class=\"pl-pds\">\"</span></span>\n    <span class=\"pl-ent\">push-cache</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>true<span class=\"pl-pds\">\"</span></span>\n  <span class=\"pl-ent\">env</span>:\n    <span class=\"pl-ent\">ATTIC_TOKEN</span>: <span class=\"pl-s\">${{ secrets.ATTIC_TOKEN }}</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>greedy-cache</code></h3><a id=\"user-content-greedy-cache\" class=\"anchor\" aria-label=\"Permalink: greedy-cache\" href=\"#greedy-cache\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Start Attic <code>watch-store</code> daemon for concurrent binary cache push. Derivations are pushed as they build, not after.</p>\n<div class=\"highlight highlight-source-yaml notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"- uses: tinyland-inc/ci-templates/.github/actions/greedy-cache@main\n  with:\n    attic-cache: &quot;tinyland-lab&quot;\n    watch-jobs: &quot;8&quot;\n  env:\n    ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}\n\n- run: nix build .#package  # derivations pushed concurrently as they build\"><pre>- <span class=\"pl-ent\">uses</span>: <span class=\"pl-s\">tinyland-inc/ci-templates/.github/actions/greedy-cache@main</span>\n  <span class=\"pl-ent\">with</span>:\n    <span class=\"pl-ent\">attic-cache</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>tinyland-lab<span class=\"pl-pds\">\"</span></span>\n    <span class=\"pl-ent\">watch-jobs</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>8<span class=\"pl-pds\">\"</span></span>\n  <span class=\"pl-ent\">env</span>:\n    <span class=\"pl-ent\">ATTIC_TOKEN</span>: <span class=\"pl-s\">${{ secrets.ATTIC_TOKEN }}</span>\n\n- <span class=\"pl-ent\">run</span>: <span class=\"pl-s\">nix build .#package  </span><span class=\"pl-c\"><span class=\"pl-c\">#</span> derivations pushed concurrently as they build</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>secrets-scan</code></h3><a id=\"user-content-secrets-scan\" class=\"anchor\" aria-label=\"Permalink: secrets-scan\" href=\"#secrets-scan\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">TruffleHog (verified secrets) + Gitleaks detection.</p>\n<div class=\"highlight highlight-source-yaml notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"- uses: actions/checkout@v4\n  with:\n    fetch-depth: 0\n- uses: tinyland-inc/ci-templates/.github/actions/secrets-scan@main\"><pre>- <span class=\"pl-ent\">uses</span>: <span class=\"pl-s\">actions/checkout@v4</span>\n  <span class=\"pl-ent\">with</span>:\n    <span class=\"pl-ent\">fetch-depth</span>: <span class=\"pl-c1\">0</span>\n- <span class=\"pl-ent\">uses</span>: <span class=\"pl-s\">tinyland-inc/ci-templates/.github/actions/secrets-scan@main</span></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Reusable Workflows</h2><a id=\"user-content-reusable-workflows\" class=\"anchor\" aria-label=\"Permalink: Reusable Workflows\" href=\"#reusable-workflows\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>js-bazel-package</code></h3><a id=\"user-content-js-bazel-package\" class=\"anchor\" aria-label=\"Permalink: js-bazel-package\" href=\"#js-bazel-package\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Reusable workflow for JS/TS packages whose release artifact is built by Bazel and then published to npm or GitHub Packages.</p>\n<p dir=\"auto\">Supports explicit runner policy (<code>compat</code>, <code>hosted</code>, <code>shared</code>, <code>repo_owned</code>), explicit workspace policy (<code>isolated</code>, <code>persistent_compat</code>), explicit publish policy (<code>same_runner</code>, <code>hosted_exception</code>), self-hosted cache contract wiring, optional advisory lint/typecheck lanes, Bazel-artifact dry-runs, and npm/GitHub Packages publication from the extracted Bazel package.</p>\n<p dir=\"auto\">Consumer-supplied validation commands and the explicit Bazel validation step\ninclude bounded retries for transient Bazel external archive fetch failures, so\npackage repos do not each vendor ad hoc GitHub release-download retry logic.</p>\n<p dir=\"auto\">See <a href=\"./docs/js-bazel-package.md\">docs/js-bazel-package.md</a> for usage and inputs.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h3 class=\"heading-element\" dir=\"auto\"><code>npm-publish</code></h3><a id=\"user-content-npm-publish\" class=\"anchor\" aria-label=\"Permalink: npm-publish\" href=\"#npm-publish\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Reusable workflow for straightforward Node package build, test, and publish\nflows that publish directly from the workspace tree.</p>\n<p dir=\"auto\">Current behavior:</p>\n<ul dir=\"auto\">\n<li>hosted-only on <code>ubuntu-latest</code></li>\n<li>build and advisory test on a Node version matrix</li>\n<li>publish to GitHub Packages and npmjs on tags</li>\n</ul>\n<p dir=\"auto\">See <a href=\"./docs/npm-publish.md\">docs/npm-publish.md</a> for usage and inputs.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Requirements</h2><a id=\"user-content-requirements\" class=\"anchor\" aria-label=\"Permalink: Requirements\" href=\"#requirements\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<ul dir=\"auto\">\n<li><strong>Self-hosted runners:</strong> Attic and Bazel cache auto-detected via cluster DNS</li>\n<li><strong>GitHub-hosted runners:</strong> Pass <code>attic-server</code> input explicitly</li>\n<li><strong>Secrets:</strong> <code>ATTIC_TOKEN</code> for cache push operations</li>\n</ul>\n</article></div>",
      "readme_excerpt": "Reusable GitHub Actions composite actions for tinyland-inc CI/CD.\nConfigure Nix and cache endpoints. Auto-detects Attic and Bazel on self-hosted ARC runners via cluster DNS.\nyaml\n- uses: tinyland-inc/ci-templates/.github/actions/nix-setup@main\n  with:\n    attic-cache: \"main\"  # optional, default: main\nRun Nix build with Attic binary cache. Installs Nix, configures caches, runs command.\nyaml\n- uses: tinyland-inc/ci-templates/.github/actions/nix-build@main\n  with:\n    command: \"nix build...",
      "install_commands": {},
      "repo_url": "https://github.com/tinyland-inc/ci-templates",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/tinyland-inc/ci-templates/releases",
      "og_image_url": "https://opengraph.githubassets.com/6aeb9a4376073612e1dcdf8ddfb5bf2dc99e228599c8addf2faa000644b65a34/tinyland-inc/ci-templates",
      "license": "",
      "pushed_at": "2026-04-26T00:03:26Z",
      "enriched_at": "2026-04-26T17:17:20.759Z",
      "gate": "override"
    },
    {
      "slug": "tinyland-inc-linear-gsuite",
      "name": "linear-gsuite",
      "repo": "tinyland-inc/linear-gsuite",
      "org": "tinyland-inc",
      "ecosystem": "npm",
      "category": "integrations",
      "description": "minimal Linear and Google Workspace automation surface for calenderable actions",
      "featured": false,
      "tags": [
        "linear",
        "gsuite",
        "calendar",
        "automation"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [
        "bazel-module",
        "gcal",
        "gsuite",
        "linear-app",
        "calendar"
      ],
      "languages": [
        {
          "name": "TypeScript",
          "color": "#3178c6",
          "bytes": 107405
        },
        {
          "name": "JavaScript",
          "color": "#f1e05a",
          "bytes": 10267
        },
        {
          "name": "Nix",
          "color": "#7e7eff",
          "bytes": 9800
        },
        {
          "name": "Just",
          "color": "#384d54",
          "bytes": 3435
        },
        {
          "name": "Starlark",
          "color": "#76d275",
          "bytes": 2689
        }
      ],
      "primary_language": "TypeScript",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">linear-gsuite</h1><a id=\"user-content-linear-gsuite\" class=\"anchor\" aria-label=\"Permalink: linear-gsuite\" href=\"#linear-gsuite\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Calendar automation engine that syncs <a href=\"https://linear.app\" rel=\"nofollow\">Linear</a> issues and\nrecurring events into Google Calendar.</p>\n<ul dir=\"auto\">\n<li>Syncs Linear issues with due dates as all-day calendar events</li>\n<li>Syncs recurring operational events from JSON definitions</li>\n<li>Automatically removes calendar events when issues are completed or canceled</li>\n<li>Runs as a macOS <code>launchd</code> background agent or one-shot CLI</li>\n<li>Declarative setup via Home Manager (NixOS / nix-darwin)</li>\n</ul>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Quick start</h2><a id=\"user-content-quick-start\" class=\"anchor\" aria-label=\"Permalink: Quick start\" href=\"#quick-start\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# Install\npnpm install &amp;&amp; pnpm build\n\n# Authenticate with Google Calendar\nlinear-gsuite auth login --client-secrets-file ~/Downloads/client_secret_*.json\n\n# Create a manifest (or use an example)\nlinear-gsuite doctor --config examples/tinyland-business-ops/linear-gsuite.package.json\n\n# Sync\nlinear-gsuite calendar sync --config examples/tinyland-business-ops/linear-gsuite.package.json\"><pre><span class=\"pl-c\"><span class=\"pl-c\">#</span> Install</span>\npnpm install <span class=\"pl-k\">&amp;&amp;</span> pnpm build\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Authenticate with Google Calendar</span>\nlinear-gsuite auth login --client-secrets-file <span class=\"pl-k\">~</span>/Downloads/client_secret_<span class=\"pl-k\">*</span>.json\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Create a manifest (or use an example)</span>\nlinear-gsuite doctor --config examples/tinyland-business-ops/linear-gsuite.package.json\n\n<span class=\"pl-c\"><span class=\"pl-c\">#</span> Sync</span>\nlinear-gsuite calendar sync --config examples/tinyland-business-ops/linear-gsuite.package.json</pre></div>\n<p dir=\"auto\">Or via nix:</p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"nix run github:tinyland-inc/linear-gsuite -- version\"><pre>nix run github:tinyland-inc/linear-gsuite -- version</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Documentation</h2><a id=\"user-content-documentation\" class=\"anchor\" aria-label=\"Permalink: Documentation\" href=\"#documentation\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Guide</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><a href=\"docs/guides/getting-started.md\">Getting Started</a></td>\n<td>Install, authenticate, first sync</td>\n</tr>\n<tr>\n<td><a href=\"docs/guides/authentication.md\">Authentication</a></td>\n<td>OAuth desktop flow, service accounts, <code>_FILE</code> secret pattern</td>\n</tr>\n<tr>\n<td><a href=\"docs/guides/configuration.md\">Configuration</a></td>\n<td>Manifest schema, source types, identity keys</td>\n</tr>\n<tr>\n<td><a href=\"docs/guides/cli-reference.md\">CLI Reference</a></td>\n<td>All commands, flags, output format</td>\n</tr>\n<tr>\n<td><a href=\"docs/guides/home-manager.md\">Home Manager</a></td>\n<td>Declarative NixOS/nix-darwin setup</td>\n</tr>\n<tr>\n<td><a href=\"docs/guides/extending.md\">Extending</a></td>\n<td>Adding new source adapters</td>\n</tr>\n<tr>\n<td><a href=\"docs/guides/troubleshooting.md\">Troubleshooting</a></td>\n<td>Common issues and diagnostics</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Source types</h2><a id=\"user-content-source-types\" class=\"anchor\" aria-label=\"Permalink: Source types\" href=\"#source-types\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>json-recurring-events</code></td>\n<td>Static recurring events from a JSON file</td>\n</tr>\n<tr>\n<td><code>linear-issues</code></td>\n<td>Linear issues with due dates, auto-removed on completion</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Install surfaces</h2><a id=\"user-content-install-surfaces\" class=\"anchor\" aria-label=\"Permalink: Install surfaces\" href=\"#install-surfaces\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<markdown-accessiblity-table><table>\n<thead>\n<tr>\n<th>Method</th>\n<th>Command</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>pnpm</strong></td>\n<td><code>pnpm install &amp;&amp; pnpm build &amp;&amp; pnpm link --global</code></td>\n</tr>\n<tr>\n<td><strong>nix run</strong></td>\n<td><code>nix run github:tinyland-inc/linear-gsuite -- &lt;args&gt;</code></td>\n</tr>\n<tr>\n<td><strong>nix develop</strong></td>\n<td><code>nix develop</code> for dev shell with all tooling</td>\n</tr>\n<tr>\n<td><strong>Home Manager</strong></td>\n<td>Import <code>homeManagerModules.default</code> (<a href=\"docs/guides/home-manager.md\">guide</a>)</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Architecture</h2><a id=\"user-content-architecture\" class=\"anchor\" aria-label=\"Permalink: Architecture\" href=\"#architecture\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">This repo is the <strong>engine</strong>. Consumer repos provide <strong>manifests</strong>.</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"Consumer repo                    linear-gsuite engine\n├── linear-gsuite.package.json   → Source resolution\n├── events.json                  → Google Calendar API (OAuth + service account)\n└── business data                → Reconciliation, dedup, launchd lifecycle\"><pre class=\"notranslate\"><code>Consumer repo                    linear-gsuite engine\n├── linear-gsuite.package.json   → Source resolution\n├── events.json                  → Google Calendar API (OAuth + service account)\n└── business data                → Reconciliation, dedup, launchd lifecycle\n</code></pre></div>\n<p dir=\"auto\">The engine is source-agnostic: it syncs any <code>ResolvedCalendarEvent[]</code>\nregardless of origin. New source adapters only need to produce that type.\nSee the <a href=\"docs/guides/extending.md\">Extending guide</a>.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Example manifest</h2><a id=\"user-content-example-manifest\" class=\"anchor\" aria-label=\"Permalink: Example manifest\" href=\"#example-manifest\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-json notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"{\n  &quot;name&quot;: &quot;my-calendar&quot;,\n  &quot;timezone&quot;: &quot;America/New_York&quot;,\n  &quot;sources&quot;: [\n    {\n      &quot;id&quot;: &quot;recurring&quot;,\n      &quot;type&quot;: &quot;json-recurring-events&quot;,\n      &quot;path&quot;: &quot;./events.json&quot;\n    },\n    {\n      &quot;id&quot;: &quot;linear-due-soon&quot;,\n      &quot;type&quot;: &quot;linear-issues&quot;,\n      &quot;apiKeyEnv&quot;: &quot;LINEAR_API_KEY&quot;,\n      &quot;assignee&quot;: &quot;me&quot;,\n      &quot;teamKey&quot;: &quot;ENG&quot;,\n      &quot;dueWithinDays&quot;: 7\n    }\n  ]\n}\"><pre>{\n  <span class=\"pl-ent\">\"name\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>my-calendar<span class=\"pl-pds\">\"</span></span>,\n  <span class=\"pl-ent\">\"timezone\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>America/New_York<span class=\"pl-pds\">\"</span></span>,\n  <span class=\"pl-ent\">\"sources\"</span>: [\n    {\n      <span class=\"pl-ent\">\"id\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>recurring<span class=\"pl-pds\">\"</span></span>,\n      <span class=\"pl-ent\">\"type\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>json-recurring-events<span class=\"pl-pds\">\"</span></span>,\n      <span class=\"pl-ent\">\"path\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>./events.json<span class=\"pl-pds\">\"</span></span>\n    },\n    {\n      <span class=\"pl-ent\">\"id\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>linear-due-soon<span class=\"pl-pds\">\"</span></span>,\n      <span class=\"pl-ent\">\"type\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>linear-issues<span class=\"pl-pds\">\"</span></span>,\n      <span class=\"pl-ent\">\"apiKeyEnv\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>LINEAR_API_KEY<span class=\"pl-pds\">\"</span></span>,\n      <span class=\"pl-ent\">\"assignee\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>me<span class=\"pl-pds\">\"</span></span>,\n      <span class=\"pl-ent\">\"teamKey\"</span>: <span class=\"pl-s\"><span class=\"pl-pds\">\"</span>ENG<span class=\"pl-pds\">\"</span></span>,\n      <span class=\"pl-ent\">\"dueWithinDays\"</span>: <span class=\"pl-c1\">7</span>\n    }\n  ]\n}</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Development</h2><a id=\"user-content-development\" class=\"anchor\" aria-label=\"Permalink: Development\" href=\"#development\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pnpm install\npnpm run typecheck   # type check\npnpm test            # vitest (13 tests)\npnpm run build       # compile to dist/\njust status          # toolchain overview\"><pre>pnpm install\npnpm run typecheck   <span class=\"pl-c\"><span class=\"pl-c\">#</span> type check</span>\npnpm <span class=\"pl-c1\">test</span>            <span class=\"pl-c\"><span class=\"pl-c\">#</span> vitest (13 tests)</span>\npnpm run build       <span class=\"pl-c\"><span class=\"pl-c\">#</span> compile to dist/</span>\njust status          <span class=\"pl-c\"><span class=\"pl-c\">#</span> toolchain overview</span></pre></div>\n<p dir=\"auto\">CI runs typecheck, test, build, and publint on every PR.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">License</h2><a id=\"user-content-license\" class=\"anchor\" aria-label=\"Permalink: License\" href=\"#license\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">MIT</p>\n</article></div>",
      "readme_excerpt": "Calendar automation engine that syncs Linear issues and\nrecurring events into Google Calendar.\n- Syncs Linear issues with due dates as all-day calendar events\n- Syncs recurring operational events from JSON definitions\n- Automatically removes calendar events when issues are completed or canceled\n- Runs as a macOS launchd background agent or one-shot CLI\n- Declarative setup via Home Manager (NixOS / nix-darwin)\nbash\npnpm install && pnpm build\nlinear-gsuite auth login --client-secrets-file...",
      "install_commands": {
        "npm": "npm install @tummycrypt/linear-gsuite"
      },
      "repo_url": "https://github.com/tinyland-inc/linear-gsuite",
      "website_url": null,
      "docs_url": null,
      "registry_url": "https://www.npmjs.com/package/@tummycrypt/linear-gsuite",
      "releases_url": "https://github.com/tinyland-inc/linear-gsuite/releases",
      "og_image_url": "https://opengraph.githubassets.com/adb3049fa61548672047c9aee96b6e66df2ac76385cb004b2748021adab14a7b/tinyland-inc/linear-gsuite",
      "license": "MIT",
      "pushed_at": "2026-04-25T15:02:27Z",
      "enriched_at": "2026-04-26T17:17:21.075Z",
      "gate": "override"
    },
    {
      "slug": "tinyland-inc-bazel-registry",
      "name": "bazel-registry",
      "repo": "tinyland-inc/bazel-registry",
      "org": "tinyland-inc",
      "ecosystem": "multi",
      "category": "devex",
      "description": "Private Bazel Central Registry for @tummycrypt packages",
      "featured": false,
      "tags": [
        "bazel",
        "registry",
        "monorepo"
      ],
      "version": "0.0.0",
      "release_date": "",
      "releases": [],
      "stars": 0,
      "topics": [],
      "languages": [
        {
          "name": "Starlark",
          "color": "#76d275",
          "bytes": 36333
        }
      ],
      "primary_language": "Starlark",
      "readme_html": "<div id=\"readme\" class=\"md\" data-path=\"README.md\"><article class=\"markdown-body entry-content container-lg\" itemprop=\"text\"><div class=\"markdown-heading\" dir=\"auto\"><h1 class=\"heading-element\" dir=\"auto\">tinyland Bazel Registry</h1><a id=\"user-content-tinyland-bazel-registry\" class=\"anchor\" aria-label=\"Permalink: tinyland Bazel Registry\" href=\"#tinyland-bazel-registry\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Private Bazel Central Registry for <code>@tummycrypt/*</code> packages.</p>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Usage</h2><a id=\"user-content-usage\" class=\"anchor\" aria-label=\"Permalink: Usage\" href=\"#usage\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Add to your <code>.bazelrc</code>:</p>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"common --registry=https://raw.githubusercontent.com/tinyland-inc/bazel-registry/main/\ncommon --registry=https://bcr.bazel.build/\"><pre class=\"notranslate\"><code>common --registry=https://raw.githubusercontent.com/tinyland-inc/bazel-registry/main/\ncommon --registry=https://bcr.bazel.build/\n</code></pre></div>\n<p dir=\"auto\">Then in your <code>MODULE.bazel</code>:</p>\n<div class=\"highlight highlight-source-python notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"bazel_dep(name = &quot;tummycrypt_tinyland_auth&quot;, version = &quot;0.1.0&quot;)\"><pre><span class=\"pl-en\">bazel_dep</span>(<span class=\"pl-s1\">name</span> <span class=\"pl-c1\">=</span> <span class=\"pl-s\">\"tummycrypt_tinyland_auth\"</span>, <span class=\"pl-s1\">version</span> <span class=\"pl-c1\">=</span> <span class=\"pl-s\">\"0.1.0\"</span>)</pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Structure</h2><a id=\"user-content-structure\" class=\"anchor\" aria-label=\"Permalink: Structure\" href=\"#structure\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<div class=\"snippet-clipboard-content notranslate position-relative overflow-auto\" data-snippet-clipboard-copy-content=\"bazel_registry.json    # Registry manifest\nmodules/\n  tummycrypt_*/        # One directory per module\n    metadata.json      # Homepage, maintainers, versions\n    0.1.0/\n      MODULE.bazel     # Module definition + deps\n      source.json      # Archive URL + integrity hash\"><pre class=\"notranslate\"><code>bazel_registry.json    # Registry manifest\nmodules/\n  tummycrypt_*/        # One directory per module\n    metadata.json      # Homepage, maintainers, versions\n    0.1.0/\n      MODULE.bazel     # Module definition + deps\n      source.json      # Archive URL + integrity hash\n</code></pre></div>\n<div class=\"markdown-heading\" dir=\"auto\"><h2 class=\"heading-element\" dir=\"auto\">Source</h2><a id=\"user-content-source\" class=\"anchor\" aria-label=\"Permalink: Source\" href=\"#source\"><svg class=\"octicon octicon-link\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"></path></svg></a></div>\n<p dir=\"auto\">Generated from <a href=\"https://github.com/tinyland-inc/tinyland.dev\">tinyland.dev</a> monorepo <code>bcr/</code> directory.</p>\n</article></div>",
      "readme_excerpt": "Private Bazel Central Registry for @tummycrypt/ packages.\nAdd to your .bazelrc:\ncommon --registry=https://raw.githubusercontent.com/tinyland-inc/bazel-registry/main/\ncommon --registry=https://bcr.bazel.build/\nThen in your MODULE.bazel:\nstarlark\nbazeldep(name = \"tummycrypttinylandauth\", version = \"0.1.0\")\nbazelregistry.json    # Registry manifest\nmodules/\n  tummycrypt/        # One directory per module\n    metadata.json      # Homepage, maintainers, versions\n    0.1.0/\n      MODULE.bazel     #...",
      "install_commands": {},
      "repo_url": "https://github.com/tinyland-inc/bazel-registry",
      "website_url": null,
      "docs_url": null,
      "registry_url": null,
      "releases_url": "https://github.com/tinyland-inc/bazel-registry/releases",
      "og_image_url": "https://opengraph.githubassets.com/eca4d1ebffbe8b3260e2f31edc48d52f89ac3870503910beb34fea9efe171a95/tinyland-inc/bazel-registry",
      "license": "",
      "pushed_at": "2026-04-16T18:52:36Z",
      "enriched_at": "2026-04-26T17:17:21.361Z",
      "gate": "override"
    }
  ]
}