Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions skills/wrench/references/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/media/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/media/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
47 changes: 47 additions & 0 deletions src/providers/beeper-local-runtime.internal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Record<string, unknown>>[];
}>;
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 {
Expand Down
41 changes: 39 additions & 2 deletions src/providers/beeper-local-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
3 changes: 2 additions & 1 deletion src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3263,7 +3263,8 @@ function foreignDataRecord(value: unknown): Record<string, unknown> | 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<object>();
const charge = (bytes: number): void => {
Expand Down
2 changes: 1 addition & 1 deletion website/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down