@devyrpauli/mddocs 0.1.0 → 0.2.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 (3) hide show
  1. package/README.md +39 -29
  2. package/cli.mjs +710 -70
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,8 +49,8 @@ server. `mddocs` takes a different approach:
49
49
  |---|---|
50
50
  | Browser editor (comments, suggestions, provenance) | `mddocs open <file>` (single-user) or `mddocs serve <file>` (multiplayer) |
51
51
  | Real-time multiplayer with presence | `mddocs serve <file>`: everyone on the URL co-edits live; edits persist to the file plus git |
52
- | Role-based share links (editor / commenter / viewer) | `serve` prints a link per role; viewers are read-only, enforced server-side |
53
- | Agent HTTP API | AI tools read state and post comments/suggestions live, attributed to `ai:<model>` |
52
+ | Role-based share links (editor / commenter / viewer) | `serve` prints a link per role; roles enforced server-side (viewers read-only, commenters cannot edit prose) |
53
+ | Agent HTTP API | AI tools read state, post comments/suggestions, or rewrite prose live, attributed to `ai:<model>` |
54
54
  | Comments and suggestions from the terminal | `mddocs comment ...`, `mddocs suggest ...`, `mddocs accept`/`reject` |
55
55
  | History and diff | `mddocs log <file>`, `mddocs diff <file> [rev]` (plain git underneath) |
56
56
  | Async multiplayer and conflict resolution | edit on branches; `mddocs resolve <file>` unions a conflicted PROOF footer |
@@ -129,23 +129,38 @@ grant:
129
129
 
130
130
  - An absent or unknown token gets the least privilege (viewer), so a leaked bare
131
131
  URL cannot edit.
132
- - Viewers are enforced server-side: a viewer's WebSocket connection is read-only,
133
- so a crafted client still cannot write. The commenter-vs-editor split is gated
134
- in the editor UI (a comment is itself a write).
132
+ - Roles are enforced server-side, not just in the editor UI. A viewer's WebSocket
133
+ connection is read-only, so a crafted client cannot write at all. A commenter
134
+ may write comments (a comment is a write to the marks map) but cannot edit the
135
+ prose: any prose change from a commenter connection is reverted server-side
136
+ before it persists or reaches other clients. Editors can do both.
135
137
 
136
138
  ## Agent HTTP API
137
139
 
138
140
  A live `serve` session also exposes an HTTP API so AI agents can read the
139
- document and post comments/suggestions. They appear in every connected editor in
140
- real time and persist to git, attributed to `ai:<model>`. `serve` prints the base
141
- URL and an agent token; send it as the `x-share-token` header.
141
+ document, post comments/suggestions, and edit the prose directly. Everything
142
+ appears in every connected editor in real time and persists to git, attributed to
143
+ `ai:<model>`. `serve` prints the base URL and an agent token; send it as the
144
+ `x-share-token` header.
142
145
 
143
146
  ```
144
147
  GET /api/agent/:slug/state -> { content, marks }
145
148
  POST /api/agent/:slug/comment { quote, text, model? } -> { id }
146
149
  POST /api/agent/:slug/suggest { quote, replace|insert|delete, model? } -> { id, kind }
150
+ POST /api/agent/:slug/rewrite { markdown, quote?, model? } -> { chars, by, markId? }
147
151
  ```
148
152
 
153
+ `suggest` proposes a change a human accepts; `rewrite` edits the prose directly.
154
+ With a `quote`, `rewrite` replaces that span; without one it replaces the whole
155
+ body. The change is applied to the live document and recorded as an authored mark.
156
+
157
+ By default `serve` issues one shared agent token. Pass `--agent <name>` (repeatable)
158
+ to register named agents, each with its own token; `serve` then prints a token per
159
+ agent. A request that omits `model` is attributed to the token's agent name
160
+ (`ai:<name>`). Per-agent rate limits are available through the engine API
161
+ (`serveShare({ agents: [{ name, rateLimit: { maxRequests, windowMs } }] })`),
162
+ returning HTTP 429 when exceeded.
163
+
149
164
  ```bash
150
165
  # Read the live document:
151
166
  curl -H "x-share-token: $TOKEN" http://127.0.0.1:<port>/api/agent/notes.md/state
@@ -175,28 +190,30 @@ git add notes.md && git commit
175
190
 
176
191
  ```
177
192
  mddocs open <file> [--port <n>] [--no-autocommit] single-user browser editor (loopback)
178
- mddocs serve <file> [--port <n>] [--host <ip>] [--no-autocommit]
193
+ mddocs serve <file> [--port <n>] [--host <ip>] [--no-autocommit] [--agent <name>]
179
194
  live multiplayer, role links, agent API
180
195
  mddocs init mark the repo as mddocs-managed
181
196
  mddocs resolve <file> union a git-conflicted PROOF footer
182
197
 
183
198
  mddocs comment add <file> --quote <q> --text <t> add a comment anchored to <q>
184
199
  mddocs comment ls <file> [--open|--resolved|--orphaned]
185
- mddocs comment reply <id> --text <t> --file <f> reply in a comment thread
186
- mddocs comment resolve <id> --file <f> resolve a comment thread
200
+ mddocs comment reply <id> --text <t> [--file <f>] reply in a comment thread
201
+ mddocs comment resolve <id> [--file <f>] resolve a comment thread
187
202
 
188
203
  mddocs suggest <file> --quote <q> (--replace <c> | --insert <c> | --delete)
189
- mddocs accept <id> --file <f> mark a suggestion accepted
190
- mddocs reject <id> --file <f> mark a suggestion rejected
204
+ mddocs accept <id> [--file <f>] apply a suggestion to the prose
205
+ mddocs reject <id> [--file <f>] mark a suggestion rejected
191
206
 
192
207
  mddocs log <file> commit history for a document
193
208
  mddocs diff <file> [rev] changes vs working tree or a revision
