emdash-emit 0.1.7 → 0.1.9

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 (125) hide show
  1. package/README.md +237 -82
  2. package/dist/cli/commands/new.d.ts.map +1 -1
  3. package/dist/cli/commands/new.js +3 -1
  4. package/dist/cli/commands/new.js.map +1 -1
  5. package/dist/cli/commands/profile.d.ts.map +1 -1
  6. package/dist/cli/commands/profile.js +67 -13
  7. package/dist/cli/commands/profile.js.map +1 -1
  8. package/dist/cli/commands/pull.d.ts +5 -0
  9. package/dist/cli/commands/pull.d.ts.map +1 -1
  10. package/dist/cli/commands/pull.js +96 -39
  11. package/dist/cli/commands/pull.js.map +1 -1
  12. package/dist/cli/commands/push.d.ts.map +1 -1
  13. package/dist/cli/commands/push.js +17 -2
  14. package/dist/cli/commands/push.js.map +1 -1
  15. package/dist/cli/commands/status.d.ts +19 -0
  16. package/dist/cli/commands/status.d.ts.map +1 -1
  17. package/dist/cli/commands/status.js +93 -4
  18. package/dist/cli/commands/status.js.map +1 -1
  19. package/dist/cli/context.d.ts +35 -0
  20. package/dist/cli/context.d.ts.map +1 -1
  21. package/dist/cli/context.js +112 -10
  22. package/dist/cli/context.js.map +1 -1
  23. package/dist/client/auth.d.ts +1 -0
  24. package/dist/client/auth.d.ts.map +1 -1
  25. package/dist/client/auth.js +1 -0
  26. package/dist/client/auth.js.map +1 -1
  27. package/dist/client/http.d.ts.map +1 -1
  28. package/dist/client/http.js +27 -10
  29. package/dist/client/http.js.map +1 -1
  30. package/dist/client/mapper.d.ts +12 -0
  31. package/dist/client/mapper.d.ts.map +1 -1
  32. package/dist/client/mapper.js +65 -0
  33. package/dist/client/mapper.js.map +1 -1
  34. package/dist/client/media.d.ts +141 -0
  35. package/dist/client/media.d.ts.map +1 -0
  36. package/dist/client/media.js +582 -0
  37. package/dist/client/media.js.map +1 -0
  38. package/dist/client/portable-text.d.ts +22 -1
  39. package/dist/client/portable-text.d.ts.map +1 -1
  40. package/dist/client/portable-text.js +85 -5
  41. package/dist/client/portable-text.js.map +1 -1
  42. package/dist/db/cache.d.ts +16 -0
  43. package/dist/db/cache.d.ts.map +1 -1
  44. package/dist/db/cache.js +67 -0
  45. package/dist/db/cache.js.map +1 -1
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +1 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/mcp/context.d.ts +1 -0
  51. package/dist/mcp/context.d.ts.map +1 -1
  52. package/dist/mcp/server.d.ts +8 -0
  53. package/dist/mcp/server.d.ts.map +1 -1
  54. package/dist/mcp/server.js +60 -15
  55. package/dist/mcp/server.js.map +1 -1
  56. package/dist/mcp/tool-errors.d.ts +11 -0
  57. package/dist/mcp/tool-errors.d.ts.map +1 -0
  58. package/dist/mcp/tool-errors.js +62 -0
  59. package/dist/mcp/tool-errors.js.map +1 -0
  60. package/dist/mcp/tools/profile.d.ts +9 -2
  61. package/dist/mcp/tools/profile.d.ts.map +1 -1
  62. package/dist/mcp/tools/profile.js +49 -36
  63. package/dist/mcp/tools/profile.js.map +1 -1
  64. package/dist/mcp/tools/sync-diff.d.ts.map +1 -1
  65. package/dist/mcp/tools/sync-diff.js +18 -70
  66. package/dist/mcp/tools/sync-diff.js.map +1 -1
  67. package/dist/mcp/tools/sync-new.d.ts.map +1 -1
  68. package/dist/mcp/tools/sync-new.js +40 -21
  69. package/dist/mcp/tools/sync-new.js.map +1 -1
  70. package/dist/mcp/tools/sync-pull.d.ts.map +1 -1
  71. package/dist/mcp/tools/sync-pull.js +110 -86
  72. package/dist/mcp/tools/sync-pull.js.map +1 -1
  73. package/dist/mcp/tools/sync-push.d.ts.map +1 -1
  74. package/dist/mcp/tools/sync-push.js +24 -32
  75. package/dist/mcp/tools/sync-push.js.map +1 -1
  76. package/dist/mcp/tools/sync-status.d.ts.map +1 -1
  77. package/dist/mcp/tools/sync-status.js +55 -4
  78. package/dist/mcp/tools/sync-status.js.map +1 -1
  79. package/dist/sync/diff.d.ts +36 -0
  80. package/dist/sync/diff.d.ts.map +1 -1
  81. package/dist/sync/diff.js +118 -0
  82. package/dist/sync/diff.js.map +1 -1
  83. package/dist/sync/media.d.ts +16 -0
  84. package/dist/sync/media.d.ts.map +1 -1
  85. package/dist/sync/media.js +180 -26
  86. package/dist/sync/media.js.map +1 -1
  87. package/dist/sync/merge.d.ts.map +1 -1
  88. package/dist/sync/merge.js +1 -0
  89. package/dist/sync/merge.js.map +1 -1
  90. package/dist/sync/orchestrator.d.ts +6 -0
  91. package/dist/sync/orchestrator.d.ts.map +1 -1
  92. package/dist/sync/orchestrator.js +52 -27
  93. package/dist/sync/orchestrator.js.map +1 -1
  94. package/dist/sync/preflight.d.ts +30 -2
  95. package/dist/sync/preflight.d.ts.map +1 -1
  96. package/dist/sync/preflight.js +274 -10
  97. package/dist/sync/preflight.js.map +1 -1
  98. package/dist/sync/pull.d.ts +47 -0
  99. package/dist/sync/pull.d.ts.map +1 -1
  100. package/dist/sync/pull.js +148 -8
  101. package/dist/sync/pull.js.map +1 -1
  102. package/dist/sync/push.d.ts +2 -0
  103. package/dist/sync/push.d.ts.map +1 -1
  104. package/dist/sync/push.js +332 -6
  105. package/dist/sync/push.js.map +1 -1
  106. package/dist/sync/status.d.ts +11 -1
  107. package/dist/sync/status.d.ts.map +1 -1
  108. package/dist/sync/status.js +40 -6
  109. package/dist/sync/status.js.map +1 -1
  110. package/dist/sync/taxonomies.d.ts.map +1 -1
  111. package/dist/sync/taxonomies.js +39 -4
  112. package/dist/sync/taxonomies.js.map +1 -1
  113. package/dist/utils/config.d.ts +11 -0
  114. package/dist/utils/config.d.ts.map +1 -1
  115. package/dist/utils/config.js +14 -4
  116. package/dist/utils/config.js.map +1 -1
  117. package/dist/utils/frontmatter.d.ts +16 -1
  118. package/dist/utils/frontmatter.d.ts.map +1 -1
  119. package/dist/utils/frontmatter.js +66 -7
  120. package/dist/utils/frontmatter.js.map +1 -1
  121. package/dist/utils/profile.d.ts +3 -3
  122. package/dist/utils/profile.d.ts.map +1 -1
  123. package/dist/utils/profile.js +105 -8
  124. package/dist/utils/profile.js.map +1 -1
  125. package/package.json +4 -1
