sanity-canvas-skill 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/README.old.md +141 -0
  4. package/SKILL.md +225 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +493 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/index.d.ts +11 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +8 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/canvasDiff.d.ts +62 -0
  14. package/dist/lib/canvasDiff.d.ts.map +1 -0
  15. package/dist/lib/canvasDiff.js +138 -0
  16. package/dist/lib/canvasDiff.js.map +1 -0
  17. package/dist/lib/canvasDocFile.d.ts +40 -0
  18. package/dist/lib/canvasDocFile.d.ts.map +1 -0
  19. package/dist/lib/canvasDocFile.js +123 -0
  20. package/dist/lib/canvasDocFile.js.map +1 -0
  21. package/dist/lib/canvasPayloads.d.ts +55 -0
  22. package/dist/lib/canvasPayloads.d.ts.map +1 -0
  23. package/dist/lib/canvasPayloads.js +89 -0
  24. package/dist/lib/canvasPayloads.js.map +1 -0
  25. package/dist/lib/canvasToMarkdown.d.ts +25 -0
  26. package/dist/lib/canvasToMarkdown.d.ts.map +1 -0
  27. package/dist/lib/canvasToMarkdown.js +114 -0
  28. package/dist/lib/canvasToMarkdown.js.map +1 -0
  29. package/dist/lib/diffAdapter.d.ts +38 -0
  30. package/dist/lib/diffAdapter.d.ts.map +1 -0
  31. package/dist/lib/diffAdapter.js +176 -0
  32. package/dist/lib/diffAdapter.js.map +1 -0
  33. package/dist/lib/diffPush.d.ts +33 -0
  34. package/dist/lib/diffPush.d.ts.map +1 -0
  35. package/dist/lib/diffPush.js +233 -0
  36. package/dist/lib/diffPush.js.map +1 -0
  37. package/dist/lib/formatOutline.d.ts +18 -0
  38. package/dist/lib/formatOutline.d.ts.map +1 -0
  39. package/dist/lib/formatOutline.js +262 -0
  40. package/dist/lib/formatOutline.js.map +1 -0
  41. package/dist/lib/formatPull.d.ts +46 -0
  42. package/dist/lib/formatPull.d.ts.map +1 -0
  43. package/dist/lib/formatPull.js +196 -0
  44. package/dist/lib/formatPull.js.map +1 -0
  45. package/dist/lib/markdownToCanvas.d.ts +30 -0
  46. package/dist/lib/markdownToCanvas.d.ts.map +1 -0
  47. package/dist/lib/markdownToCanvas.js +213 -0
  48. package/dist/lib/markdownToCanvas.js.map +1 -0
  49. package/dist/lib/pathParser.d.ts +30 -0
  50. package/dist/lib/pathParser.d.ts.map +1 -0
  51. package/dist/lib/pathParser.js +66 -0
  52. package/dist/lib/pathParser.js.map +1 -0
  53. package/dist/lib/resolveResource.d.ts +9 -0
  54. package/dist/lib/resolveResource.d.ts.map +1 -0
  55. package/dist/lib/resolveResource.js +45 -0
  56. package/dist/lib/resolveResource.js.map +1 -0
  57. package/dist/lib/targetedPush.d.ts +148 -0
  58. package/dist/lib/targetedPush.d.ts.map +1 -0
  59. package/dist/lib/targetedPush.js +467 -0
  60. package/dist/lib/targetedPush.js.map +1 -0
  61. package/dist/types.d.ts +268 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +6 -0
  64. package/dist/types.js.map +1 -0
  65. package/package.json +50 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sanity.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # sanity-canvas-skill
