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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.6.0

- Call them diffs: a post made from someone's published post is a diff, in the
skills, the helper's messages and the docs (gdiffer decision 0109).
- The helper's problem code for a half-named parent is now `invalid_diff`
(was `invalid_remix`).
- Refresh both distributions from Control's verified public skill snapshot.

## 0.5.1

- Teach posts to restore returning participants on open with
Expand Down
2 changes: 1 addition & 1 deletion directory/differ/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "differ",
"version": "0.5.1",
"version": "0.6.0",
"description": "Create, preview, publish and revise interactive social posts.",
"author": {
"name": "Sky Valley",
Expand Down
2 changes: 1 addition & 1 deletion directory/differ/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "differ",
"version": "0.5.1",
"version": "0.6.0",
"description": "Create, preview, publish and revise interactive social posts.",
"author": {
"name": "Sky Valley",
Expand Down
2 changes: 1 addition & 1 deletion directory/differ/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "differ",
"version": "0.5.1",
"version": "0.6.0",
"description": "Create, preview, publish and revise interactive social posts.",
"author": {
"name": "Sky Valley",
Expand Down
20 changes: 10 additions & 10 deletions directory/differ/skills.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"files": [
{
"uri": "skill://interactive-content/SKILL.md",
"digest": "sha256:575a5e0f04de4204144fe42b010538307f01241c6d5a12277617d9af46d5174c",
"size": 5071
"digest": "sha256:e7dd43cc3e032c535973986c02f523a707044d2c8d690aa7afe077e1a6a17e39",
"size": 5068
},
{
"uri": "skill://interactive-content/references/recording.md",
Expand All @@ -12,8 +12,8 @@
},
{
"uri": "skill://publish-content/SKILL.md",
"digest": "sha256:b827901738fb24f0810df998bbe6fd2203505d287a1086d1f448cb2f77701e69",
"size": 4030
"digest": "sha256:132436fd378af087cf1558ca49458c93ae206d721a49bd966c57dadf52f6d108",
"size": 5494
},
{
"uri": "skill://publish-content/examples/board/README.md",
Expand Down Expand Up @@ -117,8 +117,8 @@
},
{
"uri": "skill://publish-content/references/connector.md",
"digest": "sha256:1e2638f643a12932be5d6b42e11d2991c73e24887e3b7e66fe277b758943e08f",
"size": 4083
"digest": "sha256:ee0f083cbb4e92b5c5896a80e664879d597ac5af88ff9fe9657f3f645ce8c47d",
"size": 4361
},
{
"uri": "skill://publish-content/references/helper.md",
Expand All @@ -127,13 +127,13 @@
},
{
"uri": "skill://publish-content/references/participation.md",
"digest": "sha256:b8ff86ab2003b6ad5a4f044f420f428d190f2478c64a9576019d17197dc67513",
"size": 8108
"digest": "sha256:cda548b481ae958728be9594a3933a993cf2bad01b58eeaee1c3adda6d3f88d0",
"size": 8109
},
{
"uri": "skill://publish-content/references/setup.md",
"digest": "sha256:5850af3cf2022594aa7e24c05493a24c661f0982e8aaf7d23d1220feadc39b90",
"size": 2003
"digest": "sha256:5cdb1cfbf94baa5a7dcef29f00960601d1580fe7454d9676894c891c1bd84290",
"size": 2208
},
{
"uri": "skill://publish-content/scripts/browser.mjs",
Expand Down
10 changes: 5 additions & 5 deletions directory/differ/skills/interactive-content/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: interactive-content
description: >-
Create or refine interactive social content, playable posts, and small browser
experiences: hooks, gestures, payoffs, participation, sharing, and remixing.
experiences: hooks, gestures, payoffs, participation, sharing, and diffing.
Use before choosing architecture. Excludes general SaaS interfaces,
deployment-only work, and read-only app analysis.
---
Expand All @@ -17,7 +17,7 @@ Images, copy, and screenshots support it; they are not substitutes.
## Frame

Before coding, identify the hook, action, payoff, sharing motive, and recipient
experience. Always assess self-insertion, response, challenge, and remix;
experience. Always assess self-insertion, response, challenge, and diffing;
implement opportunities inherent in the premise, not unrelated mechanics.
Participation is optional for visitors, not optional to consider.
Infer from the brief; ask only about consequential gaps.
Expand All @@ -35,9 +35,9 @@ Infer from the brief; ask only about consequential gaps.
- Deliver a perceptible payoff: reveal, transformation, answer, punchline,
personal artifact, win/loss, or satisfying loop. Spend effort on assets,
composition, timing, motion, sound, and exact copy.
- Participation fills the format (your location/picks/answers); remixing changes
- Participation fills the format (your location/picks/answers); diffing changes
it (time spans/rules/style/premise). Make invitations concrete and optional.
Consumption and participation must stand alone without remixing.
Consumption and participation must stand alone without diffing.
- Separate invariants from variation. Preserve the agreed subject/context;
vary meaningful form or behavior, not merely branding, unless a reskin was requested.
- Identify why someone would send this to whom: challenge, gift, identity,
Expand Down Expand Up @@ -83,7 +83,7 @@ reach its payoff, and test participation/sharing as a fresh recipient when
present. Check loading, gestures, framing, and failure states; report what was
observed versus unverified. Remove friction and generic product chrome.

When testing remix behaviour, distinguish consumption, participation, remixing,
When testing diff behaviour, distinguish consumption, participation, diffing,
and subsequent sharing. Forced steps do not demonstrate desire. Inherited
exposure is a distribution condition to verify, not assume; accept null results.

Expand Down
33 changes: 27 additions & 6 deletions directory/differ/skills/publish-content/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: publish-content
description: Publish, revise, remix, or prepare interactive posts on Differ/app-player. Not Git pushes or generic hosting.
description: Publish, revise, diff, or prepare interactive posts on Differ/app-player. Not Git pushes or generic hosting.
---
# Publish content

Expand All @@ -20,9 +20,14 @@ the post must be claimed before signed-in visitors can contribute shared state.
original browser owns the claim, and the upload pass cannot claim it.
- With the Differ helper (the plugin's `publish_directory` tool or its
`differ.cjs` script), follow [helper](references/helper.md). It reads files
from disk, so prefer it whenever it is available.
from disk, so prefer it whenever it is available, except as below.
- With the Differ connector (`start_creation`, `publish_creation`,
`publish_as_me`), follow [connector](references/connector.md).
- With both, when `get_profile` names a connected creator and the helper is
not already connected, publish through the connector. Its sign-in belongs to
the human's account and lasts across sessions; the helper's is saved on this
machine, so in a cloud or disposable sandbox it is lost when the session
ends. Do not start the helper's connection there.
- With neither, say publishing needs the Differ helper, the Differ connector or
a prompt copied from the player's Create page. Never invent an upload URL.

Expand Down Expand Up @@ -54,15 +59,31 @@ Each path's reference says how to connect an account.
request; the helper does this for you. On a revision conflict, compare the
current post before deliberately continuing; never silently overwrite or
turn an update into a new post.
5. A remix names both its parent post and that post's exact revision. Another
person's source or public link grants no edit rights. Remixes start from
5. A diff names both its parent post and that post's exact revision. Another
person's source or public link grants no edit rights. Diffs start from
authored files and seeds, never accumulated visitor data or identities.
Updates preserve participation: change its meaning only in a remix.
Updates preserve participation: change its meaning only in a diff.
6. Check the actual player frame. Its controls sit outside the content in a
bottom lip or desktop rail. Separate committed publication, HTTP
verification, and interaction actually observed in a browser.

Follow connection errors' recovery. Restricted networking needs normal network
permission, not new credentials. Do not routinely patch the publisher, edit DNS,
permission, not new credentials: see [when Differ can't be reached](#when-differ-cant-be-reached). Do not routinely patch the publisher, edit DNS,
rebuild the player or switch hosts during content work. Preserve files and
pending identity when blocked; local readiness is not publication.

## When Differ can't be reached

Differ's own refusals are JSON with a `code` and `message`. A request that
fails before Differ answers (a proxy's 403, "Tunnel connection failed", a DNS
or connection error) was stopped by this environment's network policy, which
cloud sandboxes apply to hosts they do not know. Retrying, new credentials or
another route will not help. Tell the human once, naming the host from the
upload URL: allow it in this environment's network settings, a one-time change
(in Claude Code on the web, the environment menu in the session's title bar,
then Edit, then Network access). Keep the files and the saved request, and
retry the identical request once it is allowed. Until then, a post small enough
for the connector's call can still publish that way, since the call travels
through the host rather than the sandbox; otherwise give the human a .zip of
the content with `index.html` at its root, which they can drop on the player's
Create page, or in a post's Update dialog to save it as a draft.
12 changes: 9 additions & 3 deletions directory/differ/skills/publish-content/references/connector.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ upload URL: a guest post's comes from `start_creation`, a connected creator's
from `start_upload_as_me`. Otherwise send every finished file in the call,
base64 encoded, with `index.html` at the root: that suits a small, mostly-text
post, and a whole call must stay under 4 MiB. If an upload URL cannot be
reached, publish a small post through the call instead of starting again.
reached, publish a small post through the call instead of starting again, and
tell the human how to allow its host, as the skill's section on Differ being
unreachable says.

The connector's sign-in belongs to the human's account, not this machine, so
in a cloud sandbox it is usually already connected: check `get_profile` before
asking them to sign in.

## As a guest

Expand Down Expand Up @@ -64,8 +70,8 @@ Anything but a small post as the creator uploads over HTTPS:
returns its receipt through the new pass, and one that did not can be
sent unchanged.

## Reading and remixing
## Reading and diffing

`get_creation` reads a post's public state and revision identifiers; it grants
no editing or claiming rights. For a remix, add `parentPostId` and
no editing or claiming rights. For a diff, add `parentPostId` and
`parentRevisionId` to the publishing request.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ authored version, `/data/` is live data and `/uploads/` is participant media.

Keep app code and rules outside `data/`. A `data/*.html`, `.json` or `.txt`
file in the bundle is a seed, applied once to that path. Revisions keep existing
participation, including deleted seeds. A remix starts fresh from the selected
authored files, rules and seeds. Never download live visitor data into a remix.
participation, including deleted seeds. A diff starts fresh from the selected
authored files, rules and seeds. Never download live visitor data into a diff.
Use relative authored asset URLs to stay on the selected version; root URLs
address the current post. Live data always uses root `/data/...` URLs.

Expand Down Expand Up @@ -130,7 +130,7 @@ If `PAGELIKE_BINARY` or browser tooling is unavailable, stop that verification
step and report the missing dependency. Do not search unrelated projects or
substitute a mock runtime as authorization evidence.

Analysis/remix previews use disposable one-hour origins, but all visitors there
Analysis and diff previews use disposable one-hour origins, but all visitors there
share one authenticated preview identity. They cannot prove anonymous access or
cross-person authorization. The rehearsal tests the runtime with fixture identity;
Player sign-in, consent and browser cookie behavior need separate verification.
Expand Down
11 changes: 7 additions & 4 deletions directory/differ/skills/publish-content/references/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ that distribution. Local and directory packages both identify as `differ`;
install one per host. A GitHub release does not imply directory approval.

The helper needs Node and file access, so installing the plugin does not make
it runnable in a mobile or cloud sandbox. There, the connector needs either the
files in its calls or outbound HTTPS to upload them. A host whose code sandbox
reaches only allowed domains needs `publish.getdiffer.com` allowed; without it,
only a post small enough to send in the calls can publish.
it runnable in a mobile or cloud sandbox, and a connection it makes there is
lost with the sandbox. There, use the connector: its sign-in lasts across
sessions. It needs either the files in its calls or outbound HTTPS to upload
them. A host whose code sandbox reaches only allowed domains needs
`publish.getdiffer.com` allowed, once per environment (in Claude Code on the
web: the environment menu, then Edit, then Network access); without it, only a
post small enough to send in the calls can publish.
8 changes: 4 additions & 4 deletions helper/client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export async function status(directory) {
}
export async function publish(directory, title, { newPost = false, parentPostId, parentRevisionId } = {}) {
directory = contentDirectory(directory); title = contentTitle(directory, title);
if (!!parentPostId !== !!parentRevisionId) throw problem('invalid_remix', 'Remix requires source post and revision IDs.', 'Read the source post first.');
if (!!parentPostId !== !!parentRevisionId) throw problem('invalid_diff', 'A diff requires source post and revision IDs.', 'Read the source post first.');
const config = await activeConfiguration(); const artifact = pack(directory); const unlock = lock(directory);
try { return await publishCreator(config, stateFor(directory, config), directory, artifact, title, { newPost, parentPostId, parentRevisionId }); }
finally { unlock(); }
Expand Down Expand Up @@ -127,13 +127,13 @@ async function bearerFor(config, state, me) {
const creation = state.creation;
if (me === undefined) me = await account(config);
if (creation.mode === 'connected') {
if (me?.username !== creation.accountUsername) throw problem('account_changed', 'This draft belongs to a different connected creator.', 'Reconnect that account, or deliberately create a separate remix.');
if (me?.username !== creation.accountUsername) throw problem('account_changed', 'This draft belongs to a different connected creator.', 'Reconnect that account, or deliberately create a separate diff.');
return config.token;
}
if (me) {
const current = await publicState(config, creation.id);
if (current.claimed) {
if (current.post?.username !== me.username) throw problem('owner_conflict', 'This claimed post belongs to another creator.', 'Connect its owner or create a remix.');
if (current.post?.username !== me.username) throw problem('owner_conflict', 'This claimed post belongs to another creator.', 'Connect its owner or create a diff.');
creation.mode = 'connected'; creation.accountUsername = me.username;
return config.token;
}
Expand Down Expand Up @@ -176,7 +176,7 @@ async function publishCreator(config, state, directory, artifact, title, { newPo
delete state.creation; delete state.receipt; delete state.lastInput;
}
if (!state.creation) {
if (state.receipt && !me) throw problem('ownership_required', 'A public post binding does not grant editing rights.', 'Use the original private workspace or connect the owner. To make your own version, create a remix.');
if (state.receipt && !me) throw problem('ownership_required', 'A public post binding does not grant editing rights.', 'Use the original private workspace or connect the owner. To make your own version, create a diff.');
state.creation = { id: state.receipt?.postId ?? randomUUID(), mode: me ? 'connected' : 'guest',
...(me ? { accountUsername: me.username } : { ownerProof: randomBytes(32).toString('hex') }) };
atomic(statePath(directory), state);
Expand Down
2 changes: 1 addition & 1 deletion helper/connection.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function configuration() {
// Control reports stable codes. The helper owns what a creator does next.
const recoveries = {
reconnect_required: reconnect,
owner_conflict: 'Connect the account that owns this post, or publish your own version as a remix.',
owner_conflict: 'Connect the account that owns this post, or publish your own version as a diff.',
revision_conflict: 'Compare get-post ID with your files, then deliberately bind DIR --post ID and publish again.',
creation_expired: 'Unclaimed guest posts expire after 24 hours. Publish again with --new to start a new post.',
};
Expand Down
2 changes: 1 addition & 1 deletion helper/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function mcp() {
server.registerTool('connection_status', { description: 'After the human approves the connection link, finish connecting. Respect retryAfter; do not poll in a tight loop.', inputSchema: {}, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false } }, wrap(connectionStatus));
server.registerTool('disconnect_account', { description: 'Only when the human requests it, revoke this agent connection and explicitly return future publishing to guest mode. Existing posts keep ownership.', inputSchema: {}, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false } }, wrap(disconnect));
server.registerTool('publication_status', { description: 'Recover a pending publication or verify the saved receipt for a local directory.', inputSchema: { directory }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } }, wrap(({ directory }) => status(directory)));
server.registerTool('get_post', { description: 'Inspect a remote post before conflict recovery or remixing.', inputSchema: { postId: z.string().uuid() }, annotations }, wrap(({ postId }) => getPost(postId)));
server.registerTool('get_post', { description: 'Inspect a remote post before conflict recovery or diffing.', inputSchema: { postId: z.string().uuid() }, annotations }, wrap(({ postId }) => getPost(postId)));
server.registerTool('image_candidate', { description: 'Create a separate WebP candidate using installed cwebp. Never replaces source or adopts visual quality automatically.', inputSchema: { source: z.string(), output: z.string(), quality: z.number().min(0).max(100).optional() }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false } }, wrap(({ source, output, quality }) => imageCandidate(source, output, quality)));
server.registerTool('inspect_video', { description: 'Read recording dimensions, codec, duration and encoded frame rate using installed ffprobe. Not proof of smooth motion or good composition.', inputSchema: { file: z.string() }, annotations }, wrap(({ file }) => inspectVideo(file)));
await server.connect(new StdioServerTransport());
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "differ-plugin",
"version": "0.5.1",
"version": "0.6.0",
"private": true,
"type": "module",
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion plugins/differ/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "differ",
"version": "0.5.1",
"version": "0.6.0",
"description": "Create, preview, publish and revise interactive social posts.",
"author": {
"name": "Sky Valley",
Expand Down
2 changes: 1 addition & 1 deletion plugins/differ/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "differ",
"version": "0.5.1",
"version": "0.6.0",
"description": "Create, preview, publish and revise interactive social posts.",
"author": {
"name": "Sky Valley",
Expand Down
Loading
Loading