package/README.md CHANGED
@@ -32,7 +32,7 @@
32
32
  - [Media Management & Upload Workflow](#media-management--upload-workflow)
33
33
  - [🤖 The Golden Path for AI & Humans (4-Step Cycle)](#-the-golden-path-for-ai--humans-4-step-cycle)
34
34
  - [The 4-Step Authoring Cycle](#the-4-step-authoring-cycle)
35
- - [Preflight Safety Guardrails (G1–G9)](#preflight-safety-guardrails-g1g9)
35
+ - [Preflight Safety Guardrails (G1–G12)](#preflight-safety-guardrails-g1g12)
36
36
  - [🛠 CLI Command Reference](#-cli-command-reference)
37
37
  - [`emit init`](#emit-init)
38
38
  - [`emit profile`](#emit-profile)
@@ -78,10 +78,10 @@ To prevent user expectations from misaligning with the tool's purpose, it is imp
78
78
 
79
79
  * **What Emit IS (Core Goal)**:
80
80
  * An **active authoring and editing workbench (Staging Desk)** where human authors and AI coding agents collaborate seamlessly.
81
- * A tool focused on drafting, reviewing, polishing, diffing, and publishing current and recent articles with strict safety guards.
81
+ * 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.
82
82
  * **What Emit is NOT (Non-Goals)**:
83
- * **Not a full-site database archiver / backup dumper**: Emit is not designed to clone an entire 15-year history of 20,000 blog posts onto your laptop.
84
- * **Not an offline bulk mass-migration converter**: Emit does not perform batch transformation across thousands of historical posts at once.
83
+ * **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.
84
+ * **Not an offline bulk mass-migration converter**: Emit does not perform batch transformation across thousands of historical posts or bulk media migrations at once.
85
85
 
86
86
  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>`.
87
87
 
@@ -116,7 +116,7 @@ By leveraging Node.js 22's built-in `node:sqlite` module (`DatabaseSync`), Emit
116
116
 
117
117
  To prevent concurrent race conditions when multiple authors or AI agents collaborate:
118
118
 
119
- * Emit acquires a distributed 7-minute lease via `POST /api/v1/content/lock` before writing to remote articles, with automatic heartbeats and `try/finally` release guarantees.
119
+ * 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.
120
120
  * If concurrent edits occur, Emit utilizes a **Hybrid 3-Way Merge** engine (`node-diff3`), merging YAML frontmatter fields and Markdown body text independently.
121
121
 
122
122
  ---
@@ -193,6 +193,9 @@ emit profile add my-blog \
193
193
  > [!NOTE]
194
194
  > Profiles are securely stored with `0600` permissions at `~/.config/emit/profiles.yaml` (on macOS / Linux) or `%USERPROFILE%\.config\emit\profiles.yaml` (on Windows).
195
195
 
196
+ > [!NOTE]
197
+ > **`emit profile add` updates in place.** Re-running it with only `--origin` (for example after rotating a domain) keeps the stored `--token` and the `--workspace` link that `emit init` created, and reports `Kept the existing token and workspace link`. Pass `--token`/`--workspace` explicitly to replace them; an empty `--token ''` clears the credential on purpose.
198
+
196
199
  ### 2. Initialize a Workspace
197
200
 
198
201
  Navigate to your local repository directory and initialize it:
@@ -202,7 +205,7 @@ cd /path/to/local/blog-repo
202
205
  emit init
203
206
  ```
204
207
 
205
- This creates the `.emit/` directory with a default `config.json` and initializes the local baseline cache SQLite database (`.emit/cache.db`).
208
+ This creates the `.emit/` directory with `config.json` (inheriting `origin` from your resolved profile when `--origin` is omitted, and automatically linking `workspace` back into `~/.config/emit/profiles.yaml` unless `--no-link` is passed) and initializes the local baseline cache SQLite database (`.emit/cache.db`).
206
209
 
207
210
  ### 3. Multi-Site Management (Directory-Driven & Zero Friction)
208
211
 
@@ -222,11 +225,60 @@ emit profile list
222
225
  ```
223
226
 
224
227
  **How Automatic Profile Resolution Works:**
225
- 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`.
228
+ 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.
226
229
  2. **Explicit Profile Flag**: You can explicitly specify `--profile <name>` (or pass `profile: "<name>"` in MCP) from any directory to override automatic detection.
227
230
  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.
228
231
 
229
- ### 4. Recommended `.gitignore`
232
+ ### 4. Authentication Resolution Order & Token Auto-Refresh
233
+
234
+ Emit resolves authentication credentials using a strict 4-tier priority chain (`src/client/auth.ts`):
235
+
236
+ 1. **Environment Variables (`EMDASH_TOKEN` or `EMDASH_API_TOKEN`)**: Highest priority; ideal for CI/CD pipelines and one-off overrides. `EMDASH_ORIGIN` (and `EMDASH_WORKSPACE`) are read here as well and documented under [Origin Resolution Precedence](#origin-resolution-precedence).
237
+ 2. **Site Profile Registry (`~/.config/emit/profiles.yaml`)**: Matched via `--profile <name>` (or MCP `profile`), current workspace directory auto-detection, or a single registered profile.
238
+ 3. **EmDash CLI Credentials (`~/.config/emdash/auth.json`)**: Fallback when no token is configured in the profile.
239
+ 4. **Emit Custom Credentials (`~/.config/emit/credentials.json`)**: Final fallback.
240
+
241
+ **Automatic OAuth Token Refresh on HTTP 401 (`src/client/http.ts`)**:
242
+ When credentials include a `refresh_token` (such as OAuth sessions in `~/.config/emdash/auth.json` or `~/.config/emit/credentials.json`) and a request returns `401 Unauthorized`, Emit automatically exchanges the `refresh_token` at `POST /_emdash/api/auth/token` (`grant_type: "refresh_token"`), persists the refreshed token back to `auth.json` when applicable, and transparently retries the request once.
243
+
244
+ #### Origin Resolution Precedence
245
+
246
+ 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:
247
+
248
+ 1. **Explicitly selected profile** — `--profile <name>` (or MCP `profile: "<name>"`)
249
+ 2. **`EMDASH_ORIGIN`** — overrides an *implicitly* resolved profile so CI/staging overrides actually take effect
250
+ 3. **Auto-resolved profile** — current-directory match, unique origin match, or the single registered profile
251
+ 4. **Resolved auth origin** — carried by `~/.config/emdash/auth.json` / `~/.config/emit/credentials.json`
252
+ 5. **Workspace `.emit/config.json`** → then the `https://example.com` sentinel default
253
+
254
+ > [!NOTE]
255
+ > Every silent override is surfaced as a `⚠️ Warning` naming the **real source** of each value (`.emit/config.json` values are never mislabeled as `EMDASH_ORIGIN` or the reverse). When you select `--profile` explicitly, `EMDASH_ORIGIN` is ignored **and reported**. Comparisons ignore trailing slashes. `emit init` follows the same ranking (`--origin` > `EMDASH_ORIGIN` > auto-resolved profile) and refuses to link the workspace to a profile whose origin does not match the initialized origin.
256
+
257
+ ### 5. Workspace Configuration (`.emit/config.json`)
258
+
259
+ Running `emit init` generates `.emit/config.json` with the following keys (`src/utils/config.ts`):
260
+
261
+ ```json
262
+ {
263
+ "origin": "https://your-blog.example.com",
264
+ "defaultCollection": "posts",
265
+ "defaultLocale": "ja",
266
+ "postsSyncLimit": 100,
267
+ "mediaSyncLimit": 300,
268
+ "concurrency": 8
269
+ }
270
+ ```
271
+
272
+ | Key | Default | Description |
273
+ | --- | --- | --- |
274
+ | `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 |
275
+ | `defaultCollection` | `"posts"` | Default content collection when `--collection` is omitted |
276
+ | `defaultLocale` | `"ja"` | Default locale used when scaffolding new drafts via `emit new` / `sync_new` |
277
+ | `postsSyncLimit` | `100` | Sliding window size for batch article pulls (when `status: all`, fetches up to `postsSyncLimit` `published` + up to `postsSyncLimit` `draft` articles in parallel; also supports legacy `syncLimit` fallback) |
278
+ | `mediaSyncLimit` | `300` | Default maximum items fetched by `emit pull-media` / `sync_pull_media` |
279
+ | `concurrency` | `8` | Parallel HTTP fetch concurrency during batch pulls |
280
+
281
+ ### 6. Recommended `.gitignore`
230
282
 
231
283
  When tracking your local blog repository with Git, use the following recommended `.gitignore` configuration:
232
284
 
@@ -256,9 +308,8 @@ tmp/
256
308
  > [!TIP]
257
309
  > * `.emit/cache.db*` **must be excluded** from Git because it is a machine-local SQLite tracking database.
258
310
  > * `.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`.
259
- > * `media/*.yaml` metadata files can be committed to Git to track asset metadata alongside your Markdown articles.
260
- >
261
- >
311
+ > * `media/*.yaml` metadata files can be committed to Git to track asset metadata (`storageKey`, `contentHash`, `status`, dimensions) alongside your Markdown articles.
312
+ > * **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.
262
313
 
263
314
  ---
264
315
 
@@ -271,68 +322,104 @@ your-blog/
271
322
  ├── .emit/ # [Hidden System Directory - Do not edit manually]
272
323
  │ ├── config.json # Workspace configuration (origin, postsSyncLimit, etc.)
273
324
  │ └── cache.db # SQLite baseline tracking & hash database
274
- ├── categories/ # Category definition files
275
- │ ├── tech.yaml
276
- │ └── lifestyle.yaml
277
- ├── tags/ # Tag definition files
278
- │ ├── typescript.yaml
279
- │ └── ai.yaml
280
- ├── media/ # Media files and metadata
281
- │ ├── 01M6ABCDEF.jpg
282
- │ └── 01M6ABCDEF.jpg.yaml
283
- └── posts/ # Local Markdown Articles
284
- ├── draft/ # Draft articles (<yyyyMMdd>_<ulid>-<slug>.md)
285
- │ └── 20261104_01M6...-my-new-post.md
286
- └── published/ # Live published articles (<yyyyMMdd>_<ulid>-<slug>.md)
287
- └── 20261015_01M5...-getting-started.md
288
-
289
- ```
325
+ ├── categories/ # Category definition files (<id>-<label>.yaml)
326
+ │ ├── 01M6CAT1-Tech.yaml
327
+ │ └── 01M6CAT2-カメラ.yaml
328
+ ├── tags/ # Tag definition files (<id>-<label>.yaml)
329
+ │ ├── 01M6TAG1-Typescript.yaml
330
+ │ └── 01M6TAG2-Ai.yaml
331
+ ├── media/ # Media files and companion metadata (<yyyyMMdd>_<id>-<name>.<ext>)
332
+ │ ├── drafts/ # Staging folder for offline draft images (auto-uploaded on push)
333
+ │ │ └── sunset.png
334
+ │ ├── 20261101_01M6ABCDEF-header.jpg
335
+ │ └── 20261101_01M6ABCDEF-header.yaml
336
+ └── posts/ # Local Markdown Articles (<yyyyMMdd>_<ulid>-<title>.md)
337
+ ├── draft/ # Draft articles (named after title; falls back to slug if empty)
338
+ │ └── 20261104_01M6...-My-New-Post.md
339
+ ├── published/ # Live published articles
340
+ │ └── 20261015_01M5...-Getting-Started.md
341
+ └── conflicts/ # Read-only rescue copies of divergent local content (written by pull; never pushed or pruned)
342
+ └── 20261101_01M5...-Getting-Started.conflict.md
343
+
344
+ ```
345
+
346
+ ### Local SQLite Cache (`.emit/cache.db`)
347
+
348
+ Emit maintains a local SQLite database via Node.js built-in `node:sqlite` (`src/db/cache.ts`) with 5 tables:
349
+
350
+ | Table | Purpose |
351
+ | --- | --- |
352
+ | `entries` | Caches the latest remote JSON envelope and revision (`rev`) for each synced article |
353
+ | `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) |
354
+ | `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>")`) |
355
+ | `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 |
356
+ | `media_state` | Tracks offline draft image uploads (`local_hash`, `media_id`, `storage_key`, `url`, `workspace_path`, `status`: `pending` \| `uploaded` \| `ready` \| `failed`) for idempotency, retry recovery, and cross-article asset reuse |
290
357
 
291
358
  ---
292
359
 
293
360
  ## 📝 Markdown & YAML Frontmatter Specification
294
361
 
295
- Each article is stored as a Markdown file with a standard YAML frontmatter header.
362
+ Each article is stored as a Markdown file with a validated YAML frontmatter header (`FrontmatterSchema` in `src/utils/frontmatter.ts`).
296
363
 
297
364
  ### Frontmatter Schema
298
365
 
299
366
  ```yaml
300
367
  ---
301
- id: 01M6ABCDEF1234567890123456 # EmDash canonical ULID (auto-generated; DO NOT EDIT)
368
+ id: 01M6ABCDEF1234567890123456 # 26-char canonical ULID (auto-generated; DO NOT EDIT)
369
+ slug: building-ai-powered-cms-tools
302
370
  title: "Building AI-Powered CMS Tools"
303
- slug: "building-ai-powered-cms-tools"
304
- status: draft # Status: "draft" or "published"
305
- publishedAt: null # ISO 8601 timestamp (null for drafts)
306
- updatedAt: "2026-11-04T12:00:00.000Z"
307
- excerpt: "A deep dive into building autonomous content workflows with EmDash and Emit."
308
- featuredImage: "01M6MEDIA123456" # Media ULID or null
309
- categories: # Slugs matching definitions in categories/*.yaml
310
- - tech
311
- tags: # Slugs matching definitions in tags/*.yaml
312
- - typescript
313
- - ai
314
- customFields: # Optional custom field definitions
315
- canonicalUrl: ""
371
+ collection: posts # Target collection (defaults to "posts")
372
+ status: draft # "draft" or "published" (defaults to "draft")
373
+ publishedAt: null # ISO 8601 timestamp string (e.g. "2026-11-04T12:00:00.000Z") or null
374
+ locale: ja # Locale code (defaults to "ja")
375
+ featuredImage: "01M67P0MBMJQQXH2K95HHCYRYB" # Optional: 26-char media ULID (uppercase-normalized) or null
376
+ taxonomies: # Map of taxonomy key -> array of term slugs (or canonical term ULIDs)
377
+ categories:
378
+ - tech
379
+ tags:
380
+ - typescript
381
+ - ai
382
+ seo: # Optional SEO metadata block
383
+ title: null # Custom SEO title string or null
384
+ description: null # Meta description string or null
385
+ canonical: null # Absolute URL (e.g. "https://example.com/posts/...") or null
386
+ noIndex: false # Boolean (optional)
387
+ image: null # SEO image string or null
388
+ bylines: # Author byline slugs (resolved via cached bylines table)
389
+ - aroooy
316
390
  ---
317
391
 
318
392
  Markdown body begins here...
319
393
 
320
394
  ```
321
395
 
396
+ > [!IMPORTANT]
397
+ > **Schema Validation & Serialization Rules (`src/utils/frontmatter.ts`)**:
398
+ > * **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()))`).
399
+ > * **`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`.
400
+ > * **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.
401
+ > * **`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.
402
+
322
403
  ### Critical Content Rules
323
404
 
324
405
  1. **Do Not Duplicate the Title in the Body**:
325
406
  - The article title is managed by the `title:` field in the frontmatter.
326
407
  - **Do not** add `# Article Title` as the first heading in the Markdown body. Doing so causes duplicate title displays on the site frontend. (Emit's preflight guard G7 will detect and automatically trim redundant top headings).
327
408
  2. **Taxonomy Slugs & Normalization**:
328
- - Items under `categories:` and `tags:` should use existing slug names from `categories/` and `tags/` (e.g. `- tech`, `- nodejs`).
409
+ - Items under `taxonomies.categories` and `taxonomies.tags` should use existing slug names from `categories/*.yaml` and `tags/*.yaml` (e.g. `- tech`, `- nodejs`). Note that while taxonomy definition filenames include the ULID (`categories/<id>-<slug>.yaml`), the frontmatter entries use the **slug**.
329
410
  - If a valid term ULID is specified in the frontmatter, Emit automatically resolves it transparently to its corresponding slug for Preflight Guard G4 validation and maintains accurate term assignment on remote sync.
330
411
  - Do not duplicate the same term using both its slug and ULID within the same taxonomy (e.g. `[- tech, 01M6CAT1]`). Emit de-duplicates identical term assignments, but a slug and a ULID that resolve to **different** terms will both be assigned to the article.
331
- - Never invent arbitrary IDs or invalid names; valid options are automatically displayed during `emit pull`, `emit pull-taxonomies`, `emit new`, `sync_pull`, `sync_pull_taxonomies`, and `sync_new`. Preflight Guard G4 strictly validates taxonomy terms before pushing.
412
+ - Never invent arbitrary IDs or invalid names; valid options are automatically displayed as hints during `emit pull`, `emit pull-taxonomies`, `emit new`, `sync_pull`, `sync_pull_taxonomies`, and `sync_new`. Hint lists are capped per command (with an `...and N more` marker), so the authoritative full list is always the `categories/` and `tags/` YAML files. Preflight Guard G4 strictly validates taxonomy terms before pushing.
332
413
  3. **Status Transitions**:
333
414
  - To publish a draft, update `status: published` in the frontmatter and execute `emit push <filePath> --allow-publish`.
334
415
  - Emit automatically updates the remote state and moves the file from `posts/draft/` to `posts/published/`.
335
416
  - **Never move or rename files manually using `mv`**.
417
+ 4. **Featured Image References**:
418
+ - `featuredImage` must be a 26-character canonical media ULID (e.g. `01M67P0MBMJQQXH2K95HHCYRYB`) or `null`. Emit normalizes the value to uppercase, so lowercase input is accepted and canonicalized on pull, merge, and push.
419
+ - To remove the hero image, set `featuredImage: null`. Omitting the field leaves the remote value untouched.
420
+ - 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.
421
+ - 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.
422
+ - Preflight Guard **G10** rejects local file paths and malformed values before pushing.
336
423
 
337
424
  ### EmDash Custom Block Preservation
338
425
 
@@ -348,18 +435,46 @@ EmDash supports rich visual blocks (e.g., galleries, callouts, audio players). W
348
435
 
349
436
  ### Media Management & Upload Workflow
350
437
 
351
- EmDash stores media files (images, documents) in Cloudflare R2 via multipart upload endpoints.
438
+ EmDash stores media files (images, documents) in Cloudflare R2 and uses **two distinct identifier spaces**:
439
+
440
+ | Usage | Identifier Used | Format / Example |
441
+ | --- | --- | --- |
442
+ | Frontmatter `featuredImage` | **Media Record `id`** | 26-character ULID (`01M67P0MBMJQQXH2K95HHCYRYB`) or `null` |
443
+ | Body `![alt](...)` URL | **Media `storageKey`** | `/_emdash/api/media/file/<storageKey>` (e.g. `/_emdash/api/media/file/01M67P07X2VPW0MYRRRGMA6T5A.jpg` or double-ULID `01M6TF1XXBSC06QQG9M648B4K3.01M6TF1Y5YVG9YYQAY6H25270M.png`) |
444
+
445
+ > [!IMPORTANT]
446
+ > 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.
447
+
448
+ #### Workflow A: Offline Draft Image Auto-Upload (`media/drafts/` — Recommended for Body Images)
449
+
450
+ You can author articles completely offline using local images and let `emit push` handle uploading, URL rewriting, and local file promotion automatically:
352
451
 
353
- **How to add and use images in your articles:**
452
+ 1. **Stage Image in `media/drafts/`**: Place your image in `<workspace>/media/drafts/<filename>` (supported extensions: `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.avif`, `.svg`; max 50 MiB).
453
+ 2. **Reference on a Standalone Line**: In your Markdown body, reference the staged image on its own standalone line:
454
+ ```markdown
455
+ ![Sunset over clouds](../../media/drafts/sunset.png)
456
+ ```
457
+ 3. **Push the Article (`emit push` / `sync_push`)**: Emit automatically:
458
+ - 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.
459
+ - Rewrites the Markdown line in place to the canonical root-relative `/_emdash/api/media/file/<storageKey>` URL.
460
+ - Pushes the article to EmDash.
461
+ - Promotes the local file from `media/drafts/<filename>` to `media/<yyyyMMdd>_<mediaId>-<filename>` alongside a companion `.yaml` metadata file (`storageKey`, `contentHash`, `status`). The `<yyyyMMdd>` prefix reflects the media record's remote `createdAt` when available; if Emit only had a provisional local date, the next `emit pull-media` canonicalizes the filename.
462
+ 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]`.
354
463
 
355
- 1. **Upload New Images**: Upload images via the **EmDash Admin Web Dashboard** (`/admin/media`) or via the official EmDash MCP tool (`media_upload`). This generates a canonical ULID and uploads the asset to Cloudflare R2.
356
- 2. **Sync Media Locally**: Run `emit pull-media` in your workspace. This downloads the images and companion `.yaml` metadata files into `media/`.
464
+ #### Workflow B: Pre-Uploaded Media & Featured Hero Images
465
+
466
+ 1. **Upload via Admin / EmDash MCP**: Upload images via the **EmDash Admin Web Dashboard** (`/admin/media`) or via the official EmDash MCP tool (`media_upload`).
467
+ 2. **Sync Media Locally (optional)**: Run `emit pull-media` in your workspace to download images and companion `.yaml` metadata files into `media/`. Note that `emit pull-media --prune` safely ignores `media/drafts/`.
357
468
  3. **Embed in Articles**:
358
- - For featured hero images: set `featuredImage: "01M6..."` in frontmatter.
359
- - For inline images in body: use standard Markdown syntax with remote URLs: `![Alt text](https://your-blog.example.com/api/v1/media/01M6.../file.jpg)`.
469
+ - For featured hero images: set `featuredImage: "01M67P0MBMJQQXH2K95HHCYRYB"` (26-character canonical media record `id`) or `null` in frontmatter.
470
+ - For body images: use `![Alt text](/_emdash/api/media/file/<storageKey>)`.
360
471
 
361
472
  > [!WARNING]
362
- > **No Local Image Paths in Markdown**: Preflight Guard **G5** strictly forbids pushing articles containing local filesystem paths like `![pic](./my-image.png)`. Images must be uploaded to Cloudflare R2 before publishing to prevent broken images in production.
473
+ > **Local Image Guardrails (G5 & G11)**:
474
+ > - Arbitrary local paths (e.g. `![pic](./my-image.png)`, 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.
475
+ > - Local images placed inline inside a paragraph (e.g. `Text ![pic](../../media/drafts/a.png) more text`) are blocked with `INLINE_LOCAL_IMAGE_NOT_SUPPORTED` because EmDash Portable Text requires images to be standalone top-level blocks.
476
+ > - 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).
477
+ > - 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.
363
478
 
364
479
  ---
365
480
 
@@ -390,21 +505,24 @@ Whether you are a human developer or an autonomous AI agent (Antigravity, Claude
390
505
  * To push a draft: `emit push posts/draft/<filename>.md`
391
506
  * To publish live: update `status: published` and run `emit push posts/draft/<filename>.md --allow-publish`.
392
507
 
393
- ### Preflight Safety Guardrails (G1–G9)
508
+ ### Preflight Safety Guardrails (G1–G12)
394
509
 
395
- To ensure autonomous AI agents and automated scripts cannot corrupt workspace state or accidentally publish unintended content, Emit enforces 9 strict preflight guardrails before executing any write operation:
510
+ To ensure autonomous AI agents and automated scripts cannot corrupt workspace state or accidentally publish unintended content, Emit enforces 12 strict preflight guardrails before executing any write operation:
396
511
 
397
512
  | Guard | Code Name | Description | Severity |
398
513
  | --- | --- | --- | --- |
399
514
  | **G1** | `SINGLE_TARGET_GUARD` | Enforces explicit target (`filePath`, `id`, or `--all`) for push/pull; forbids blind bulk push without inspection | Error |
400
515
  | **G2** | `ID_MISMATCH_ERROR` | Verifies that frontmatter `id` exactly matches the filename ULID; forbids manual `mv` renames | Error |
401
516
  | **G3** | `UNINTENDED_PUBLISH_GUARD` | Rejects promoting `draft` to `published` unless `--allow-publish` (`allowPublish: true`) is explicitly given | Error |
402
- | **G4** | `INVALID_TAXONOMY_ERROR` | Validates category and tag slugs against local YAML definitions to stop AI taxonomy hallucination | Error |
403
- | **G5** | `LOCAL_IMAGE_DETECTED` | Detects unresolved local image paths (`![alt](./path.png)`); requires remote R2 URLs | Error |
517
+ | **G4** | `INVALID_TAXONOMY_ERROR` | Validates taxonomy terms against local definitions (transparently resolves valid term ULIDs) to stop AI taxonomy hallucination | Error |
518
+ | **G5** | `LOCAL_IMAGE_DETECTED` / `INLINE_LOCAL_IMAGE_NOT_SUPPORTED` / `MISSING_LOCAL_IMAGE_FILE` | Rejects local image paths other than `media/drafts/<file>` (or an already-promoted `media/<yyyyMMdd>_<id>-<name>.<ext>` asset) and rejects inline paragraph local images (must be a standalone line) | Error |
404
519
  | **G6** | `UNRESOLVED_CONFLICT_MARKERS` | Scans for Git 3-way merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and blocks push | Error |
405
520
  | **G7** | `TITLE_DUPLICATION_AUTOTRIM` | Detects redundant `# Title` headings at the top of the body and automatically trims them safely | Warning |
406
521
  | **G8** | `UNSUPPORTED_SYNTAX_WARNING` | Warns if unsupported block HTML or incompatible constructs are detected in Markdown | Warning |
407
522
  | **G9** | `MODIFIED_OPAQUE_BLOCK` | Verifies that `<!--ec:block ... -->` opaque fences have not been tampered with or corrupted | Error |
523
+ | **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 |
524
+ | **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 |
525
+ | **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 |
408
526
 
409
527
  ---
410
528
 
@@ -460,7 +578,7 @@ emit profile use <name>
460
578
  Scaffolds a new draft article with a canonical 26-character ULID timestamp.
461
579
 
462
580
  ```bash
463
- emit new <slug> [-t, --title <title>] [-c, --collection <collection>] [-l, --locale <locale>]
581
+ emit new <slug> [-t, --title <title>] [-c, --collection <collection>] [-l, --locale <locale>] [-b, --byline <slug>] [-p, --profile <name>]
464
582
 
465
583
  ```
466
584
 
@@ -468,6 +586,8 @@ emit new <slug> [-t, --title <title>] [-c, --collection <collection>] [-l, --loc
468
586
  * `-t, --title <title>`: Human-readable article title.
469
587
  * `-c, --collection <name>`: Collection name (defaults to `posts`).
470
588
  * `-l, --locale <locale>`: Locale code (defaults to `ja` or config).
589
+ * `-b, --byline <slug>`: Author byline slug (defaults to the first cached non-guest author ordered by `slug ASC`).
590
+ * `-p, --profile <name>`: Explicitly target a specific connection profile.
471
591
 
472
592
  **Example:**
473
593
 
@@ -480,7 +600,7 @@ emit new ai-workflow-guide --title "AI Workflow Guide"
480
600
 
481
601
  ```text
482
602
  ✅ Created new article scaffold (ID: 01M6ABCDEF1234567890123456)
483
- Path: posts/draft/20261104_01M6ABCDEF1234567890123456-ai-workflow-guide.md
603
+ Path: posts/draft/20261104_01M6ABCDEF1234567890123456-AI-Workflow-Guide.md
484
604
 
485
605
  Available Categories:
486
606
  - tech (技術)
@@ -490,7 +610,7 @@ Available Tags:
490
610
  - ai, cloudflare, emit, typescript
491
611
 
492
612
  Next: Write your content in Markdown, then run:
493
- emit push posts/draft/20261104_01M6ABCDEF1234567890123456-ai-workflow-guide.md
613
+ emit push posts/draft/20261104_01M6ABCDEF1234567890123456-AI-Workflow-Guide.md
494
614
 
495
615
  ```
496
616
 
@@ -506,21 +626,24 @@ emit pull [id] [options]
506
626
  ```
507
627
 
508
628
  * `[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.
509
- * `-s, --status <draft|published|all>`: Filter by publication status (default: `all`).
629
+ * `-s, --status <draft|published|all>`: Filter by publication status (default: `all`). When set to `all`, the sliding window (`postsSyncLimit`, default `100`) is applied **per status** (fetching up to `postsSyncLimit` `published` + up to `postsSyncLimit` `draft` articles in parallel).
510
630
  * `-c, --collection <collection>`: Collection name (default: `posts`).
511
631
  * `--no-prune`: Do not prune local articles that have fallen outside the sliding window (escape hatch; not intended for routine use).
512
632
  * `--force`: Force overwrite local files and bypass differential hash checking.
513
633
  * `--concurrency <n>`: Parallel processing task limit (default: `8`).
514
- * `--posts-only`: Skip automatic taxonomy (categories/tags) sync. Pull articles only.
515
- * `--media`: Also pull media assets and companion metadata into `media/`.
634
+ * `--posts-only`: Skip automatic remote taxonomy sync. Pulls articles only (local taxonomy candidates hint is still displayed from cached definitions).
635
+ * `--media`: Also pull media assets and companion metadata into `media/` (bounded by `mediaSyncLimit`, default `300`). Use `emit pull-media` for `--limit`, `--all`, and `--prune` control.
516
636
  * `--metadata-only`: With `--media`: download only metadata YAML, skip binary file downloads.
517
637
 
518
638
  > [!NOTE]
519
- > **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 slugs). This eliminates a common source of push failures. Use `--posts-only` if you need articles only.
639
+ > **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.
520
640
 
521
641
  > [!NOTE]
522
642
  > **Why `--no-prune` is rarely needed:**
523
- > Automatic pruning is mathematically loss-free: Emit's unpushed edits safeguard strictly aborts the pull if an older article contains local modifications. You only need `--no-prune` in exceptional circumstances, such as temporarily suppressing Git working-tree changes during a quick pull.
643
+ > 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.
644
+
645
+ > [!NOTE]
646
+ > **Unpushed writing is never discarded.** When a pull finds local content that diverges from the last synced baseline, Emit first moves that file to `posts/conflicts/<name>.conflict.md` (a read-only rescue copy) and then writes the canonical article. Rescue copies are never pushed or pruned automatically, and pushing one directly is refused with `QUARANTINED_COPY_NOT_PUSHABLE` (Guard **G12**): read the copy, merge any valuable content into the canonical article, then delete it. If an invalid file also carries local edits, `--force` is the escape hatch (it quarantines the divergent copy, then writes the repaired file).
524
647
 
525
648
  **Example:**
526
649
 
@@ -547,11 +670,19 @@ emit pull 01M5XYZ9876543210123456789
547
670
  ```text
548
671
  Syncing categories and tags...
549
672
  ✅ Taxonomies: 12 categories, 34 tags
550
- Fetching articles from remote (posts, status: all, window: 100, concurrency: 8)...
551
- [1/100] ✅ Pulled hello-world -> posts/published/20261101_01M5...-Hello-World.md
552
- [2/100] ✅ Pulled draft-post -> posts/draft/20261104_01M6...-Draft-Post.md
553
673
 
554
- 🎉 Articles: 100 total (12 updated, 88 skipped/up-to-date)
674
+ Available Categories:
675
+ - tech (技術)
676
+ - column (コラム)
677
+
678
+ Available Tags:
679
+ - ai, cloudflare, emit, typescript
680
+
681
+ Fetching articles from remote (posts, status: all, window: 100/status, concurrency: 8)...
682
+ [1/169] ✅ Pulled hello-world -> posts/published/20261101_01M5...-Hello-World.md
683
+ [2/169] ✅ Pulled draft-post -> posts/draft/20261104_01M6...-Draft-Post.md
684
+
685
+ 🎉 Articles: 169 total [100 published, 69 draft] (12 updated, 157 skipped/up-to-date)
555
686
 
556
687
  ```
557
688
 
@@ -597,16 +728,20 @@ emit status [-c, --collection <collection>] [-p, --profile <name>]
597
728
 
598
729
  ```text
599
730
  --- Workspace Status (posts | origin: https://your-blog.example.com) ---
600
- 🟡 [modified] draft posts/draft/20261104_01M6BBB...-ai-workflow-guide.md (AI Workflow Guide)
601
- ⚪ [untracked] draft posts/draft/20261104_01M6AAA...-notes.md (Draft Notes)
602
- 🟢 [synced ] published posts/published/20261015_01M5ZZZ...-getting-started.md (Getting Started)
731
+ 🟡 [modified] draft posts/draft/20261104_01M6BBB...-AI-Workflow-Guide.md (AI Workflow Guide)
732
+ ⚪ [untracked] draft posts/draft/20261104_01M6AAA...-Draft-Notes.md (Draft Notes)
733
+ 🟢 [synced ] published posts/published/20261015_01M5ZZZ...-Getting-Started.md (Getting Started)
734
+ 🔴 [invalid ] published posts/published/20190405_01M6CCC...-Broken-Post.md (Broken-Post) [uncached] — bad indentation of a mapping entry at line 5
603
735
 
604
- Summary: 3 total (1 synced, 1 modified, 1 untracked)
736
+ Summary: 4 total (1 synced, 1 modified, 1 untracked, 1 invalid)
605
737
  ```
606
738
 
607
739
  > [!NOTE]
608
740
  > Articles are sorted newest-first by date prefix and ULID. The date prefix reflects the article's publication or creation date, not the last-modified time. To locate currently active edits, look for files marked `[modified]`.
609
741
 
742
+ > [!NOTE]
743
+ > `[invalid]` marks a file whose YAML frontmatter could not be parsed (usually an older serializer wrote unquoted percent-encoded taxonomy slugs). Batch `emit pull` self-heals cached invalid files; files outside the sliding window are marked `[uncached]` and are repaired with `emit pull <ULID>` (MCP: `sync_pull` with `id: "<ULID>"`). When `<collection>/conflicts/` holds rescue copies, the count is reported separately (`conflicts`) and never counted as an article.
744
+
610
745
  ---
611
746
 
612
747
  ### `emit diff`
@@ -614,15 +749,19 @@ Summary: 3 total (1 synced, 1 modified, 1 untracked)
614
749
  Shows line-by-line differences between the working Markdown file and the baseline snapshot.
615
750
 
616
751
  ```bash
617
- emit diff [filePath]
752
+ emit diff [filePath] [-i, --id <id>] [-p, --profile <name>]
618
753
 
619
754
  ```
620
755
 
756
+ * `[filePath]`: Relative path to the local Markdown file (e.g., `posts/draft/20261104_01M6...-my-article.md`).
757
+ * `-i, --id <id>`: Inspect by 26-character article ULID instead of file path.
758
+ * `-p, --profile <name>`: Explicitly target a specific connection profile.
759
+
621
760
  ---
622
761
 
623
762
  ### `emit pull-taxonomies`
624
763
 
625
- Downloads categories and tags from EmDash and generates local YAML definitions under `categories/` and `tags/`. This is also performed automatically by `emit pull` in batch mode.
764
+ Downloads categories and tags from EmDash and generates local YAML definitions under `categories/` and `tags/`. This is also performed automatically by `emit pull` in batch mode. If one taxonomy endpoint fails, existing local definitions for that taxonomy are preserved with a warning; if both endpoints fail, the command exits with code `1`.
626
765
 
627
766
  ```bash
628
767
  emit pull-taxonomies
@@ -635,6 +774,13 @@ emit pull-taxonomies
635
774
  Pulling categories and tags from remote...
636
775
  ✅ Successfully synced taxonomies: 12 categories, 34 tags.
637
776
 
777
+ Available Categories:
778
+ - tech (技術)
779
+ - column (コラム)
780
+
781
+ Available Tags:
782
+ - ai, cloudflare, emit, typescript
783
+
638
784
  ```
639
785
 
640
786
  ---
@@ -731,15 +877,18 @@ Emit features a first-class **Model Context Protocol (MCP)** server over stdio.
731
877
 
732
878
  | Tool | Purpose | Key Parameters |
733
879
  | --- | --- | --- |
734
- | `sync_status` | Inspect local sync state (articles returned newest-first) | `collection` |
735
- | `sync_pull` | Pull remote articles **and taxonomies** into local workspace | `id`, `force`, `status`, `noPrune`, `postsOnly`, `includeMedia`, `metadataOnly` |
736
- | `sync_new` | Scaffold new article with canonical ULID and frontmatter | `slug`, `title`, `locale`, `collection` |
737
- | `sync_push` | Push local article changes to remote with preflight checks | `filePath`, `id`, `allowPublish` |
738
- | `sync_diff` | Inspect diff between working file and baseline snapshot | `filePath` |
739
- | `sync_pull_taxonomies` | Pull remote category and tag definitions (standalone; also included in `sync_pull`) | (none) |
740
- | `sync_pull_media` | Pull remote media files and metadata into `media/` | `limit`, `all`, `prune`, `metadataOnly` |
880
+ | `sync_status` | Inspect local sync state (articles newest-first, plus `invalid` and `conflicts` diagnostics) | `collection`, `profile` |
881
+ | `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` |
882
+ | `sync_new` | Scaffold new article with canonical ULID, frontmatter, default byline, and taxonomy hints | `slug` (required), `title`, `collection`, `locale`, `byline`, `profile` |
883
+ | `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` |
884
+ | `sync_diff` | Inspect diff between working file and baseline snapshot | `filePath`, `id`, `profile` |
885
+ | `sync_pull_taxonomies` | Pull remote category and tag definitions (standalone; returns taxonomy hints) | `profile` |
886
+ | `sync_pull_media` | Pull remote media files and metadata into `media/` | `limit`, `all`, `downloadFiles`, `profile` |
741
887
  | `profile_list` | List all configured connection profiles | (none) |
742
- | `profile_switch` | Switch active profile for multi-site environments | `name` |
888
+ | `profile_switch` | Switch active profile for multi-site environments | `name` (required), `persist` |
889
+
890
+ > [!NOTE]
891
+ > **Every tool failure is a structured result, never a raw throw.** Handlers return `isError: true` with the same diagnostics the CLI prints — `[CODE]`, HTTP status, endpoint, and a `💡 Suggestion` line (or a `PREFLIGHT_VALIDATION_FAILED` JSON payload with guard codes). This includes workspace/profile resolution failures, which list the available profiles so the agent can retry with an explicit `profile` argument.
743
892
 
744
893
  ### Autonomous Agent Workflows
745
894
 
@@ -757,7 +906,7 @@ With Emit connected via MCP, you can give high-level natural language instructio
757
906
 
758
907
  To eliminate race conditions when multiple users or agents edit the same post:
759
908
 
760
- 1. Emit issues a `POST /api/v1/content/lock` request to EmDash before pushing.
909
+ 1. Emit issues a `POST /_emdash/api/content/{collection}/{id}/lock` request to EmDash before pushing.
761
910
  2. EmDash grants a 7-minute mutual exclusion lease.
762
911
  3. If another author currently holds the lock, Emit aborts with an informative message indicating who owns the lock and when the lease expires.
763
912
  4. Emit guarantees lock release using strict `try/finally` execution.
@@ -804,6 +953,12 @@ Push your changes with 'emit push <file>' or discard them before pulling.
804
953
 
805
954
  **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.
806
955
 
956
+ ### Q: `[NETWORK_ERROR]`: Network request failed (`fetch failed`)
957
+
958
+ **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`.
959
+
960
+ **Solution**: Verify the `origin` URL reported in `Endpoint:` and check `emit profile list` / `.emit/config.json`. If `.emit/config.json` and your active profile point to different origins, Emit prints an origin mismatch warning to help you reconcile them.
961
+
807
962
  ---
808
963
 
809
964
  ## 🧪 Testing & Quality Assurance
@@ -1 +1 @@
1
- {"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/new.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,kBAAkB,GAAG,IAAI,CAkC7F"}
1
+ {"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/new.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,kBAAkB,GAAG,IAAI,CAyC7F"}
@@ -24,10 +24,11 @@ import { getLocalTaxonomiesSummary, formatTaxonomiesHint } from '../../sync/taxo
24
24
  export function registerNewCommand(program, resolveContext) {
25
25
  program
26
26
  .command('new <slug>')
27
- .description('Create a new draft article template with an auto-generated ULID')
27
+ .description('Create a new draft article template with an auto-generated ULID and taxonomy hints')
28
28
  .option('-t, --title <title>', 'Article title')
29
29
  .option('-c, --collection <collection>', 'Target collection (defaults to "posts")')
30
30
  .option('-l, --locale <locale>', 'Article locale (defaults to "ja")')
31
+ .option('-b, --byline <slug>', 'Author byline slug (defaults to first cached non-guest author by slug)')
31
32
  .option('-p, --profile <name>', 'Explicitly use a specific connection profile')
32
33
  .action((slug, options) => {
33
34
  const ctx = resolveContext(options.profile);
@@ -38,6 +39,7 @@ export function registerNewCommand(program, resolveContext) {
38
39
  title: options.title,
39
40
  collection,
40
41
  locale,
42
+ byline: options.byline,
41
43
  workspaceRoot: ctx.root,
42
44
  cache: ctx.cache,
43
45
  });
@@ -1 +1 @@
1
- {"version":3,"file":"new.js","sourceRoot":"","sources":["../../../src/cli/commands/new.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAG3F,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,cAAkC;IACrF,OAAO;SACJ,OAAO,CAAC,YAAY,CAAC;SACrB,WAAW,CAAC,iEAAiE,CAAC;SAC9E,MAAM,CAAC,qBAAqB,EAAE,eAAe,CAAC;SAC9C,MAAM,CAAC,+BAA+B,EAAE,yCAAyC,CAAC;SAClF,MAAM,CAAC,uBAAuB,EAAE,mCAAmC,CAAC;SACpE,MAAM,CAAC,sBAAsB,EAAE,8CAA8C,CAAC;SAC9E,MAAM,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE;QACxB,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC;QACtE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC;QAE1D,MAAM,GAAG,GAAG,qBAAqB,CAAC;YAChC,IAAI;YACJ,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,UAAU;YACV,MAAM;YACN,aAAa,EAAE,GAAG,CAAC,IAAI;YACvB,KAAK,EAAE,GAAG,CAAC,KAAK;SACjB,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,uCAAuC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;QAExC,MAAM,UAAU,GAAG,yBAAyB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,oBAAoB,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;QAClE,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,GAAG,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC;QAC9B,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;QACjE,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACP,CAAC"}
1
+ {"version":3,"file":"new.js","sourceRoot":"","sources":["../../../src/cli/commands/new.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAG3F,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,cAAkC;IACrF,OAAO;SACJ,OAAO,CAAC,YAAY,CAAC;SACrB,WAAW,CACV,oFAAoF,CACrF;SACA,MAAM,CAAC,qBAAqB,EAAE,eAAe,CAAC;SAC9C,MAAM,CAAC,+BAA+B,EAAE,yCAAyC,CAAC;SAClF,MAAM,CAAC,uBAAuB,EAAE,mCAAmC,CAAC;SACpE,MAAM,CACL,qBAAqB,EACrB,wEAAwE,CACzE;SACA,MAAM,CAAC,sBAAsB,EAAE,8CAA8C,CAAC;SAC9E,MAAM,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE;QACxB,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC;QACtE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC;QAE1D,MAAM,GAAG,GAAG,qBAAqB,CAAC;YAChC,IAAI;YACJ,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,UAAU;YACV,MAAM;YACN,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,aAAa,EAAE,GAAG,CAAC,IAAI;YACvB,KAAK,EAAE,GAAG,CAAC,KAAK;SACjB,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,uCAAuC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;QAC9D,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;QAExC,MAAM,UAAU,GAAG,yBAAyB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,oBAAoB,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;QAClE,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,GAAG,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC;QAC9B,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;QACjE,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/profile.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuF9D"}
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/profile.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0I9D"}