194
209
  ```
195
210
 
196
- Notes. Id-only commands (`reply`, `resolve`, `accept`, `reject`) take an explicit
197
- `--file`; a global mark-to-file index is a later milestone. `accept` and `reject`
198
- record the decision on the mark (`status`); the prose rewrite for an accepted
199
- suggestion is applied in the editor.
211
+ Notes. Id-only commands (`reply`, `resolve`, `accept`, `reject`) find their
212
+ document automatically by scanning the managed `.md` files for the mark; pass
213
+ `--file <path>` to skip the scan or disambiguate. `accept` applies the suggested
214
+ change to the prose (replace, insert, or delete, anchored by the suggestion's
215
+ quote) and consumes the suggestion; `reject` records the decision on the mark and
216
+ leaves the prose unchanged.
200
217
 
201
218
  ## Architecture
202
219
 
@@ -259,24 +276,17 @@ browser-interactive path is verified manually.
259
276
 
260
277
  - M1: local-first editor and CLI (comments, suggestions, provenance, git history). Done.
261
278
  - M2: live collaboration server (real-time multiplayer, file plus git canonical). Done.
262
- - M2.5: share links and roles (editor/commenter/viewer, server-side viewer enforcement). Done.
263
- - M3: agent HTTP API (read state, post comments/suggestions live). Done.
279
+ - M2.5: share links and roles (editor/commenter/viewer, server-side role
280
+ enforcement: viewers read-only, commenters cannot edit prose). Done.
281
+ - M3: agent HTTP API (read state, comment, suggest, and rewrite prose live). Done.
264
282
 
265
283
  ## Upcoming updates
266
284
 
267
285
  Contributions welcome. Next on the list:
268
286
 
269
- - Agent direct-rewrite endpoint: let agents edit prose directly, not just propose
270
- (v1 is propose-only; humans accept).
271
- - Per-agent identity tokens and rate limiting, instead of one shared agent token.
272
- - Commenter-granularity enforcement: viewers are enforced server-side; enforce the
273
- comment-vs-edit split on the wire too.
274
- - CLI `accept` applies the prose rewrite (today it records the decision; applying
275
- the edit to the body is editor-only).
276
- - Global mark-to-file index, so id-only commands no longer need an explicit `--file`.
277
287
  - Presence and events for agents.
278
- - Publish as an installable `mddocs` binary on npm, with a real project name.
279
- - CI to run both test suites on every push.
288
+ - Publish under a real, unscoped npm project name (currently the `@devyrpauli`
289
+ scope while the name is settled).
280
290
  - Upstream the `@proof/core` TS2308 fix
281
291
  ([proof-sdk#57](https://github.com/EveryInc/proof-sdk/pull/57)) and drop the
282
292
  local fork patch once merged.
package/cli.mjs CHANGED
@@ -4166,8 +4166,8 @@ var require_lib = __commonJS({
4166
4166
  if (typeof cb !== "function") {
4167
4167
  opts = cb;
4168
4168
  cb = null;
4169
- deferred2 = new this.Promise(function(resolve3, reject) {
4170
- deferredResolve = resolve3;
4169
+ deferred2 = new this.Promise(function(resolve4, reject) {
4170
+ deferredResolve = resolve4;
4171
4171
  deferredReject = reject;
4172
4172
  });
4173
4173
  }
@@ -4315,17 +4315,17 @@ var require_lib = __commonJS({
4315
4315
  if (typeof cb === "function") {
4316
4316
  fnx(cb);
4317
4317
  } else {
4318
- return new this.Promise(function(resolve3, reject) {
4318
+ return new this.Promise(function(resolve4, reject) {
4319
4319
  if (fnx.length === 1) {
4320
4320
  fnx(function(err, ret) {
4321
4321
  if (err) {
4322
4322
  reject(err);
4323
4323
  } else {
4324
- resolve3(ret);
4324
+ resolve4(ret);
4325
4325
  }
4326
4326
  });
4327
4327
  } else {
4328
- resolve3(fnx());
4328
+ resolve4(fnx());
4329
4329
  }
4330
4330
  });
4331
4331
  }
@@ -11708,7 +11708,7 @@ function canAppendWithSubstitutedLinebreaks(a, b2) {
11708
11708
  function joinable2(a, b2) {
11709
11709
  return !!(a && b2 && !a.isLeaf && canAppendWithSubstitutedLinebreaks(a, b2));
11710
11710
  }
11711
- function join2(tr, pos, depth) {
11711
+ function join3(tr, pos, depth) {
11712
11712
  let convertNewlines = null;
11713
11713
  let { linebreakReplacement } = tr.doc.type.schema;
11714
11714
  let $before = tr.doc.resolve(pos - depth), beforeType = $before.node().type;
@@ -13095,7 +13095,7 @@ var init_dist3 = __esm({
13095
13095
  last and first siblings are also joined, and so on.
13096
13096
  */
13097
13097
  join(pos, depth = 1) {
13098
- join2(this, pos, depth);
13098
+ join3(this, pos, depth);
13099
13099
  return this;
13100
13100
  }
13101
13101
  /**
@@ -19733,14 +19733,14 @@ var init_lib2 = __esm({
19733
19733
  this.#listener = null;
19734
19734
  this.#status = "pending";
19735
19735
  this.start = () => {
19736
- this.#promise ??= new Promise((resolve3, reject) => {
19736
+ this.#promise ??= new Promise((resolve4, reject) => {
19737
19737
  this.#listener = (e) => {
19738
19738
  if (!(e instanceof CustomEvent)) return;
19739
19739
  if (e.detail.id === this.#eventUniqId) {
19740
19740
  this.#status = "resolved";
19741
19741
  this.#removeListener();
19742
19742
  e.stopImmediatePropagation();
19743
- resolve3();
19743
+ resolve4();
19744
19744
  }
19745
19745
  };
19746
19746
  this.#waitTimeout(() => {
@@ -22430,10 +22430,10 @@ function resolveAll(constructs2, events, context) {
22430
22430
  const called = [];
22431
22431
  let index2 = -1;
22432
22432
  while (++index2 < constructs2.length) {
22433
- const resolve3 = constructs2[index2].resolveAll;
22434
- if (resolve3 && !called.includes(resolve3)) {
22435
- events = resolve3(events, context);
22436
- called.push(resolve3);
22433
+ const resolve4 = constructs2[index2].resolveAll;
22434
+ if (resolve4 && !called.includes(resolve4)) {
22435
+ events = resolve4(events, context);
22436
+ called.push(resolve4);
22437
22437
  }
22438
22438
  }
22439
22439
  return events;
@@ -28608,12 +28608,12 @@ function joinDefaults(left, right, parent, state) {
28608
28608
  return parent.spread ? 1 : 0;
28609
28609
  }
28610
28610
  }
28611
- var join3;
28611
+ var join4;
28612
28612
  var init_join = __esm({
28613
28613
  "node_modules/mdast-util-to-markdown/lib/join.js"() {
28614
28614
  init_format_code_as_indented();
28615
28615
  init_format_heading_as_setext();
28616
- join3 = [joinDefaults];
28616
+ join4 = [joinDefaults];
28617
28617
  }
28618
28618
  });
28619
28619
 
@@ -29060,7 +29060,7 @@ function toMarkdown(tree, options) {
29060
29060
  handle: void 0,
29061
29061
  indentLines,
29062
29062
  indexStack: [],
29063
- join: [...join3],
29063
+ join: [...join4],
29064
29064
  options: {},
29065
29065
  safe: safeBound,
29066
29066
  stack: [],
@@ -30357,7 +30357,7 @@ var init_lib16 = __esm({
30357
30357
  assertParser("process", this.parser || this.Parser);
30358
30358
  assertCompiler("process", this.compiler || this.Compiler);
30359
30359
  return done ? executor(void 0, done) : new Promise(executor);
30360
- function executor(resolve3, reject) {
30360
+ function executor(resolve4, reject) {
30361
30361
  const realFile = vfile(file);
30362
30362
  const parseTree = (
30363
30363
  /** @type {HeadTree extends undefined ? Node : HeadTree} */
@@ -30388,8 +30388,8 @@ var init_lib16 = __esm({
30388
30388
  function realDone(error, file2) {
30389
30389
  if (error || !file2) {
30390
30390
  reject(error);
30391
- } else if (resolve3) {
30392
- resolve3(file2);
30391
+ } else if (resolve4) {
30392
+ resolve4(file2);
30393
30393
  } else {
30394
30394
  ok2(done, "`done` is defined if `resolve` is not");
30395
30395
  done(void 0, file2);
@@ -30491,7 +30491,7 @@ var init_lib16 = __esm({
30491
30491
  file = void 0;
30492
30492
  }
30493
30493
  return done ? executor(void 0, done) : new Promise(executor);
30494
- function executor(resolve3, reject) {
30494
+ function executor(resolve4, reject) {
30495
30495
  ok2(
30496
30496
  typeof file !== "function",
30497
30497
  "`file` can\u2019t be a `done` anymore, we checked"
@@ -30505,8 +30505,8 @@ var init_lib16 = __esm({
30505
30505
  );
30506
30506
  if (error) {
30507
30507
  reject(error);
30508
- } else if (resolve3) {
30509
- resolve3(resultingTree);
30508
+ } else if (resolve4) {
30509
+ resolve4(resultingTree);
30510
30510
  } else {
30511
30511
  ok2(done, "`done` is defined if `resolve` is not");
30512
30512
  done(void 0, resultingTree, file2);
@@ -32846,9 +32846,9 @@ var init_lib19 = __esm({
32846
32846
  this.remove = async (plugins3) => {
32847
32847
  if (this.#status === EditorStatus.OnCreate) {
32848
32848
  console.warn("[Milkdown]: You are trying to remove plugins when the editor is creating, this is not recommended, please check your code.");
32849
- return new Promise((resolve3) => {
32849
+ return new Promise((resolve4) => {
32850
32850
  setTimeout(() => {
32851
- resolve3(this.remove(plugins3));
32851
+ resolve4(this.remove(plugins3));
32852
32852
  }, 50);
32853
32853
  });
32854
32854
  }
@@ -32867,9 +32867,9 @@ var init_lib19 = __esm({
32867
32867
  };
32868
32868
  this.destroy = async (clearPlugins = false) => {
32869
32869
  if (this.#status === EditorStatus.Destroyed || this.#status === EditorStatus.OnDestroy) return this;
32870
- if (this.#status === EditorStatus.OnCreate) return new Promise((resolve3) => {
32870
+ if (this.#status === EditorStatus.OnCreate) return new Promise((resolve4) => {
32871
32871
  setTimeout(() => {
32872
- resolve3(this.destroy(clearPlugins));
32872
+ resolve4(this.destroy(clearPlugins));
32873
32873
  }, 50);
32874
32874
  });
32875
32875
  if (clearPlugins) this.#configureList = [];
@@ -42630,6 +42630,28 @@ function reanchorMarks(content3, marks) {
42630
42630
  return { marks: out, orphaned };
42631
42631
  }
42632
42632
 
42633
+ // packages/mddocs-local/src/apply.ts
42634
+ function applySuggestion(content3, mark) {
42635
+ const kind = mark.kind;
42636
+ if (kind !== "insert" && kind !== "delete" && kind !== "replace") {
42637
+ throw new Error(`mark ${mark.id} is a ${kind}, not a suggestion`);
42638
+ }
42639
+ const span = resolveQuote(content3, mark.quote);
42640
+ if (!span) {
42641
+ throw new Error(`cannot apply suggestion ${mark.id}: quoted text not found`);
42642
+ }
42643
+ const { from: from3, to } = span;
42644
+ const replacement = mark.data?.content ?? "";
42645
+ switch (kind) {
42646
+ case "replace":
42647
+ return content3.slice(0, from3) + replacement + content3.slice(to);
42648
+ case "delete":
42649
+ return content3.slice(0, from3) + content3.slice(to);
42650
+ case "insert":
42651
+ return content3.slice(0, to) + replacement + content3.slice(to);
42652
+ }
42653
+ }
42654
+
42633
42655
  // packages/mddocs-local/src/footer.ts
42634
42656
  var CONFLICT_RE = /^<{7} .*$[\s\S]*?^>{7} .*$/m;
42635
42657
  var OURS_MARKER = /^<{7} .*$\n?/m;
@@ -47658,11 +47680,58 @@ async function diff(path2, rev) {
47658
47680
  return g.diff(args2);
47659
47681
  }
47660
47682
 
47683
+ // packages/mddocs-local/src/markindex.ts
47684
+ import { readdir } from "node:fs/promises";
47685
+ import { join, resolve } from "node:path";
47686
+ async function listManagedDocs(cwd) {
47687
+ if (await isGitRepo(cwd)) {
47688
+ const root2 = (await esm_default(cwd).revparse(["--show-toplevel"])).trim();
47689
+ const out = await esm_default(root2).raw([
47690
+ "ls-files",
47691
+ "--cached",
47692
+ "--others",
47693
+ "--exclude-standard",
47694
+ "--",
47695
+ "*.md"
47696
+ ]);
47697
+ return out.split("\n").filter(Boolean).map((p2) => resolve(root2, p2));
47698
+ }
47699
+ return walkMd(cwd);
47700
+ }
47701
+ async function walkMd(dir) {
47702
+ const out = [];
47703
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
47704
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
47705
+ const full = join(dir, entry.name);
47706
+ if (entry.isDirectory()) out.push(...await walkMd(full));
47707
+ else if (entry.isFile() && entry.name.endsWith(".md")) out.push(full);
47708
+ }
47709
+ return out;
47710
+ }
47711
+ async function buildMarkIndex(cwd) {
47712
+ const index2 = /* @__PURE__ */ new Map();
47713
+ for (const file of await listManagedDocs(cwd)) {
47714
+ let marks;
47715
+ try {
47716
+ marks = (await loadDoc(file)).marks;
47717
+ } catch {
47718
+ continue;
47719
+ }
47720
+ for (const id3 of Object.keys(marks)) {
47721
+ if (!index2.has(id3)) index2.set(id3, file);
47722
+ }
47723
+ }
47724
+ return index2;
47725
+ }
47726
+ async function findFileForMark(id3, cwd) {
47727
+ return (await buildMarkIndex(cwd)).get(id3);
47728
+ }
47729
+
47661
47730
  // packages/mddocs-local/src/serve.ts
47662
47731
  import { createServer } from "node:http";
47663
47732
  import { readFile as readFile2 } from "node:fs/promises";
47664
47733
  import { fileURLToPath } from "node:url";
47665
- import { basename, dirname as dirname2, join, normalize as normalize2, resolve } from "node:path";
47734
+ import { basename, dirname as dirname2, join as join2, normalize as normalize2, resolve as resolve2 } from "node:path";
47666
47735
  import { existsSync } from "node:fs";
47667
47736
  async function readRaw(path2) {
47668
47737
  try {
@@ -47698,7 +47767,7 @@ async function createSession(path2, opts = {}) {
47698
47767
  }
47699
47768
  };
47700
47769
  }
47701
- var DEFAULT_DIST = process.env.MDDOCS_DIST ?? resolve(dirname2(fileURLToPath(import.meta.url)), "../../../dist");
47770
+ var DEFAULT_DIST = process.env.MDDOCS_DIST ?? resolve2(dirname2(fileURLToPath(import.meta.url)), "../../../dist");
47702
47771
  var CONTENT_TYPES = {
47703
47772
  ".html": "text/html; charset=utf-8",
47704
47773
  ".js": "text/javascript; charset=utf-8",
@@ -47731,10 +47800,10 @@ function sendJson(res, status, body) {
47731
47800
  async function serve(path2, opts = {}) {
47732
47801
  const session = await createSession(path2, opts);
47733
47802
  const distDir = opts.distDir ?? DEFAULT_DIST;
47734
- const absPath = resolve(path2);
47803
+ const absPath = resolve2(path2);
47735
47804
  async function serveStatic(urlPath, res) {
47736
47805
  const rel = urlPath === "/" ? "index.html" : urlPath.replace(/^\/+/, "");
47737
- const target = normalize2(join(distDir, rel));
47806
+ const target = normalize2(join2(distDir, rel));
47738
47807
  if (target !== distDir && !target.startsWith(distDir + "/")) {
47739
47808
  res.writeHead(403).end("Forbidden");
47740
47809
  return;
@@ -47967,6 +48036,14 @@ var appendTo = (dest, src) => {
47967
48036
  }
47968
48037
  };
47969
48038
  var from = Array.from;
48039
+ var some = (arr, f) => {
48040
+ for (let i2 = 0; i2 < arr.length; i2++) {
48041
+ if (f(arr[i2], i2, arr)) {
48042
+ return true;
48043
+ }
48044
+ }
48045
+ return false;
48046
+ };
47970
48047
  var isArray = Array.isArray;
47971
48048
 
47972
48049
  // node_modules/lib0/observable.js
@@ -49245,17 +49322,17 @@ var Doc = class _Doc extends ObservableV2 {
49245
49322
  this.isLoaded = false;
49246
49323
  this.isSynced = false;
49247
49324
  this.isDestroyed = false;
49248
- this.whenLoaded = create5((resolve3) => {
49325
+ this.whenLoaded = create5((resolve4) => {
49249
49326
  this.on("load", () => {
49250
49327
  this.isLoaded = true;
49251
- resolve3(this);
49328
+ resolve4(this);
49252
49329
  });
49253
49330
  });
49254
- const provideSyncedPromise = () => create5((resolve3) => {
49331
+ const provideSyncedPromise = () => create5((resolve4) => {
49255
49332
  const eventHandler = (isSynced) => {
49256
49333
  if (isSynced === void 0 || isSynced === true) {
49257
49334
  this.off("sync", eventHandler);
49258
- resolve3();
49335
+ resolve4();
49259
49336
  }
49260
49337
  };
49261
49338
  this.on("sync", eventHandler);
@@ -50327,6 +50404,16 @@ var findRootTypeKey = (type2) => {
50327
50404
  }
50328
50405
  throw unexpectedCase();
50329
50406
  };
50407
+ var isParentOf = (parent, child) => {
50408
+ while (child !== null) {
50409
+ if (child.parent === parent) {
50410
+ return true;
50411
+ }
50412
+ child = /** @type {AbstractType<any>} */
50413
+ child.parent._item;
50414
+ }
50415
+ return false;
50416
+ };
50330
50417
  var Snapshot = class {
50331
50418
  /**
50332
50419
  * @param {DeleteSet} ds
@@ -50726,6 +50813,310 @@ var transact = (doc4, f, origin = null, local = true) => {
50726
50813
  }
50727
50814
  return result;
50728
50815
  };
50816
+ var StackItem = class {
50817
+ /**
50818
+ * @param {DeleteSet} deletions
50819
+ * @param {DeleteSet} insertions
50820
+ */
50821
+ constructor(deletions, insertions) {
50822
+ this.insertions = insertions;
50823
+ this.deletions = deletions;
50824
+ this.meta = /* @__PURE__ */ new Map();
50825
+ }
50826
+ };
50827
+ var clearUndoManagerStackItem = (tr, um, stackItem) => {
50828
+ iterateDeletedStructs(tr, stackItem.deletions, (item) => {
50829
+ if (item instanceof Item && um.scope.some((type2) => type2 === tr.doc || isParentOf(
50830
+ /** @type {AbstractType<any>} */
50831
+ type2,
50832
+ item
50833
+ ))) {
50834
+ keepItem(item, false);
50835
+ }
50836
+ });
50837
+ };
50838
+ var popStackItem = (undoManager, stack, eventType) => {
50839
+ let _tr = null;
50840
+ const doc4 = undoManager.doc;
50841
+ const scope = undoManager.scope;
50842
+ transact(doc4, (transaction) => {
50843
+ while (stack.length > 0 && undoManager.currStackItem === null) {
50844
+ const store = doc4.store;
50845
+ const stackItem = (
50846
+ /** @type {StackItem} */
50847
+ stack.pop()
50848
+ );
50849
+ const itemsToRedo = /* @__PURE__ */ new Set();
50850
+ const itemsToDelete = [];
50851
+ let performedChange = false;
50852
+ iterateDeletedStructs(transaction, stackItem.insertions, (struct) => {
50853
+ if (struct instanceof Item) {
50854
+ if (struct.redone !== null) {
50855
+ let { item, diff: diff2 } = followRedone(store, struct.id);
50856
+ if (diff2 > 0) {
50857
+ item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff2));
50858
+ }
50859
+ struct = item;
50860
+ }
50861
+ if (!struct.deleted && scope.some((type2) => type2 === transaction.doc || isParentOf(
50862
+ /** @type {AbstractType<any>} */
50863
+ type2,
50864
+ /** @type {Item} */
50865
+ struct
50866
+ ))) {
50867
+ itemsToDelete.push(struct);
50868
+ }
50869
+ }
50870
+ });
50871
+ iterateDeletedStructs(transaction, stackItem.deletions, (struct) => {
50872
+ if (struct instanceof Item && scope.some((type2) => type2 === transaction.doc || isParentOf(
50873
+ /** @type {AbstractType<any>} */
50874
+ type2,
50875
+ struct
50876
+ )) && // Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
50877
+ !isDeleted(stackItem.insertions, struct.id)) {
50878
+ itemsToRedo.add(struct);
50879
+ }
50880
+ });
50881
+ itemsToRedo.forEach((struct) => {
50882
+ performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges, undoManager) !== null || performedChange;
50883
+ });
50884
+ for (let i2 = itemsToDelete.length - 1; i2 >= 0; i2--) {
50885
+ const item = itemsToDelete[i2];
50886
+ if (undoManager.deleteFilter(item)) {
50887
+ item.delete(transaction);
50888
+ performedChange = true;
50889
+ }
50890
+ }
50891
+ undoManager.currStackItem = performedChange ? stackItem : null;
50892
+ }
50893
+ transaction.changed.forEach((subProps, type2) => {
50894
+ if (subProps.has(null) && type2._searchMarker) {
50895
+ type2._searchMarker.length = 0;
50896
+ }
50897
+ });
50898
+ _tr = transaction;
50899
+ }, undoManager);
50900
+ const res = undoManager.currStackItem;
50901
+ if (res != null) {
50902
+ const changedParentTypes = _tr.changedParentTypes;
50903
+ undoManager.emit("stack-item-popped", [{ stackItem: res, type: eventType, changedParentTypes, origin: undoManager }, undoManager]);
50904
+ undoManager.currStackItem = null;
50905
+ }
50906
+ return res;
50907
+ };
50908
+ var UndoManager = class extends ObservableV2 {
50909
+ /**
50910
+ * @param {Doc|AbstractType<any>|Array<AbstractType<any>>} typeScope Limits the scope of the UndoManager. If this is set to a ydoc instance, all changes on that ydoc will be undone. If set to a specific type, only changes on that type or its children will be undone. Also accepts an array of types.
50911
+ * @param {UndoManagerOptions} options
50912
+ */
50913
+ constructor(typeScope, {
50914
+ captureTimeout = 500,
50915
+ captureTransaction = (_tr) => true,
50916
+ deleteFilter = () => true,
50917
+ trackedOrigins = /* @__PURE__ */ new Set([null]),
50918
+ ignoreRemoteMapChanges = false,
50919
+ doc: doc4 = (
50920
+ /** @type {Doc} */
50921
+ isArray(typeScope) ? typeScope[0].doc : typeScope instanceof Doc ? typeScope : typeScope.doc
50922
+ )
50923
+ } = {}) {
50924
+ super();
50925
+ this.scope = [];
50926
+ this.doc = doc4;
50927
+ this.addToScope(typeScope);
50928
+ this.deleteFilter = deleteFilter;
50929
+ trackedOrigins.add(this);
50930
+ this.trackedOrigins = trackedOrigins;
50931
+ this.captureTransaction = captureTransaction;
50932
+ this.undoStack = [];
50933
+ this.redoStack = [];
50934
+ this.undoing = false;
50935
+ this.redoing = false;
50936
+ this.currStackItem = null;
50937
+ this.lastChange = 0;
50938
+ this.ignoreRemoteMapChanges = ignoreRemoteMapChanges;
50939
+ this.captureTimeout = captureTimeout;
50940
+ this.afterTransactionHandler = (transaction) => {
50941
+ if (!this.captureTransaction(transaction) || !this.scope.some((type2) => transaction.changedParentTypes.has(
50942
+ /** @type {AbstractType<any>} */
50943
+ type2
50944
+ ) || type2 === this.doc) || !this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor))) {
50945
+ return;
50946
+ }
50947
+ const undoing = this.undoing;
50948
+ const redoing = this.redoing;
50949
+ const stack = undoing ? this.redoStack : this.undoStack;
50950
+ if (undoing) {
50951
+ this.stopCapturing();
50952
+ } else if (!redoing) {
50953
+ this.clear(false, true);
50954
+ }
50955
+ const insertions = new DeleteSet();
50956
+ transaction.afterState.forEach((endClock, client) => {
50957
+ const startClock = transaction.beforeState.get(client) || 0;
50958
+ const len = endClock - startClock;
50959
+ if (len > 0) {
50960
+ addToDeleteSet(insertions, client, startClock, len);
50961
+ }
50962
+ });
50963
+ const now = getUnixTime();
50964
+ let didAdd = false;
50965
+ if (this.lastChange > 0 && now - this.lastChange < this.captureTimeout && stack.length > 0 && !undoing && !redoing) {
50966
+ const lastOp = stack[stack.length - 1];
50967
+ lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet]);
50968
+ lastOp.insertions = mergeDeleteSets([lastOp.insertions, insertions]);
50969
+ } else {
50970
+ stack.push(new StackItem(transaction.deleteSet, insertions));
50971
+ didAdd = true;
50972
+ }
50973
+ if (!undoing && !redoing) {
50974
+ this.lastChange = now;
50975
+ }
50976
+ iterateDeletedStructs(
50977
+ transaction,
50978
+ transaction.deleteSet,
50979
+ /** @param {Item|GC} item */
50980
+ (item) => {
50981
+ if (item instanceof Item && this.scope.some((type2) => type2 === transaction.doc || isParentOf(
50982
+ /** @type {AbstractType<any>} */
50983
+ type2,
50984
+ item
50985
+ ))) {
50986
+ keepItem(item, true);
50987
+ }
50988
+ }
50989
+ );
50990
+ const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? "redo" : "undo", changedParentTypes: transaction.changedParentTypes }, this];
50991
+ if (didAdd) {
50992
+ this.emit("stack-item-added", changeEvent);
50993
+ } else {
50994
+ this.emit("stack-item-updated", changeEvent);
50995
+ }
50996
+ };
50997
+ this.doc.on("afterTransaction", this.afterTransactionHandler);
50998
+ this.doc.on("destroy", () => {
50999
+ this.destroy();
51000
+ });
51001
+ }
51002
+ /**
51003
+ * Extend the scope.
51004
+ *
51005
+ * @param {Array<AbstractType<any> | Doc> | AbstractType<any> | Doc} ytypes
51006
+ */
51007
+ addToScope(ytypes) {
51008
+ const tmpSet = new Set(this.scope);
51009
+ ytypes = isArray(ytypes) ? ytypes : [ytypes];
51010
+ ytypes.forEach((ytype) => {
51011
+ if (!tmpSet.has(ytype)) {
51012
+ tmpSet.add(ytype);
51013
+ if (ytype instanceof AbstractType ? ytype.doc !== this.doc : ytype !== this.doc) warn("[yjs#509] Not same Y.Doc");
51014
+ this.scope.push(ytype);
51015
+ }
51016
+ });
51017
+ }
51018
+ /**
51019
+ * @param {any} origin
51020
+ */
51021
+ addTrackedOrigin(origin) {
51022
+ this.trackedOrigins.add(origin);
51023
+ }
51024
+ /**
51025
+ * @param {any} origin
51026
+ */
51027
+ removeTrackedOrigin(origin) {
51028
+ this.trackedOrigins.delete(origin);
51029
+ }
51030
+ clear(clearUndoStack = true, clearRedoStack = true) {
51031
+ if (clearUndoStack && this.canUndo() || clearRedoStack && this.canRedo()) {
51032
+ this.doc.transact((tr) => {
51033
+ if (clearUndoStack) {
51034
+ this.undoStack.forEach((item) => clearUndoManagerStackItem(tr, this, item));
51035
+ this.undoStack = [];
51036
+ }
51037
+ if (clearRedoStack) {
51038
+ this.redoStack.forEach((item) => clearUndoManagerStackItem(tr, this, item));
51039
+ this.redoStack = [];
51040
+ }
51041
+ this.emit("stack-cleared", [{ undoStackCleared: clearUndoStack, redoStackCleared: clearRedoStack }]);
51042
+ });
51043
+ }
51044
+ }
51045
+ /**
51046
+ * UndoManager merges Undo-StackItem if they are created within time-gap
51047
+ * smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next
51048
+ * StackItem won't be merged.
51049
+ *
51050
+ *
51051
+ * @example
51052
+ * // without stopCapturing
51053
+ * ytext.insert(0, 'a')
51054
+ * ytext.insert(1, 'b')
51055
+ * um.undo()
51056
+ * ytext.toString() // => '' (note that 'ab' was removed)
51057
+ * // with stopCapturing
51058
+ * ytext.insert(0, 'a')
51059
+ * um.stopCapturing()
51060
+ * ytext.insert(0, 'b')
51061
+ * um.undo()
51062
+ * ytext.toString() // => 'a' (note that only 'b' was removed)
51063
+ *
51064
+ */
51065
+ stopCapturing() {
51066
+ this.lastChange = 0;
51067
+ }
51068
+ /**
51069
+ * Undo last changes on type.
51070
+ *
51071
+ * @return {StackItem?} Returns StackItem if a change was applied
51072
+ */
51073
+ undo() {
51074
+ this.undoing = true;
51075
+ let res;
51076
+ try {
51077
+ res = popStackItem(this, this.undoStack, "undo");
51078
+ } finally {
51079
+ this.undoing = false;
51080
+ }
51081
+ return res;
51082
+ }
51083
+ /**
51084
+ * Redo last undo operation.
51085
+ *
51086
+ * @return {StackItem?} Returns StackItem if a change was applied
51087
+ */
51088
+ redo() {
51089
+ this.redoing = true;
51090
+ let res;
51091
+ try {
51092
+ res = popStackItem(this, this.redoStack, "redo");
51093
+ } finally {
51094
+ this.redoing = false;
51095
+ }
51096
+ return res;
51097
+ }
51098
+ /**
51099
+ * Are undo steps available?
51100
+ *
51101
+ * @return {boolean} `true` if undo is possible
51102
+ */
51103
+ canUndo() {
51104
+ return this.undoStack.length > 0;
51105
+ }
51106
+ /**
51107
+ * Are redo steps available?
51108
+ *
51109
+ * @return {boolean} `true` if redo is possible
51110
+ */
51111
+ canRedo() {
51112
+ return this.redoStack.length > 0;
51113
+ }
51114
+ destroy() {
51115
+ this.trackedOrigins.delete(this);
51116
+ this.doc.off("afterTransaction", this.afterTransactionHandler);
51117
+ super.destroy();
51118
+ }
51119
+ };
50729
51120
  function* lazyStructReaderGenerator(decoder) {
50730
51121
  const numOfStateUpdates = readVarUint(decoder.restDecoder);
50731
51122
  for (let i2 = 0; i2 < numOfStateUpdates; i2++) {
@@ -54998,6 +55389,30 @@ var ContentType = class _ContentType {
54998
55389
  }
54999
55390
  };
55000
55391
  var readContentType = (decoder) => new ContentType(typeRefs[decoder.readTypeRef()](decoder));
55392
+ var followRedone = (store, id3) => {
55393
+ let nextID = id3;
55394
+ let diff2 = 0;
55395
+ let item;
55396
+ do {
55397
+ if (diff2 > 0) {
55398
+ nextID = createID(nextID.client, nextID.clock + diff2);
55399
+ }
55400
+ item = getItem(store, nextID);
55401
+ diff2 = nextID.clock - item.id.clock;
55402
+ nextID = item.redone;
55403
+ } while (nextID !== null && item instanceof Item);
55404
+ return {
55405
+ item,
55406
+ diff: diff2
55407
+ };
55408
+ };
55409
+ var keepItem = (item, keep) => {
55410
+ while (item !== null && item.keep !== keep) {
55411
+ item.keep = keep;
55412
+ item = /** @type {AbstractType<any>} */
55413
+ item.parent._item;
55414
+ }
55415
+ };
55001
55416
  var splitItem = (transaction, leftItem, diff2) => {
55002
55417
  const { client, clock } = leftItem.id;
55003
55418
  const rightItem = new Item(
@@ -55030,6 +55445,105 @@ var splitItem = (transaction, leftItem, diff2) => {
55030
55445
  leftItem.length = diff2;
55031
55446
  return rightItem;
55032
55447
  };
55448
+ var isDeletedByUndoStack = (stack, id3) => some(
55449
+ stack,
55450
+ /** @param {StackItem} s */
55451
+ (s) => isDeleted(s.deletions, id3)
55452
+ );
55453
+ var redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) => {
55454
+ const doc4 = transaction.doc;
55455
+ const store = doc4.store;
55456
+ const ownClientID = doc4.clientID;
55457
+ const redone = item.redone;
55458
+ if (redone !== null) {
55459
+ return getItemCleanStart(transaction, redone);
55460
+ }
55461
+ let parentItem = (
55462
+ /** @type {AbstractType<any>} */
55463
+ item.parent._item
55464
+ );
55465
+ let left = null;
55466
+ let right;
55467
+ if (parentItem !== null && parentItem.deleted === true) {
55468
+ if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) === null)) {
55469
+ return null;
55470
+ }
55471
+ while (parentItem.redone !== null) {
55472
+ parentItem = getItemCleanStart(transaction, parentItem.redone);
55473
+ }
55474
+ }
55475
+ const parentType = parentItem === null ? (
55476
+ /** @type {AbstractType<any>} */
55477
+ item.parent
55478
+ ) : (
55479
+ /** @type {ContentType} */
55480
+ parentItem.content.type
55481
+ );
55482
+ if (item.parentSub === null) {
55483
+ left = item.left;
55484
+ right = item;
55485
+ while (left !== null) {
55486
+ let leftTrace = left;
55487
+ while (leftTrace !== null && /** @type {AbstractType<any>} */
55488
+ leftTrace.parent._item !== parentItem) {
55489
+ leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone);
55490
+ }
55491
+ if (leftTrace !== null && /** @type {AbstractType<any>} */
55492
+ leftTrace.parent._item === parentItem) {
55493
+ left = leftTrace;
55494
+ break;
55495
+ }
55496
+ left = left.left;
55497
+ }
55498
+ while (right !== null) {
55499
+ let rightTrace = right;
55500
+ while (rightTrace !== null && /** @type {AbstractType<any>} */
55501
+ rightTrace.parent._item !== parentItem) {
55502
+ rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone);
55503
+ }
55504
+ if (rightTrace !== null && /** @type {AbstractType<any>} */
55505
+ rightTrace.parent._item === parentItem) {
55506
+ right = rightTrace;
55507
+ break;
55508
+ }
55509
+ right = right.right;
55510
+ }
55511
+ } else {
55512
+ right = null;
55513
+ if (item.right && !ignoreRemoteMapChanges) {
55514
+ left = item;
55515
+ while (left !== null && left.right !== null && (left.right.redone || isDeleted(itemsToDelete, left.right.id) || isDeletedByUndoStack(um.undoStack, left.right.id) || isDeletedByUndoStack(um.redoStack, left.right.id))) {
55516
+ left = left.right;
55517
+ while (left.redone) left = getItemCleanStart(transaction, left.redone);
55518
+ }
55519
+ if (left && left.right !== null) {
55520
+ return null;
55521
+ }
55522
+ } else {
55523
+ left = parentType._map.get(item.parentSub) || null;
55524
+ }
55525
+ if (left !== null && /** @type {AbstractType<any>} */
55526
+ left.parent._item !== parentItem) {
55527
+ left = parentType._map.get(item.parentSub) || null;
55528
+ }
55529
+ }
55530
+ const nextClock = getState(store, ownClientID);
55531
+ const nextId = createID(ownClientID, nextClock);
55532
+ const redoneItem = new Item(
55533
+ nextId,
55534
+ left,
55535
+ left && left.lastId,
55536
+ right,
55537
+ right && right.id,
55538
+ parentType,
55539
+ item.parentSub,
55540
+ item.content.copy()
55541
+ );
55542
+ item.redone = nextId;
55543
+ keepItem(redoneItem, true);
55544
+ redoneItem.integrate(transaction, 0);
55545
+ return redoneItem;
55546
+ };
55033
55547
  var Item = class _Item extends AbstractStruct {
55034
55548
  /**
55035
55549
  * @param {ID} id
@@ -57401,7 +57915,7 @@ var Hocuspocus = class {
57401
57915
  process.on("SIGQUIT", signalHandler);
57402
57916
  process.on("SIGTERM", signalHandler);
57403
57917
  }
57404
- return new Promise((resolve3, reject) => {
57918
+ return new Promise((resolve4, reject) => {
57405
57919
  var _a2;
57406
57920
  (_a2 = this.server) === null || _a2 === void 0 ? void 0 : _a2.httpServer.listen({
57407
57921
  port: this.configuration.port,
@@ -57417,7 +57931,7 @@ var Hocuspocus = class {
57417
57931
  };
57418
57932
  try {
57419
57933
  await this.hooks("onListen", onListenPayload);
57420
- resolve3(this);
57934
+ resolve4(this);
57421
57935
  } catch (e) {
57422
57936
  reject(e);
57423
57937
  }
@@ -57501,19 +58015,19 @@ var Hocuspocus = class {
57501
58015
  * Destroy the server
57502
58016
  */
57503
58017
  async destroy() {
57504
- await new Promise(async (resolve3) => {
58018
+ await new Promise(async (resolve4) => {
57505
58019
  var _a2, _b, _c, _d;
57506
58020
  (_b = (_a2 = this.server) === null || _a2 === void 0 ? void 0 : _a2.httpServer) === null || _b === void 0 ? void 0 : _b.close();
57507
58021
  try {
57508
58022
  this.configuration.extensions.push({
57509
58023
  async afterUnloadDocument({ instance }) {
57510
58024
  if (instance.getDocumentsCount() === 0)
57511
- resolve3("");
58025
+ resolve4("");
57512
58026
  }
57513
58027
  });
57514
58028
  (_d = (_c = this.server) === null || _c === void 0 ? void 0 : _c.webSocketServer) === null || _d === void 0 ? void 0 : _d.close();
57515
58029
  if (this.getDocumentsCount() === 0)
57516
- resolve3("");
58030
+ resolve4("");
57517
58031
  this.closeConnections();
57518
58032
  } catch (error) {
57519
58033
  console.error(error);
@@ -58214,11 +58728,38 @@ async function seedFragmentFromMarkdown(markdown, fragment) {
58214
58728
  const node2 = parser5.parseMarkdown(markdown);
58215
58729
  prosemirrorToYXmlFragment(node2, fragment);
58216
58730
  }
58731
+ async function parseMarkdownNode(markdown) {
58732
+ const parser5 = await getParser();
58733
+ return parser5.parseMarkdown(markdown);
58734
+ }
58735
+ function setFragmentFromNode(fragment, node2) {
58736
+ fragment.delete(0, fragment.length);
58737
+ prosemirrorToYXmlFragment(node2, fragment);
58738
+ }
58217
58739
 
58218
58740
  // packages/mddocs-local/src/collab.ts
58219
58741
  async function configureCollab(file, opts = {}) {
58220
58742
  const slug = opts.slug ?? basename2(file);
58221
58743
  const session = await createSession(file, opts);
58744
+ const enforced = /* @__PURE__ */ new WeakSet();
58745
+ function enforceCommenterProse(doc4) {
58746
+ if (enforced.has(doc4)) return;
58747
+ enforced.add(doc4);
58748
+ const trackedOrigins = {
58749
+ has: (o2) => !!o2 && typeof o2 === "object" && o2.context?.role === "commenter",
58750
+ add: () => void 0,
58751
+ delete: () => void 0
58752
+ };
58753
+ const undo = new UndoManager(doc4.getXmlFragment("prosemirror"), {
58754
+ trackedOrigins,
58755
+ captureTimeout: 0
58756
+ });
58757
+ undo.on("stack-item-added", () => {
58758
+ queueMicrotask(() => {
58759
+ if (undo.undoStack.length > 0) undo.undo();
58760
+ });
58761
+ });
58762
+ }
58222
58763
  const hocuspocus = new Hocuspocus().configure({
58223
58764
  port: opts.port ?? 0,
58224
58765
  debounce: opts.storeDebounceMs ?? 150,
@@ -58237,6 +58778,7 @@ async function configureCollab(file, opts = {}) {
58237
58778
  for (const [id3, mark] of Object.entries(marks)) {
58238
58779
  if (!ymarks.has(id3)) ymarks.set(id3, mark);
58239
58780
  }
58781
+ if (opts.authenticate) enforceCommenterProse(data.document);
58240
58782
  return data.document;
58241
58783
  },
58242
58784
  // Persist the settled doc back to the file (+ optional autocommit). The
@@ -58259,7 +58801,7 @@ async function configureCollab(file, opts = {}) {
58259
58801
  const verdict = opts.authenticate(data.token);
58260
58802
  if (!verdict) throw new Error("Unauthorized");
58261
58803
  data.connection.readOnly = verdict.readOnly;
58262
- return { readOnly: verdict.readOnly };
58804
+ return { readOnly: verdict.readOnly, role: verdict.role };
58263
58805
  }
58264
58806
  } : {}
58265
58807
  });
@@ -58271,7 +58813,7 @@ import { createServer as createServer3 } from "node:http";
58271
58813
  import { readFile as readFile3 } from "node:fs/promises";
58272
58814
  import { fileURLToPath as fileURLToPath3 } from "node:url";
58273
58815
  import { randomUUID as randomUUID2 } from "node:crypto";
58274
- import { dirname as dirname3, join as join4, normalize as normalize4, resolve as resolve2 } from "node:path";
58816
+ import { dirname as dirname3, join as join5, normalize as normalize4, resolve as resolve3 } from "node:path";
58275
58817
 
58276
58818
  // packages/mddocs-local/src/agent.ts
58277
58819
  function createAgentApi(hocuspocus, slug, opts = {}) {
@@ -58307,6 +58849,38 @@ function createAgentApi(hocuspocus, slug, opts = {}) {
58307
58849
  await inject(mark);
58308
58850
  return { id: mark.id, kind: mark.kind };
58309
58851
  },
58852
+ // Edit the prose directly (not a proposal): replace a quoted span, or the
58853
+ // whole body when no quote is given. The new markdown is applied to the live
58854
+ // `prosemirror` fragment so it syncs to every editor and persists to the file
58855
+ // (+ git) via onStoreDocument. Authorship is recorded as an `ai:<model>`
58856
+ // authored mark over the new text.
58857
+ async rewrite({ quote, markdown, model }) {
58858
+ const conn = await connect();
58859
+ const doc4 = conn.document;
58860
+ if (!doc4) throw new Error("no live document to rewrite");
58861
+ const current = await fragmentToMarkdown(doc4.getXmlFragment("prosemirror")) ?? "";
58862
+ let next;
58863
+ if (quote && quote.length > 0) {
58864
+ const span = resolveQuote(current, quote);
58865
+ if (!span) throw new Error("quoted text not found in the live document");
58866
+ next = current.slice(0, span.from) + markdown + current.slice(span.to);
58867
+ } else {
58868
+ next = markdown;
58869
+ }
58870
+ const node2 = await parseMarkdownNode(next);
58871
+ await conn.transact((d) => {
58872
+ setFragmentFromNode(d.getXmlFragment("prosemirror"), node2);
58873
+ });
58874
+ const by = actor2(model);
58875
+ const snippet = normalizeQuote(markdown).slice(0, 200);
58876
+ let markId;
58877
+ if (snippet.length > 0) {
58878
+ const mark = createAuthored(by, { from: 0, to: 0 }, snippet);
58879
+ await inject(mark);
58880
+ markId = mark.id;
58881
+ }
58882
+ return { chars: next.length, by, markId };
58883
+ },
58310
58884
  async stop() {
58311
58885
  if (connPromise) {
58312
58886
  const conn = await connPromise;
@@ -58323,7 +58897,7 @@ var CAPABILITIES = {
58323
58897
  commenter: { canRead: true, canComment: true, canEdit: false },
58324
58898
  viewer: { canRead: true, canComment: false, canEdit: false }
58325
58899
  };
58326
- var DEFAULT_DIST2 = process.env.MDDOCS_DIST ?? resolve2(dirname3(fileURLToPath3(import.meta.url)), "../../../dist");
58900
+ var DEFAULT_DIST2 = process.env.MDDOCS_DIST ?? resolve3(dirname3(fileURLToPath3(import.meta.url)), "../../../dist");
58327
58901
  var CONTENT_TYPES2 = {
58328
58902
  ".html": "text/html; charset=utf-8",
58329
58903
  ".js": "text/javascript; charset=utf-8",
@@ -58378,16 +58952,35 @@ async function serveShare(file, opts = {}) {
58378
58952
  const q2 = (req.url ?? "").split("?")[1];
58379
58953
  return q2 ? new URLSearchParams(q2).get("token") ?? void 0 : void 0;
58380
58954
  }
58381
- const agentToken = randomUUID2();
58955
+ const agentEntries = opts.agents && opts.agents.length > 0 ? opts.agents.map((a2) => ({ name: a2.name, token: randomUUID2(), rateLimit: a2.rateLimit })) : [{ name: "agent", token: randomUUID2() }];
58956
+ const agentToken = agentEntries[0].token;
58957
+ const agentByToken = new Map(agentEntries.map((e) => [e.token, e]));
58958
+ const rateHits = /* @__PURE__ */ new Map();
58959
+ function withinRateLimit(entry) {
58960
+ if (!entry.rateLimit) return true;
58961
+ const { maxRequests, windowMs } = entry.rateLimit;
58962
+ const now = Date.now();
58963
+ const recent = (rateHits.get(entry.token) ?? []).filter((t2) => now - t2 < windowMs);
58964
+ if (recent.length >= maxRequests) {
58965
+ rateHits.set(entry.token, recent);
58966
+ return false;
58967
+ }
58968
+ recent.push(now);
58969
+ rateHits.set(entry.token, recent);
58970
+ return true;
58971
+ }
58382
58972
  const { hocuspocus, session, slug } = await configureCollab(file, {
58383
58973
  ...opts,
58384
- authenticate: (token) => ({ readOnly: roleForToken(token) === "viewer" })
58974
+ authenticate: (token) => {
58975
+ const role = roleForToken(token);
58976
+ return { readOnly: role === "viewer", role };
58977
+ }
58385
58978
  });
58386
58979
  const agent4 = createAgentApi(hocuspocus, slug);
58387
58980
  async function serveStatic(urlPath, res) {
58388
58981
  const isDocRoute = urlPath === "/" || /^\/d\/[^/]+\/?$/.test(urlPath);
58389
58982
  const rel = isDocRoute ? "index.html" : urlPath.replace(/^\/d\//, "").replace(/^\/+/, "");
58390
- const target = normalize4(join4(distDir, rel));
58983
+ const target = normalize4(join5(distDir, rel));
58391
58984
  if (target !== distDir && !target.startsWith(distDir + "/")) {
58392
58985
  res.writeHead(403).end("Forbidden");
58393
58986
  return;
@@ -58429,10 +59022,16 @@ async function serveShare(file, opts = {}) {
58429
59022
  return;
58430
59023
  }
58431
59024
  if (urlPath.startsWith(`/api/agent/${slug}/`)) {
58432
- if (tokenFromRequest(req) !== agentToken) {
59025
+ const entry = agentByToken.get(tokenFromRequest(req) ?? "");
59026
+ if (!entry) {
58433
59027
  sendJson2(res, 403, { error: "invalid or missing agent token" });
58434
59028
  return;
58435
59029
  }
59030
+ if (!withinRateLimit(entry)) {
59031
+ sendJson2(res, 429, { error: "rate limit exceeded", agent: entry.name });
59032
+ return;
59033
+ }
59034
+ const modelFrom = (b2) => b2.model ?? entry.name;
58436
59035
  if (urlPath === `/api/agent/${slug}/state` && req.method === "GET") {
58437
59036
  sendJson2(res, 200, await agent4.getState());
58438
59037
  return;
@@ -58443,7 +59042,7 @@ async function serveShare(file, opts = {}) {
58443
59042
  sendJson2(res, 400, { error: "comment needs { quote, text }" });
58444
59043
  return;
58445
59044
  }
58446
- sendJson2(res, 200, await agent4.addComment({ quote: b2.quote, text: b2.text, model: b2.model }));
59045
+ sendJson2(res, 200, await agent4.addComment({ quote: b2.quote, text: b2.text, model: modelFrom(b2) }));
58447
59046
  return;
58448
59047
  }
58449
59048
  if (urlPath === `/api/agent/${slug}/suggest` && req.method === "POST") {
@@ -58457,7 +59056,20 @@ async function serveShare(file, opts = {}) {
58457
59056
  replace: b2.replace,
58458
59057
  insert: b2.insert,
58459
59058
  delete: b2.delete,
58460
- model: b2.model
59059
+ model: modelFrom(b2)
59060
+ }));
59061
+ return;
59062
+ }
59063
+ if (urlPath === `/api/agent/${slug}/rewrite` && req.method === "POST") {
59064
+ const b2 = await readJsonBody(req);
59065
+ if (typeof b2.markdown !== "string") {
59066
+ sendJson2(res, 400, { error: "rewrite needs { markdown, quote? }" });
59067
+ return;
59068
+ }
59069
+ sendJson2(res, 200, await agent4.rewrite({
59070
+ markdown: b2.markdown,
59071
+ quote: typeof b2.quote === "string" ? b2.quote : void 0,
59072
+ model: modelFrom(b2)
58461
59073
  }));
58462
59074
  return;
58463
59075
  }
@@ -58489,6 +59101,7 @@ async function serveShare(file, opts = {}) {
58489
59101
  url: links.editor,
58490
59102
  links,
58491
59103
  agentToken,
59104
+ agentTokens: opts.agents ? Object.fromEntries(agentEntries.map((e) => [e.name, e.token])) : void 0,
58492
59105
  host,
58493
59106
  port: boundPort,
58494
59107
  slug,
@@ -58503,11 +59116,15 @@ async function serveShare(file, opts = {}) {
58503
59116
  }
58504
59117
 
58505
59118
  // packages/mddocs-cli/src/util/resolve-file.ts
58506
- function fileForId(opts) {
58507
- if (!opts.file) {
58508
- throw new Error("This command needs --file <path> in M1 (id->file index is a later milestone).");
59119
+ async function fileForId(id3, opts) {
59120
+ if (opts.file) return opts.file;
59121
+ const found2 = await findFileForMark(id3, process.cwd());
59122
+ if (!found2) {
59123
+ throw new Error(
59124
+ `could not find mark ${id3} in any managed .md file under ${process.cwd()} (pass --file <path> to point at it directly)`
59125
+ );
58509
59126
  }
58510
- return opts.file;
59127
+ return found2;
58511
59128
  }
58512
59129
  function actor() {
58513
59130
  return `human:${process.env.USER ?? "unknown"}`;
@@ -58544,7 +59161,7 @@ function registerComment(program2) {
58544
59161
  }
58545
59162
  });
58546
59163
  cmd.command("reply <id>").requiredOption("--text <t>").option("--file <f>").action(async (id3, o2) => {
58547
- const file = fileForId(o2);
59164
+ const file = await fileForId(id3, o2);
58548
59165
  const doc4 = await loadDoc(file);
58549
59166
  const mark = doc4.marks[id3];
58550
59167
  if (!mark || mark.kind !== "comment") throw new Error(`no comment with id ${id3} in ${file}`);
@@ -58557,7 +59174,7 @@ function registerComment(program2) {
58557
59174
  console.log(`replied to ${id3}`);
58558
59175
  });
58559
59176
  cmd.command("resolve <id>").option("--file <f>").action(async (id3, o2) => {
58560
- const file = fileForId(o2);
59177
+ const file = await fileForId(id3, o2);
58561
59178
  const doc4 = await loadDoc(file);
58562
59179
  const next = proof_exports.resolveComment(toArray(doc4.marks), id3);
58563
59180
  await saveDoc(file, doc4.content, toRecord(next));
@@ -58586,18 +59203,28 @@ function registerSuggest(program2) {
58586
59203
  }
58587
59204
 
58588
59205
  // packages/mddocs-cli/src/commands/accept-reject.ts
58589
- function decide(apply2, verb) {
58590
- return async (id3, o2) => {
58591
- const file = fileForId(o2);
59206
+ var SUGGESTION_KINDS = ["insert", "delete", "replace"];
59207
+ function registerAcceptReject(program2) {
59208
+ program2.command("accept <id>").option("--file <f>").action(async (id3, o2) => {
59209
+ const file = await fileForId(id3, o2);
58592
59210
  const doc4 = await loadDoc(file);
58593
- const next = apply2(toArray(doc4.marks), id3);
59211
+ const stored = doc4.marks[id3];
59212
+ const mark = stored && { ...stored, id: id3 };
59213
+ if (!mark || !SUGGESTION_KINDS.includes(mark.kind)) {
59214
+ throw new Error(`no suggestion with id ${id3} in ${file}`);
59215
+ }
59216
+ const content3 = applySuggestion(doc4.content, mark);
59217
+ const { [id3]: _applied, ...rest } = doc4.marks;
59218
+ await saveDoc(file, content3, rest);
59219
+ console.log(`accepted ${id3} (applied to ${file})`);
59220
+ });
59221
+ program2.command("reject <id>").option("--file <f>").action(async (id3, o2) => {
59222
+ const file = await fileForId(id3, o2);
59223
+ const doc4 = await loadDoc(file);
59224
+ const next = proof_exports.rejectSuggestion(toArray(doc4.marks), id3);
58594
59225
  await saveDoc(file, doc4.content, toRecord(next));
58595
- console.log(`${verb} ${id3}`);
58596
- };
58597
- }
58598
- function registerAcceptReject(program2) {
58599
- program2.command("accept <id>").option("--file <f>").action(decide(proof_exports.acceptSuggestion, "accepted"));
58600
- program2.command("reject <id>").option("--file <f>").action(decide(proof_exports.rejectSuggestion, "rejected"));
59226
+ console.log(`rejected ${id3}`);
59227
+ });
58601
59228
  }
58602
59229
 
58603
59230
  // packages/mddocs-cli/src/commands/history.ts
@@ -58654,21 +59281,34 @@ function registerOpen(program2) {
58654
59281
  // packages/mddocs-cli/src/commands/serve.ts
58655
59282
  import { dirname as dirname5 } from "node:path";
58656
59283
  function registerServe(program2) {
58657
- program2.command("serve <file>").description("host a live multiplayer editing session (share the URL on your LAN)").option("--port <n>", "port to listen on", (v) => parseInt(v, 10)).option("--host <h>", "interface to bind (use 0.0.0.0 to share on your LAN)", "127.0.0.1").option("--no-autocommit", "do not auto-commit edits to git").action(async (file, o2) => {
59284
+ program2.command("serve <file>").description("host a live multiplayer editing session (share the URL on your LAN)").option("--port <n>", "port to listen on", (v) => parseInt(v, 10)).option("--host <h>", "interface to bind (use 0.0.0.0 to share on your LAN)", "127.0.0.1").option("--no-autocommit", "do not auto-commit edits to git").option("--agent <name>", "register a named agent with its own token (repeatable)", (v, acc) => [...acc, v], []).action(async (file, o2) => {
58658
59285
  const autocommit = o2.autocommit !== false;
58659
59286
  if (autocommit && !await isGitRepo(dirname5(file))) {
58660
59287
  console.warn("mddocs: not a git repo - history/autocommit disabled. Run `mddocs init` + `git init` to enable.");
58661
59288
  }
58662
- const handle2 = await serveShare(file, { port: o2.port, host: o2.host, autocommit });
59289
+ const agents = (o2.agent ?? []).map((name2) => ({ name: name2 }));
59290
+ const handle2 = await serveShare(file, {
59291
+ port: o2.port,
59292
+ host: o2.host,
59293
+ autocommit,
59294
+ agents: agents.length > 0 ? agents : void 0
59295
+ });
58663
59296
  console.log(`mddocs: live session for ${file}`);
58664
59297
  console.log(` edit (you): ${handle2.links.editor}`);
58665
59298
  console.log(` comment link: ${handle2.links.commenter}`);
58666
59299
  console.log(` view link: ${handle2.links.viewer}`);
58667
59300
  console.log(" (share the link matching the access you want to grant)");
58668
59301
  console.log("");
58669
- console.log(" agent API (programmatic comments/suggestions, live + git-backed):");
59302
+ console.log(" agent API (programmatic comments/suggestions/rewrites, live + git-backed):");
58670
59303
  console.log(` base: http://${handle2.host}:${handle2.port}/api/agent/${handle2.slug}`);
