diff --git a/CHANGELOG.md b/CHANGELOG.md index f16c825..62a8421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.13.1 - 2026-08-23 + +- Keep Beeper account selector aliases internal while returning plain, + bounded-JSON-safe account projections from contact and messaging reads. + ## 0.13.0 - 2026-08-22 - Add a pinned, read-only Beeper Desktop provider for contacts and messaging diff --git a/README.md b/README.md index fa0d59a..3b7b84c 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,10 @@ The skill teaches Codex, Claude Code, Cursor, and other compatible coding agents when to use Wrench, how to preserve its trust boundaries, and how to install the CLI if it is missing. Start a new agent session after installation. -Install the current immutable CLI release from the `v0.13.0` tag: +Install the current immutable CLI release from the `v0.13.1` tag: ```sh -bun add --global github:hraness/wrench#v0.13.0 +bun add --global github:hraness/wrench#v0.13.1 wrench adapter sync-bundled --json wrench doctor ``` @@ -102,7 +102,7 @@ Install Wrench in an agent or application that owns its own model, planning, tool loop, approvals, and interface: ```sh -bun add github:hraness/wrench#v0.13.0 +bun add github:hraness/wrench#v0.13.1 ``` ```ts diff --git a/package.json b/package.json index 918be36..97d41d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hraness/wrench", - "version": "0.13.0", + "version": "0.13.1", "description": "Open-source CLI and TypeScript SDK for precise web capabilities for AI agents: page capture, verified media archives, encrypted reads, and typed provider operations.", "license": "MIT", "type": "module", diff --git a/skills/wrench/references/install.md b/skills/wrench/references/install.md index 5036ed9..8d4c138 100644 --- a/skills/wrench/references/install.md +++ b/skills/wrench/references/install.md @@ -21,7 +21,7 @@ When the user asked to install or use Wrench, install the current immutable release and its reviewed bundled adapter manifests: ```sh -bun add --global github:hraness/wrench#v0.13.0 +bun add --global github:hraness/wrench#v0.13.1 wrench adapter sync-bundled --json wrench --help wrench doctor --json @@ -31,7 +31,7 @@ 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 github:hraness/wrench#v0.13.0 +bun add github:hraness/wrench#v0.13.1 ``` ## Add only required optional tools diff --git a/src/media/manifest.test.ts b/src/media/manifest.test.ts index 60a2dd4..340049d 100644 --- a/src/media/manifest.test.ts +++ b/src/media/manifest.test.ts @@ -465,7 +465,7 @@ function trackedYtDlpManifest( describe("Wrench media manifest", () => { test("uses one Wrench-owned schema and transcriber identity", () => { expect(WRENCH_MEDIA_SCHEMA_VERSION).toBe(1); - expect(WRENCH_MEDIA_VERSION).toBe("0.13.0"); + expect(WRENCH_MEDIA_VERSION).toBe("0.13.1"); expect(localTranscriptVariantSegments(localIdentity)).toEqual([ "transcript", "local", diff --git a/src/media/manifest.ts b/src/media/manifest.ts index e8d71ac..f99bba7 100644 --- a/src/media/manifest.ts +++ b/src/media/manifest.ts @@ -39,7 +39,7 @@ import { import { compareUtf8 } from "./utf8-order"; export const WRENCH_MEDIA_SCHEMA_VERSION = 1 as const; -export const WRENCH_MEDIA_VERSION = "0.13.0" as const; +export const WRENCH_MEDIA_VERSION = "0.13.1" as const; export const WRENCH_MEDIA_MANIFEST_FILE = "wrench-media.json" as const; export const WRENCH_MEDIA_CHECKSUM_FILE = "manifest-sha256.txt" as const; const MAX_ITEM_ENTRIES = 4_096; diff --git a/src/providers/beeper-local-runtime.internal.test.ts b/src/providers/beeper-local-runtime.internal.test.ts index 548b892..3f65b70 100644 --- a/src/providers/beeper-local-runtime.internal.test.ts +++ b/src/providers/beeper-local-runtime.internal.test.ts @@ -18,6 +18,7 @@ import { describe, expect, test } from "bun:test"; import type { WrenchAuth } from "../auth"; import type { OperationInput, WebSessionRecipe } from "../model"; +import { boundedJsonOutput } from "../runtime"; import { materializeBeeperMessagingList, materializeBeeperMessagingRead, @@ -466,6 +467,52 @@ describe("Beeper local read runtime", () => { } }); + test("keeps selector aliases internal while public projections cross bounded JSON", async () => { + const path = privateStore(); + const calls: BeeperCliInvocation[] = []; + try { + const contactsResult = await execute( + path, + "contacts.list", + { account_id: NETWORK_ACCOUNT_ID, limit: 2 }, + calls, + ); + const listResult = await execute( + path, + "messaging.list", + { account_id: NETWORK_ACCOUNT_ID, limit: 2 }, + calls, + ); + const readResult = await execute( + path, + "messaging.read", + { account_id: NETWORK_ACCOUNT_ID, conversation_id: CHAT_ID, limit: 2 }, + calls, + ); + + for (const result of [contactsResult, listResult, readResult]) { + expect(boundedJsonOutput(result.output, 32 * 1024 * 1024)) + .toEqual(result.output); + } + for (const result of [contactsResult, listResult]) { + const output = result.output as Readonly<{ + accounts: readonly Readonly>[]; + }>; + expect(output.accounts).toHaveLength(2); + for (const account of output.accounts) { + expect(Reflect.ownKeys(account)).not.toContain("selectorAliases"); + expect(Object.values(Object.getOwnPropertyDescriptors(account)).every( + (descriptor) => descriptor.enumerable && "value" in descriptor, + )).toBeTrue(); + } + expect(JSON.stringify(output)).not.toContain("Official Display Alias"); + expect(JSON.stringify(output)).not.toContain("Official Name Alias"); + } + } finally { + rmSync(path, { recursive: true, force: true }); + } + }); + test("rejects message drift when style-critical isSender is absent", async () => { const path = privateStore(); try { diff --git a/src/providers/beeper-local-runtime.ts b/src/providers/beeper-local-runtime.ts index 001139f..a24ca7a 100644 --- a/src/providers/beeper-local-runtime.ts +++ b/src/providers/beeper-local-runtime.ts @@ -1456,6 +1456,43 @@ function unavailableContactStats() { ); } +type BeeperPublicAccountProjection = Omit< + BeeperAccountProjection, + "selectorAliases" +>; + +function publicAccountProjection( + account: BeeperAccountProjection, +): BeeperPublicAccountProjection { + return Object.freeze({ + accountId: account.accountId, + bridge: Object.freeze({ + id: account.bridge.id, + type: account.bridge.type, + provider: account.bridge.provider, + }), + network: account.network, + loginId: account.loginId, + status: account.status, + statusText: account.statusText, + user: Object.freeze({ + id: account.user.id, + fullName: account.user.fullName, + username: account.user.username, + phoneNumber: account.user.phoneNumber, + email: account.user.email, + isSelf: account.user.isSelf, + cannotMessage: account.user.cannotMessage, + }), + }); +} + +function publicAccountProjections( + accounts: readonly BeeperAccountProjection[], +): readonly BeeperPublicAccountProjection[] { + return Object.freeze(accounts.map(publicAccountProjection)); +} + function contactOutput( accounts: readonly BeeperAccountProjection[], subject: string, @@ -1478,7 +1515,7 @@ function contactOutput( operation: "contacts.list", accountSubject: subject, projection: "bounded-local-desktop-api", - accounts, + accounts: publicAccountProjections(accounts), requestedAccountId: accountId, contacts: Object.freeze(contacts), completeness: Object.freeze({ @@ -1519,7 +1556,7 @@ function conversationOutput( operation: "messaging.list", accountSubject: subject, projection: "bounded-local-desktop-api", - accounts, + accounts: publicAccountProjections(accounts), requestedAccountId: accountId, conversations: Object.freeze(conversations), completeness: Object.freeze({ diff --git a/src/runtime.ts b/src/runtime.ts index e34da03..7b15645 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -3263,7 +3263,8 @@ function foreignDataRecord(value: unknown): Record | null { } } -function boundedJsonOutput(value: unknown, maxBytes: number): unknown { +/** @internal Exported only for owned runtime-boundary tests. */ +export function boundedJsonOutput(value: unknown, maxBytes: number): unknown { const state = { bytes: 0, nodes: 0 }; const ancestors = new WeakSet(); const charge = (bytes: number): void => { diff --git a/website/build.ts b/website/build.ts index 119d08c..2648b2d 100644 --- a/website/build.ts +++ b/website/build.ts @@ -24,7 +24,7 @@ export const REPOSITORY_URL = "https://github.com/hraness/wrench" as const; export const PUBLISHER_URL = "https://github.com/hraness" as const; export const SKILL_INSTALL_COMMAND = "npx skills add hraness/wrench" as const; export const SKILL_INSTALL_COMMAND_BUNX = "bunx skills add hraness/wrench" as const; -export const CONTENT_REVIEWED_RELEASE = "v0.13.0" as const; +export const CONTENT_REVIEWED_RELEASE = "v0.13.1" as const; export const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com" as const; export const PUBLIC_PAGES = [