emdash-emit 0.0.0-stage โ†’ 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +794 -2
  3. package/dist/bin/emit.d.ts +5 -0
  4. package/dist/bin/emit.d.ts.map +1 -0
  5. package/dist/bin/emit.js +62 -0
  6. package/dist/bin/emit.js.map +1 -0
  7. package/dist/cli/commands/new.d.ts +4 -0
  8. package/dist/cli/commands/new.d.ts.map +1 -0
  9. package/dist/cli/commands/new.js +48 -0
  10. package/dist/cli/commands/new.js.map +1 -0
  11. package/dist/cli/commands/profile.d.ts +3 -0
  12. package/dist/cli/commands/profile.d.ts.map +1 -0
  13. package/dist/cli/commands/profile.js +104 -0
  14. package/dist/cli/commands/profile.js.map +1 -0
  15. package/dist/cli/commands/pull.d.ts +4 -0
  16. package/dist/cli/commands/pull.d.ts.map +1 -0
  17. package/dist/cli/commands/pull.js +157 -0
  18. package/dist/cli/commands/pull.js.map +1 -0
  19. package/dist/cli/commands/push.d.ts +4 -0
  20. package/dist/cli/commands/push.d.ts.map +1 -0
  21. package/dist/cli/commands/push.js +107 -0
  22. package/dist/cli/commands/push.js.map +1 -0
  23. package/dist/cli/commands/status.d.ts +4 -0
  24. package/dist/cli/commands/status.d.ts.map +1 -0
  25. package/dist/cli/commands/status.js +100 -0
  26. package/dist/cli/commands/status.js.map +1 -0
  27. package/dist/cli/context.d.ts +15 -0
  28. package/dist/cli/context.d.ts.map +1 -0
  29. package/dist/cli/context.js +75 -0
  30. package/dist/cli/context.js.map +1 -0
  31. package/dist/client/api.d.ts +19 -0
  32. package/dist/client/api.d.ts.map +1 -0
  33. package/dist/client/api.js +96 -0
  34. package/dist/client/api.js.map +1 -0
  35. package/dist/client/auth.d.ts +48 -0
  36. package/dist/client/auth.d.ts.map +1 -0
  37. package/dist/client/auth.js +165 -0
  38. package/dist/client/auth.js.map +1 -0
  39. package/dist/client/error.d.ts +22 -0
  40. package/dist/client/error.d.ts.map +1 -0
  41. package/dist/client/error.js +57 -0
  42. package/dist/client/error.js.map +1 -0
  43. package/dist/client/http.d.ts +31 -0
  44. package/dist/client/http.d.ts.map +1 -0
  45. package/dist/client/http.js +97 -0
  46. package/dist/client/http.js.map +1 -0
  47. package/dist/client/lock.d.ts +40 -0
  48. package/dist/client/lock.d.ts.map +1 -0
  49. package/dist/client/lock.js +104 -0
  50. package/dist/client/lock.js.map +1 -0
  51. package/dist/client/mapper.d.ts +37 -0
  52. package/dist/client/mapper.d.ts.map +1 -0
  53. package/dist/client/mapper.js +134 -0
  54. package/dist/client/mapper.js.map +1 -0
  55. package/dist/client/portable-text.d.ts +94 -0
  56. package/dist/client/portable-text.d.ts.map +1 -0
  57. package/dist/client/portable-text.js +428 -0
  58. package/dist/client/portable-text.js.map +1 -0
  59. package/dist/client/schemas.d.ts +1928 -0
  60. package/dist/client/schemas.d.ts.map +1 -0
  61. package/dist/client/schemas.js +123 -0
  62. package/dist/client/schemas.js.map +1 -0
  63. package/dist/db/cache.d.ts +46 -0
  64. package/dist/db/cache.d.ts.map +1 -0
  65. package/dist/db/cache.js +230 -0
  66. package/dist/db/cache.js.map +1 -0
  67. package/dist/index.d.ts +23 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +44 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/mcp/context.d.ts +14 -0
  72. package/dist/mcp/context.d.ts.map +1 -0
  73. package/dist/mcp/context.js +23 -0
  74. package/dist/mcp/context.js.map +1 -0
  75. package/dist/mcp/server.d.ts +12 -0
  76. package/dist/mcp/server.d.ts.map +1 -0
  77. package/dist/mcp/server.js +105 -0
  78. package/dist/mcp/server.js.map +1 -0
  79. package/dist/mcp/tools/profile.d.ts +6 -0
  80. package/dist/mcp/tools/profile.d.ts.map +1 -0
  81. package/dist/mcp/tools/profile.js +81 -0
  82. package/dist/mcp/tools/profile.js.map +1 -0
  83. package/dist/mcp/tools/sync-diff.d.ts +4 -0
  84. package/dist/mcp/tools/sync-diff.d.ts.map +1 -0
  85. package/dist/mcp/tools/sync-diff.js +115 -0
  86. package/dist/mcp/tools/sync-diff.js.map +1 -0
  87. package/dist/mcp/tools/sync-new.d.ts +4 -0
  88. package/dist/mcp/tools/sync-new.d.ts.map +1 -0
  89. package/dist/mcp/tools/sync-new.js +54 -0
  90. package/dist/mcp/tools/sync-new.js.map +1 -0
  91. package/dist/mcp/tools/sync-pull.d.ts +4 -0
  92. package/dist/mcp/tools/sync-pull.d.ts.map +1 -0
  93. package/dist/mcp/tools/sync-pull.js +180 -0
  94. package/dist/mcp/tools/sync-pull.js.map +1 -0
  95. package/dist/mcp/tools/sync-push.d.ts +4 -0
  96. package/dist/mcp/tools/sync-push.d.ts.map +1 -0
  97. package/dist/mcp/tools/sync-push.js +105 -0
  98. package/dist/mcp/tools/sync-push.js.map +1 -0
  99. package/dist/mcp/tools/sync-status.d.ts +4 -0
  100. package/dist/mcp/tools/sync-status.d.ts.map +1 -0
  101. package/dist/mcp/tools/sync-status.js +45 -0
  102. package/dist/mcp/tools/sync-status.js.map +1 -0
  103. package/dist/sync/diff.d.ts +16 -0
  104. package/dist/sync/diff.d.ts.map +1 -0
  105. package/dist/sync/diff.js +68 -0
  106. package/dist/sync/diff.js.map +1 -0
  107. package/dist/sync/media.d.ts +37 -0
  108. package/dist/sync/media.d.ts.map +1 -0
  109. package/dist/sync/media.js +191 -0
  110. package/dist/sync/media.js.map +1 -0
  111. package/dist/sync/merge.d.ts +36 -0
  112. package/dist/sync/merge.d.ts.map +1 -0
  113. package/dist/sync/merge.js +112 -0
  114. package/dist/sync/merge.js.map +1 -0
  115. package/dist/sync/new.d.ts +21 -0
  116. package/dist/sync/new.d.ts.map +1 -0
  117. package/dist/sync/new.js +65 -0
  118. package/dist/sync/new.js.map +1 -0
  119. package/dist/sync/orchestrator.d.ts +55 -0
  120. package/dist/sync/orchestrator.d.ts.map +1 -0
  121. package/dist/sync/orchestrator.js +211 -0
  122. package/dist/sync/orchestrator.js.map +1 -0
  123. package/dist/sync/preflight.d.ts +25 -0
  124. package/dist/sync/preflight.d.ts.map +1 -0
  125. package/dist/sync/preflight.js +128 -0
  126. package/dist/sync/preflight.js.map +1 -0
  127. package/dist/sync/pull.d.ts +67 -0
  128. package/dist/sync/pull.d.ts.map +1 -0
  129. package/dist/sync/pull.js +382 -0
  130. package/dist/sync/pull.js.map +1 -0
  131. package/dist/sync/push.d.ts +33 -0
  132. package/dist/sync/push.d.ts.map +1 -0
  133. package/dist/sync/push.js +363 -0
  134. package/dist/sync/push.js.map +1 -0
  135. package/dist/sync/status.d.ts +24 -0
  136. package/dist/sync/status.d.ts.map +1 -0
  137. package/dist/sync/status.js +85 -0
  138. package/dist/sync/status.js.map +1 -0
  139. package/dist/sync/taxonomies.d.ts +49 -0
  140. package/dist/sync/taxonomies.d.ts.map +1 -0
  141. package/dist/sync/taxonomies.js +220 -0
  142. package/dist/sync/taxonomies.js.map +1 -0
  143. package/dist/utils/config.d.ts +18 -0
  144. package/dist/utils/config.d.ts.map +1 -0
  145. package/dist/utils/config.js +79 -0
  146. package/dist/utils/config.js.map +1 -0
  147. package/dist/utils/frontmatter.d.ts +115 -0
  148. package/dist/utils/frontmatter.d.ts.map +1 -0
  149. package/dist/utils/frontmatter.js +147 -0
  150. package/dist/utils/frontmatter.js.map +1 -0
  151. package/dist/utils/naming.d.ts +28 -0
  152. package/dist/utils/naming.d.ts.map +1 -0
  153. package/dist/utils/naming.js +94 -0
  154. package/dist/utils/naming.js.map +1 -0
  155. package/dist/utils/profile.d.ts +72 -0
  156. package/dist/utils/profile.d.ts.map +1 -0
  157. package/dist/utils/profile.js +180 -0
  158. package/dist/utils/profile.js.map +1 -0
  159. package/dist/utils/workspace.d.ts +22 -0
  160. package/dist/utils/workspace.d.ts.map +1 -0
  161. package/dist/utils/workspace.js +74 -0
  162. package/dist/utils/workspace.js.map +1 -0
  163. package/package.json +68 -4
