diff --git a/CHANGELOG.md b/CHANGELOG.md index 77d008f0..9f569270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ Historical entries retain their original delivery coordinates. ## Unreleased +## 0.18.65 + +Make GhostGet's articles and comparisons calmer, clearer, and more consistent. + +- Rewrite the blog and essays around useful explanations, with accurate sources + and independent AI review records. +- Add a consistent SlopCamera illustration series with responsive images, + including the missing image for the web-access guide. +- Keep linked headings below navigation and settled analytics preferences in + the footer, clear of reading content. +- Remove private values embedded in analytics property names, including nested + records, before sending website events. + ## 0.18.64 Supported global CLI installs update to the latest stable release before an diff --git a/README.md b/README.md index f25bf4aa..289a2bb6 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Install [Bun 1.3.14](https://bun.sh/docs/installation) if needed, then install GhostGet and read a public page: ```sh -bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.64/hraness-ghostget-0.18.64.tgz +bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.65/hraness-ghostget-0.18.65.tgz ghostget read https://example.com ``` @@ -49,9 +49,9 @@ which always names the latest published release. Upgrading from Wrench? Read the The optional Agent Skill teaches your agent when and how to use GhostGet: ```sh -npx skills add hraness/ghostget#v0.18.64 +npx skills add hraness/ghostget#v0.18.65 # With Bun instead: -bunx skills add hraness/ghostget#v0.18.64 +bunx skills add hraness/ghostget#v0.18.65 ``` Start a new agent session, then ask: “Use GhostGet to read https://example.com @@ -183,7 +183,7 @@ firewall. ## Built-in provider catalog -This v0.18.64 source tree supports executable actions for 21 services: Beeper, +This v0.18.65 source tree supports executable actions for 21 services: Beeper, Bluesky, ClasificadosOnline, Facebook, Facebook Groups, Facebook Marketplace, GitHub, Gmail, Hacker News, Instagram, iMessage, LinkedIn, Reddit, Substack, Threads, TikTok, Twitch, WebMCP Registry, WhatsApp, X, and YouTube. LinkedIn @@ -267,7 +267,7 @@ For that same released coordinate, install GhostGet in an agent or application that owns its own model, planning, tool loop, approvals, and interface: ```sh -bun add https://github.com/hraness/ghostget/releases/download/v0.18.64/hraness-ghostget-0.18.64.tgz +bun add https://github.com/hraness/ghostget/releases/download/v0.18.65/hraness-ghostget-0.18.65.tgz ``` ```ts diff --git a/STYLE.md b/STYLE.md index 58e5f9bd..ad4b21ca 100644 --- a/STYLE.md +++ b/STYLE.md @@ -1,10 +1,10 @@ # Public writing style -This guide covers everything written for readers outside a repository: product pages, documentation, READMEs, interface text, metadata, and text a model writes for publication. Apply the voice rules in [`WRITING.md`](WRITING.md) first. The [documentation guidelines](https://github.com/hraness/.github/blob/main/DOCUMENTATION_GUIDELINES.md) choose a document's purpose and shape, and the [README guidelines](https://github.com/hraness/.github/blob/main/README_GUIDELINES.md) cover the repository front door. +This guide covers everything written for readers outside a repository: product pages, documentation, READMEs, interface text, metadata, and text a model writes for publication. Apply the voice rules in [`WRITING.md`](WRITING.md) first. The [documentation guidelines](DOCUMENTATION_GUIDELINES.md) choose a document's purpose and shape, and the [README guidelines](README_GUIDELINES.md) cover the repository front door. Public prose must be precise, useful, and free of hype. Use a direct, natural voice that reads well aloud. -This is a synced copy of the [Hraness public writing style](https://github.com/hraness/.github/blob/main/STYLE.md), kept here so agents can read it offline. Repository-specific rules appear under “Repository additions” below. +This is the canonical copy. Repositories keep a synced copy so agents can read it offline; rules that apply to one repository go under “Repository additions” at the end of that copy. ## Leave the reader with a clearer model @@ -120,11 +120,20 @@ Most Hraness copy is drafted by agents working inside repository guides full of - Use shared foreground and surface pairs for interactive controls. Control text and glyphs must reach at least 4.5:1 contrast in both themes, including selected, hover, and focus states. Never assume white is readable on a brand accent. - Review the page as a new visitor at desktop and phone widths. Confirm that the headline, example, and next action make sense before reading the documentation, and that essential limits appear beside the claims they qualify. +## Write evergreen explanations + +- Give an educational article one useful question, a clear answer, and a concrete example the reader can reason through or apply. Explain the cause, choice, or tradeoff; a feature list with an introduction is not an explanation. +- Write about the enduring idea and the public behavior a reader can use. Keep implementation diaries, internal file paths, test totals, task records, deployment history, and plans out of the article body. Link to public reference material when exact syntax or architecture helps the reader go further. +- Keep release chronology in release notes and current setup requirements beside the relevant command. Do not date an evergreen explanation with “as of”, “recently”, a build number, or the author's review date. Preserve real publication and review dates in their metadata; include a historical date in prose only when the event's timing is part of the explanation. +- State claims at their supported scope. Remove repeated permissions language, generic disclaimers, and defensive lists of things the article does not establish. Keep a material limitation beside the decision it changes, and link to detailed policy or reference where needed. +- Check product names, destination links, public behavior, and image credits together. Replace a stale explanation rather than adding a caveat about its age. Never relabel historical evidence or a tool-generated asset as if it came from a different source. +- Improve the existing collection before adding articles. Add a post for a distinct reader question that existing pages do not answer, with its own example and sources. Do not create thin variations to fill a series or repeat the product's pitch. + ## State each limit once Readers trust a page that states its limits plainly. They skim a page that repeats them. -- State the product's status once, near the top, with one of these labels: *In development*, *Preview*, *Beta*, *Latest release: vX.Y.Z*, *Paused*, or *Retired*. Follow it with one sentence on how to install or use it today, such as “Install from source; there is no signed release yet.” +- State release status where it changes how someone can use the product. Keep the current version with installation instructions and explain an unavailable capability beside the affected action. Do not add a release label or status paragraph to every educational page. - Put each other limit beside the feature it limits, once. Link to the status or limits page instead of restating the caveat in each section. Never drop a true limit to make the copy read better. - Write a claim at its true scope instead of following it with what it does not prove. “Tests cover local networks only” replaces “These are tested local cases, not hosted private networking or evidence about independent devices.” - State a privacy or scope rule once, positively (“Only documents you choose to publish become public”), and keep the full list of exclusions on the privacy or security page. @@ -164,7 +173,7 @@ Readers trust a page that states its limits plainly. They skim a page that repea - Put literal input and interface values in `code`. - Use an ellipsis glyph (`…`) only when an action opens another input step. - Do not use em dashes in authored text: prose, titles, meta descriptions, social text, alt text, captions, image credits, list separators, and the templates that generate them. Rewrite the sentence instead of substituting a spaced hyphen. Quoted third-party titles keep their own punctuation. Use parentheses only for a short, necessary explanation. -- Use each product's prose name exactly as its messaging record spells it (`names.name`), including case (xcb, Textbutler, AI Charts, Soundfish, Sys1). The all-capitals `names.catalog` form belongs only in designs that set every name in capitals. Do not use the repository slug or the domain as the name in prose, and do not use a product name as a common noun. +- Use each product's prose name exactly as its messaging record spells it (`names.name`), including case. The all-capitals `names.catalog` form belongs only in designs that set every name in capitals. Do not use the repository slug or the domain as the name in prose, and do not use a product name as a common noun. - Give each destination one label across the header, footer, breadcrumbs, and Markdown twins. - Make interpolated counts agree with their nouns (“1 check”, “2 checks”), and test zero, one, and several. - Spell out zero through nine in prose. Use numerals for 10 or more, measurements, dates, and money. @@ -177,6 +186,22 @@ Readers trust a page that states its limits plainly. They skim a page that repea - Preserve meaningful evidence captions, legal notices, and actual feature limits. State each once beside the claim it qualifies. - Credit tools and models by their current names. +## Keep article illustrations coherent + +- Give a product's article collection one authored visual direction: consistent drawing technique, line weight, texture, framing, and degree of abstraction. Start with a reviewed image from the collection and the product's brand palette. +- Use a quiet neutral ground, one dominant brand color, and at most one supporting accent. Keep the accent subordinate and use tonal variations for depth. Avoid unrelated bright colors, glossy stock-art treatments, and a different style for each topic. +- Generate editorial artwork through [SlopCamera](https://slopcamera.com). Retain its authored prompt, reference assets, and generation record with the source. Inspect the image at article and card sizes in both page themes before accepting it. +- Make each illustration explain the article's central idea through one clear visual relationship. Avoid generic technology collages, decorative interface fragments, and text baked into images. A diagram may use labels when those labels carry the explanation. +- Give every illustrated article a complete, intentional card and lead-image treatment. Preserve the actual generation history when replacing an old asset; credit the tool that made the new image and link its current public site. + +## Write social text + +- Posts on X, Bluesky, Threads, and LinkedIn follow every rule here, including no em dashes and no exclamation marks. +- Use no emoji, hashtags, or thread numbering unless a repository addition allows them for one channel. +- Keep image alt text to 125 characters or fewer. +- Link the canonical URL without tracking parameters. +- Launch posts follow the launch beats and social posts addendum in [`GENERATION_STYLE.md`](GENERATION_STYLE.md), and channel limits are in [`MESSAGING.md`](MESSAGING.md). + ## Write focused documentation - Decide whether a page is a tutorial, how-to guide, explanation, or reference. @@ -196,7 +221,7 @@ Readers trust a page that states its limits plainly. They skim a page that repea - Use one literal heading for the object, task, data view, or state. - Add supporting text only for a distinct instruction, constraint, status, or scope. - Name the action, object, current state, limit, or recovery step. -- Do not narrate the interface or repeat visible information. +- Do not narrate the interface or repeat visible information. Omit labels announcing how many cards are on screen or describing an obvious preview; retain counts only when they help navigation, selection, or comparison. - Keep normal readiness silent. Show status text for pending work, important results, or problems that the reader can fix. - Add search only when the collection is too large or varied for direct selection. - Move secondary actions and settings out of persistent primary controls. @@ -211,7 +236,7 @@ Readers trust a page that states its limits plainly. They skim a page that repea - Name the consequence in a confirmation. Repeat the exact verb and object for a destructive action. - Use nouns for labels. Use placeholders for a format or example, not a repeated label. - State the completed result in past tense in a toast notification. -- Follow [`CLI_MENU_STYLE.md`](https://github.com/hraness/.github/blob/main/CLI_MENU_STYLE.md) for command-line output, menu bar menus, and macOS permission notices. +- Follow [`CLI_MENU_STYLE.md`](https://github.com/hraness/.github/blob/main/CLI_MENU_STYLE.md) for command-line output, the shared status and control commands, and macOS permission notices. ## Vary a generated series @@ -236,7 +261,7 @@ A prompt, skill, or template that makes a model write published text is public c - Read a sample of real outputs after every prompt change. - Tell the model who reads the output and that the reader has not seen the inputs or the instructions. Name every field that is published, including rationales and labels. - Set length limits as maximums. A minimum longer than the evidence forces padding. -- Include the shared generation block from [`GENERATION_STYLE.md`](https://github.com/hraness/.github/blob/main/GENERATION_STYLE.md) and record its version with the prompt version. +- Include the shared generation block from [`GENERATION_STYLE.md`](GENERATION_STYLE.md) and record its version with the prompt version. - Check the prompt, skill, and examples for the patterns they forbid; a prompt that uses em dashes and staged contrasts produces them. ## Keep tests and guides from freezing copy @@ -248,7 +273,7 @@ A prompt, skill, or template that makes a model write published text is public c ## Say who wrote and who checked -- Show AI-drafting disclosure on hraness.com through its shared disclosure component, on every page with AI-drafted text. Essays and blog posts on any Hraness site also show the provenance note from [`GENERATION_STYLE.md`](https://github.com/hraness/.github/blob/main/GENERATION_STYLE.md), naming the recorded reviewer. Other pages on other Hraness sites and products do not carry AI-drafting disclosures, labels, or badges. +- Show AI-drafting disclosure on hraness.com through its shared disclosure component, on every page with AI-drafted text. Essays and blog posts on any Hraness site also show the provenance note from [`GENERATION_STYLE.md`](GENERATION_STYLE.md), naming the recorded reviewer. Other pages on other Hraness sites and products do not carry AI-drafting disclosures, labels, or badges. - Everywhere, keep a record of who drafted and who reviewed generated or agent-drafted text: the author, an independent human, or an AI agent, by name. - Never credit AI-drafted text to a person as its sole author, never describe AI review as human review, and never claim a review that has no record. A page without a review record makes no review claim. - Text an agent posts from a person's account does not claim that person wrote AI-drafted work. @@ -258,5 +283,5 @@ A prompt, skill, or template that makes a model write published text is public c ### Generated pages and status notes - The WebMCP provider pages are generated from third-party registry data. Every interpolated count agrees with its noun (test zero, one, and several), third-party text is clipped at a word boundary and ends with “…”, and a page promises use (“Use X with your agent”) only when the registry lists a tool the agent can call. -- A review note states which Ghostget release a page describes. It does not say the page “was checked” unless a review record exists. +- Keep release versions and check dates in reference material and review metadata. Evergreen editorial pages omit publication, update, and review-date chrome. Cite dates in the prose only when the date affects the explanation. - `website/AGENTS.md` lists the internal words that most often reach ghostget.com and what to write instead. diff --git a/bun.lock b/bun.lock index e6683614..2d56de90 100644 --- a/bun.lock +++ b/bun.lock @@ -21,7 +21,7 @@ "typescript": "6.0.3", }, "devDependencies": { - "@hraness/design-kit": "github:hraness/design-kit#v0.35.0", + "@hraness/design-kit": "github:hraness/design-kit#v0.35.2", "@hraness/direct": "https://github.com/hraness/direct/releases/download/v0.7.21/hraness-direct-0.7.21.tgz", "@hraness/posthog": "https://github.com/hraness/posthog/releases/download/v0.3.9/hraness-posthog-0.3.9.tgz", "@hraness/site-footer": "https://github.com/hraness/site-footer/releases/download/v0.20.5/hraness-site-footer-0.20.5.tgz", @@ -131,7 +131,7 @@ "@hraness/cli-update": ["@hraness/cli-update@https://github.com/hraness/cli-update/releases/download/v0.1.0/hraness-cli-update-0.1.0.tgz", {}, "sha512-g0Bg0FYiyJqEJC9JGtAZsZ+r599c8iSNDd7ADi6weFCKBGuRXa2tOnn93+pTfLe1tcayXiLuAtDPp8meFHgKbg=="], - "@hraness/design-kit": ["@hraness/design-kit@github:hraness/design-kit#1f4aa08", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.16 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-1f4aa08", "sha512-3Xa04ZJGeSFcwWJ+ncG5opo2lp8op4dLqdzjVokoVe4Itd3YUHNtJbOk9wb5t9TF8EX1T7jWfWlJCkSTmejkPA=="], + "@hraness/design-kit": ["@hraness/design-kit@github:hraness/design-kit#229720e", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.16 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-229720e", "sha512-GTknQ8JDWEVucTlpy+mEpipgc4vVOanbwp0k9R6/w93KsKSXL/xdpuCoZBJh3XUbSFs5HdrIXI2s5IldmbPovQ=="], "@hraness/desktop-foundation": ["@hraness/desktop-foundation@https://github.com/hraness/desktop-foundation/releases/download/v0.9.0/hraness-desktop-foundation-0.9.0.tgz", { "bin": { "companion": "./dist/src/cli.js" } }, "sha512-+TlrHoM1wZ2rE4p3u1CIAz1+aGKPZhmiNOKjEKOYPZmTx8BFSyFLPeL06tupfX2WxcRzRZ3QwmOsyjlgSu/p1Q=="], diff --git a/dist/apple-photos-client.js b/dist/apple-photos-client.js index 285d306b..af69dfd3 100644 --- a/dist/apple-photos-client.js +++ b/dist/apple-photos-client.js @@ -1,7 +1,7 @@ // @bun import { GHOSTGET_VERSION -} from "./index-rayqxqrk.js"; +} from "./index-4wh7rchw.js"; import { canonicalJson, sha256 diff --git a/dist/beeper-client.js b/dist/beeper-client.js index 9ffb8b60..93519b30 100644 --- a/dist/beeper-client.js +++ b/dist/beeper-client.js @@ -4,7 +4,7 @@ import { } from "./index-26yq8q16.js"; import { GHOSTGET_VERSION -} from "./index-rayqxqrk.js"; +} from "./index-4wh7rchw.js"; import { canonicalJson, canonicalJsonSha256Matches, diff --git a/dist/index-rayqxqrk.js b/dist/index-4wh7rchw.js similarity index 62% rename from dist/index-rayqxqrk.js rename to dist/index-4wh7rchw.js index c96cb4a8..34940d51 100644 --- a/dist/index-rayqxqrk.js +++ b/dist/index-4wh7rchw.js @@ -1,5 +1,5 @@ // @bun // src/version.ts -var GHOSTGET_VERSION = "0.18.64"; +var GHOSTGET_VERSION = "0.18.65"; export { GHOSTGET_VERSION }; diff --git a/docs/contracts.md b/docs/contracts.md index 48bd1fa0..d8608e1d 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -128,7 +128,7 @@ matches some IDs lists the installed ones and exits 0. { "ok": true, "contract": "ghostget.contract-catalog.v1", - "ghostget": { "version": "0.18.64" }, + "ghostget": { "version": "0.18.65" }, "generatedAt": "2026-09-21T20:00:00.000Z", "vocabulary": { "risks": ["R1", "R2", "R3", "R4"], @@ -186,7 +186,7 @@ does not parse is an error (exit 3) and no catalog is read. { "ok": false, "contract": "ghostget.contract-check.v1", - "ghostget": { "version": "0.18.64" }, + "ghostget": { "version": "0.18.65" }, "plan": { "collectionKey": "hraness-social-profile-statistics", "reads": 15 }, "reads": [ { "index": 0, "accountKey": "x-hraness", "adapter": "x-web", "operation": "profiles.read", diff --git a/docs/publishing.md b/docs/publishing.md index 933824f0..02d37c91 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -243,12 +243,12 @@ delivery proceeds through a new source-qualified version. ## Install the canonical release -These commands require the matching published immutable v0.18.64 release. +These commands require the matching published immutable v0.18.65 release. For the CLI: ```sh -bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.64/hraness-ghostget-0.18.64.tgz +bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.65/hraness-ghostget-0.18.65.tgz ghostget --version ghostget doctor --json ``` diff --git a/kb/launch/social-kit.md b/kb/launch/social-kit.md index cf3c5a98..2579d125 100644 --- a/kb/launch/social-kit.md +++ b/kb/launch/social-kit.md @@ -57,7 +57,7 @@ The plan is for GhostGet to stay small. Your agent does the thinking, and GhostG Post 9 of 9, 192 characters ```text -GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.64. +GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.65. https://ghostget.com/blog/introducing-ghostget/ ``` @@ -115,7 +115,7 @@ The plan is for GhostGet to stay small. Your agent does the thinking, and GhostG Post 9 of 9, 192 characters ```text -GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.64. +GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.65. https://ghostget.com/blog/introducing-ghostget/ ``` @@ -173,7 +173,7 @@ The plan is for GhostGet to stay small. Your agent does the thinking, and GhostG Post 9 of 9, 192 characters ```text -GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.64. +GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.65. https://ghostget.com/blog/introducing-ghostget/ ``` @@ -197,7 +197,7 @@ GhostGet is for Claude Code, Codex, Cursor, and other agents that run commands o The plan is for GhostGet to stay small. Your agent does the thinking, and GhostGet runs only actions someone has reviewed. Each new service arrives as reviewed actions with their own previews. -GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.64. +GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.65. https://ghostget.com/blog/introducing-ghostget/ ``` @@ -224,8 +224,8 @@ Topics: Developer Tools, Artificial Intelligence, Open Source - Sometimes a post goes through but the answer gets lost on the way back. GhostGet writes down every send before it leaves and never sends it again on its own until it knows what happened. - Anything beyond a read starts as a preview that shows the service, the account, and exactly what will be sent. Your agent can prepare it. Nothing is sent until someone confirms that exact preview. - GhostGet is for Claude Code, Codex, Cursor, and other agents that run commands on your Mac or Linux machine and need to read the web and use the accounts you already have. -- GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.64. -- Latest release: v0.18.64. https://ghostget.com/blog/introducing-ghostget/ +- GhostGet is free, MIT licensed, and runs on macOS and Linux. The first step reads a public page and needs no account. Latest release: v0.18.65. +- Latest release: v0.18.65. https://ghostget.com/blog/introducing-ghostget/ ## Beats @@ -251,4 +251,4 @@ Topics: Developer Tools, Artificial Intelligence, Open Source - claimsTotal: 247. verification/claims.json, every claim - claimsEvidenced: 228. verification/claims.json, status evidenced - claimsConfigReadback: 15. verification/claims.json, layer configuration-readback -- status: Latest release: v0.18.64. package.json version +- status: Latest release: v0.18.65. package.json version diff --git a/package.json b/package.json index 27e3e5bc..8390b3d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hraness/ghostget", - "version": "0.18.64", + "version": "0.18.65", "description": "GhostGet gives your AI agent named web actions: read a page, archive one media item, or use a connected account, without credentials or a browser to steer.", "license": "MIT", "type": "module", @@ -710,7 +710,7 @@ "@hraness/cli-update": "https://github.com/hraness/cli-update/releases/download/v0.1.0/hraness-cli-update-0.1.0.tgz" }, "devDependencies": { - "@hraness/design-kit": "github:hraness/design-kit#v0.35.0", + "@hraness/design-kit": "github:hraness/design-kit#v0.35.2", "@hraness/direct": "https://github.com/hraness/direct/releases/download/v0.7.21/hraness-direct-0.7.21.tgz", "@hraness/posthog": "https://github.com/hraness/posthog/releases/download/v0.3.9/hraness-posthog-0.3.9.tgz", "@hraness/site-footer": "https://github.com/hraness/site-footer/releases/download/v0.20.5/hraness-site-footer-0.20.5.tgz", diff --git a/scripts/npm-release-workflow.test.ts b/scripts/npm-release-workflow.test.ts index 01e65a9f..2f285233 100644 --- a/scripts/npm-release-workflow.test.ts +++ b/scripts/npm-release-workflow.test.ts @@ -1220,7 +1220,7 @@ describe("npm publication contract", () => { (MAX_UNPACKED_BYTES + MAX_PACKED_ENTRIES * 1_023 + 1_024) / 512, ) * 512, ); - expect(MAX_PACKAGE_TAR_BYTES).toBe(24_695_296); + expect(MAX_PACKAGE_TAR_BYTES).toBe(24_695_808); expect(MAX_PACKAGE_TAR_BYTES % 512).toBe(0); expect(artifact).toContain("maxOutputLength: MAX_PACKAGE_TAR_BYTES"); expect(artifact).not.toContain("const maximumTarBytes"); @@ -1430,8 +1430,8 @@ describe("npm publication contract", () => { expect(budget).toContain("785b8fa60c329d7ac46bc8fcf4d959b5fa9d96455bba9e7cdea63f6a3827c4f6"); expect(Object.isFrozen(repairPackageMeasurement)).toBeTrue(); expect(repairPackageMeasurement).toMatchObject({ - archiveSha256: "99bdbc86d7e2016b7a003496d4d6a2bec847d6c8c3b01aed57c1b55a3b25cbb3", - packedBytes: 12_154_912, unpackedBytes: 24_054_352, entryCount: 625, + archiveSha256: "09e69ff231a08b63c399662cdcb0fe9a08c5f5faa563e5e855e2c6b832e8003f", + packedBytes: 12_155_065, unpackedBytes: 24_054_930, entryCount: 625, packedPlatformProjection: 12_387, packedPortabilityAllowance: 4_096, payloadPlatformProjection: 353, payloadAllowance: 65, }); @@ -1447,8 +1447,8 @@ describe("npm publication contract", () => { expect(budget).toContain("24,024,705 + 353 + 65 = 24,025,123 unpacked"); expect(budget).toContain("12,152,562 + 12,387 + 4,096 = 12,169,045 packed"); expect(budget).toContain("24,093,786 + 353 + 65 = 24,094,204 unpacked"); - expect(MAX_PACKED_BYTES).toBe(12_171_395); - expect(MAX_PACKED_BYTES).toBe(12_154_912 + 12_387 + 4_096); + expect(MAX_PACKED_BYTES).toBe(12_171_548); + expect(MAX_PACKED_BYTES).toBe(12_155_065 + 12_387 + 4_096); expect(budget).toContain("aa127b3193c9bb3b0cb5deece5927be60ccb7111a50169320d322ffdeaa13f39"); expect(budget).toContain("0c331bab3ab3df69a108e18f5f29845b0db90c281cbd6455c0d90fa0b24081e2"); expect(budget).toContain("873cad8139fda303e2d19c6afd61cf549cf9b4d1d76b2a1d6d632a6afe6bd0d1"); @@ -1569,7 +1569,7 @@ describe("npm publication contract", () => { expect(budget).toContain("47684b3e2eb5cf3ed07fbb520aade8c7251d993f75262fbf1af627d9081a1a5f"); expect(budget).toContain("23,688,277 + 353 + 65 = 23,688,695"); expect(budget).toContain("23,759,283 + 353 + 65 = 23,759,701"); - expect(MAX_UNPACKED_BYTES).toBe(24_054_770); + expect(MAX_UNPACKED_BYTES).toBe(24_055_348); expect(budget).toContain("23,037,873 + 65 = 23,037,938"); expect(budget).toContain("f9f3ab38a682690ceaa2699a7309997512030f0fa500a9dc29dcd108123dc41f"); expect(budget).toContain("23,038,557 + 65 = 23,038,622"); @@ -1602,7 +1602,7 @@ describe("npm publication contract", () => { expect(budget).toContain("01875f12ab73a49d6c7d6bf520dc3d318db816addee2fa7981889f35c958cf7c"); expect(budget).toContain("b12909f08f7c19460ced56e30619f4860a1183f4b0106170c07837dae577a937"); expect(budget).toContain("0b212ac291218528dcf979370110a36f10850e046ca90a536057d9a44e807d1d"); - expect(MAX_UNPACKED_BYTES).toBe(24_054_352 + 353 + 65); + expect(MAX_UNPACKED_BYTES).toBe(24_054_930 + 353 + 65); expect(budget).toContain("22,794,052 + 65 = 22,794,117"); expect(budget).toContain("c482efe748f880e3717727d6d39fd92a68953e6eea766642b329ba47ae772d80"); expect(budget).toContain("22,759,423 + 65 = 22,759,488"); @@ -1638,8 +1638,8 @@ describe("npm publication contract", () => { expect(packageArtifactBudget).toEqual({ entryCount: { min: 625, max: 625 }, fileCount: { min: 625, max: 625 }, - packedBytes: { min: 1_600_000, max: 12_171_395 }, - unpackedBytes: { min: 9_000_000, max: 24_054_770 }, + packedBytes: { min: 1_600_000, max: 12_171_548 }, + unpackedBytes: { min: 9_000_000, max: 24_055_348 }, }); }); diff --git a/scripts/package-budget.ts b/scripts/package-budget.ts index 904535c2..27bb3ca5 100644 --- a/scripts/package-budget.ts +++ b/scripts/package-budget.ts @@ -2382,21 +2382,29 @@ // SHA-256 ed461e46d1d9d4514cffe577415bd641d3cbfbd1c3cde0619d0fe55acf6170bb. // The forward 0.18.64 updater preserves that studio source and failed-tag // history. Two identical packs under the same toolchain measured 625 entries, -// 12,154,912 packed and 24,054,352 payload bytes; SHA-256 below. The measured -// addition is one src/update.ts file, 2,069 packed and 7,325 payload bytes. +// 12,154,912 packed and 24,054,352 payload bytes; SHA-256 +// 99bdbc86d7e2016b7a003496d4d6a2bec847d6c8c3b01aed57c1b55a3b25cbb3. +// The measured addition is one src/update.ts file, 2,069 packed and 7,325 payload bytes. // The 425-byte payload increase over the previous updater candidate is solely // CHANGELOG.md, retaining studio changes and the annotated-tag failure. // Preserve the same projections and allowances: // 12,154,912 + 12,387 + 4,096 = 12,171,395 packed; // 24,054,352 + 353 + 65 = 24,054,770 payload. +// The 0.18.65 editorial release preserves that runtime and file inventory. +// Two byte-identical packs with the same qualified toolchain measured 625 +// entries, 12,155,065 packed and 24,054,930 payload bytes. The 578-byte payload +// increase is the new changelog entry; article and image files are website-only. +// Retain every projection and allowance: +// 12,155,065 + 12,387 + 4,096 = 12,171,548 packed; +// 24,054,930 + 353 + 65 = 24,055,348 payload. export const repairPackageMeasurement = Object.freeze({ - scope: "Ghostget 0.18.64 CLI updater over preserved tagged studio main773c537", + scope: "GhostGet 0.18.65 editorial release over the verified 0.18.64 runtime", command: "npm pack --ignore-scripts", npmVersion: "11.19.0", platform: "darwin-arm64", - archiveSha256: "99bdbc86d7e2016b7a003496d4d6a2bec847d6c8c3b01aed57c1b55a3b25cbb3", - packedBytes: 12_154_912, - unpackedBytes: 24_054_352, + archiveSha256: "09e69ff231a08b63c399662cdcb0fe9a08c5f5faa563e5e855e2c6b832e8003f", + packedBytes: 12_155_065, + unpackedBytes: 24_054_930, entryCount: 625, packedPlatformProjection: 12_387, packedPortabilityAllowance: 4_096, diff --git a/skills/ghostget/references/install.md b/skills/ghostget/references/install.md index 3ebea533..4fbcfaef 100644 --- a/skills/ghostget/references/install.md +++ b/skills/ghostget/references/install.md @@ -17,14 +17,14 @@ If Bun is missing, stop and direct the user to the official [Bun installation guide](https://bun.sh/docs/installation). Do not switch package managers or pipe an unreviewed installer into a shell. -This reference is authored for the exact v0.18.64 release coordinate. Use it +This reference is authored for the exact v0.18.65 release coordinate. Use it only from the matching release-bound Agent Skill after its canonical archive and immutable GitHub Release exist. If the coordinate is not public, stop instead of substituting `main`, another tag, or a different package version. Install that exact release and verify a public-page read: ```sh -bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.64/hraness-ghostget-0.18.64.tgz +bun add --global https://github.com/hraness/ghostget/releases/download/v0.18.65/hraness-ghostget-0.18.65.tgz ghostget read https://example.com ``` @@ -47,21 +47,21 @@ for the requested workflow; an unconnected provider or missing media tool does not prevent a public-page read, and overall provider readiness can be false on a fresh installation. -The package is `@hraness/ghostget`; `@hraness/ghostget@0.18.64` is an optional npm +The package is `@hraness/ghostget`; `@hraness/ghostget@0.18.65` is an optional npm mirror only after verified registry publication. Canonical installation does not wait for registry publication. When upgrading from Wrench, use `ghostget` for new commands and `GHOSTGET_STATE_HOME` for an explicit state root. Existing state is selected in place; do not rename, copy, or delete a state directory as part of the upgrade. -The [migration guide](https://github.com/hraness/ghostget/blob/v0.18.64/docs/ghostget-migration.md) +The [migration guide](https://github.com/hraness/ghostget/blob/v0.18.65/docs/ghostget-migration.md) explains the retained state aliases and durable protocol names. Do not clone the repository merely to run the CLI. Importing the SDK is a separate project dependency and does not install a global command: ```sh -bun add https://github.com/hraness/ghostget/releases/download/v0.18.64/hraness-ghostget-0.18.64.tgz +bun add https://github.com/hraness/ghostget/releases/download/v0.18.65/hraness-ghostget-0.18.65.tgz ``` `ghostget adapter sync-bundled` upgrades exact bundled baselines, including an diff --git a/src/beeper-client-types.ts b/src/beeper-client-types.ts index 21a822e1..6b0b9d96 100644 --- a/src/beeper-client-types.ts +++ b/src/beeper-client-types.ts @@ -96,7 +96,7 @@ export type BeeperContactInteractionExportReceipt = Readonly<{ implementation: Readonly<{ producer: Readonly<{ package: "@hraness/ghostget"; - version: "0.18.64"; + version: "0.18.65"; }>; officialCli: Readonly<{ implementation: "github.com/beeper/cli"; diff --git a/src/cli.ts b/src/cli.ts index 9086a455..2fa23163 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -114,7 +114,7 @@ export function isImmediateGhostgetVersionRequest( && (second === undefined || second === "--json"); } -/** `ghostget 0.18.64`, or `{"name":"ghostget","version":"0.18.64"}` with `--json`. */ +/** `ghostget 0.18.65`, or `{"name":"ghostget","version":"0.18.65"}` with `--json`. */ export function ghostgetVersionText(rawArguments: readonly string[]): string { return rawArguments.includes("--json") ? `${JSON.stringify({ name: "ghostget", version: GHOSTGET_VERSION })}\n` diff --git a/src/contracts-check.test.ts b/src/contracts-check.test.ts index 9d1c5f9c..266e3557 100644 --- a/src/contracts-check.test.ts +++ b/src/contracts-check.test.ts @@ -35,7 +35,7 @@ describe("checkCollectionPlan", () => { const check = checkCollectionPlan(examplePlan(), exampleCatalog()); expect(check.ok).toBeTrue(); expect(check.contract).toBe("ghostget.contract-check.v1"); - expect(check.ghostget).toEqual({ version: "0.18.64" }); + expect(check.ghostget).toEqual({ version: "0.18.65" }); expect(check.plan).toEqual({ collectionKey: "example-social-statistics", reads: 3 }); expect(check.reads[0]).toEqual({ index: 0, diff --git a/src/contracts.test-support.ts b/src/contracts.test-support.ts index 52f35f32..392677ce 100644 --- a/src/contracts.test-support.ts +++ b/src/contracts.test-support.ts @@ -95,7 +95,7 @@ export function exampleCatalog(): ContractCatalogV1 { return { ok: true, contract: CONTRACT_CATALOG_V1, - ghostget: { version: "0.18.64" }, + ghostget: { version: "0.18.65" }, generatedAt: "2026-09-21T20:00:00.000Z", vocabulary: catalogVocabularyValue, adapters: [ diff --git a/src/media/manifest.test.ts b/src/media/manifest.test.ts index 96ab50ec..007018e8 100644 --- a/src/media/manifest.test.ts +++ b/src/media/manifest.test.ts @@ -465,7 +465,7 @@ function trackedYtDlpManifest( describe("Ghostget media manifest", () => { test("uses one Ghostget-owned schema and transcriber identity", () => { expect(GHOSTGET_MEDIA_SCHEMA_VERSION).toBe(1); - expect(GHOSTGET_MEDIA_VERSION).toBe("0.18.64"); + expect(GHOSTGET_MEDIA_VERSION).toBe("0.18.65"); expect(localTranscriptVariantSegments(localIdentity)).toEqual([ "transcript", "local", diff --git a/src/version.ts b/src/version.ts index e37f2737..05c1d69e 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ /** Canonical immutable Ghostget package release identity. */ -export const GHOSTGET_VERSION = "0.18.64" as const; +export const GHOSTGET_VERSION = "0.18.65" as const; diff --git a/website/analytics.test.ts b/website/analytics.test.ts index c03bbf63..71201d42 100644 --- a/website/analytics.test.ts +++ b/website/analytics.test.ts @@ -216,6 +216,35 @@ describe("ghostget.com analytics contract", () => { expect(ATTRIBUTION_PARAMETERS.has("ref")).toBe(false); }); + test("drops private data in property names at the root and inside objects and arrays", () => { + for (const key of [ + "0@-.AA", + "reader@example.com", + "reader%40example.com", + "phx_private_key", + "Bearer private_access_token", + "https://example.com/path?code=private_value", + "/path?token=private_value", + ]) { + const capture = sanitizeCapture({ + event: "$pageview", + properties: { + [key]: "present", + $current_url: home.href, + $lib: "posthog-js", + token, + utm_source: "news", + safe_metric: 1, + diagnostic: { [key]: "present", safe_metric: 2, items: [{ [key]: "present", safe_metric: 3 }] }, + }, + }, home); + expect(capture).not.toBeNull(); + expect(Object.hasOwn(capture!.properties, key)).toBe(false); + expect(capture!.properties.diagnostic).toEqual({ safe_metric: 2, items: [{ safe_metric: 3 }] }); + expect(capture!.properties).toMatchObject({ $lib: "posthog-js", token, utm_source: "news", safe_metric: 1 }); + } + }); + test("drops foreign, preview, and local hosts", () => { for (const href of ["https://preview.example.com/", "https://ghostget-git-x.vercel.app/", "http://localhost:3000/"]) { expect(sanitizeCapture({ event: "$pageview", properties: { $current_url: href, token } }, evidenceFor("/", href))).toBeNull(); diff --git a/website/blog.test.ts b/website/blog.test.ts index c3f9e0d8..474922aa 100644 --- a/website/blog.test.ts +++ b/website/blog.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -14,6 +15,7 @@ import { BLOG_POSTS, BUILT_ON_RELATIONS, blogPostPath, + blogPostJsonLd, blogSitemapPaths, fillBlogShell, ghostgetRelations, @@ -27,6 +29,8 @@ import { type BlogSite, } from "./blog"; +import { editorialImages, editorialImage, editorialImageUrl, editorialImageSrcSet } from "./editorial-images"; + const websiteRoot = dirname(fileURLToPath(import.meta.url)); const site: BlogSite = { description: "GhostGet test site.", @@ -47,7 +51,7 @@ describe("GhostGet blog admission", () => { expect(() => assertArticleAdmissions(admissions)).not.toThrow(); for (const post of BLOG_POSTS) { expect(post.admission.href).toBe(blogPostPath(post.slug)); - expect(post.admission.drafting).toBe("ai-from-source"); + expect(post.admission.drafting).toBe("ai"); expect(post.admission.review?.reviewerType).toBe("ai"); expect(post.admission.humanReview).toBeNull(); if (post.admission.lifecycle === "indexable") { @@ -60,24 +64,25 @@ describe("GhostGet blog admission", () => { test("an AI review is disclosed as AI and never called human", () => { for (const post of BLOG_POSTS) { const sentence = articleProvenanceSentence(articleProvenanceFromAdmission(post.admission)); - expect(sentence).toBe(`Drafted with AI from the source code and reviewed by ${post.admission.review?.reviewer}.`); - expect(sentence).toMatch(/\bClaude\b/u); + expect(sentence).toBe(`Drafted with AI and reviewed by ${post.admission.review?.reviewer}.`); + expect(sentence).toMatch(/\bAI\b/u); expect(sentence).not.toMatch(/human/iu); } }); - test("every post body renders with the Hraness byline, provenance note, sources, and related products", async () => { + test("every post body renders with the Hraness byline, provenance note, sources, and the article image", async () => { for (const post of BLOG_POSTS) { const fragment = await readFile(join(websiteRoot, "source/blog", post.bodyFile), "utf8"); expect(fragment).not.toContain("By Hraness"); + expect(main).toContain('By '); expect(main).toContain('class="plain-publication__provenance"'); expect(main).toContain('class="plain-publication__sources"'); - expect(main).toContain('class="plain-publication__related"'); - expect(main.match(/\{\{[A-Z0-9_]+\}\}/gu) ?? []).toEqual( - fragment.includes("{{GHOSTGET_RELEASE}}") ? expect.arrayContaining(["{{GHOSTGET_RELEASE}}"]) : [], - ); + expect(main).toContain('class="editorial-figure"'); + expect(main).not.toMatch(/(?:Published|Updated|Checked)