{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreidlkyywarggxmve6ugri54mwgdwl25byg52fln7zzuw3ytnu7y2wa",
    "commit": {
      "cid": "bafyreib7ucs7ei73cdcjtshs6zifejez5uy6a26hwel3gsustdqpbwbo6u",
      "rev": "3mpspq44ufy24"
    },
    "uri": "at://did:plc:2cxgdrgtsmrbqnjkwyplmp43/app.bsky.feed.post/3mpspq42kx22j",
    "validationStatus": "valid"
  },
  "content": {
    "$type": "pub.leaflet.content",
    "pages": [
      {
        "$type": "pub.leaflet.pages.linearDocument",
        "blocks": [
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "I made a Ghost blog export to offprint script that I call \"GhostOff\" and it's available on Tangled:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.website",
              "description": "Migrating content using the Ghost API to Offprint / Standard Site on atproto",
              "previewImage": {
                "$type": "blob",
                "ref": {
                  "$link": "bafkreiedesyhe7bvugri5fbyvfiidwbd3vcecf7kzeeo2sha32vwfgsh6e"
                },
                "mimeType": "image/png",
                "size": 21784
              },
              "src": "https://tangled.org/bmann.ca/ghostoff",
              "title": "bmann.ca/ghostoff"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#footnote",
                      "contentFacets": [
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://bmannconsulting.com/blog/2026/06/28/bmc-next-agentic-coding/"
                            }
                          ],
                          "index": {
                            "byteEnd": 154,
                            "byteStart": 109
                          }
                        }
                      ],
                      "contentPlaintext": "Literally my second thing, and the first one from scratch, from an empty directory. Some more about this and why I made the move now in a recent blog post.",
                      "footnoteId": "019f2ad5-44c3-7775-9106-ce174acc6563"
                    }
                  ],
                  "index": {
                    "byteEnd": 72,
                    "byteStart": 71
                  }
                }
              ],
              "plaintext": "This also happens to be one of my first experiments with agentic coding*."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#footnote",
                      "contentFacets": [
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://opencode.ai/"
                            }
                          ],
                          "index": {
                            "byteEnd": 24,
                            "byteStart": 16
                          }
                        },
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://umans.ai"
                            }
                          ],
                          "index": {
                            "byteEnd": 66,
                            "byteStart": 61
                          }
                        },
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://news.z-space.ca/zai/"
                            }
                          ],
                          "index": {
                            "byteEnd": 144,
                            "byteStart": 120
                          }
                        },
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://cocore.dev"
                            }
                          ],
                          "index": {
                            "byteEnd": 155,
                            "byteStart": 149
                          }
                        }
                      ],
                      "contentPlaintext": "Currently using OpenCode, and as mentioned in the blog post, Umans with open model GLM 5.2. I'll be doing more with our Z-Space Local AI cluster (or cocore!) soon.",
                      "footnoteId": "019f2ad6-6d94-7775-9108-638d16406209"
                    }
                  ],
                  "index": {
                    "byteEnd": 35,
                    "byteStart": 34
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ],
                  "index": {
                    "byteEnd": 86,
                    "byteStart": 82
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#link",
                      "uri": "https://tangled.org/mary.my.id/atcute"
                    }
                  ],
                  "index": {
                    "byteEnd": 161,
                    "byteStart": 155
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#didMention",
                      "did": "did:plc:ia76kvnndjutgedggx2ibrem"
                    }
                  ],
                  "index": {
                    "byteEnd": 184,
                    "byteStart": 173
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#link",
                      "uri": "https://lexicon.garden/help/mcp"
                    }
                  ],
                  "index": {
                    "byteEnd": 225,
                    "byteStart": 200
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#footnote",
                      "contentFacets": [
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#italic"
                            }
                          ],
                          "index": {
                            "byteEnd": 69,
                            "byteStart": 52
                          }
                        }
                      ],
                      "contentPlaintext": "Before I connected Lexicon Garden, it was making up app.test.whatever lexicons, so I interrupted it.",
                      "footnoteId": "019f2ad8-8bbf-7775-910b-26d6dbd00f0f"
                    }
                  ],
                  "index": {
                    "byteEnd": 226,
                    "byteStart": 225
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#didMention",
                      "did": "did:plc:pgjkomf37an4czloay5zeth6"
                    }
                  ],
                  "index": {
                    "byteEnd": 279,
                    "byteStart": 266
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#link",
                      "uri": "https://ghost.org/help/cards/"
                    }
                  ],
                  "index": {
                    "byteEnd": 389,
                    "byteStart": 378
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#footnote",
                      "contentFacets": [
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://tangled.org/bmann.ca/ghostoff/blob/main/docs/ghost-to-offprint-mapping.md"
                            }
                          ],
                          "index": {
                            "byteEnd": 63,
                            "byteStart": 11
                          }
                        }
                      ],
                      "contentPlaintext": "There's a table showing the mapping in the GhostOff docs folder.",
                      "footnoteId": "019f2ad2-a310-7775-9104-9f5612d22306"
                    }
                  ],
                  "index": {
                    "byteEnd": 437,
                    "byteStart": 436
                  }
                }
              ],
              "plaintext": "I spent an hour or so in plan mode*, explaining how it should work, detailing the .env file and credentials that would be needed, requiring the use of the atcute library by @mary.my.id, attaching the Lexicon Garden MCP server*, and preparing a test account and test @offprint.app publication. The Ghost Content API is pretty standardized. I walked through some of the things in Ghost Cards that might not have offprint block equivalents*."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#link",
                      "uri": "https://atprotocol.dev"
                    }
                  ],
                  "index": {
                    "byteEnd": 305,
                    "byteStart": 291
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#footnote",
                      "contentFacets": [
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://blog.atmosphere.community"
                            }
                          ],
                          "index": {
                            "byteEnd": 126,
                            "byteStart": 101
                          }
                        }
                      ],
                      "contentPlaintext": "Screenshot is of posts listed in Skyreader.app - will need to re-run this to make it into the actual Atmosphere Community Blog, which is also waiting on some Offprint atproto sync updates.",
                      "footnoteId": "019f2beb-1e79-7775-9112-7c8b4a02bfa0"
                    }
                  ],
                  "index": {
                    "byteEnd": 363,
                    "byteStart": 362
                  }
                }
              ],
              "plaintext": "And it worked pretty great first try in build mode. It built everything and then ran the script to test it with the credentials in the env file, downloaded the Ghost posts, and wrote them out as atproto native posts. It verified that the records validated. There are only 26 posts in on the atprotocol.dev Ghost blog, and it ported them all to Offprint! Amazing!*"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.image",
              "alt": "Screenshot of the ported posts in Skyreader",
              "aspectRatio": {
                "height": 972,
                "width": 1628
              },
              "image": {
                "$type": "blob",
                "ref": {
                  "$link": "bafkreibkgcjc7m5wrcs34y6e3ocalzqk3mwlala4xgofecjrakvercb22e"
                },
                "mimeType": "image/webp",
                "size": 104666
              }
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ],
                  "index": {
                    "byteEnd": 44,
                    "byteStart": 31
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ],
                  "index": {
                    "byteEnd": 90,
                    "byteStart": 70
                  }
                }
              ],
              "plaintext": "There was some confusion about standard.site wrappers then containing app.offprint.content containers and the block structure. At one point it decided to write everything as leaflet lexicon items instead. Yes, the standard site wrapper vs content types is confusing for everyone."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "I tweaked a few things and ran the script a couple of more times. I'm...done???"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 2,
              "plaintext": "OAuth for the CLI"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "One of the suggestions on what to do next was moving to OAuth. OAuth??? For a CLI tool???"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "This is in fact totally possible, and there are lots of very nice auth / CLI / website flows. But maybe tricky? I vaguely remembered that atproto needs 127.0.0.1 rather than just \"localhost\". Made a plan, told it to go ahead, asked it to write some docs about how to test it manually."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "And now we don't need an app password, and we don't need to include a PDS endpoint because it looks it up from the handle."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.page",
              "id": "019f2bf6-8992-7775-911d-fb01bbb3a6df"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#link",
                      "uri": "https://offprint.app"
                    }
                  ],
                  "index": {
                    "byteEnd": 80,
                    "byteStart": 72
                  }
                }
              ],
              "plaintext": "And that's it. It works and we can start mass exporting Ghost blogs  to Offprint: GhostOff indeed!"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 2,
              "plaintext": "What's Next"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ],
                  "index": {
                    "byteEnd": 221,
                    "byteStart": 212
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#didMention",
                      "did": "did:plc:revjuqmkvrw6fnkxppqtszpv"
                    }
                  ],
                  "index": {
                    "byteEnd": 365,
                    "byteStart": 355
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#didMention",
                      "did": "did:plc:btxrwcaeyodrap5mnjw2fvmz"
                    }
                  ],
                  "index": {
                    "byteEnd": 379,
                    "byteStart": 367
                  }
                }
              ],
              "plaintext": "Of course, a CLI script that no one knows about that has to be downloaded from a git forge doesn't solve much of anything in bring people to the Atmosphere. Even with the magic of OAuth, and maybe expanding that localhost trick to gather env variables - enter in your Ghost API key, select or create the publication you want to import to, add support for @pckt.blog  @leaflet.pub ."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "Something that is a desktop app would probably be more accessible. Yeah, it could be a website too."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "Substack is a much juicier target, at least for those that aren't actively running monetized newsletters."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "I have an Instagram export sitting around somewhere. And a really giant Flickr export…"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 2,
              "plaintext": "Networked, Personalized Futures"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "All of the above sounds like a lot of work. And people might send me PRs, or give me user feedback, or report bugs. Or advocate that I should add supporting their flavour of Markdown to my roadmap which if I build or accept a PR for, I am cursed to maintain forever in \"my\" codebase."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "Nah. I ain't doing all that. Just fork it and tinker with it on your own."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#didMention",
                      "did": "did:plc:htdgq2bysctfhnzstw7blmjj"
                    }
                  ],
                  "index": {
                    "byteEnd": 66,
                    "byteStart": 57
                  }
                }
              ],
              "plaintext": "But I actually think the future is way more amazing, and @v-it.org is a glimpse in that direction."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.blockquote",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#bold"
                    }
                  ],
                  "index": {
                    "byteEnd": 75,
                    "byteStart": 60
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#bold"
                    }
                  ],
                  "index": {
                    "byteEnd": 242,
                    "byteStart": 208
                  }
                }
              ],
              "plaintext": "a codebase is not a distribution artifact. a codebase is a living organism that can adapt to each install, and it deserves a living ecosystem.\n\nthe future is not “one repo, one roadmap.” the future is millions of personalized codebases—each one evolving continuously, shaped by its caretakers’s needs, values, constraints, and taste.\n\nvit is the mechanism for that future."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#link",
                      "uri": "https://v-it.org/doctrine/"
                    }
                  ],
                  "index": {
                    "byteEnd": 26,
                    "byteStart": 3
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#didMention",
                      "did": "did:plc:gnmc53xy3rhbyepipqv2b3rl"
                    }
                  ],
                  "index": {
                    "byteEnd": 74,
                    "byteStart": 62
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#footnote",
                      "contentFacets": [
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://bsky.app/profile/jeremie.com"
                            }
                          ],
                          "index": {
                            "byteEnd": 12,
                            "byteStart": 0
                          }
                        },
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://solpbc.org/"
                            }
                          ],
                          "index": {
                            "byteEnd": 108,
                            "byteStart": 101
                          }
                        },
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://localfirstconf.com"
                            }
                          ],
                          "index": {
                            "byteEnd": 142,
                            "byteStart": 126
                          }
                        }
                      ],
                      "contentPlaintext": "@jeremie.com is the creator of Jabber/XMPP and also happens to be a director of Bluesky PBC. More at sol pbc. And he'll be at Local-First Conf in Berlin with a few new items in his bag of tracks.",
                      "footnoteId": "019f2c1f-b671-7775-913f-9a6a060a7a63"
                    }
                  ],
                  "index": {
                    "byteEnd": 76,
                    "byteStart": 75
                  }
                }
              ],
              "plaintext": "Go read the whole doctrine. And yes, it's built on atproto by @jeremie.com.*"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "What do I think this means? Well, I've started using the phrase \"infinite forks\". And it scares me. I got my start in open source in Drupal. \"Don't hack core\". Forking core is bad and hard. We go farther when we collect in a repo and work on one codebase together."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "So we get our current picture. Not decentralized git, but centralized git forges where we collect issues and discussions and of course the little images of avatars that are people that sometimes come into the real world so you can hug them. And it's been good. It's been great."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "But only for coders. For Open Sourcerers. And forks are bad because software is hard."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#link",
                      "uri": "https://bmannconsulting.com/notes/networkedorgs/"
                    }
                  ],
                  "index": {
                    "byteEnd": 38,
                    "byteStart": 24
                  }
                }
              ],
              "plaintext": "I've been writing about Networked Orgs for 4 years. Vit feels a lot like this shape of things. Not a git forge single repo hub, with a couple of branches and forks as spokes, downstream/upstream. It is, as described by vit, searching your trusted network for capabilities you can integrate. Evolving constantly."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.image",
              "alt": "diagram of source code movement today, and with vit an interconnected network",
              "aspectRatio": {
                "height": 948,
                "width": 1728
              },
              "image": {
                "$type": "blob",
                "ref": {
                  "$link": "bafkreia3eukhpzqlv72n3cisje6zbh42hofzekmtq7w43ert3rwppzi75i"
                },
                "mimeType": "image/webp",
                "size": 56890
              }
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#footnote",
                      "contentPlaintext": "I got stuck because I'm using opencode and vit doesn't detect it. Next up contributing to the vit upstream, which is still on boring old github.",
                      "footnoteId": "019f2c4a-a727-7775-914d-0863e7e282a7"
                    }
                  ],
                  "index": {
                    "byteEnd": 148,
                    "byteStart": 147
                  }
                }
              ],
              "plaintext": "So, uh, I made a beacon for the ghostoff project and published some caps. I think this is important and we need more people to come try things out.*"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.code",
              "language": "shellsession",
              "plaintext": "bmann% vit vet ghost-content-export\n=== v̇it cap review ===\nReview this cap carefully before trusting it.\n\n  Ref:     ghost-content-export\n  Title:   Ghost Content API Export\n  Author:  did:plc:2cxgdrgtsmrbqnjkwyplmp43\n\n  Fetches public Ghost CMS posts via the Content API \n  and caches them locally for further processing.\n\n--- Text ---\nThis cap authenticates to a Ghost CMS instance \nusing a Content API key, paginates through published posts, \nand writes the raw post data to a local directory. \nIt handles pagination, rate limits, and featured image URLs \nso downstream steps can work with clean, cached JSON.\n---\n\nTo trust this cap, run:\n\n  vit vet ghost-content-export --trust"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#footnote",
                      "contentFacets": [
                        {
                          "features": [
                            {
                              "$type": "pub.leaflet.richtext.facet#link",
                              "uri": "https://bsky.app/profile/commonscomputer.com"
                            }
                          ],
                          "index": {
                            "byteEnd": 126,
                            "byteStart": 114
                          }
                        }
                      ],
                      "contentPlaintext": "Will it? This is, uh, highly alpha software on a distributed network with code hosted on a \"knot\" that lives on a NixOS minipc at my coworking space. Ping me if this does anything.",
                      "footnoteId": "019f2c55-ea59-7775-9151-2274b102b02b"
                    }
                  ],
                  "index": {
                    "byteEnd": 115,
                    "byteStart": 114
                  }
                }
              ],
              "plaintext": "I've got caps published to the network. If you want to add OAuth login for your CLI apps, this might work for you.*"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.image",
              "alt": "A screenshot of pdsls a vit cap record https://pdsls.dev/at://did:plc:2cxgdrgtsmrbqnjkwyplmp43/org.v-it.cap/3mpsohtszms2x#info\n\nref:\n\"atproto-oauth-cli\"\ntext:\n\"This cap integrates @atproto/oauth-client-node into a Node.js CLI. It starts an ephemeral loopback server on 127.0.0.1 for the OAuth callback, stores session state in a local file with restricted permissions, restores the session on later runs, and uses DPoP-signed request handlers to read from and write to the user's AT Protocol PDS.\"\n$type:\n\"org.v-it.cap\"\ntitle:\n\"ATProto OAuth for CLI Apps\"\nbeacon:\n\"vit:knot.commonscomputer.com//did:plc:mfquhie7kthb4ig453glwgdk\"\ncreatedAt:\n\"2026-07-04T08:48:40.093Z\"\ndescription:\n\"Adds OAuth login with a local loopback callback to Node.js CLI tools using AT Protocol.\" ",
              "aspectRatio": {
                "height": 546,
                "width": 1578
              },
              "image": {
                "$type": "blob",
                "ref": {
                  "$link": "bafkreih7njjuqhtuc6plszryng6gsk5nr3t7y2h2a6w2hzixtimozo674m"
                },
                "mimeType": "image/webp",
                "size": 79874
              }
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "Infinite forks. Hug your people. We can get through this together. Let's get busy internecting."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": ""
            }
          }
        ],
        "id": "019f28ad-613b-7777-98be-8bce2975bc94"
      },
      {
        "$type": "pub.leaflet.pages.linearDocument",
        "blocks": [
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.header",
              "level": 1,
              "plaintext": "GhostOff OAuth for CLI Flow"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ],
                  "index": {
                    "byteEnd": 57,
                    "byteStart": 46
                  }
                },
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#code"
                    }
                  ],
                  "index": {
                    "byteEnd": 133,
                    "byteStart": 129
                  }
                }
              ],
              "plaintext": "Just a quick couple of screenshots with URLs. npm run dev to start the flow, you're going to need a couple of things filled into .env."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "Depending on your settings, it will pretty much just pop open a browser to the login page of your PDS:"
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.image",
              "alt": "Screenshot of the selfhosted.social login page, with the atprotocol.dev account. Yes, I should make a different throwaway account, because I'm going to hand this one to Brittany",
              "aspectRatio": {
                "height": 1210,
                "width": 1588
              },
              "image": {
                "$type": "blob",
                "ref": {
                  "$link": "bafkreicn5uczhz5wg6uvgs2iuuxuw6jafwnjce4mh4glfdb4ydjwws2vyi"
                },
                "mimeType": "image/webp",
                "size": 62346
              }
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "facets": [
                {
                  "features": [
                    {
                      "$type": "pub.leaflet.richtext.facet#footnote",
                      "contentPlaintext": "And transition:generic is pretty much the same scope as handing a script an app password.",
                      "footnoteId": "019f2bfe-fcbd-7775-9124-3eeba2738708"
                    }
                  ],
                  "index": {
                    "byteEnd": 86,
                    "byteStart": 85
                  }
                }
              ],
              "plaintext": "Yes, it is very much using that old classic, transition:generic, because I told it to*. I will tackle OAuth Scopes at some point."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.image",
              "alt": "Screenshot of authorizing OAuth scopes, which IYKYK is transition:generic and will make everyone mad. But is in fact the same set of permissions as app passwords.",
              "aspectRatio": {
                "height": 968,
                "width": 1830
              },
              "image": {
                "$type": "blob",
                "ref": {
                  "$link": "bafkreihg5bgz4twdltcfzg24w2j5mbqlmusjwmmo76suwmkedhz7v6xl4a"
                },
                "mimeType": "image/webp",
                "size": 94918
              }
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "Authentication successful. You may return to the natural environment of your terminal and watch log messages of atproto-ization being visited on your Ghost posts."
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.image",
              "aspectRatio": {
                "height": 260,
                "width": 1076
              },
              "image": {
                "$type": "blob",
                "ref": {
                  "$link": "bafkreif3z7mqwoou7xwb2bjvb5combl4fjchiq7lmzexqpgoipbbqx4nam"
                },
                "mimeType": "image/webp",
                "size": 18542
              }
            }
          },
          {
            "$type": "pub.leaflet.pages.linearDocument#block",
            "block": {
              "$type": "pub.leaflet.blocks.text",
              "plaintext": "I realized in running this again that… the session sticks around. So going to need to work on cleaning that up."
            }
          }
        ],
        "id": "019f2bf6-8992-7775-911d-fb01bbb3a6df"
      }
    ]
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreia3eukhpzqlv72n3cisje6zbh42hofzekmtq7w43ert3rwppzi75i"
    },
    "mimeType": "image/webp",
    "size": 56890
  },
  "description": "In which I agentic code Ghost to Offprint script and fall into the future of networked code sharing with Vit",
  "path": "/3mpsppyyr2s2d",
  "publishedAt": "2026-07-04T09:11:02.289Z",
  "site": "at://did:plc:2cxgdrgtsmrbqnjkwyplmp43/site.standard.publication/3lxsr74rnk22h",
  "tags": [
    "atproto",
    "vit",
    "offprint",
    "standard.site",
    "agentic development",
    "ghost-blog"
  ],
  "title": "Building GhostOff and our networked, personalized software futures"
}