package/README.md CHANGED
@@ -1,3 +1,795 @@
1
- # Temporary Holding Version
1
+ # Emit (`emdash-emit`)
2
+
3
+ [![CI](https://github.com/aroooy/emdash-emit/actions/workflows/ci.yml/badge.svg)](https://github.com/aroooy/emdash-emit/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/emdash-emit.svg)](https://www.npmjs.com/package/emdash-emit)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22.0.0-brightgreen.svg)](https://nodejs.org/)
7
+
8
+ **Emit** is an AI-first local workspace and bidirectional synchronization tool for [EmDash](https://github.com/aroooy/emdash) (Cloudflare Workers + D1). It provides a full-featured CLI and a Model Context Protocol (MCP) server designed for autonomous coding agents (Antigravity, Claude Code, Cursor) and human writers.
9
+
10
+ ---
11
+
12
+ ## ๐Ÿ“‘ Table of Contents
13
+
14
+ - [Overview & Architecture Philosophy](#-overview--architecture-philosophy)
15
+ - [The Authoring Desk Philosophy & Non-Goals](#1-the-authoring-desk-philosophy--non-goals)
16
+ - [The Sliding Window "Staging Desk" Model](#2-the-sliding-window-staging-desk-model)
17
+ - [Unpushed Edits Safeguard](#3-unpushed-edits-safeguard)
18
+ - [Pure TypeScript & Zero Native Builds](#4-pure-typescript--zero-native-builds)
19
+ - [Distributed Mutex Lock & 3-Way Merge](#5-distributed-mutex-lock--3-way-merge)
20
+ - [๐ŸŒŸ Key Features](#-key-features)
21
+ - [๐Ÿ“ฆ Installation](#-installation)
22
+ - [๐Ÿš€ Initial Setup & Profiles](#-initial-setup--profiles)
23
+ - [Register a Profile](#1-register-a-profile)
24
+ - [Initialize a Workspace](#2-initialize-a-workspace)
25
+ - [Multi-Site Management](#3-multi-site-management)
26
+ - [Recommended `.gitignore`](#4-recommended-gitignore)
27
+ - [๐Ÿ“‚ Directory Structure & Data Specifications](#-directory-structure--data-specifications)
28
+ - [๐Ÿ“ Markdown & YAML Frontmatter Specification](#-markdown--yaml-frontmatter-specification)
29
+ - [Frontmatter Schema](#frontmatter-schema)
30
+ - [Critical Content Rules](#critical-content-rules)
31
+ - [EmDash Custom Block Preservation](#emdash-custom-block-preservation)
32
+ - [Media Management & Upload Workflow](#media-management--upload-workflow)
33
+ - [๐Ÿค– The Golden Path for AI & Humans (4-Step Cycle)](#-the-golden-path-for-ai--humans-4-step-cycle)
34
+ - [The 4-Step Authoring Cycle](#the-4-step-authoring-cycle)
35
+ - [Preflight Safety Guardrails (G1โ€“G9)](#preflight-safety-guardrails-g1g9)
36
+ - [๐Ÿ›  CLI Command Reference](#-cli-command-reference)
37
+ - [`emit init`](#emit-init)
38
+ - [`emit profile`](#emit-profile)
39
+ - [`emit new`](#emit-new)
40
+ - [`emit pull`](#emit-pull)
41
+ - [`emit push`](#emit-push)
42
+ - [`emit status`](#emit-status)
43
+ - [`emit diff`](#emit-diff)
44
+ - [`emit pull-taxonomies`](#emit-pull-taxonomies)
45
+ - [`emit pull-media`](#emit-pull-media)
46
+ - [`emit guide`](#emit-guide)
47
+ - [`emit mcp`](#emit-mcp)
48
+ - [๐Ÿค– Model Context Protocol (MCP) Support](#-model-context-protocol-mcp-support)
49
+ - [Setup Guide](#setup-guide)
50
+ - [Available MCP Tools](#available-mcp-tools)
51
+ - [Autonomous Agent Workflows](#autonomous-agent-workflows)
52
+ - [๐Ÿ”’ Distributed Concurrency & 3-Way Merge](#-distributed-concurrency--3-way-merge)
53
+ - [๐Ÿ”ง Troubleshooting & FAQ](#-troubleshooting--faq)
54
+ - [๐Ÿงช Testing & Quality Assurance](#-testing--quality-assurance)
55
+ - [๐Ÿ“„ License & Copyright](#-license--copyright)
56
+
57
+ ---
58
+
59
+ ## ๐Ÿ’ก Overview & Architecture Philosophy
60
+
61
+ EmDash is a modern serverless headless CMS built on Cloudflare Workers and D1. While Cloudflare D1 provides blazing-fast edge performance, authoring long-form blog posts and articles directly in a web dashboard can be cumbersome.
62
+
63
+ **Emit** bridges this gap. It turns any local directory into a Git-like local authoring workspace, allowing you and AI coding agents to edit Markdown files using your favorite editors (VS Code, Neovim, Cursor, Antigravity) and seamlessly sync them with EmDash.
64
+
65
+ ```text
66
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
67
+ โ”‚ Local Workspace โ”‚ emit push / pull โ”‚ Remote EmDash Server โ”‚
68
+ โ”‚ posts/draft/*.md โ”‚ โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ Cloudflare Workers โ”‚
69
+ โ”‚ posts/published/*.md โ”‚ (AST + Frontmatter)โ”‚ Cloudflare D1 SQL โ”‚
70
+ โ”‚ .emit/cache.db โ”‚ โ”‚ Cloudflare R2 Media โ”‚
71
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
72
+
73
+ ```
74
+
75
+ ### 1. The Authoring Desk Philosophy & Non-Goals
76
+
77
+ To prevent user expectations from misaligning with the tool's purpose, it is important to clarify **what Emit is and what it is not**:
78
+
79
+ * **What Emit IS (Core Goal)**:
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.
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.
85
+
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
+
88
+ > [!NOTE]
89
+ > **Can the sync limit be adjusted?**
90
+ > Yes. While `--all` is intentionally omitted from the CLI to protect the sliding window discipline, you can customize the window size by setting `postsSyncLimit: 300` (or any desired number) in `.emit/config.json`. Emit's internal pagination engine will automatically batch-fetch up to that configured limit.
91
+
92
+ ### 2. The Sliding Window "Staging Desk" Model
93
+
94
+ Traditional synchronization tools attempt to download the entire history of a site. If your blog has thousands of articles, local directories become cluttered, Ripgrep/VSCode search slows down, and AI context windows get overwhelmed with irrelevant historical data.
95
+
96
+ Emit adopts a **bounded sliding window**:
97
+
98
+ * It maintains an active sliding window of recent posts (default: **latest 100 posts**).
99
+ * 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.
100
+ * Any pruned article can be pulled back into the workspace at any time with `emit pull <id>`.
101
+
102
+ ### 3. Unpushed Edits Safeguard
103
+
104
+ What happens if an article with unpushed local modifications is pushed out of the sliding window?
105
+ Emit guarantees **zero data loss**:
106
+
107
+ * If an article has local modifications not yet pushed to the remote server, Emit's pruning engine **aborts the pull immediately** with a `SyncPruneConflictError`.
108
+ * It lists the exact unpushed file and informs the user to either push the changes (`emit push <file>`) or discard them before proceeding.
109
+
110
+ ### 4. Pure TypeScript & Zero Native Builds
111
+
112
+ Emit has **zero C++ native dependencies** (`node-gyp`).
113
+ By leveraging Node.js 22's built-in `node:sqlite` module (`DatabaseSync`), Emit installs in seconds on macOS, Linux, and Windows without requiring system build tools or Python.
114
+
115
+ ### 5. Distributed Mutex Lock & 3-Way Merge
116
+
117
+ To prevent concurrent race conditions when multiple authors or AI agents collaborate:
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.
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
+
122
+ ---
123
+
124
+ ## ๐ŸŒŸ Key Features
125
+
126
+ * **AI-First Authoring Workflow**: Built specifically for autonomous agents and humans to draft, inspect, and sync local Markdown files safely.
127
+ * **Sliding Window Synchronization**: Automatically manages an active sliding window of recent posts (default: latest 100), pruning older cached entries from disk.
128
+ * **Unpushed Edits Safeguard**: Detects unpushed local changes outside the sync window and aborts sync safely before any files can be deleted.
129
+ * **Git-Style Workspace Encapsulation**: Houses local SQLite cache (`node:sqlite`) and workspace configuration neatly inside a `.emit/` hidden folder.
130
+ * **Multi-Site Profile Management**: Manage multiple EmDash sites and credentials in `~/.config/emit/profiles.yaml` with automatic workspace directory detection.
131
+ * **Hybrid 3-Way Merge Engine**: Resolves concurrent changes between remote and local versions like Git, independently merging YAML frontmatter and Markdown body.
132
+ * **Distributed Mutual Exclusion Lock**: Acquires 7-minute leases via `POST /lock` with `try/finally` release semantics to eliminate race conditions during publishing.
133
+ * **Opaque Block Preservation**: Preserves EmDash custom blocks (e.g., `core:gallery`) losslessly across AST roundtrips via `<!--ec:block ... -->` fences.
134
+ * **Taxonomy & Media Synchronization**: Sync category/tag YAML schemas and download media assets with metadata.
135
+ * **Zero Native Dependencies**: Completely eliminates native C++ builds (`node-gyp`) by leveraging Node.js 22+ built-in `node:sqlite`.
136
+
137
+ ---
138
+
139
+ ## ๐Ÿ“ฆ Installation
140
+
141
+ Emit requires **Node.js v22.0.0** or higher.
142
+
143
+ ### Method A: Global Installation via npm (Recommended)
144
+
145
+ ```bash
146
+ npm install -g emdash-emit
147
+
148
+ # Verify installation
149
+ emit --version
150
+
151
+ ```
152
+
153
+ ### Method B: Temporary Execution via npx
154
+
155
+ Run commands without installing globally:
156
+
157
+ ```bash
158
+ npx emdash-emit status
159
+ npx emdash-emit pull
160
+
161
+ ```
162
+
163
+ ### Method C: Local Development Link
164
+
165
+ ```bash
166
+ git clone https://github.com/aroooy/emdash-emit.git
167
+ cd emdash-emit
168
+ npm install
169
+ npm run build
170
+ npm install -g .
171
+
172
+ ```
173
+
174
+ ---
175
+
176
+ ## ๐Ÿš€ Initial Setup & Profiles
177
+
178
+ ### 1. Register a Profile
179
+
180
+ Add your EmDash instance credentials to the global profile registry:
181
+
182
+ ```bash
183
+ emit profile add my-blog \
184
+ --origin https://your-blog.example.com \
185
+ --token <your-personal-access-token> \
186
+ --workspace /path/to/local/blog-repo \
187
+ --active
188
+
189
+ ```
190
+
191
+ * `--origin`: The base URL of your EmDash Cloudflare Workers instance.
192
+ * `--token`: Your Personal Access Token (PAT) generated from the EmDash dashboard.
193
+ * `--workspace`: The local directory where articles and assets will be synchronized.
194
+ * `--active`: Immediately marks this profile as the default.
195
+
196
+ > [!NOTE]
197
+ > Profiles are securely stored with `0600` permissions at `~/.config/emit/profiles.yaml` (on macOS / Linux) or `%USERPROFILE%\.config\emit\profiles.yaml` (on Windows).
198
+
199
+ ### 2. Initialize a Workspace
200
+
201
+ Navigate to your local repository directory and initialize it:
202
+
203
+ ```bash
204
+ cd /path/to/local/blog-repo
205
+ emit init
206
+
207
+ ```
208
+
209
+ This creates the `.emit/` directory with a default `config.json` and initializes the local baseline cache SQLite database (`.emit/cache.db`).
210
+
211
+ ### 3. Multi-Site Management
212
+
213
+ Switch between multiple EmDash sites effortlessly:
214
+
215
+ ```bash
216
+ # List all registered profiles
217
+ emit profile list
218
+
219
+ # Switch active profile
220
+ emit profile use staging-blog
221
+
222
+ # Remove a profile
223
+ emit profile remove old-blog
224
+
225
+ ```
226
+
227
+ ### 4. Recommended `.gitignore`
228
+
229
+ When tracking your local blog repository with Git, use the following recommended `.gitignore` configuration:
230
+
231
+ ```gitignore
232
+ # Emit Local Cache Database (Strictly exclude binary SQLite DBs)
233
+ .emit/cache.db*
234
+ .emit/*.db*
235
+ *.db
236
+ *.db-wal
237
+ *.db-shm
238
+
239
+ # OS & Editor temporary files
240
+ .DS_Store
241
+ Thumbs.db
242
+ Temp/
243
+ tmp/
244
+
245
+ # Optional: Media binaries (Uncomment if you prefer not to commit large images to Git)
246
+ # media/*.jpg
247
+ # media/*.png
248
+ # media/*.webp
249
+ # media/*.avif
250
+ # media/*.mp4
251
+
252
+ ```
253
+
254
+ > [!TIP]
255
+ > * `.emit/cache.db*` **must be excluded** from Git because it is a machine-local SQLite tracking database.
256
+ > * `.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`.
257
+ > * `media/*.yaml` metadata files can be committed to Git to track asset metadata alongside your Markdown articles.
258
+ >
259
+ >
260
+
261
+ ---
262
+
263
+ ## ๐Ÿ“‚ Directory Structure & Data Specifications
264
+
265
+ When synchronized, your local workspace will be organized as follows:
266
+
267
+ ```text
268
+ your-blog/
269
+ โ”œโ”€โ”€ .emit/ # [Hidden System Directory - Do not edit manually]
270
+ โ”‚ โ”œโ”€โ”€ config.json # Workspace configuration (origin, postsSyncLimit, etc.)
271
+ โ”‚ โ””โ”€โ”€ cache.db # SQLite baseline tracking & hash database
272
+ โ”œโ”€โ”€ categories/ # Category definition files
273
+ โ”‚ โ”œโ”€โ”€ tech.yaml
274
+ โ”‚ โ””โ”€โ”€ lifestyle.yaml
275
+ โ”œโ”€โ”€ tags/ # Tag definition files
276
+ โ”‚ โ”œโ”€โ”€ typescript.yaml
277
+ โ”‚ โ””โ”€โ”€ ai.yaml
278
+ โ”œโ”€โ”€ media/ # Media files and metadata
279
+ โ”‚ โ”œโ”€โ”€ 01M6ABCDEF.jpg
280
+ โ”‚ โ””โ”€โ”€ 01M6ABCDEF.jpg.yaml
281
+ โ””โ”€โ”€ posts/ # Local Markdown Articles
282
+ โ”œโ”€โ”€ draft/ # Draft articles (<yyyyMMdd>_<ulid>-<slug>.md)
283
+ โ”‚ โ””โ”€โ”€ 20261104_01M6...-my-new-post.md
284
+ โ””โ”€โ”€ published/ # Live published articles (<yyyyMMdd>_<ulid>-<slug>.md)
285
+ โ””โ”€โ”€ 20261015_01M5...-getting-started.md
286
+
287
+ ```
288
+
289
+ ---
290
+
291
+ ## ๐Ÿ“ Markdown & YAML Frontmatter Specification
292
+
293
+ Each article is stored as a Markdown file with a standard YAML frontmatter header.
294
+
295
+ ### Frontmatter Schema
296
+
297
+ ```yaml
298
+ ---
299
+ id: 01M6ABCDEF1234567890123456 # EmDash canonical ULID (auto-generated; DO NOT EDIT)
300
+ title: "Building AI-Powered CMS Tools"
301
+ slug: "building-ai-powered-cms-tools"
302
+ status: draft # Status: "draft" or "published"
303
+ publishedAt: null # ISO 8601 timestamp (null for drafts)
304
+ updatedAt: "2026-11-04T12:00:00.000Z"
305
+ excerpt: "A deep dive into building autonomous content workflows with EmDash and Emit."
306
+ featuredImage: "01M6MEDIA123456" # Media ULID or null
307
+ categories: # Slugs matching definitions in categories/*.yaml
308
+ - tech
309
+ tags: # Slugs matching definitions in tags/*.yaml
310
+ - typescript
311
+ - ai
312
+ customFields: # Optional custom field definitions
313
+ canonicalUrl: ""
314
+ ---
315
+
316
+ Markdown body begins here...
317
+
318
+ ```
319
+
320
+ ### Critical Content Rules
321
+
322
+ 1. **Do Not Duplicate the Title in the Body**:
323
+ - The article title is managed by the `title:` field in the frontmatter.
324
+ - **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).
325
+ 2. **Taxonomy Slugs**:
326
+ - Items under `categories:` and `tags:` must match existing slug names from `categories/` and `tags/`.
327
+ - Never invent arbitrary IDs or invalid names; pull taxonomies with `emit pull-taxonomies` to see available options.
328
+ 3. **Status Transitions**:
329
+ - To publish a draft, update `status: published` in the frontmatter and execute `emit push <filePath> --allow-publish`.
330
+ - Emit automatically updates the remote state and moves the file from `posts/draft/` to `posts/published/`.
331
+ - **Never move or rename files manually using `mv`**.
332
+
333
+ ### EmDash Custom Block Preservation
334
+
335
+ EmDash supports rich visual blocks (e.g., galleries, callouts, audio players). When converted from EmDash's internal AST to Markdown, Emit wraps non-standard blocks in opaque comment fences:
336
+
337
+ ```html
338
+ <!--ec:block type="core:gallery" id="01M6XYZ..." data="{\"columns\":3,\"images\":[...]}" -->
339
+ <!--/ec:block-->
340
+
341
+ ```
342
+
343
+ * **Rule**: Do not modify or delete these comment fences unless you intentionally want to remove the block. Emit preserves them losslessly on round-trips.
344
+
345
+ ### Media Management & Upload Workflow
346
+
347
+ EmDash stores media files (images, documents) in Cloudflare R2 via multipart upload endpoints.
348
+
349
+ **How to add and use images in your articles:**
350
+
351
+ 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.
352
+ 2. **Sync Media Locally**: Run `emit pull-media` in your workspace. This downloads the images and companion `.yaml` metadata files into `media/`.
353
+ 3. **Embed in Articles**:
354
+ - For featured hero images: set `featuredImage: "01M6..."` in frontmatter.
355
+ - 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)`.
356
+
357
+ > [!WARNING]
358
+ > **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.
359
+
360
+ ---
361
+
362
+ ## ๐Ÿค– The Golden Path for AI & Humans (4-Step Cycle)
363
+
364
+ Whether you are a human developer or an autonomous AI agent (Antigravity, Claude Code, Cursor), always follow the **4-Step Cycle**:
365
+
366
+ ```text
367
+ [1. CHECK] Inspect local workspace state โ”€โ”€โ–บ emit status
368
+ โ”‚
369
+ [2. PULL] Sync remote updates safely โ”€โ”€โ–บ emit pull
370
+ โ”‚
371
+ [3. EDIT] Write & refine Markdown directly โ”€โ”€โ–บ posts/draft/*.md
372
+ โ”‚
373
+ [4. PUSH] Push changes to remote โ”€โ”€โ–บ emit push <filePath>
374
+
375
+ ```
376
+
377
+ ### The 4-Step Authoring Cycle
378
+
379
+ 1. **CHECK (`emit status`)**: Check your workspace status to see synced, modified, and untracked files.
380
+ 2. **PULL (`emit pull`)**: Synchronize the latest sliding window from the server into your local workspace.
381
+ 3. **EDIT (Direct Editing)**:
382
+ * For new posts: run `emit new <slug> --title "My Title"` to scaffold a canonical draft.
383
+ * Edit the Markdown body and YAML frontmatter in `posts/draft/` or `posts/published/`.
384
+
385
+ 4. **PUSH (`emit push`)**:
386
+ * To push a draft: `emit push posts/draft/<filename>.md`
387
+ * To publish live: update `status: published` and run `emit push posts/draft/<filename>.md --allow-publish`.
388
+
389
+ ### Preflight Safety Guardrails (G1โ€“G9)
390
+
391
+ 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:
392
+
393
+ | Guard | Code Name | Description | Severity |
394
+ | --- | --- | --- | --- |
395
+ | **G1** | `SINGLE_TARGET_GUARD` | Enforces explicit target (`filePath`, `id`, or `--all`) for push/pull; forbids blind bulk push without inspection | Error |
396
+ | **G2** | `ID_MISMATCH_ERROR` | Verifies that frontmatter `id` exactly matches the filename ULID; forbids manual `mv` renames | Error |
397
+ | **G3** | `UNINTENDED_PUBLISH_GUARD` | Rejects promoting `draft` to `published` unless `--allow-publish` (`allowPublish: true`) is explicitly given | Error |
398
+ | **G4** | `INVALID_TAXONOMY_ERROR` | Validates category and tag slugs against local YAML definitions to stop AI taxonomy hallucination | Error |
399
+ | **G5** | `LOCAL_IMAGE_DETECTED` | Detects unresolved local image paths (`![alt](./path.png)`); requires remote R2 URLs | Error |
400
+ | **G6** | `UNRESOLVED_CONFLICT_MARKERS` | Scans for Git 3-way merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and blocks push | Error |
401
+ | **G7** | `TITLE_DUPLICATION_AUTOTRIM` | Detects redundant `# Title` headings at the top of the body and automatically trims them safely | Warning |
402
+ | **G8** | `UNSUPPORTED_SYNTAX_WARNING` | Warns if unsupported block HTML or incompatible constructs are detected in Markdown | Warning |
403
+ | **G9** | `MODIFIED_OPAQUE_BLOCK` | Verifies that `<!--ec:block ... -->` opaque fences have not been tampered with or corrupted | Error |
404
+
405
+ ---
406
+
407
+ ## ๐Ÿ›  CLI Command Reference
408
+
409
+ ### `emit init`
410
+
411
+ Initializes the current directory as an Emit local workspace.
412
+
413
+ ```bash
414
+ emit init [--origin <url>]
415
+
416
+ ```
417
+
418
+ * `--origin <url>`: Optional EmDash origin URL. If omitted, uses the active profile.
419
+
420
+ **Example Output:**
421
+
422
+ ```text
423
+ โœ… Initialized Emit workspace at: /Volumes/ext/github/my-blog
424
+ Config: /Volumes/ext/github/my-blog/.emit/config.json
425
+ Database: /Volumes/ext/github/my-blog/.emit/cache.db
426
+
427
+ ```
428
+
429
+ ---
430
+
431
+ ### `emit profile`
432
+
433
+ Manage connection profiles across multiple EmDash instances.
434
+
435
+ ```bash
436
+ # List all profiles
437
+ emit profile list
438
+
439
+ # Add or update a profile
440
+ emit profile add <name> --origin <url> [--token <pat>] [--workspace <path>] [--active]
441
+
442
+ # Switch active default profile
443
+ emit profile use <name>
444
+
445
+ # Remove a profile
446
+ emit profile remove <name>
447
+
448
+ ```
449
+
450
+ ---
451
+
452
+ ### `emit new`
453
+
454
+ Scaffolds a new draft article with a canonical 26-character ULID timestamp.
455
+
456
+ ```bash
457
+ emit new <slug> [-t, --title <title>] [-c, --collection <collection>] [-l, --locale <locale>]
458
+
459
+ ```
460
+
461
+ * `<slug>`: URL slug for the article (e.g., `modern-web-architecture`).
462
+ * `-t, --title <title>`: Human-readable article title.
463
+ * `-c, --collection <name>`: Collection name (defaults to `posts`).
464
+ * `-l, --locale <locale>`: Locale code (defaults to `ja` or config).
465
+
466
+ **Example:**
467
+
468
+ ```bash
469
+ emit new ai-workflow-guide --title "AI Workflow Guide"
470
+
471
+ ```
472
+
473
+ **Output:**
474
+
475
+ ```text
476
+ โœ… Created new article scaffold (ID: 01M6ABCDEF1234567890123456)
477
+ Path: posts/draft/20261104_01M6ABCDEF1234567890123456-ai-workflow-guide.md
478
+
479
+ Next: Write your content in Markdown, then run:
480
+ emit push posts/draft/20261104_01M6ABCDEF1234567890123456-ai-workflow-guide.md
481
+
482
+ ```
483
+
484
+ ---
485
+
486
+ ### `emit pull`
487
+
488
+ Synchronizes remote articles into the local workspace sliding window.
489
+
490
+ ```bash
491
+ emit pull [id] [options]
492
+
493
+ ```
494
+
495
+ * `[id]`: Optional article ID or ULID. If specified, pulls only that specific article (even if outside the window).
496
+ * `-s, --status <draft|published|all>`: Filter by publication status (default: `all`).
497
+ * `-c, --collection <collection>`: Collection name (default: `posts`).
498
+ * `--no-prune`: Do not prune local articles that have fallen outside the sliding window (escape hatch; not intended for routine use).
499
+ * `--force`: Force overwrite local files and bypass differential hash checking.
500
+ * `--concurrency <n>`: Parallel processing task limit (default: `8`).
501
+
502
+ > [!NOTE]
503
+ > **Why `--no-prune` is rarely needed:**
504
+ > 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.
505
+
506
+ **Example:**
507
+
508
+ ```bash
509
+ # Pull the latest sliding window (prunes older cached files automatically)
510
+ emit pull
511
+
512
+ # Pull a specific legacy article by ID
513
+ emit pull 01M5XYZ9876543210123456789
514
+
515
+ ```
516
+
517
+ ---
518
+
519
+ ### `emit push`
520
+
521
+ Pushes local markdown article changes to the remote EmDash instance.
522
+
523
+ ```bash
524
+ emit push [file] [options]
525
+
526
+ ```
527
+
528
+ * `[file]`: Relative or absolute path to the Markdown file.
529
+ * `--allow-publish`: Required when promoting an article to `status: published`.
530
+ * `--all`: Pushes all modified files in the workspace.
531
+ * `-p, --profile <name>`: Explicitly target a specific connection profile.
532
+
533
+ **Example:**
534
+
535
+ ```bash
536
+ # Push draft updates
537
+ emit push posts/draft/20261104_01M6...-my-article.md
538
+
539
+ # Publish live to remote
540
+ emit push posts/draft/20261104_01M6...-my-article.md --allow-publish
541
+
542
+ ```
543
+
544
+ ---
545
+
546
+ ### `emit status`
547
+
548
+ Displays the local workspace synchronization status compared against the baseline cache.
549
+
550
+ ```bash
551
+ emit status [-c, --collection <collection>] [-p, --profile <name>]
552
+
553
+ ```
554
+
555
+ **Example Output:**
556
+
557
+ ```text
558
+ --- Workspace Status (posts | origin: https://your-blog.example.com) ---
559
+ ๐ŸŸข [synced ] published posts/published/20261015_01M5...-getting-started.md (Getting Started)
560
+ ๐ŸŸก [modified] draft posts/draft/20261104_01M6...-ai-workflow-guide.md (AI Workflow Guide)
561
+ โšช [untracked] draft posts/draft/20261104_01M6...-notes.md (Draft Notes)
562
+
563
+ Summary: 3 total (1 synced, 1 modified, 1 untracked)
564
+
565
+ ```
566
+
567
+ ---
568
+
569
+ ### `emit diff`
570
+
571
+ Shows line-by-line differences between the working Markdown file and the baseline snapshot.
572
+
573
+ ```bash
574
+ emit diff [filePath]
575
+
576
+ ```
577
+
578
+ ---
579
+
580
+ ### `emit pull-taxonomies`
581
+
582
+ Downloads categories and tags from EmDash and generates local YAML definitions under `categories/` and `tags/`.
583
+
584
+ ```bash
585
+ emit pull-taxonomies
586
+
587
+ ```
588
+
589
+ **Output:**
590
+
591
+ ```text
592
+ Pulling categories and tags from remote...
593
+ โœ… Successfully synced taxonomies: 12 categories, 34 tags.
594
+
595
+ ```
596
+
597
+ ---
598
+
599
+ ### `emit pull-media`
600
+
601
+ Downloads media assets and their companion metadata YAML files into `media/`.
602
+
603
+ ```bash
604
+ emit pull-media [-l, --limit <n>] [--all] [--prune] [--metadata-only]
605
+
606
+ ```
607
+
608
+ * `-l, --limit <n>`: Maximum number of media items to fetch (default: `300`).
609
+ * `--all`: Fetch all media items without limit.
610
+ * `--prune`: Remove local media files that no longer exist on remote.
611
+ * `--metadata-only`: Download metadata YAML files only, skipping large binary downloads.
612
+
613
+ ---
614
+
615
+ ### `emit guide`
616
+
617
+ Prints the concise Golden Path cheat sheet and AI safety rules directly to the console.
618
+
619
+ ```bash
620
+ emit guide
621
+
622
+ ```
623
+
624
+ ---
625
+
626
+ ### `emit mcp`
627
+
628
+ Launches Emit as a Model Context Protocol (MCP) stdio server for AI agents.
629
+
630
+ ```bash
631
+ emit mcp [-w, --workspace <path>]
632
+
633
+ ```
634
+
635
+ ---
636
+
637
+ ## ๐Ÿค– Model Context Protocol (MCP) Support
638
+
639
+ Emit features a first-class **Model Context Protocol (MCP)** server over stdio. This empowers AI assistantsโ€”such as **Google Antigravity**, **Claude Code**, and **Cursor**โ€”to autonomously manage, draft, review, and synchronize EmDash content.
640
+
641
+ ### Setup Guide
642
+
643
+ #### 1. Claude Desktop (`claude_desktop_config.json`)
644
+
645
+ ```json
646
+ {
647
+ "mcpServers": {
648
+ "emit": {
649
+ "command": "emit",
650
+ "args": ["mcp"]
651
+ }
652
+ }
653
+ }
654
+
655
+ ```
656
+
657
+ #### 2. Cursor (`.cursor/mcp.json`)
658
+
659
+ ```json
660
+ {
661
+ "mcpServers": {
662
+ "emit": {
663
+ "command": "npx",
664
+ "args": ["-y", "emdash-emit", "mcp"]
665
+ }
666
+ }
667
+ }
668
+
669
+ ```
670
+
671
+ #### 3. Google Antigravity (`~/.gemini/antigravity/mcp_config.json`)
672
+
673
+ ```json
674
+ {
675
+ "mcpServers": {
676
+ "emit": {
677
+ "command": "emit",
678
+ "args": ["mcp"]
679
+ }
680
+ }
681
+ }
682
+
683
+ ```
684
+
685
+ > **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.
686
+
687
+ ### Available MCP Tools
688
+
689
+ | Tool | Purpose | Key Parameters |
690
+ | --- | --- | --- |
691
+ | `sync_status` | Inspect local sync state (`synced`, `modified`, `untracked`) | `collection` |
692
+ | `sync_pull` | Pull remote posts into local sliding window or pull by ID | `id`, `force`, `status`, `noPrune` (escape hatch) |
693
+ | `sync_new` | Scaffold new article with canonical ULID and frontmatter | `slug`, `title`, `locale`, `collection` |
694
+ | `sync_push` | Push local article changes to remote with preflight checks | `filePath`, `id`, `allowPublish` |
695
+ | `sync_diff` | Inspect diff between working file and baseline snapshot | `filePath` |
696
+ | `sync_pull_taxonomies` | Pull remote category and tag definitions into local YAML files | (none) |
697
+ | `sync_pull_media` | Pull remote media files and metadata into `media/` | `limit`, `all`, `prune`, `metadataOnly` |
698
+ | `profile_list` | List all configured connection profiles | (none) |
699
+ | `profile_switch` | Switch active profile for multi-site environments | `name` |
700
+
701
+ ### Autonomous Agent Workflows
702
+
703
+ With Emit connected via MCP, you can give high-level natural language instructions to your AI:
704
+
705
+ * *"Check the workspace status, pull the latest posts, and show me what was changed recently."*
706
+ * *"Draft a new blog post titled 'Next-Gen Serverless Architectures' with slug `serverless-next`, add the `tech` category, and write a comprehensive outline."*
707
+ * *"Review the draft in `posts/draft/...`, proofread the content, and push it to remote as a draft."*
708
+
709
+ ---
710
+
711
+ ## ๐Ÿ”’ Distributed Concurrency & 3-Way Merge
712
+
713
+ ### Distributed Mutex Lock Protocol
714
+
715
+ To eliminate race conditions when multiple users or agents edit the same post:
716
+
717
+ 1. Emit issues a `POST /api/v1/content/lock` request to EmDash before pushing.
718
+ 2. EmDash grants a 7-minute mutual exclusion lease.
719
+ 3. If another author currently holds the lock, Emit aborts with an informative message indicating who owns the lock and when the lease expires.
720
+ 4. Emit guarantees lock release using strict `try/finally` execution.
721
+
722
+ ### Hybrid 3-Way Merge
723
+
724
+ If a remote article was updated after you pulled it:
725
+
726
+ 1. Emit loads the **Base Snapshot** (the state when you last pulled), the **Local Working File**, and the **Remote Server State**.
727
+ 2. **YAML Frontmatter**: Fields are merged key-by-key. Non-conflicting changes are applied automatically.
728
+ 3. **Markdown Body**: Merged line-by-line using `node-diff3`.
729
+ 4. If a conflict occurs, standard Git conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) are injected for human or agent resolution.
730
+
731
+ ---
732
+
733
+ ## ๐Ÿ”ง Troubleshooting & FAQ
734
+
735
+ ### Q: `SyncPruneConflictError`: Pull aborted due to unpushed modifications
736
+
737
+ ```text
738
+ โŒ Cannot prune 1 local article(s) outside the sync window because they contain unpushed changes:
739
+ - posts/draft/20261001_01M4...-old-draft.md
740
+ Push your changes with 'emit push <file>' or discard them before pulling.
741
+
742
+ ```
743
+
744
+ **Cause**: An article that has fallen outside the sliding window (e.g., beyond the latest 100 posts) contains local edits that have not been pushed to the remote server. To prevent accidental data loss, Emit aborts the pull.
745
+
746
+ **Solution**:
747
+
748
+ * If you want to keep the changes: Push them to remote using `emit push posts/draft/...`.
749
+ * If you want to discard local edits: Delete or revert the file, then re-run `emit pull`.
750
+ * If you want to temporarily skip pruning: Run `emit pull --no-prune`.
751
+
752
+ ### Q: `PushValidationError`: Cannot publish article live without `--allow-publish`
753
+
754
+ **Cause**: Frontmatter has `status: published`, but `emit push` was executed without `--allow-publish`.
755
+
756
+ **Solution**: Run `emit push <filePath> --allow-publish`. This safety guardrail prevents accidental live publication.
757
+
758
+ ### Q: Taxonomies not recognized during push
759
+
760
+ **Cause**: Categories or tags in frontmatter do not exist in the remote database.
761
+
762
+ **Solution**: Run `emit pull-taxonomies` to refresh your local category and tag list, and ensure frontmatter values match existing slugs.
763
+
764
+ ---
765
+
766
+ ## ๐Ÿงช Testing & Quality Assurance
767
+
768
+ Emit adheres to high code quality and test-driven development standards:
769
+
770
+ ```bash
771
+ # Run unit test suite (Vitest)
772
+ npm test
773
+
774
+ # Run TypeScript strict typecheck
775
+ npm run typecheck
776
+
777
+ # Verify Prettier code style
778
+ npm run format:check
779
+
780
+ # Auto-format codebase
781
+ npm run format
782
+
783
+ # Run complete CI verification suite
784
+ npm run check
785
+
786
+ ```
787
+
788
+ ---
789
+
790
+ ## ๐Ÿ“„ License & Copyright
791
+
792
+ This project is open-source software licensed under the [MIT License](LICENSE).
793
+
794
+ Copyright (c) 2026 SPORT-SERVICE RSโ˜…R. All rights reserved.
2
795
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.