2
+
3
+ > ### 🤖 Vibe coded by a [Miriad](https://miriad.app) Team
4
+ > This package was built entirely by a team of AI agents collaborating on Miriad — from spec to implementation to tests. 362 tests, zero hand-written lines.
5
+
6
+ Read and write [Sanity Canvas](https://www.sanity.io/canvas) documents programmatically. Markdown ↔ Canvas Portable Text conversion with diff-based push for concurrent editing.
7
+
8
+ > 📖 **Run `npx sanity-canvas-skill@latest --help` for the full command reference.**
9
+
10
+ ## Quick Start
11
+
12
+ ```bash
13
+ export SANITY_TOKEN="your-global-sanity-token"
14
+
15
+ # Pull a Canvas doc to markdown (with block keys for roundtrip)
16
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> > doc.md
17
+
18
+ # Edit doc.md, then push changes back (diff-based, only patches changed blocks)
19
+ cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push
20
+
21
+ # Preview changes without applying
22
+ cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push --dry-run
23
+
24
+ # Force full overwrite (skips diff)
25
+ cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push --force
26
+
27
+ # List docs
28
+ npx sanity-canvas-skill@latest --org <orgId> list
29
+ ```
30
+
31
+ ## Getting a Token
32
+
33
+ You need a **global** Sanity auth token (not project-scoped). The easiest way:
34
+
35
+ ```bash
36
+ npx sanity debug --secrets
37
+ ```
38
+
39
+ This prints your CLI's auth token, which works as a global token. Alternatively, create one at [manage.sanity.io](https://manage.sanity.io) → API → Tokens.
40
+
41
+ ## Programmatic API
42
+
43
+ ```typescript
44
+ import {
45
+ canvasToMarkdown,
46
+ markdownToCanvas,
47
+ normalizeKeys,
48
+ computeOps,
49
+ diffPush,
50
+ resolveResource,
51
+ } from 'sanity-canvas-skill'
52
+ ```
53
+
54
+ ### Convert Canvas blocks to markdown
55
+
56
+ ```typescript
57
+ const markdown = canvasToMarkdown(doc.content, {emitKeys: true})
58
+ // Produces markdown with <!-- k:KEY --> comments for roundtrip
59
+ ```
60
+
61
+ ### Convert markdown to Canvas blocks
62
+
63
+ ```typescript
64
+ const blocks = markdownToCanvas(markdown)
65
+ // Parses key comments, restores block identity
66
+ ```
67
+
68
+ ### Diff two block arrays
69
+
70
+ ```typescript
71
+ const normalized = normalizeKeys(originalBlocks, editedBlocks)
72
+ const ops = computeOps(originalBlocks, normalized)
73
+ // [{type: 'set', key: 'abc', block: {...}}, {type: 'insert', afterKey: 'def', blocks: [...]}]
74
+ ```
75
+
76
+ ### Full push pipeline
77
+
78
+ ```typescript
79
+ import {createClient} from '@sanity/client'
80
+
81
+ const resource = await resolveResource(token, orgId)
82
+ const client = createClient({resource: {type: 'canvas', id: resource.id}})
83
+
84
+ const result = await diffPush(client, markdownFileContent)
85
+ // {docId: '...', ops: [...], summary: '1 changed, 2 inserted, 0 deleted', rev: '...'}
86
+ ```
87
+
88
+ ## How it works
89
+
90
+ **Pull** serializes Canvas Portable Text blocks to markdown with `<!-- k:KEY -->` HTML comments that preserve block identity.
91
+
92
+ **Push** is diff-based:
93
+ 1. Pulls the current doc from Canvas (fresh revision for locking)
94
+ 2. Parses edited markdown back to blocks
95
+ 3. Matches blocks by `_key` from key comments
96
+ 4. Computes minimal SET/INSERT/UNSET operations via `@sanity/diff`
97
+ 5. Applies atomically with `ifRevisionId` optimistic locking
98
+
99
+ Only changed blocks are patched. Notes are preserved. Concurrent edits are detected (409 on conflict).
100
+
101
+ Unknown block types survive roundtrip via opaque base64 encoding (`<!-- canvas:TYPE:BASE64 -->`).
102
+
103
+ ## Block type support
104
+
105
+ | Markdown | Canvas Type | Roundtrip |
106
+ |---|---|---|
107
+ | Text, headings, blockquotes | `block` | ✅ |
108
+ | Bold, italic, code, links | marks/annotations | ✅ |
109
+ | Lists (bullet, numbered) | `block` with `listItem` | ✅ |
110
+ | Code blocks | `canvasCode` | ✅ |
111
+ | Images | `canvasImage` | ✅ |
112
+ | Horizontal rules | `canvasDivider` | ✅ |
113
+ | Unknown types | opaque base64 | ✅ |
114
+
115
+ ## Environment variables
116
+
117
+ | Variable | Required | Description |
118
+ |---|---|---|
119
+ | `SANITY_TOKEN` | Yes | Global Sanity auth token (get with `npx sanity debug --secrets`) |
120
+ | `SANITY_ORG` | No | Default org ID (alternative to `--org`) |
121
+
122
+ ## License
123
+
124
+ MIT © [Sanity.io](https://www.sanity.io)
package/README.old.md ADDED
@@ -0,0 +1,141 @@
1
+ # Canvas Writer
2
+
3
+ Create and update [Sanity Canvas](https://www.sanity.io/canvas) documents programmatically.
4
+
5
+ Includes a reusable TypeScript library for markdown ↔ Canvas Portable Text conversion, and a CLI tool for reading/writing Canvas documents directly.
6
+
7
+ ## How It Works
8
+
9
+ Canvas documents live in a **resource** (not a regular Sanity project/dataset). The CLI uses `@sanity/client` with a special `resource` config to talk directly to the Canvas API.
10
+
11
+ The conversion library handles mapping between standard markdown and Canvas-specific Portable Text block types:
12
+
13
+ | Markdown | Canvas PTE Type |
14
+ |---|---|
15
+ | Paragraphs, headings, blockquotes | `block` (with style) |
16
+ | Bold, italic, code, strikethrough | `span` marks (`strong`, `em`, `code`, `strike-through`) |
17
+ | Bullet/numbered lists | `block` with `listItem` |
18
+ | Links `[text](url)` | `link` annotation with `href` |
19
+ | Code blocks ` ```lang ``` ` | `canvasCode` (lines as nested blocks) |
20
+ | Images `![alt](url)` | `canvasImage` (ephemeral src) |
21
+ | Horizontal rules `---` | `canvasDivider` |
22
+
23
+ ## What an Agent Needs From the User
24
+
25
+ To write to Canvas, an agent needs two things:
26
+
27
+ 1. **A Sanity auth token** — a global token (not project-scoped). Set as `SANITY_TOKEN` environment variable. The user can generate one at [sanity.io/manage](https://www.sanity.io/manage).
28
+
29
+ 2. **A Canvas resource ID** — identifies which Canvas workspace to write to. The user can find this by:
30
+ - Going to `canvas.sanity.io` and looking at the URL path, OR
31
+ - Using the Canvas API: `GET https://api.sanity.io/vX/canvases?organizationId={orgId}` returns resources with an `id` field.
32
+
33
+ That's it. With those two values, the agent can create, read, update, and delete Canvas documents.
34
+
35
+ ## Setup
36
+
37
+ ```bash
38
+ # Clone and install
39
+ git clone git@github.com:snorrees/canvas-writer-studio.git
40
+ cd canvas-writer-studio
41
+ pnpm install
42
+
43
+ # Set your Sanity token
44
+ export SANITY_TOKEN="your-global-sanity-token"
45
+ ```
46
+
47
+ ## CLI Usage
48
+
49
+ All commands require `--resource <id>` to specify the Canvas resource.
50
+
51
+ ```bash
52
+ # List all documents
53
+ pnpm canvas --resource <resourceId> list
54
+ pnpm canvas --resource <resourceId> list --search "query"
55
+
56
+ # Read a document
57
+ pnpm canvas --resource <resourceId> read <docId>
58
+
59
+ # Create a new document from markdown
60
+ echo "# Hello World" | pnpm canvas --resource <resourceId> create "My Document"
61
+
62
+ # Set (replace) content from markdown
63
+ cat article.md | pnpm canvas --resource <resourceId> set <docId> content
64
+
65
+ # Set (replace) notes from markdown
66
+ cat notes.md | pnpm canvas --resource <resourceId> set <docId> notes
67
+
68
+ # Set title
69
+ pnpm canvas --resource <resourceId> set <docId> title "New Title"
70
+
71
+ # Append to content
72
+ cat extra.md | pnpm canvas --resource <resourceId> append <docId> content
73
+
74
+ # Append to notes
75
+ cat extra.md | pnpm canvas --resource <resourceId> append <docId> notes
76
+
77
+ # Delete a document
78
+ pnpm canvas --resource <resourceId> delete <docId>
79
+ ```
80
+
81
+ ## Library API
82
+
83
+ The conversion functions can be imported directly for use in other tools:
84
+
85
+ ```typescript
86
+ import {markdownToCanvas} from './src/lib/markdownToCanvas'
87
+ import {canvasToMarkdown} from './src/lib/canvasToMarkdown'
88
+
89
+ // Markdown → Canvas Portable Text blocks
90
+ const blocks = markdownToCanvas('# Hello\n\nA paragraph with **bold**.')
91
+
92
+ // Canvas Portable Text blocks → Markdown
93
+ const markdown = canvasToMarkdown(blocks)
94
+ ```
95
+
96
+ ## Development
97
+
98
+ ```bash
99
+ # Run tests (37 tests covering all block types + roundtrip)
100
+ pnpm test
101
+
102
+ # Type check
103
+ pnpm typecheck
104
+
105
+ # Regenerate types after schema changes
106
+ pnpm typegen
107
+
108
+ # Lint
109
+ pnpm lint
110
+
111
+ # Build Studio
112
+ pnpm build
113
+
114
+ # Deploy Studio
115
+ pnpm deploy
116
+ ```
117
+
118
+ ## Project Structure
119
+
120
+ ```
121
+ src/
122
+ lib/
123
+ markdownToCanvas.ts # Markdown → Canvas PTE conversion
124
+ canvasToMarkdown.ts # Canvas PTE → Markdown conversion
125
+ __tests__/
126
+ canvasMarkdown.test.ts # 37 vitest tests
127
+ scripts/
128
+ canvas-write.ts # CLI tool
129
+ schemaTypes/ # Canvas document schema (mirrors sanity-io/canvas)
130
+ components/
131
+ SyncToCanvasInput.tsx # Studio sync component (browser-side)
132
+ structure.ts # Custom Studio structure
133
+ sanity.types.ts # Generated types (via pnpm typegen)
134
+ sanity.config.ts # Studio config
135
+ sanity.cli.ts # CLI config + typegen config
136
+ schema.json # Extracted schema (for typegen)
137
+ ```
138
+
139
+ ## Studio
140
+
141
+ This repo also includes a Sanity Studio deployed at [canvas-writer.sanity.studio](https://canvas-writer.sanity.studio/). The Studio mirrors the Canvas schema and includes a sync component that can push documents to Canvas from the browser. The CLI approach is generally more useful for agents.
package/SKILL.md ADDED
@@ -0,0 +1,225 @@
1
+ ---
2
+ name: canvas-skill
3
+ description: "Read and write Sanity Canvas documents. Three-verb CLI with path-based access, diff-based push, and notes diffing."
4
+ ---
5
+
6
+ # Canvas Skill
7
+
8
+ Read and write [Sanity Canvas](https://www.sanity.io/canvas) documents. Three verbs (`pull`, `push`, `list`), path-based access to content and notes, diff-based push for concurrent editing.
9
+
10
+ ## Prerequisites
11
+
12
+ - **Node.js 18+**
13
+ - **SANITY_TOKEN** — a global Sanity auth token (not project-scoped). Get one with `npx sanity debug --secrets`, or from [manage.sanity.io](https://manage.sanity.io) → API → Tokens.
14
+ - **Organization ID** — the Sanity org that owns the Canvas.
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ # Pull a Canvas doc to markdown
20
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId>
21
+
22
+ # Pull just the content (no notes, no frontmatter)
23
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> content
24
+
25
+ # Edit, then push changes back (diff-based — only changed blocks are patched)
26
+ cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push
27
+
28
+ # List all Canvas docs
29
+ npx sanity-canvas-skill@latest --org <orgId> list
30
+ ```
31
+
32
+ ## Three Verbs
33
+
34
+ | Verb | What it does |
35
+ |------|-------------|
36
+ | `pull` | Read document data → stdout as markdown |
37
+ | `push` | Write markdown from stdin → document (diff-based) |
38
+ | `list` | List documents in a Canvas resource |
39
+
40
+ ## Path Model
41
+
42
+ A Canvas document is a tree. The CLI exposes it through **paths**:
43
+
44
+ | Path | What it addresses |
45
+ |------|-------------------|
46
+ | (none) | Entire document (content + notes + title) |
47
+ | `content` | The content block array |
48
+ | `notes` | All notes |
49
+ | `notes[0]` | A single note (by index) |
50
+ | `notes[_key=="k"]` | A single note (by key) |
51
+ | `title` | The document title string |
52
+
53
+ Paths are positional arguments: `npx sanity-canvas-skill@latest pull <docId> <path>`
54
+
55
+ ## Pull
56
+
57
+ ```bash
58
+ # Full document (frontmatter + content + notes)
59
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId>
60
+
61
+ # Content blocks only (with key comments)
62
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> content
63
+
64
+ # All notes with headers
65
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> notes
66
+
67
+ # Single note by index or key
68
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> 'notes[0]'
69
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> 'notes[_key=="abc"]'
70
+
71
+ # Just the title
72
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> title
73
+
74
+ # Range reads (content or note body)
75
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> content --from <key> --to <key>
76
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> content --from <key>
77
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> content --range 0-10
78
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> content --range -5
79
+
80
+ # Outline — structural overview (heading hierarchy + block counts)
81
+ npx sanity-canvas-skill@latest --org <orgId> pull <docId> --outline
82
+ ```
83
+
84
+ ### Pull Output Format
85
+
86
+ Full document pull produces:
87
+
88
+ ```markdown
89
+ ---
90
+ id: n36GqUekRrdauhzK15EtOM
91
+ org: oSyH1iET5
92
+ rev: 2TJuQTedaNK0J2PgZg0gcu
93
+ ---
94
+
95
+ <!-- k:abc123 -->
96
+ # Document Title
97
+
98
+ <!-- k:def456 -->
99
+ First paragraph.
100
+
101
+ ---notes---
102
+
103
+ ## Note: "Research Sources" [fact] <!-- k:note1 -->
104
+
105
+ <!-- k:n1_b0 -->
106
+ Note body here.
107
+
108
+ ## Note: "Style Guide" [style] <!-- k:note2 -->
109
+
110
+ <!-- k:n2_b0 -->
111
+ Another note body.
112
+ ```
113
+
114
+ - **Frontmatter**: document ID, org, revision (for conflict detection)
115
+ - **`<!-- k:KEY -->`**: block identity preserved through roundtrip
116
+ - **`---notes---`**: delimiter between content and notes
117
+ - **`## Note: "Title" [category]`**: note headers with key comment
118
+
119
+ ## Push
120
+
121
+ Push is **diff-based by default** — only changed blocks are patched.
122
+
123
+ ```bash
124
+ # Full document push (diffs both content AND notes)
125
+ cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push
126
+
127
+ # Preview changes without applying
128
+ cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push --dry-run
129
+
130
+ # Force full overwrite (no diff)
131
+ cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push --force
132
+
133
+ # Targeted writes
134
+ npx sanity-canvas-skill@latest --org <orgId> push <docId> content --keys < blocks.md
135
+ npx sanity-canvas-skill@latest --org <orgId> push <docId> content --after <key> < new.md
136
+ npx sanity-canvas-skill@latest --org <orgId> push <docId> content --after-index 3 < new.md
137
+ npx sanity-canvas-skill@latest --org <orgId> push <docId> content --append < new.md
138
+ npx sanity-canvas-skill@latest --org <orgId> push <docId> title "New Title"
139
+
140
+ # Create a new document
141
+ cat content.md | npx sanity-canvas-skill@latest --org <orgId> push --new "Document Title"
142
+ ```
143
+
144
+ ### How Push Works
145
+
146
+ 1. Pulls the current document from Canvas (fresh `_rev` for locking)
147
+ 2. Parses your edited markdown back to Portable Text blocks
148
+ 3. Normalizes keys (restores span-level keys that markdown roundtrip regenerates)
149
+ 4. Computes minimal operations via `@sanity/diff`: SET, INSERT, UNSET
150
+ 5. Diffs both `content` AND `notes` arrays
151
+ 6. Applies all operations in a single atomic transaction with `ifRevisionId`
152
+
153
+ This means:
154
+ - **Only changed blocks are patched** — unchanged content is untouched
155
+ - **Notes are diffed too** — edit a note body, only that note is patched
156
+ - **Concurrent edits are safe** — 409 on conflict, re-pull and try again
157
+ - **New blocks** (without `<!-- k:KEY -->`) are inserted at the correct position
158
+
159
+ ## Programmatic API
160
+
161
+ ```typescript
162
+ import {
163
+ canvasToMarkdown,
164
+ markdownToCanvas,
165
+ normalizeKeys,
166
+ computeOps,
167
+ diffPush,
168
+ parseCanvasDoc,
169
+ serializeCanvasDoc,
170
+ resolveResourceId,
171
+ } from 'sanity-canvas-skill'
172
+ import {createClient} from '@sanity/client'
173
+
174
+ // Setup
175
+ const resourceId = await resolveResourceId(token, orgId)
176
+ const client = createClient({
177
+ resource: {type: 'canvas', id: resourceId},
178
+ token,
179
+ apiVersion: '2025-10-01',
180
+ useCdn: false,
181
+ })
182
+
183
+ // Pull → markdown
184
+ const doc = await client.fetch('*[_id == $id][0]{ _id, _rev, title, content, notes }', {id: docId})
185
+ const markdown = canvasToMarkdown(doc.content, {emitKeys: true})
186
+
187
+ // Markdown → blocks
188
+ const blocks = markdownToCanvas(markdown)
189
+
190
+ // Diff two block arrays
191
+ const normalized = normalizeKeys(originalBlocks, editedBlocks)
192
+ const ops = computeOps(originalBlocks, normalized)
193
+
194
+ // Full push pipeline (pull + diff + apply atomically)
195
+ const result = await diffPush(client, markdownFileContent)
196
+ // result: {docId, ops, summary, rev}
197
+ ```
198
+
199
+ ## Block Type Support
200
+
201
+ | Markdown | Canvas Type | Roundtrip |
202
+ |---|---|---|
203
+ | Paragraphs, headings, blockquotes | `block` (with style) | ✅ Lossless |
204
+ | Bold, italic, code, strikethrough | `span` marks | ✅ Lossless |
205
+ | Bullet/numbered lists | `block` with `listItem` | ✅ Lossless |
206
+ | Links `[text](url)` | `link` annotation | ✅ Lossless |
207
+ | Code blocks `` ```lang ``` `` | `canvasCode` | ✅ Lossless |
208
+ | Images `![alt](url)` | `canvasImage` | ✅ Lossless |
209
+ | Horizontal rules `---` | `canvasDivider` | ✅ Lossless |
210
+ | Unknown types | `<!-- canvas:TYPE:BASE64 -->` | ✅ Opaque preservation |
211
+
212
+ ## Key Concepts
213
+
214
+ - **Key comments** (`<!-- k:KEY -->`) — preserve block identity. Don't remove them unless you want to delete that block.
215
+ - **Generated keys** — new blocks get `g_` prefixed keys (e.g., `g_0`). Replaced by Canvas-generated keys on push.
216
+ - **Optimistic locking** — push uses `ifRevisionId`. If the doc changed, you get a 409. Re-pull and try again.
217
+ - **Opaque blocks** — unknown types preserved as `<!-- canvas:TYPE:BASE64 -->`. Don't edit these.
218
+ - **Notes diffing** — notes are diffed the same way as content. Edit a note body, only that note is patched.
219
+
220
+ ## Environment Variables
221
+
222
+ | Variable | Required | Description |
223
+ |---|---|---|
224
+ | `SANITY_TOKEN` | Yes | Global Sanity auth token (get with `npx sanity debug --secrets`) |
225
+ | `SANITY_ORG` | No | Default organization ID (alternative to `--org`) |
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}