58671
- console.log(` token: ${handle2.agentToken} (send as header: x-share-token)`);
59304
+ if (handle2.agentTokens) {
59305
+ console.log(" tokens (send as header: x-share-token):");
59306
+ for (const [name2, token] of Object.entries(handle2.agentTokens)) {
59307
+ console.log(` ${name2}: ${token}`);
59308
+ }
59309
+ } else {
59310
+ console.log(` token: ${handle2.agentToken} (send as header: x-share-token)`);
59311
+ }
58672
59312
  console.log(` e.g. curl -H "x-share-token: ${handle2.agentToken}" -H 'content-type: application/json' \\`);
58673
59313
  console.log(` -d '{"quote":"...","text":"..."}' http://${handle2.host}:${handle2.port}/api/agent/${handle2.slug}/comment`);
58674
59314
  console.log(" (Ctrl-C to stop)");
@@ -58696,11 +59336,11 @@ function registerResolve(program2) {
58696
59336
 
58697
59337
  // packages/mddocs-cli/src/commands/init.ts
58698
59338
  import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
58699
- import { join as join5 } from "node:path";
59339
+ import { join as join6 } from "node:path";
58700
59340
  var MD_LINE = "*.md diff text";
58701
59341
  function registerInit(program2) {
58702
59342
  program2.command("init").description("mark this repo as mddocs-managed (.gitattributes)").action(async () => {
58703
- const attrsPath = join5(process.cwd(), ".gitattributes");
59343
+ const attrsPath = join6(process.cwd(), ".gitattributes");
58704
59344
  let existing = "";
58705
59345
  try {
58706
59346
  existing = await readFile5(attrsPath, "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devyrpauli/mddocs",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Local-first, git-native collaborative Markdown: real-time multiplayer, comments and suggestions, and an HTTP API for AI agents. Self-hostable, built on proof-sdk.",
5
5
  "type": "module",
6
6
  "bin": {