emdash-emit 0.1.14 → 0.1.16
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.
- package/CHANGELOG.md +96 -1
- package/README.md +259 -188
- package/dist/bin/emit.d.ts.map +1 -1
- package/dist/bin/emit.js +2 -0
- package/dist/bin/emit.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +6 -0
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/media.d.ts +4 -0
- package/dist/cli/commands/media.d.ts.map +1 -0
- package/dist/cli/commands/media.js +128 -0
- package/dist/cli/commands/media.js.map +1 -0
- package/dist/cli/commands/pull.d.ts +32 -0
- package/dist/cli/commands/pull.d.ts.map +1 -1
- package/dist/cli/commands/pull.js +136 -9
- package/dist/cli/commands/pull.js.map +1 -1
- package/dist/cli/commands/push.d.ts.map +1 -1
- package/dist/cli/commands/push.js +18 -0
- package/dist/cli/commands/push.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +16 -5
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/client/media.d.ts.map +1 -1
- package/dist/client/media.js +16 -5
- package/dist/client/media.js.map +1 -1
- package/dist/db/cache.d.ts +19 -4
- package/dist/db/cache.d.ts.map +1 -1
- package/dist/db/cache.js +127 -16
- package/dist/db/cache.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +7 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/sync-doctor.d.ts.map +1 -1
- package/dist/mcp/tools/sync-doctor.js +12 -0
- package/dist/mcp/tools/sync-doctor.js.map +1 -1
- package/dist/mcp/tools/sync-media-search.d.ts +4 -0
- package/dist/mcp/tools/sync-media-search.d.ts.map +1 -0
- package/dist/mcp/tools/sync-media-search.js +102 -0
- package/dist/mcp/tools/sync-media-search.js.map +1 -0
- package/dist/mcp/tools/sync-pull.d.ts.map +1 -1
- package/dist/mcp/tools/sync-pull.js +85 -13
- package/dist/mcp/tools/sync-pull.js.map +1 -1
- package/dist/mcp/tools/sync-push.d.ts.map +1 -1
- package/dist/mcp/tools/sync-push.js +5 -0
- package/dist/mcp/tools/sync-push.js.map +1 -1
- package/dist/mcp/tools/sync-status.d.ts.map +1 -1
- package/dist/mcp/tools/sync-status.js +6 -2
- package/dist/mcp/tools/sync-status.js.map +1 -1
- package/dist/sync/delete.d.ts +2 -1
- package/dist/sync/delete.d.ts.map +1 -1
- package/dist/sync/delete.js +6 -19
- package/dist/sync/delete.js.map +1 -1
- package/dist/sync/diff.d.ts.map +1 -1
- package/dist/sync/diff.js +18 -2
- package/dist/sync/diff.js.map +1 -1
- package/dist/sync/doctor.d.ts +27 -1
- package/dist/sync/doctor.d.ts.map +1 -1
- package/dist/sync/doctor.js +309 -26
- package/dist/sync/doctor.js.map +1 -1
- package/dist/sync/guards.d.ts +45 -0
- package/dist/sync/guards.d.ts.map +1 -0
- package/dist/sync/guards.js +236 -0
- package/dist/sync/guards.js.map +1 -0
- package/dist/sync/media/body_scan.d.ts +15 -0
- package/dist/sync/media/body_scan.d.ts.map +1 -0
- package/dist/sync/media/body_scan.js +132 -0
- package/dist/sync/media/body_scan.js.map +1 -0
- package/dist/sync/media/companions.d.ts +32 -0
- package/dist/sync/media/companions.d.ts.map +1 -0
- package/dist/sync/media/companions.js +260 -0
- package/dist/sync/media/companions.js.map +1 -0
- package/dist/sync/media/index.d.ts +9 -0
- package/dist/sync/media/index.d.ts.map +1 -0
- package/dist/sync/media/index.js +9 -0
- package/dist/sync/media/index.js.map +1 -0
- package/dist/sync/media/mirror_item.d.ts +27 -0
- package/dist/sync/media/mirror_item.d.ts.map +1 -0
- package/dist/sync/media/mirror_item.js +207 -0
- package/dist/sync/media/mirror_item.js.map +1 -0
- package/dist/sync/media/naming_mirror.d.ts +36 -0
- package/dist/sync/media/naming_mirror.d.ts.map +1 -0
- package/dist/sync/media/naming_mirror.js +143 -0
- package/dist/sync/media/naming_mirror.js.map +1 -0
- package/dist/sync/media/pull_article.d.ts +37 -0
- package/dist/sync/media/pull_article.d.ts.map +1 -0
- package/dist/sync/media/pull_article.js +273 -0
- package/dist/sync/media/pull_article.js.map +1 -0
- package/dist/sync/media/pull_library.d.ts +12 -0
- package/dist/sync/media/pull_library.d.ts.map +1 -0
- package/dist/sync/media/pull_library.js +255 -0
- package/dist/sync/media/pull_library.js.map +1 -0
- package/dist/sync/media/remote_index.d.ts +49 -0
- package/dist/sync/media/remote_index.d.ts.map +1 -0
- package/dist/sync/media/remote_index.js +241 -0
- package/dist/sync/media/remote_index.js.map +1 -0
- package/dist/sync/media/types.d.ts +237 -0
- package/dist/sync/media/types.d.ts.map +1 -0
- package/dist/sync/media/types.js +11 -0
- package/dist/sync/media/types.js.map +1 -0
- package/dist/sync/media.d.ts +1 -74
- package/dist/sync/media.d.ts.map +1 -1
- package/dist/sync/media.js +1 -592
- package/dist/sync/media.js.map +1 -1
- package/dist/sync/orchestrator.d.ts +23 -0
- package/dist/sync/orchestrator.d.ts.map +1 -1
- package/dist/sync/orchestrator.js +85 -1
- package/dist/sync/orchestrator.js.map +1 -1
- package/dist/sync/preflight.d.ts +1 -1
- package/dist/sync/preflight.d.ts.map +1 -1
- package/dist/sync/preflight.js +129 -17
- package/dist/sync/preflight.js.map +1 -1
- package/dist/sync/pull.d.ts +19 -0
- package/dist/sync/pull.d.ts.map +1 -1
- package/dist/sync/pull.js +169 -15
- package/dist/sync/pull.js.map +1 -1
- package/dist/sync/push.d.ts +7 -0
- package/dist/sync/push.d.ts.map +1 -1
- package/dist/sync/push.js +37 -10
- package/dist/sync/push.js.map +1 -1
- package/dist/sync/status.d.ts +4 -0
- package/dist/sync/status.d.ts.map +1 -1
- package/dist/sync/status.js +68 -4
- package/dist/sync/status.js.map +1 -1
- package/dist/utils/config.d.ts +25 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +28 -2
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +15 -4
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/media-body-refs.d.ts +81 -0
- package/dist/utils/media-body-refs.d.ts.map +1 -0
- package/dist/utils/media-body-refs.js +240 -0
- package/dist/utils/media-body-refs.js.map +1 -0
- package/dist/utils/media-refs.d.ts +44 -0
- package/dist/utils/media-refs.d.ts.map +1 -0
- package/dist/utils/media-refs.js +101 -0
- package/dist/utils/media-refs.js.map +1 -0
- package/dist/utils/naming.d.ts +31 -0
- package/dist/utils/naming.d.ts.map +1 -1
- package/dist/utils/naming.js +58 -0
- package/dist/utils/naming.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
- [Multi-Site Management](#3-multi-site-management)
|
|
27
27
|
- [Recommended `.gitignore`](#4-recommended-gitignore)
|
|
28
28
|
- [📂 Directory Structure & Data Specifications](#-directory-structure--data-specifications)
|
|
29
|
+
- [Two Media ID Spaces: `id` vs `storageKey`](#two-media-id-spaces-id-vs-storagekey)
|
|
29
30
|
- [📝 Markdown & YAML Frontmatter Specification](#-markdown--yaml-frontmatter-specification)
|
|
30
31
|
- [Frontmatter Schema](#frontmatter-schema)
|
|
31
32
|
- [Critical Content Rules](#critical-content-rules)
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
- [Media Management & Upload Workflow](#media-management--upload-workflow)
|
|
34
35
|
- [🤖 The Golden Path for AI & Humans (4-Step Cycle)](#-the-golden-path-for-ai--humans-4-step-cycle)
|
|
35
36
|
- [The 4-Step Authoring Cycle](#the-4-step-authoring-cycle)
|
|
36
|
-
- [Preflight Safety Guardrails (G1–
|
|
37
|
+
- [Preflight Safety Guardrails (G1–G19)](#preflight-safety-guardrails-g1g19)
|
|
37
38
|
- [🛠 CLI Command Reference](#-cli-command-reference)
|
|
38
39
|
- [`emit init`](#emit-init)
|
|
39
40
|
- [`emit profile`](#emit-profile)
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
- [`emit diff`](#emit-diff)
|
|
48
49
|
- [`emit pull-taxonomies`](#emit-pull-taxonomies)
|
|
49
50
|
- [`emit pull-media`](#emit-pull-media)
|
|
51
|
+
- [`emit media-search`](#emit-media-search)
|
|
50
52
|
- [`emit guide`](#emit-guide)
|
|
51
53
|
- [`emit mcp`](#emit-mcp)
|
|
52
54
|
- [🤖 Model Context Protocol (MCP) Support](#-model-context-protocol-mcp-support)
|
|
@@ -80,20 +82,23 @@ EmDash is a modern serverless headless CMS built on Cloudflare Workers and D1. W
|
|
|
80
82
|
|
|
81
83
|
To prevent user expectations from misaligning with the tool's purpose, it is important to clarify **what Emit is and what it is not**:
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
- **What Emit IS (Core Goal)**:
|
|
86
|
+
- An **active authoring and editing workbench (Staging Desk)** where human authors and AI coding agents collaborate seamlessly.
|
|
87
|
+
- A tool focused on drafting, reviewing, polishing, diffing, and publishing current and recent articles with strict safety guards—including **automatic offline draft image upload** (`media/drafts/`) for images referenced in the article being pushed.
|
|
88
|
+
- **What Emit is NOT (Non-Goals)**:
|
|
89
|
+
- **Not a full-site database archiver / backup dumper**: Emit is not designed to clone an entire 15-year history of 20,000 blog posts or an entire multi-gigabyte media library onto your laptop.
|
|
90
|
+
- **Not an offline bulk mass-migration converter**: Emit does not perform batch transformation across thousands of historical posts or bulk media migrations at once.
|
|
89
91
|
|
|
90
92
|
If your site contains thousands of articles, they remain safe, searchable, and intact in your Cloudflare D1 database. Emit only pulls the active working set into your local workspace, keeping your directory clean and searches instant. Need to edit a specific older article outside the active window? Simply pull it on demand using `emit pull <article-id>`.
|
|
91
93
|
|
|
92
|
-
These Non-Goals are **deliberate design decisions, not missing capabilities**: raising `postsSyncLimit` (see below) can in fact fetch tens of thousands of articles, so Emit
|
|
94
|
+
These Non-Goals are **deliberate design decisions, not missing capabilities**: raising `postsSyncLimit` (see below) can in fact fetch tens of thousands of articles, so Emit _could_ clone a whole site — it chooses not to expose that as its default workflow. The sliding window is a boundary on what stays _on disk_, never on what you can _reach_: every historical article remains editable through an on-demand `emit pull <id>`, and writing a long-form piece that cites or revises many past posts works exactly the same way — fetch each referenced article by ULID, edit it, push it. For operations that genuinely belong to the whole library (bulk media management, site-wide content queries), pair Emit with the [official EmDash MCP](#6-division-of-labor-with-the-official-emdash-mcp) instead of asking Emit to reinvent them.
|
|
93
95
|
|
|
94
96
|
> [!NOTE]
|
|
95
97
|
> **Can the sync limit be adjusted — and why is there no `--all` for articles?**
|
|
96
|
-
>
|
|
98
|
+
>
|
|
99
|
+
> **Articles**: Set `postsSyncLimit: 300` (or any larger number, e.g. your full article count) in `.emit/config.json`, and Emit's internal pagination engine automatically batch-fetches up to that configured limit. What Emit deliberately omits is a `--all` _flag_ for articles: pulling the entire history into the workspace is an explicit configuration choice, not a routine default, so the "staging desk" discipline is preserved in day-to-day UX while remaining fully escapable when you truly want a larger working set.
|
|
100
|
+
>
|
|
101
|
+
> **Media**: Media is handled via an on-demand local-first pipeline — referenced media files in pulled articles are mirrored automatically. While `emit pull-media --all` exists for full offline backup or migration diagnostics, it is not intended for routine workflows because pulling thousands of binary files can overwhelm local disk and network bandwidth.
|
|
97
102
|
|
|
98
103
|
### 2. The Sliding Window "Staging Desk" Model
|
|
99
104
|
|
|
@@ -101,17 +106,17 @@ Traditional synchronization tools attempt to download the entire history of a si
|
|
|
101
106
|
|
|
102
107
|
Emit adopts a **bounded sliding window**:
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
- It maintains an active sliding window of recent posts (default: **latest 30 posts per status**, up to 60 files total; the legacy `syncLimit` config key is still accepted for backwards compatibility).
|
|
110
|
+
- When older posts fall outside the sliding window during `emit pull`, Emit cleanly **prunes them from disk**, keeping your local repository lean, fast, and organized.
|
|
111
|
+
- Any pruned article can be pulled back into the workspace at any time with `emit pull <id>`.
|
|
107
112
|
|
|
108
113
|
### 3. Unpushed Edits Safeguard
|
|
109
114
|
|
|
110
115
|
What happens if an article with unpushed local modifications is pushed out of the sliding window?
|
|
111
116
|
Emit guarantees **zero data loss**:
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
- If an article has local modifications not yet pushed to the remote server, Emit's pruning engine **aborts the pull immediately** with a `SyncPruneConflictError`.
|
|
119
|
+
- It lists the exact unpushed file and informs the user to either push the changes (`emit push <file>`) or discard them before proceeding.
|
|
115
120
|
|
|
116
121
|
### 4. Pure TypeScript & Zero Native Builds
|
|
117
122
|
|
|
@@ -122,36 +127,37 @@ By leveraging Node.js 22's built-in `node:sqlite` module (`DatabaseSync`), Emit
|
|
|
122
127
|
|
|
123
128
|
To prevent concurrent race conditions when multiple authors or AI agents collaborate:
|
|
124
129
|
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
- Emit acquires a distributed 7-minute lease via `POST /_emdash/api/content/{collection}/{id}/lock` before writing to remote articles, with automatic heartbeats and `try/finally` release guarantees.
|
|
131
|
+
- If concurrent edits occur, Emit utilizes a **Hybrid 3-Way Merge** engine (`node-diff3`), merging YAML frontmatter fields and Markdown body text independently.
|
|
127
132
|
|
|
128
133
|
### 6. Division of Labor with the Official EmDash MCP
|
|
129
134
|
|
|
130
135
|
Emit is intentionally not a complete EmDash client. The [official EmDash MCP server](https://github.com/aroooy/emdash) (and the Admin Dashboard) remains the canonical surface for whole-library operations, and the two tools are designed to be used **side by side against the same site**:
|
|
131
136
|
|
|
132
|
-
| Operation
|
|
133
|
-
|
|
|
134
|
-
| Drafting, reviewing, diffing, publishing articles with safety guards
|
|
135
|
-
| On-demand retrieval of any single historical article by ULID
|
|
136
|
-
|
|
|
137
|
-
|
|
|
137
|
+
| Operation | Right Tool |
|
|
138
|
+
| --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
139
|
+
| Drafting, reviewing, diffing, publishing articles with safety guards | **Emit** (`emit pull` / `emit push`, MCP `sync_*` tools) |
|
|
140
|
+
| On-demand retrieval of any single historical article by ULID | **Emit** (`emit pull <id>`) |
|
|
141
|
+
| Finding a media asset to use in the article being drafted (keyword search over the remote library, with mirror status) | **Emit** (`emit media-search` / MCP `sync_media_search`) |
|
|
142
|
+
| Bulk media library management (mass upload, mass delete, full-library listing) | **Official EmDash MCP** (e.g. `media_upload`) / Admin Dashboard |
|
|
143
|
+
| Site-wide content queries, arbitrary collection operations outside the authoring loop | **Official EmDash MCP** raw content tools |
|
|
138
144
|
|
|
139
|
-
This division is a design stance, not a limitation to work around: Emit focuses its guardrails (G1–G14, locks, 3-way merge, quarantine) on the
|
|
145
|
+
This division is a design stance, not a limitation to work around: Emit focuses its guardrails (G1–G14, locks, 3-way merge, quarantine) on the _authoring loop_ where data loss actually happens, and leaves fleet-scale operations to the server-native tooling that already handles them safely.
|
|
140
146
|
|
|
141
147
|
---
|
|
142
148
|
|
|
143
149
|
## 🌟 Key Features
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
- **AI-First Authoring Workflow**: Built specifically for autonomous agents and humans to draft, inspect, and sync local Markdown files safely.
|
|
152
|
+
- **Sliding Window Synchronization**: Automatically manages an active sliding window of recent posts (default: latest 30 published + 30 drafts = up to 60 files), pruning older cached entries from disk. The window bounds what stays on disk, not what you can reach — any historical article remains available via `emit pull <id>`, and the window size is configurable (`postsSyncLimit`) when a larger working set is genuinely needed.
|
|
153
|
+
- **Unpushed Edits Safeguard**: Detects unpushed local changes outside the sync window and aborts sync safely before any files can be deleted.
|
|
154
|
+
- **Git-Style Workspace Encapsulation**: Houses local SQLite cache (`node:sqlite`) and workspace configuration neatly inside a `.emit/` hidden folder.
|
|
155
|
+
- **Multi-Site Profile Management**: Manage multiple EmDash sites and credentials in `~/.config/emit/profiles.yaml` with automatic workspace directory detection.
|
|
156
|
+
- **Hybrid 3-Way Merge Engine**: Resolves concurrent changes between remote and local versions like Git, independently merging YAML frontmatter and Markdown body.
|
|
157
|
+
- **Distributed Mutual Exclusion Lock**: Acquires 7-minute leases via `POST /lock` with `try/finally` release semantics to eliminate race conditions during publishing.
|
|
158
|
+
- **Opaque Block Preservation**: Preserves EmDash custom blocks (e.g., `core:gallery`) losslessly across AST roundtrips via `<!--ec:block ... -->` fences.
|
|
159
|
+
- **Taxonomy & Media Synchronization**: Sync category/tag YAML schemas and download media assets with metadata. EmDash identifies each asset by two separate ULIDs (record `id` for the API, `storageKey` for the file route), and Emit keeps them strictly separated — see [Two Media ID Spaces](#two-media-id-spaces-id-vs-storagekey).
|
|
160
|
+
- **Zero Native Dependencies**: Completely eliminates native C++ builds (`node-gyp`) by leveraging Node.js 22+ built-in `node:sqlite`.
|
|
155
161
|
|
|
156
162
|
---
|
|
157
163
|
|
|
@@ -205,9 +211,9 @@ emit profile add my-blog \
|
|
|
205
211
|
--workspace /path/to/local/blog-repo
|
|
206
212
|
```
|
|
207
213
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
214
|
+
- `--origin`: The base URL of your EmDash Cloudflare Workers instance.
|
|
215
|
+
- `--token`: Your Personal Access Token (PAT) generated from the EmDash dashboard.
|
|
216
|
+
- `--workspace`: The local directory where articles and assets will be synchronized.
|
|
211
217
|
|
|
212
218
|
> [!NOTE]
|
|
213
219
|
> Profiles are securely stored with `0600` permissions at `~/.config/emit/profiles.yaml` (on macOS / Linux) or `%USERPROFILE%\.config\emit\profiles.yaml` (on Windows).
|
|
@@ -244,6 +250,7 @@ emit profile list
|
|
|
244
250
|
```
|
|
245
251
|
|
|
246
252
|
**How Automatic Profile Resolution Works:**
|
|
253
|
+
|
|
247
254
|
1. **Directory-Driven Auto Detection (Recommended)**: When you `cd ~/blogs/personal` and run `emit pull`, `emit push`, or launch an AI agent, Emit detects your current folder and automatically uses the `personal` profile. When you `cd ~/blogs/work`, it seamlessly connects to `work`. Directory comparison is symlink-safe (`fs.realpathSync`), so a workspace reached through a symlinked path (or a macOS `/tmp`-style alias) still resolves to the right profile.
|
|
248
255
|
2. **Explicit Profile Flag**: You can explicitly specify `--profile <name>` (or pass `profile: "<name>"` in MCP) from any directory to override automatic detection.
|
|
249
256
|
3. **Ambiguity Safeguard**: If you run a command outside of any registered workspace and omit `--profile`, Emit aborts with an `AmbiguousProfileError` instead of guessing or silently pushing to the wrong site.
|
|
@@ -265,7 +272,7 @@ When credentials include a `refresh_token` (such as OAuth sessions in `~/.config
|
|
|
265
272
|
The target instance (`origin`) is resolved by one shared function (`resolveOriginPriority()` in `src/cli/context.ts`, reused by the MCP server) so CLI and MCP always agree:
|
|
266
273
|
|
|
267
274
|
1. **Explicitly selected profile** — `--profile <name>` (or MCP `profile: "<name>"`)
|
|
268
|
-
2. **`EMDASH_ORIGIN`** — overrides an
|
|
275
|
+
2. **`EMDASH_ORIGIN`** — overrides an _implicitly_ resolved profile so CI/staging overrides actually take effect
|
|
269
276
|
3. **Auto-resolved profile** — current-directory match, unique origin match, or the single registered profile
|
|
270
277
|
4. **Resolved auth origin** — carried by `~/.config/emdash/auth.json` / `~/.config/emit/credentials.json`
|
|
271
278
|
5. **Workspace `.emit/config.json`** → then the `https://example.com` sentinel default
|
|
@@ -282,20 +289,21 @@ Running `emit init` generates `.emit/config.json` with the following keys (`src/
|
|
|
282
289
|
"origin": "https://your-blog.example.com",
|
|
283
290
|
"defaultCollection": "posts",
|
|
284
291
|
"defaultLocale": "ja",
|
|
285
|
-
"postsSyncLimit":
|
|
292
|
+
"postsSyncLimit": 30,
|
|
286
293
|
"mediaSyncLimit": 300,
|
|
287
294
|
"concurrency": 8
|
|
288
295
|
}
|
|
289
296
|
```
|
|
290
297
|
|
|
291
|
-
| Key
|
|
292
|
-
|
|
|
293
|
-
| `origin`
|
|
294
|
-
| `defaultCollection` | `"posts"`
|
|
295
|
-
| `defaultLocale`
|
|
296
|
-
| `postsSyncLimit`
|
|
297
|
-
| `mediaSyncLimit`
|
|
298
|
-
| `
|
|
298
|
+
| Key | Default | Description |
|
|
299
|
+
| ------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
300
|
+
| `origin` | `"https://example.com"` | Base URL of the target EmDash instance, exactly as written to disk. It is never rewritten in-memory by `EMDASH_ORIGIN`; see [Origin Resolution Precedence](#origin-resolution-precedence) for how the effective origin is chosen |
|
|
301
|
+
| `defaultCollection` | `"posts"` | Default content collection when `--collection` is omitted |
|
|
302
|
+
| `defaultLocale` | `"ja"` | Default locale used when scaffolding new drafts via `emit new` / `sync_new` |
|
|
303
|
+
| `postsSyncLimit` | `30` | Sliding window size for batch article pulls (when `status: all`, fetches up to `postsSyncLimit` `published` + up to `postsSyncLimit` `draft` articles in parallel; legacy `syncLimit` is also accepted) |
|
|
304
|
+
| `mediaSyncLimit` | `300` | Default maximum items fetched by `emit pull-media` / `sync_pull_media` |
|
|
305
|
+
| `localMediaRefs` | `true` | Default-ON. Mirrored body images are stored as article-relative `../../media/<storageKey>` references (depth derived from the article's own path) instead of canonical `/_emdash/api/media/file/<storageKey>` URLs, so Markdown preview resolves them offline. Opt out with `emit pull --no-local-media-refs`; pull, push, diff, and MCP all honour it |
|
|
306
|
+
| `concurrency` | `8` | Parallel HTTP fetch concurrency during batch pulls |
|
|
299
307
|
|
|
300
308
|
### 6. Recommended `.gitignore`
|
|
301
309
|
|
|
@@ -325,10 +333,11 @@ tmp/
|
|
|
325
333
|
```
|
|
326
334
|
|
|
327
335
|
> [!TIP]
|
|
328
|
-
>
|
|
329
|
-
>
|
|
330
|
-
>
|
|
331
|
-
>
|
|
336
|
+
>
|
|
337
|
+
> - `.emit/cache.db*` **must be excluded** from Git because it is a machine-local SQLite tracking database.
|
|
338
|
+
> - `.emit/config.json` **can be safely committed** to Git because it contains workspace configuration (origin URL, default collections), while sensitive authentication tokens remain securely isolated in `~/.config/emit/profiles.yaml`.
|
|
339
|
+
> - `media/*.yaml` metadata files can be committed to Git to track asset metadata (`storageKey`, `contentHash`, `status`, dimensions) alongside your Markdown articles.
|
|
340
|
+
> - **Local-First Git Tracking for `media/`**: If you use your Git repository as a self-contained Local-First source of truth (including offline drafts in `media/drafts/`), keep `media/` tracked in Git (or via Git LFS). If you treat Cloudflare R2 as the sole binary store and want a lightweight Git repo, uncomment the `media/*.jpg` ignore rules above while keeping `media/*.yaml` tracked.
|
|
332
341
|
|
|
333
342
|
---
|
|
334
343
|
|
|
@@ -347,7 +356,7 @@ your-blog/
|
|
|
347
356
|
├── tags/ # Tag definition files (<id>-<label>.yaml)
|
|
348
357
|
│ ├── 01M6TAG1-Typescript.yaml
|
|
349
358
|
│ └── 01M6TAG2-Ai.yaml
|
|
350
|
-
├── media/ # Media files and companion metadata (
|
|
359
|
+
├── media/ # Media files and companion metadata (`<markdownRef>.<ext>` + `.yaml`; markdownRef is derived from the storageKey)
|
|
351
360
|
│ ├── drafts/ # Staging folder for offline draft images (auto-uploaded on push)
|
|
352
361
|
│ │ └── sunset.png
|
|
353
362
|
│ ├── 20261101_01M6ABCDEF-header.jpg
|
|
@@ -366,13 +375,26 @@ your-blog/
|
|
|
366
375
|
|
|
367
376
|
Emit maintains a local SQLite database via Node.js built-in `node:sqlite` (`src/db/cache.ts`) with 5 tables:
|
|
368
377
|
|
|
369
|
-
| Table
|
|
370
|
-
|
|
|
371
|
-
| `entries`
|
|
372
|
-
| `base_snapshots` | Stores the baseline Markdown body and baseline frontmatter at the last synced revision to power Hybrid 3-Way Merge (`frontmatter_yaml` holds a **JSON-encoded** `Frontmatter` object, not YAML text)
|
|
373
|
-
| `sync_state`
|
|
374
|
-
| `bylines`
|
|
375
|
-
| `media_state`
|
|
378
|
+
| Table | Purpose |
|
|
379
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
380
|
+
| `entries` | Caches the latest remote JSON envelope and revision (`rev`) for each synced article |
|
|
381
|
+
| `base_snapshots` | Stores the baseline Markdown body and baseline frontmatter at the last synced revision to power Hybrid 3-Way Merge (`frontmatter_yaml` holds a **JSON-encoded** `Frontmatter` object, not YAML text) |
|
|
382
|
+
| `sync_state` | Tracks each article's workspace-relative file path and SHA-256 content hash to detect local modifications (`synced` vs `modified` vs `untracked`, plus `invalid` when YAML frontmatter fails parsing; `invalid` files are excluded from `emit push --all`, cached `invalid` files are automatically self-healed on batch `emit pull` / `sync_pull`, and uncached `invalid` files outside the sliding window can be repaired via `emit pull <ULID>` or `sync_pull(id: "<ULID>")`) |
|
|
383
|
+
| `bylines` | Caches remote author records (`id`, `slug`, `display_name`, `is_guest`) synced automatically via `/_emdash/api/mcp` (`byline_list`) on pull, enabling slug ⇔ ULID resolution and default author selection |
|
|
384
|
+
| `media_state` | Tracks offline draft image uploads and mirrored assets with **`storage_key` as PRIMARY KEY** (Schema v5): `media_id`, `local_content_hash` (SHA-256 of the local mirror bytes), `remote_record_ref` (`media-record:<id>` for remote mirrors), `url`, `workspace_path`, `markdown_ref`, `uploaded_at`, and `status`: `pending` \| `uploaded` \| `ready` \| `failed` — for idempotency, retry recovery, cross-article asset reuse, and index-based lookups (indexed on `media_id`, `status`, and `local_content_hash`). Note the two ULIDs in one row: `media_id` is the D1 record id and `storage_key` is the R2 object key; `markdown_ref` is the workspace-relative path of the mirror named after that storageKey — see [Two Media ID Spaces](#two-media-id-spaces-id-vs-storagekey). |
|
|
385
|
+
|
|
386
|
+
### Two Media ID Spaces: `id` vs `storageKey`
|
|
387
|
+
|
|
388
|
+
Every EmDash asset carries **two unrelated 26-character ULIDs**: the D1 media record `id` (primary key, used by `GET /_emdash/api/media/{id}`) and the R2 object `storageKey` (the filename served by `/_emdash/api/media/file/{storageKey}`). They are generated independently, so for the same image the local mirror name `media/<yyyyMMdd>_<record-id>-<name>.<ext>` never matches the storageKey baked into its own body URL — and neither value is a typo of the other.
|
|
389
|
+
|
|
390
|
+
Consequences worth internalizing before debugging media:
|
|
391
|
+
|
|
392
|
+
- A record `id` on the file route returns **404**; a `storageKey` on `GET /_emdash/api/media/{id}` returns **401** (no bearer token) or **404**. Both codes describe the _lookup_, never the health of the asset.
|
|
393
|
+
- Because a 404 proves only that one lookup failed, Emit never concludes a record is missing and never suggests inserting one — see `[UNRESOLVED_MEDIA_ASSET]` under [`emit pull-media`](#emit-pull-media).
|
|
394
|
+
- An old article whose images do not render **locally** is usually an unmirrored asset (the media window is newest-first, `mediaSyncLimit` default 300) rather than a broken reference. `emit doctor --check-media-library` performs a metadata-only walk comparing remote size against local mirrors, and `emit pull-media --article <id>` fetches referenced images on demand without pulling the full library (reserve `emit pull-media --all` for complete offline backups).
|
|
395
|
+
- Local mirrors are named after the **storageKey** (`media/<markdownRef>.<ext>`), so a body URL and the file on disk share one key. Workspaces built before this rule still hold legacy `media/<yyyyMMdd>_<id>-<name>.<ext>` names; pulling an article (or a one-time maintenance run of `emit pull-media --all`) renames them in place with no re-downloading, and Emit reads both forms meanwhile.
|
|
396
|
+
|
|
397
|
+
The full specification — identifier table, endpoint matrix, migrated-corpus behavior, and resolution order (`verifiedStorageKeys` → `orphanStorageKeys`) — lives in **[docs/media-id-spaces.md](docs/media-id-spaces.md)**.
|
|
376
398
|
|
|
377
399
|
---
|
|
378
400
|
|
|
@@ -384,40 +406,39 @@ Each article is stored as a Markdown file with a validated YAML frontmatter head
|
|
|
384
406
|
|
|
385
407
|
```yaml
|
|
386
408
|
---
|
|
387
|
-
id: 01M6ABCDEF1234567890123456
|
|
409
|
+
id: 01M6ABCDEF1234567890123456 # 26-char canonical ULID (auto-generated; DO NOT EDIT)
|
|
388
410
|
slug: building-ai-powered-cms-tools
|
|
389
|
-
title:
|
|
390
|
-
collection: posts
|
|
391
|
-
status: draft
|
|
392
|
-
publishedAt: null
|
|
393
|
-
locale: ja
|
|
394
|
-
featuredImage:
|
|
395
|
-
taxonomies:
|
|
411
|
+
title: 'Building AI-Powered CMS Tools'
|
|
412
|
+
collection: posts # Target collection (defaults to "posts")
|
|
413
|
+
status: draft # "draft" or "published" (defaults to "draft")
|
|
414
|
+
publishedAt: null # ISO 8601 timestamp string (e.g. "2026-11-04T12:00:00.000Z") or null
|
|
415
|
+
locale: ja # Locale code (defaults to "ja")
|
|
416
|
+
featuredImage: '01M67P0MBMJQQXH2K95HHCYRYB' # Optional: 26-char media record ULID (uppercase-normalized) or null — NOT a storageKey
|
|
417
|
+
taxonomies: # Map of taxonomy key -> array of term slugs (or canonical term ULIDs)
|
|
396
418
|
categories:
|
|
397
419
|
- tech
|
|
398
420
|
tags:
|
|
399
421
|
- typescript
|
|
400
422
|
- ai
|
|
401
|
-
seo:
|
|
402
|
-
title: null
|
|
403
|
-
description: null
|
|
404
|
-
canonical: null
|
|
405
|
-
noIndex: false
|
|
406
|
-
image: null
|
|
407
|
-
bylines:
|
|
423
|
+
seo: # Optional SEO metadata block
|
|
424
|
+
title: null # Custom SEO title string or null
|
|
425
|
+
description: null # Meta description string or null
|
|
426
|
+
canonical: null # Absolute URL (e.g. "https://example.com/posts/...") or null
|
|
427
|
+
noIndex: false # Boolean (optional)
|
|
428
|
+
image: null # SEO image string or null
|
|
429
|
+
bylines: # Author byline slugs (resolved via cached bylines table)
|
|
408
430
|
- aroooy
|
|
409
431
|
---
|
|
410
|
-
|
|
411
432
|
Markdown body begins here...
|
|
412
|
-
|
|
413
433
|
```
|
|
414
434
|
|
|
415
435
|
> [!IMPORTANT]
|
|
416
436
|
> **Schema Validation & Serialization Rules (`src/utils/frontmatter.ts`)**:
|
|
417
|
-
>
|
|
418
|
-
>
|
|
419
|
-
>
|
|
420
|
-
>
|
|
437
|
+
>
|
|
438
|
+
> - **Always nest categories and tags under `taxonomies:`**: `parseMarkdown()` validates frontmatter with Zod (`FrontmatterSchema`), which strips unknown top-level keys, and `serializeMarkdown()` regenerates the YAML header on every pull and push. Writing `categories:` or `tags:` at the top level will silently discard them without triggering Preflight Guard G4. `taxonomies` accepts `categories`, `tags`, and any additional custom taxonomy keys defined in EmDash (`z.record(z.array(z.string()))`).
|
|
439
|
+
> - **`seo.canonical` must be an absolute URL**: Validated via `z.string().url().nullable().optional()`. Relative paths such as `"/posts/my-slug"` will fail frontmatter parsing; always specify a full `https://...` URL or `null`.
|
|
440
|
+
> - **Author `bylines` auto-selection**: When scaffolding with `emit new` / `sync_new` without `--byline` / `byline`, Emit populates `bylines` with the first cached non-guest author ordered by `slug ASC` from `.emit/cache.db`. On single-author sites this is always your author slug; on multi-author sites, pass `-b, --byline <slug>` (or `byline` in MCP) or edit `bylines:` in frontmatter explicitly.
|
|
441
|
+
> - **`id` and `featuredImage` normalization**: `id` must be an exact 26-character ULID. `featuredImage` values matching a 26-character Crockford base32 ULID are normalized to uppercase automatically.
|
|
421
442
|
|
|
422
443
|
### Critical Content Rules
|
|
423
444
|
|
|
@@ -434,7 +455,7 @@ Markdown body begins here...
|
|
|
434
455
|
- Emit automatically updates the remote state and moves the file from `posts/draft/` to `posts/published/`.
|
|
435
456
|
- **Never move or rename files manually using `mv`**.
|
|
436
457
|
4. **Featured Image References**:
|
|
437
|
-
- `featuredImage` must be a 26-character canonical media ULID (e.g. `01M67P0MBMJQQXH2K95HHCYRYB`) or `null
|
|
458
|
+
- `featuredImage` must be a 26-character canonical media **record** ULID (e.g. `01M67P0MBMJQQXH2K95HHCYRYB`) or `null` — never a `storageKey`, which is a different ULID for the same asset ([Two Media ID Spaces](#two-media-id-spaces-id-vs-storagekey)). Emit normalizes the value to uppercase, so lowercase input is accepted and canonicalized on pull, merge, and push.
|
|
438
459
|
- To remove the hero image, set `featuredImage: null`. Omitting the field leaves the remote value untouched.
|
|
439
460
|
- When the referenced ULID matches the cached remote entry, Emit resends the original remote `featured_image` object verbatim (provider, `src`, `meta.storageKey`, dimensions, MIME type) so nothing is lost. When it does not match, Emit sends the bare `{ id: <ULID> }` and lets EmDash resolve the media record. **This contract is confirmed against a live EmDash instance:** `featured_image: { "id": "<ULID>" }` is accepted (HTTP 200) and the server stores the id and normalizes `"provider": "local"` itself.
|
|
440
461
|
- Emit never fabricates media metadata from local `media/*.yaml` files: companion files are used only to cross-check a reference. In a workspace that mirrors media (`media/` exists and is non-empty), a missing or mismatched companion file is reported as a push warning — it means Emit could not confirm the media ID exists, not that the payload is invalid. Workspaces that do not mirror media produce no such warning.
|
|
@@ -447,19 +468,18 @@ EmDash supports rich visual blocks (e.g., galleries, callouts, audio players). W
|
|
|
447
468
|
```html
|
|
448
469
|
<!--ec:block type="core:gallery" id="01M6XYZ..." data="{\"columns\":3,\"images\":[...]}" -->
|
|
449
470
|
<!--/ec:block-->
|
|
450
|
-
|
|
451
471
|
```
|
|
452
472
|
|
|
453
|
-
|
|
473
|
+
- **Rule**: Do not modify or delete these comment fences unless you intentionally want to remove the block. Emit preserves them losslessly on round-trips.
|
|
454
474
|
|
|
455
475
|
### Media Management & Upload Workflow
|
|
456
476
|
|
|
457
477
|
EmDash stores media files (images, documents) in Cloudflare R2 and uses **two distinct identifier spaces**:
|
|
458
478
|
|
|
459
|
-
| Usage
|
|
460
|
-
|
|
|
461
|
-
| Frontmatter `featuredImage` | **Media Record `id`**
|
|
462
|
-
| Body `` URL
|
|
479
|
+
| Usage | Identifier Used | Format / Example |
|
|
480
|
+
| --------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
481
|
+
| Frontmatter `featuredImage` | **Media Record `id`** | 26-character ULID (`01M67P0MBMJQQXH2K95HHCYRYB`) or `null` |
|
|
482
|
+
| Body `` URL | **Media `storageKey`** | `/_emdash/api/media/file/<storageKey>` (e.g. `/_emdash/api/media/file/01M67P07X2VPW0MYRRRGMA6T5A.jpg` or double-ULID `01M6TF1XXBSC06QQG9M648B4K3.01M6TF1Y5YVG9YYQAY6H25270M.png`) |
|
|
463
483
|
|
|
464
484
|
> [!IMPORTANT]
|
|
465
485
|
> Never put a media record `id` into `/_emdash/api/media/file/<id>.<ext>`—the public file route resolves by **`storageKey`**, so using a record `id` returns HTTP 404.
|
|
@@ -477,7 +497,7 @@ You can author articles completely offline using local images and let `emit push
|
|
|
477
497
|
- Uploads the image via EmDash's 4-stage flow (`POST /_emdash/api/media/upload-url` with `sha1:<hex>` deduplication → `PUT` upload → `POST /_emdash/api/media/{id}/confirm`), cleaning up any orphan `pending` record if an upload fails midway.
|
|
478
498
|
- Rewrites the Markdown line in place to the canonical root-relative `/_emdash/api/media/file/<storageKey>` URL.
|
|
479
499
|
- Pushes the article to EmDash.
|
|
480
|
-
- Promotes the local file from `media/drafts/<filename>` to `media/<
|
|
500
|
+
- Promotes the local file from `media/drafts/<filename>` to `media/<markdownRef>.<ext>` alongside a companion `media/<markdownRef>.yaml` metadata file (`id`, `storageKey`, `contentHash`, `status`). `markdownRef` is derived from the record's `storageKey` (basename, sanitized, extension removed), so the promoted file matches the body URL exactly and no date prefix or later renaming pass is needed. If two records sanitize to the same stem, the second deterministically takes a `<stem>--<mediaId>` suffix.
|
|
481
501
|
4. **Reuse Across Articles (no re-upload):** The same `media/drafts/<filename>` line can be used in several articles. After the first push promotes the asset, every later push resolves that reference through `media_state` / `media/*.yaml` and rewrites it to the already-confirmed `/_emdash/api/media/file/<storageKey>` URL instead of uploading again (reported as `[LOCAL_IMAGE_AUTO_UPLOAD] Reused N already-promoted media asset(s)`). A direct reference to the promoted path works as well. Emit verifies each reused record with `GET /_emdash/api/media/{id}`: if the record was deleted on the EmDash side the push aborts with `PROMOTED_MEDIA_NOT_FOUND_REMOTE` (run `emit pull-media` or re-stage the image in `media/drafts/`), and servers without that endpoint push successfully but warn with `[MEDIA_EXISTENCE_UNVERIFIED]`.
|
|
482
502
|
|
|
483
503
|
#### Workflow B: Pre-Uploaded Media & Featured Hero Images
|
|
@@ -490,10 +510,11 @@ You can author articles completely offline using local images and let `emit push
|
|
|
490
510
|
|
|
491
511
|
> [!WARNING]
|
|
492
512
|
> **Local Image Guardrails (G5 & G11)**:
|
|
513
|
+
>
|
|
493
514
|
> - Arbitrary local paths (e.g. ``, absolute filesystem paths, or a `media/` path that does not resolve to a promoted media record) are blocked by Preflight Guard **G5 (`LOCAL_IMAGE_DETECTED`)**. Paths under `media/drafts/` and already-promoted `media/<yyyyMMdd>_<id>-<name>.<ext>` paths are the only accepted local forms.
|
|
494
515
|
> - Local images placed inline inside a paragraph (e.g. `Text  more text`) are blocked with `INLINE_LOCAL_IMAGE_NOT_SUPPORTED` because EmDash Portable Text requires images to be standalone top-level blocks.
|
|
495
516
|
> - A `media/drafts/` reference whose file no longer exists and which has no promoted match is blocked with `MISSING_LOCAL_IMAGE_FILE` (the promoted location is reported in the error).
|
|
496
|
-
> - Only **backtick** fences (
|
|
517
|
+
> - Only **backtick** fences (` ``` `) are exempt from image scanning. Tilde fences (`~~~`) are not recognized as code by the Portable Text mapper, so they are scanned like normal prose (and emit a `G8` warning because they degrade to plain paragraphs). Write code examples with backticks.
|
|
497
518
|
|
|
498
519
|
---
|
|
499
520
|
|
|
@@ -517,34 +538,40 @@ Whether you are a human developer or an autonomous AI agent (Antigravity, Claude
|
|
|
517
538
|
1. **CHECK (`emit status`)**: Check your workspace status to see synced, modified, and untracked files.
|
|
518
539
|
2. **PULL (`emit pull`)**: Synchronize the latest articles and taxonomies from the server into your local workspace. Taxonomies (categories/tags) are synced automatically in batch mode.
|
|
519
540
|
3. **EDIT (Direct Editing)**:
|
|
520
|
-
|
|
521
|
-
|
|
541
|
+
|
|
542
|
+
- For new posts: run `emit new <slug> --title "My Title"` to scaffold a canonical draft.
|
|
543
|
+
- Edit the Markdown body and YAML frontmatter in `posts/draft/` or `posts/published/`.
|
|
522
544
|
|
|
523
545
|
4. **PUSH (`emit push`)**:
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
|
533
|
-
|
|
|
534
|
-
| **
|
|
535
|
-
| **
|
|
536
|
-
| **
|
|
537
|
-
| **
|
|
538
|
-
| **
|
|
539
|
-
| **
|
|
540
|
-
| **
|
|
541
|
-
| **
|
|
542
|
-
| **
|
|
546
|
+
|
|
547
|
+
- To push a draft: `emit push posts/draft/<filename>.md`
|
|
548
|
+
- To publish live: update `status: published` and run `emit push posts/draft/<filename>.md --allow-publish`.
|
|
549
|
+
|
|
550
|
+
### Preflight Safety Guardrails (G1–G19)
|
|
551
|
+
|
|
552
|
+
To ensure autonomous AI agents and automated scripts cannot corrupt workspace state or accidentally publish unintended content, Emit enforces 19 strict safety guardrails before executing any write operation:
|
|
553
|
+
|
|
554
|
+
| Guard | Code Name | Description | Severity |
|
|
555
|
+
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
|
|
556
|
+
| **G1** | `SINGLE_TARGET_GUARD` | Enforces explicit target (`filePath`, `id`, or `--all`) for push/pull; forbids blind bulk push without inspection | Error |
|
|
557
|
+
| **G2** | `ID_MISMATCH_ERROR` | Verifies that frontmatter `id` exactly matches the filename ULID; forbids manual `mv` renames | Error |
|
|
558
|
+
| **G3** | `UNINTENDED_PUBLISH_GUARD` | Rejects promoting `draft` to `published` unless `--allow-publish` (`allowPublish: true`) is explicitly given | Error |
|
|
559
|
+
| **G4** | `INVALID_TAXONOMY_ERROR` | Validates taxonomy terms against local definitions (transparently resolves valid term ULIDs) to stop AI taxonomy hallucination | Error |
|
|
560
|
+
| **G5** | `LOCAL_IMAGE_DETECTED` / `INLINE_LOCAL_IMAGE_NOT_SUPPORTED` | Rejects local image paths other than `media/drafts/<file>` (or an already-promoted `media/<markdownRef>.<ext>` asset) and rejects inline paragraph local images (must be a standalone line) | Error |
|
|
561
|
+
| **G6** | `UNRESOLVED_CONFLICT_MARKERS` | Scans for Git 3-way merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and blocks push | Error |
|
|
562
|
+
| **G7** | `TITLE_DUPLICATION_AUTOTRIM` | Detects redundant `# Title` headings at the top of the body and automatically trims them safely | Warning |
|
|
563
|
+
| **G8** | `UNSUPPORTED_SYNTAX_WARNING` | Warns if unsupported block HTML, tables, or tilde code fences are detected in Markdown (`detectUnsupportedMarkdownSyntax`) | Warning |
|
|
564
|
+
| **G9** | `MODIFIED_OPAQUE_BLOCK` | Verifies that `<!--ec:block ... -->` opaque fences have not been tampered with or corrupted | Error |
|
|
565
|
+
| **G10** | `INVALID_FEATURED_IMAGE` | Verifies that `featuredImage` is a 26-character canonical media ULID (normalized to uppercase) or `null`. A remote media URL that contains a ULID is resolved with a warning; local file paths and malformed values are rejected | Error |
|
|
543
566
|
| **G11** | `LOCAL_IMAGE_AUTO_UPLOAD` (`MISSING_LOCAL_IMAGE_FILE` / `INVALID_LOCAL_IMAGE_TYPE` / `LOCAL_IMAGE_SIZE_EXCEEDED` / `PROMOTED_MEDIA_NOT_FOUND_REMOTE` / `[MEDIA_EXISTENCE_UNVERIFIED]`) | Detects standalone `media/drafts/<file>` image lines (or reuses an already-promoted `media/` asset across articles), validates file existence, extension, and size (≤ 50 MiB), queues them for automatic upload, URL rewrite, and `media/` promotion during `push`, and verifies every reused asset against `GET /_emdash/api/media/{id}` before its URL is baked into the body | Info / Warning / Error |
|
|
544
|
-
| **G12** | `QUARANTINED_COPY_NOT_PUSHABLE`
|
|
545
|
-
| **G13** | `CANONICAL_HAS_UNPUSHED_EDITS`
|
|
546
|
-
| **G14** | `QUARANTINED_PATH_REQUIRED`
|
|
547
|
-
| **
|
|
567
|
+
| **G12** | `QUARANTINED_COPY_NOT_PUSHABLE` | Refuses to push a rescue copy under `posts/conflicts/` (written by a pull that found divergent local content) before any network request, because it would overwrite the canonical article of the same ULID with stale content and recreate duplicate local files | Error |
|
|
568
|
+
| **G13** | `CANONICAL_HAS_UNPUSHED_EDITS` | Protects canonical articles from being silently overwritten during conflict resolution (`accept-local`) when unpushed local modifications exist; requires explicit `--force` (`force: true`) | Error |
|
|
569
|
+
| **G14** | `QUARANTINED_PATH_REQUIRED` | Restricts conflict resolution targets strictly within `<collection>/conflicts/`, preventing accidental overwrites or unlinks outside the quarantine directory | Error |
|
|
570
|
+
| **G15** | `DELETE_PENDING_ARTICLE` | Blocks pushing articles marked delete-pending when remote deletion was unconfirmed; requires `--revive` or `--force` | Error |
|
|
571
|
+
| **G16** | `TAXONOMY_CONFLICT_PENDING` | Blocks re-pushing articles with unresolved taxonomy conflicts from a previous sync; requires `--resolve-taxonomy` or `--force` | Error |
|
|
572
|
+
| **G17** | `TAXONOMY_APPLY_PENDING` | Blocks pushing when previous taxonomy assignment failed remotely; requires `--allow-unapplied-taxonomies` or `--force` | Error |
|
|
573
|
+
| **G18** | `DUPLICATE_SLUG_CONFLICT` | Scans local drafts, published posts, cache index, and remote collection during `emit new` / `sync_new` to strictly block creating scaffold articles with already-existing slugs | Error |
|
|
574
|
+
| **G19** | `DESTRUCTIVE_TAXONOMY_WIPE` | Blocks pushing empty taxonomy arrays that would wipe existing remote assignments; requires `--allow-wipe-taxonomies` or `--force` | Error |
|
|
548
575
|
|
|
549
576
|
---
|
|
550
577
|
|
|
@@ -559,7 +586,7 @@ emit init [--origin <url>]
|
|
|
559
586
|
|
|
560
587
|
```
|
|
561
588
|
|
|
562
|
-
|
|
589
|
+
- `--origin <url>`: Optional EmDash origin URL. If omitted, uses the active profile.
|
|
563
590
|
|
|
564
591
|
**Example Output:**
|
|
565
592
|
|
|
@@ -604,12 +631,12 @@ emit new <slug> [-t, --title <title>] [-c, --collection <collection>] [-l, --loc
|
|
|
604
631
|
|
|
605
632
|
```
|
|
606
633
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
634
|
+
- `<slug>`: URL slug for the article (e.g., `modern-web-architecture`).
|
|
635
|
+
- `-t, --title <title>`: Human-readable article title.
|
|
636
|
+
- `-c, --collection <name>`: Collection name (defaults to `posts`).
|
|
637
|
+
- `-l, --locale <locale>`: Locale code (defaults to `ja` or config).
|
|
638
|
+
- `-b, --byline <slug>`: Author byline slug (defaults to the first cached non-guest author ordered by `slug ASC`).
|
|
639
|
+
- `-p, --profile <name>`: Explicitly target a specific connection profile.
|
|
613
640
|
|
|
614
641
|
**Example:**
|
|
615
642
|
|
|
@@ -647,19 +674,27 @@ emit pull [id] [options]
|
|
|
647
674
|
|
|
648
675
|
```
|
|
649
676
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
677
|
+
- `[id]`: Optional article ID or ULID. If specified, pulls only that specific article (even if outside the window). Taxonomies are not pulled in single-article mode.
|
|
678
|
+
- `-s, --status <draft|published|all>`: Filter by publication status (default: `all`). When set to `all`, the sliding window (`postsSyncLimit`, default `30`) is applied **per status** (fetching up to `postsSyncLimit` `published` + up to `postsSyncLimit` `draft` articles in parallel).
|
|
679
|
+
- `-c, --collection <collection>`: Collection name (default: `posts`).
|
|
680
|
+
- `--no-prune`: Do not prune local articles that have fallen outside the sliding window (escape hatch; not intended for routine use).
|
|
681
|
+
- `--force`: Force overwrite local files and bypass differential hash checking.
|
|
682
|
+
- `--concurrency <n>`: Parallel processing task limit (default: `8`).
|
|
683
|
+
- `--posts-only`: Skip automatic remote taxonomy sync. Pulls articles only (local taxonomy candidates hint is still displayed from cached definitions).
|
|
684
|
+
- `--media`: Bounded media-library pass: pull media assets and companion metadata into `media/` (bounded by `mediaSyncLimit`, default `300`). Note that referenced media in pulled articles is mirrored on-demand by default (local-first media pipeline) without requiring this flag. Use `emit pull-media` for `--limit`, `--all`, and `--prune` control.
|
|
685
|
+
- `--metadata-only`: With `--media`: download only metadata YAML, skip binary file downloads.
|
|
686
|
+
- `--local-media-refs`: Write mirrored body images as article-relative `../../media/<storageKey>` references instead of the canonical `/_emdash/api/media/file/<storageKey>` URL, so Markdown preview and local tooling resolve them offline from the article's own directory. The choice is **remembered in `.emit/config.json`** and is **default-ON**, so every later pull, push, and diff keeps the same spelling without repeating the flag.
|
|
687
|
+
- `--no-local-media-refs`: Clear a previously remembered preference and store canonical EmDash URLs on disk again.
|
|
659
688
|
|
|
660
689
|
> [!NOTE]
|
|
661
690
|
> **Taxonomies are pulled by default in batch mode.** Categories and tags are lightweight (2 API calls) and required for valid push operations (Preflight Guard G4 validates taxonomy terms against these local definitions). This eliminates a common source of push failures. Use `--posts-only` if you need articles only.
|
|
662
691
|
|
|
692
|
+
> [!NOTE]
|
|
693
|
+
> **Local media references change only the disk spelling, never the remote payload.** With `localMediaRefs` enabled (the default), `base_snapshots` and the body sent to EmDash keep the canonical `/_emdash/api/media/file/<storageKey>` URL, while the Markdown file carries `../../media/<storageKey>` spelled from its own directory; only `sync_state.local_hash` tracks those disk bytes. Because the baseline stays canonical, `emit diff` renders it in whichever spelling the disk already uses — an untouched article reports no change, and only a genuine edit appears.
|
|
694
|
+
|
|
695
|
+
> [!NOTE]
|
|
696
|
+
> **A workspace written by an older Emit heals itself.** Files that still carry the depth-free `media/<storageKey>` spelling are re-spelled to the article-relative form on the next `emit pull`, and re-tracked against their own repaired hash — no content changes, no phantom "modified", and the following pull of the same revision skips.
|
|
697
|
+
|
|
663
698
|
> [!NOTE]
|
|
664
699
|
> **Why `--no-prune` is rarely needed:**
|
|
665
700
|
> Automatic pruning is mathematically loss-free: Emit's unpushed edits safeguard inspects both the canonical file and any duplicate filenames sharing the same 26-character ULID, strictly aborting the pull (`SyncPruneConflictError`) if any of them contain local modifications. Clean duplicate files are removed alongside the canonical file. You only need `--no-prune` in exceptional circumstances, such as temporarily suppressing Git working-tree changes during a quick pull.
|
|
@@ -700,11 +735,11 @@ Available Categories:
|
|
|
700
735
|
Available Tags:
|
|
701
736
|
- ai, cloudflare, emit, typescript
|
|
702
737
|
|
|
703
|
-
Fetching articles from remote (posts, status: all, window:
|
|
704
|
-
[1/
|
|
705
|
-
[2/
|
|
738
|
+
Fetching articles from remote (posts, status: all, window: 30/status, concurrency: 8)...
|
|
739
|
+
[1/50] ✅ Pulled hello-world -> posts/published/20261101_01M5...-Hello-World.md
|
|
740
|
+
[2/50] ✅ Pulled draft-post -> posts/draft/20261104_01M6...-Draft-Post.md
|
|
706
741
|
|
|
707
|
-
🎉 Articles:
|
|
742
|
+
🎉 Articles: 50 total [30 published, 20 draft] (12 updated, 38 skipped/up-to-date)
|
|
708
743
|
|
|
709
744
|
```
|
|
710
745
|
|
|
@@ -719,16 +754,17 @@ emit push [file] [options]
|
|
|
719
754
|
|
|
720
755
|
```
|
|
721
756
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
757
|
+
- `[file]`: Relative or absolute path to the Markdown file.
|
|
758
|
+
- `--allow-publish`: Required when promoting an article to `status: published`.
|
|
759
|
+
- `--all`: Pushes all modified files in the workspace.
|
|
760
|
+
- `--no-taxonomy-audit`: Skips pre-PUT remote taxonomy divergence audit.
|
|
761
|
+
- `--allow-unapplied-taxonomies`: Pushes article body even if taxonomy term assignment fails (bypasses Guard G17).
|
|
762
|
+
- `--allow-wipe-taxonomies`: Confirms intentional deletion of all assigned categories or tags.
|
|
763
|
+
- `--resolve-taxonomy <strategy>`: Resolves taxonomy conflict explicitly (`union`, `local`, or `remote`).
|
|
764
|
+
- `--revive`: Pushes an article that was previously marked delete_pending.
|
|
765
|
+
- `-f, --force`: Bypasses non-critical safety guardrails.
|
|
766
|
+
- `--local-media-refs` / `--no-local-media-refs`: Override the remembered workspace preference for how mirrored body images are spelled on disk (see [`emit pull`](#emit-pull)). Push always sends the canonical URL to remote; the flag only decides what stays in the local file, and passing it persists the choice to `.emit/config.json`.
|
|
767
|
+
- `-p, --profile <name>`: Explicitly target a specific connection profile.
|
|
732
768
|
|
|
733
769
|
**Example:**
|
|
734
770
|
|
|
@@ -751,11 +787,11 @@ Safely deletes a local Markdown article and its remote counterpart on EmDash.
|
|
|
751
787
|
emit delete <filePathOrId> [--allow-delete-published] [--dry-run] [-y, --yes] [-p, --profile <name>]
|
|
752
788
|
```
|
|
753
789
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
790
|
+
- `<filePathOrId>`: Relative/absolute path to the Markdown file, or 26-character article ULID.
|
|
791
|
+
- `--allow-delete-published`: Required when deleting a published article (safety guardrail).
|
|
792
|
+
- `--dry-run`: Previews the deletion, inspecting referenced media and lines without modifying disk or remote.
|
|
793
|
+
- `-y, --yes`: Skips interactive confirmation prompt.
|
|
794
|
+
- `-p, --profile <name>`: Explicitly target a specific connection profile.
|
|
759
795
|
|
|
760
796
|
> [!NOTE]
|
|
761
797
|
> **Rescue Backups on Deletion**: When an article is deleted, Emit automatically creates a safety rescue backup at `posts/conflicts/<name>.deleted.md` before removing the file, preserving your work in case of accidental deletion.
|
|
@@ -778,10 +814,10 @@ emit conflicts resolve <filePath> --accept-local [-f, --force] [-c, --collection
|
|
|
778
814
|
emit conflicts resolve <filePath> --accept-canonical [-c, --collection <collection>]
|
|
779
815
|
```
|
|
780
816
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
817
|
+
- `list`: Scans `<collection>/conflicts/`, classifying items as `⚠️ SYNC CONFLICT` or `🗑️ DELETED RESCUE`, showing sizes and dates.
|
|
818
|
+
- `diff`: Displays a unified diff against the current canonical article.
|
|
819
|
+
- `resolve --accept-local`: Restores the quarantined content to the canonical location and purges the quarantine file. Guard **G13** blocks overwriting canonical articles that contain unpushed local modifications unless `-f, --force` is explicitly provided.
|
|
820
|
+
- `resolve --accept-canonical`: Safely discards the quarantined copy, retaining the canonical version.
|
|
785
821
|
|
|
786
822
|
---
|
|
787
823
|
|
|
@@ -790,10 +826,11 @@ emit conflicts resolve <filePath> --accept-canonical [-c, --collection <collecti
|
|
|
790
826
|
Runs an automated diagnostic health check across the local workspace and cache database.
|
|
791
827
|
|
|
792
828
|
```bash
|
|
793
|
-
emit doctor [-c, --collection <collection>] [-p, --profile <name>]
|
|
829
|
+
emit doctor [-c, --collection <collection>] [--strict-media] [--check-media-urls] [--check-media-library] [-p, --profile <name>]
|
|
794
830
|
```
|
|
795
831
|
|
|
796
832
|
Validates 6 critical health vectors:
|
|
833
|
+
|
|
797
834
|
1. **Version Compatibility**: Checks tool version against `createdWithVersion` / `lastSyncedVersion` in config.
|
|
798
835
|
2. **Origin Consistency**: Verifies `.emit/config.json`, profile origin, and environment variables.
|
|
799
836
|
3. **Frontmatter Integrity**: Ensures all local Markdown files have valid parseable frontmatter.
|
|
@@ -801,6 +838,11 @@ Validates 6 critical health vectors:
|
|
|
801
838
|
5. **Sync Status**: Verifies local baseline hash tracking.
|
|
802
839
|
6. **Referenced Media Existence**: Validates that all local media referenced in article Markdown bodies exist on disk.
|
|
803
840
|
|
|
841
|
+
Media flags:
|
|
842
|
+
|
|
843
|
+
- `--check-media-urls`: Probe `HEAD /_emdash/api/media/file/<storageKey>` for references no local source resolves, downgrading ones the origin still serves to informational notes.
|
|
844
|
+
- `--check-media-library`: Walk the whole remote library page by page via metadata only (no binary downloads) and state how many records it holds against how many this workspace mirrors, so a missing-file count reads as coverage rather than data loss.
|
|
845
|
+
|
|
804
846
|
---
|
|
805
847
|
|
|
806
848
|
### `emit status`
|
|
@@ -841,9 +883,9 @@ emit diff [filePath] [-i, --id <id>] [-p, --profile <name>]
|
|
|
841
883
|
|
|
842
884
|
```
|
|
843
885
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
886
|
+
- `[filePath]`: Relative path to the local Markdown file (e.g., `posts/draft/20261104_01M6...-my-article.md`).
|
|
887
|
+
- `-i, --id <id>`: Inspect by 26-character article ULID instead of file path.
|
|
888
|
+
- `-p, --profile <name>`: Explicitly target a specific connection profile.
|
|
847
889
|
|
|
848
890
|
---
|
|
849
891
|
|
|
@@ -878,14 +920,40 @@ Available Tags:
|
|
|
878
920
|
Downloads media assets and their companion metadata YAML files into `media/`. Supports pulling all media, a limited batch, or targeted media referenced by a specific article.
|
|
879
921
|
|
|
880
922
|
```bash
|
|
881
|
-
emit pull-media [-a, --article <id-or-path>] [-l, --limit <n>] [--all] [--prune] [--metadata-only]
|
|
923
|
+
emit pull-media [-a, --article <id-or-path>] [--storage-key <key>] [-l, --limit <n>] [--all] [--prune] [--metadata-only]
|
|
882
924
|
```
|
|
883
925
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
926
|
+
- `-a, --article <id-or-path>`: Pulls only media assets referenced by the specified article (featured image and body images), saving bandwidth and disk space. The summary separates what Emit actually mirrored from what it merely verified, and states plainly that a migrated body's ULIDs are `storageKey`s rather than record ids — so an id lookup resolving nothing is reported as expected, never as `failed`.
|
|
927
|
+
- `--storage-key <key>`: Mirror a specific storageKey directly (can be specified multiple times; mutually exclusive with `--article` and `--all`).
|
|
928
|
+
- `-l, --limit <n>`: Maximum number of media items to fetch (default: `300`).
|
|
929
|
+
- `--all`: Fetch all media items without limit, walking the remote library cursor-by-cursor until it is exhausted. **Maintenance / Backup Only**: Routine workflows do not need this because `emit pull` automatically mirrors referenced media on-demand. Fetching the full library can download thousands of images and consume substantial bandwidth and disk space.
|
|
930
|
+
- `--prune`: Remove local media files whose companion `.yaml` names a media record that no longer exists on remote. Ownership is read from the companion, never guessed from a filename, so a file with no companion is left untouched. Residue written by older Emit versions is never pruned and must be deleted manually.
|
|
931
|
+
- `--metadata-only`: Download metadata YAML files only, skipping large binary downloads.
|
|
932
|
+
|
|
933
|
+
> **Two ID spaces, one lookup path:** Body URLs carry the R2 `storageKey`, while `GET /_emdash/api/media/{id}` searches the D1 primary key, so an existing record can 404 when queried with a body URL's ULID. The list operation is the only endpoint that reports each record's own `storageKey`, and it exposes no storageKey filter (`q` matches filenames only) — which is why `emit doctor --check-media-library` (or `emit pull-media --all` for maintenance backup) is how Emit reaches migrated assets, not a targeted lookup.
|
|
934
|
+
|
|
935
|
+
> **How a migrated article reads:** for an article whose body bakes R2 `storageKey`s, `emit pull-media --article` reports something like `8 referenced item(s) (0 downloaded, 0 skipped/up-to-date, 4 verified/mirrored). (4 body ULID(s) are R2 storageKeys, not media record ids, so no id lookup was expected to match)`. The assets already under `media/` are named after the **storageKey** the body references (or, in a workspace that has not been re-pulled yet, after the legacy **record id**), and each companion `.yaml` carries both identifiers, so those images resolve through local mirror metadata instead of re-downloading. Nothing in that line is a failure: `failed` counts only references that no id lookup _and_ no storageKey check could match.
|
|
936
|
+
|
|
937
|
+
> **Unresolved assets:** When an article body references a `/_emdash/api/media/file/<storageKey>` image that Emit cannot match to a media record or to confirmed local mirror metadata, Emit reports it as `⚠️ [UNRESOLVED_MEDIA_ASSET]` and mirrors nothing. That is a _lookup_ failure, not proof that the record is missing. Index the whole library first with `emit doctor --check-media-library` (metadata-only walk) or `emit pull-media --all` (offline backup), which resolves these keys without any server-side query; only then verify anything left over by storageKey before registering it — a blind INSERT can duplicate a record that already exists. Registering records is an EmDash/Wrangler task; once Emit can resolve the record, a normal `emit pull-media` mirrors the asset normally. See [docs/media-id-spaces.md](docs/media-id-spaces.md) for the full two-ID-space specification.
|
|
938
|
+
|
|
939
|
+
---
|
|
940
|
+
|
|
941
|
+
### `emit media-search`
|
|
942
|
+
|
|
943
|
+
Search the remote EmDash media library by keyword across filename, alt text, caption, and storageKey, displaying whether each hit is already mirrored locally:
|
|
944
|
+
|
|
945
|
+
```bash
|
|
946
|
+
emit media-search <query> [-l, --limit <n>] [--max-scan <n>] [--mirrored-only] [--suggest-keywords] [-p, --profile <name>]
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
Options:
|
|
950
|
+
- `-l, --limit <n>`: Maximum hits to return (default: 20, max: 50).
|
|
951
|
+
- `--max-scan <n>`: Maximum remote records to read before stopping (default: 500; `0` = walk entire library).
|
|
952
|
+
- `--mirrored-only`: Show only assets already mirrored in this workspace.
|
|
953
|
+
- `--suggest-keywords`: Append top local tag slugs as search hints.
|
|
954
|
+
- `-p, --profile <name>`: Explicitly select a connection profile.
|
|
955
|
+
|
|
956
|
+
Absence is only proven when `walkComplete: true` (a complete walk). When stopped early, `[WALK_INCOMPLETE]` indicates that an asset may still exist in the unread portion of the library.
|
|
889
957
|
|
|
890
958
|
---
|
|
891
959
|
|
|
@@ -928,7 +996,6 @@ Emit features a first-class **Model Context Protocol (MCP)** server over stdio.
|
|
|
928
996
|
}
|
|
929
997
|
}
|
|
930
998
|
}
|
|
931
|
-
|
|
932
999
|
```
|
|
933
1000
|
|
|
934
1001
|
#### 2. Cursor (`.cursor/mcp.json`)
|
|
@@ -942,7 +1009,6 @@ Emit features a first-class **Model Context Protocol (MCP)** server over stdio.
|
|
|
942
1009
|
}
|
|
943
1010
|
}
|
|
944
1011
|
}
|
|
945
|
-
|
|
946
1012
|
```
|
|
947
1013
|
|
|
948
1014
|
#### 3. Google Antigravity (`~/.gemini/antigravity/mcp_config.json`)
|
|
@@ -956,27 +1022,27 @@ Emit features a first-class **Model Context Protocol (MCP)** server over stdio.
|
|
|
956
1022
|
}
|
|
957
1023
|
}
|
|
958
1024
|
}
|
|
959
|
-
|
|
960
1025
|
```
|
|
961
1026
|
|
|
962
1027
|
> **Tip:** When profiles are registered using `emit profile add --workspace <dir>`, the MCP server automatically discovers credentials and paths without requiring secrets in your agent configuration.
|
|
963
1028
|
|
|
964
1029
|
### Available MCP Tools
|
|
965
1030
|
|
|
966
|
-
| Tool
|
|
967
|
-
|
|
|
968
|
-
| `sync_status`
|
|
969
|
-
| `sync_pull`
|
|
970
|
-
| `sync_new`
|
|
971
|
-
| `sync_push`
|
|
972
|
-
| `sync_delete`
|
|
973
|
-
| `sync_conflicts`
|
|
974
|
-
| `sync_doctor`
|
|
975
|
-
| `sync_diff`
|
|
976
|
-
| `sync_pull_taxonomies` | Pull remote category and tag definitions (standalone; returns taxonomy hints)
|
|
977
|
-
| `sync_pull_media`
|
|
978
|
-
| `
|
|
979
|
-
| `
|
|
1031
|
+
| Tool | Purpose | Key Parameters |
|
|
1032
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
1033
|
+
| `sync_status` | Inspect local sync state (summary-first structure with capped actionRequired, articles, and search filters) | `collection`, `status`, `search`, `profile` |
|
|
1034
|
+
| `sync_pull` | Pull remote articles **and taxonomies** into local workspace (includes taxonomy hints; preserves divergent local copies under `posts/conflicts/`) | `collection`, `id`, `filePath`, `status`, `force`, `noPrune`, `prune`, `postsOnly`, `includeMedia`, `metadataOnly`, `profile` |
|
|
1035
|
+
| `sync_new` | Scaffold new article with canonical ULID, frontmatter, default byline, duplicate slug guard, and taxonomy hints | `slug` (required), `title`, `collection`, `locale`, `byline`, `profile` |
|
|
1036
|
+
| `sync_push` | Push local article changes to remote with preflight checks and `media/drafts/*` auto-upload; refuses `posts/conflicts/` rescue copies (G12) | `filePath` (required), `allowPublish`, `profile` |
|
|
1037
|
+
| `sync_delete` | Safely delete article locally and remotely with rescue backup (`.deleted.md`) and unpushed edit protection | `filePath`, `id`, `allowDeletePublished`, `dryRun`, `profile` |
|
|
1038
|
+
| `sync_conflicts` | List, diff, and resolve quarantined conflict copies and deletion backups in `posts/conflicts/` (G13 unpushed edit guard with `force`) | `action` ("list"\|"diff"\|"resolve"), `filePath`, `strategy` ("accept-local"\|"accept-canonical"), `force`, `profile` |
|
|
1039
|
+
| `sync_doctor` | Run comprehensive workspace diagnostics (version, origin, frontmatter, conflicts, sync, referenced media) | `collection`, `profile` |
|
|
1040
|
+
| `sync_diff` | Inspect diff between working file and baseline snapshot | `filePath`, `id`, `profile` |
|
|
1041
|
+
| `sync_pull_taxonomies` | Pull remote category and tag definitions (standalone; returns taxonomy hints) | `profile` |
|
|
1042
|
+
| `sync_pull_media` | Pull remote media files and metadata into `media/` (supports batch, targeted article media, or targeted storageKey) | `article`, `storageKey`, `limit`, `all`, `downloadFiles`, `profile` |
|
|
1043
|
+
| `sync_media_search` | Search remote media library by keyword across filename, alt, caption, and storageKey with local mirror decoration | `query` (required), `limit`, `maxScan`, `mirroredOnly`, `suggestKeywords`, `profile` |
|
|
1044
|
+
| `profile_list` | List all configured connection profiles | (none) |
|
|
1045
|
+
| `profile_switch` | Switch active profile for multi-site environments | `name` (required), `persist` |
|
|
980
1046
|
|
|
981
1047
|
> [!NOTE]
|
|
982
1048
|
> **Uniform `_meta` Context & Structured Diagnostics**: Every MCP tool response (success and error) includes structured `_meta` context containing `profile`, `workspace`, `origin`, and `resolvedBy` resolution rationale. Handlers return structured error payloads with actionable suggestions, HTTP status, and preflight codes rather than uncaught raw exceptions.
|
|
@@ -985,9 +1051,9 @@ Emit features a first-class **Model Context Protocol (MCP)** server over stdio.
|
|
|
985
1051
|
|
|
986
1052
|
With Emit connected via MCP, you can give high-level natural language instructions to your AI:
|
|
987
1053
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1054
|
+
- _"Check the workspace status, pull the latest posts, and show me what was changed recently."_
|
|
1055
|
+
- _"Draft a new blog post titled 'Next-Gen Serverless Architectures' with slug `serverless-next`, add the `tech` category, and write a comprehensive outline."_
|
|
1056
|
+
- _"Review the draft in `posts/draft/...`, proofread the content, and push it to remote as a draft."_
|
|
991
1057
|
|
|
992
1058
|
---
|
|
993
1059
|
|
|
@@ -1024,17 +1090,17 @@ Push your changes with 'emit push <file>' or discard them before pulling.
|
|
|
1024
1090
|
|
|
1025
1091
|
```
|
|
1026
1092
|
|
|
1027
|
-
**Cause**: An article that has fallen outside the sliding window (e.g., beyond the latest
|
|
1093
|
+
**Cause**: An article that has fallen outside the sliding window (e.g., beyond the latest 30 posts per status) contains local edits that have not been pushed to the remote server. To prevent accidental data loss, Emit aborts the pull.
|
|
1028
1094
|
|
|
1029
1095
|
**Solution**:
|
|
1030
1096
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1097
|
+
- If you want to keep the changes: Push them to remote using `emit push posts/draft/...`.
|
|
1098
|
+
- If you want to discard local edits: Delete or revert the file, then re-run `emit pull`.
|
|
1099
|
+
- If you want to temporarily skip pruning: Run `emit pull --no-prune`.
|
|
1034
1100
|
|
|
1035
1101
|
### Q: I want to write a comprehensive piece that references my entire back-catalog — is that supported?
|
|
1036
1102
|
|
|
1037
|
-
**Yes.** The sliding window limits which articles
|
|
1103
|
+
**Yes.** The sliding window limits which articles _stay on disk_, not which articles you can _work with_. To reference, quote, or revise any historical post, fetch it individually: `emit pull <article-ULID>` (MCP: `sync_pull` with `id`). Each fetched article enters the normal authoring loop — edit, diff, push — with all guardrails applied. If you prefer a permanently larger local working set instead, raise `postsSyncLimit` in `.emit/config.json`; Emit's pagination engine will fetch up to that size automatically. What Emit deliberately does not offer is a default "clone the whole site" mode; for genuine whole-library operations (bulk media management, site-wide queries), use the official EmDash MCP alongside Emit — see [Division of Labor with the Official EmDash MCP](#6-division-of-labor-with-the-official-emdash-mcp).
|
|
1038
1104
|
|
|
1039
1105
|
### Q: `PushValidationError`: Cannot publish article live without `--allow-publish`
|
|
1040
1106
|
|
|
@@ -1048,6 +1114,12 @@ Push your changes with 'emit push <file>' or discard them before pulling.
|
|
|
1048
1114
|
|
|
1049
1115
|
**Solution**: Run `emit pull` or `sync_pull` (taxonomies are synced automatically), or use `emit pull-taxonomies` standalone to refresh your local category and tag list. Preflight Guard G4 will guide you with valid options.
|
|
1050
1116
|
|
|
1117
|
+
### Q: Images in an older article do not render in a local Markdown preview
|
|
1118
|
+
|
|
1119
|
+
**Cause**: Two different things look identical here. (1) The asset was never mirrored locally: `emit pull-media` walks the remote library newest-first and stops at `mediaSyncLimit` (default 300), so images referenced by older articles have no file under `media/`. (2) You are trying to resolve a body URL against local filenames in a workspace that still holds legacy names: body URLs carry the R2 `storageKey`, while a legacy `media/<yyyyMMdd>_<ULID>-<name>.<ext>` is named after the media record `id`, and those two ULIDs never match for the same image. Run `emit pull-media --article <id>` (or a one-time maintenance run of `emit pull-media --all`) and mirrors are renamed to the storageKey-derived stem, so the body URL and the file on disk share one key.
|
|
1120
|
+
|
|
1121
|
+
**Solution**: Run `emit doctor --check-media-library` first (a metadata-only walk) — its `Media References` check reports how many references have no local file _and_ how many records the remote library holds against how many this workspace mirrors, which distinguishes "not mirrored" from "broken reference". To fetch missing images without pulling the entire library, run `emit pull-media --article <id>`. Reserve `emit pull-media --all` strictly for deliberate full offline backups. Only for what remains unresolved after indexing should you verify by `storageKey` through the EmDash admin UI or `wrangler d1 SELECT`, never by guessing a record id. The live site is unaffected in either case, because the public file route serves the R2 object directly. See [docs/media-id-spaces.md](docs/media-id-spaces.md).
|
|
1122
|
+
|
|
1051
1123
|
### Q: `[NETWORK_ERROR]`: Network request failed (`fetch failed`)
|
|
1052
1124
|
|
|
1053
1125
|
**Cause**: The configured origin URL is unreachable due to DNS resolution failure, offline network/VPN state, or an incorrect origin in `~/.config/emit/profiles.yaml` or `.emit/config.json`.
|
|
@@ -1085,4 +1157,3 @@ npm run check
|
|
|
1085
1157
|
This project is open-source software licensed under the [MIT License](LICENSE).
|
|
1086
1158
|
|
|
1087
1159
|
Copyright (c) 2026 SPORT-SERVICE RS★R. All rights reserved.
|
|
1088
|
-
|